content: // Copyright 2016 Google Inc. Use of this source code is governed by an
  // MIT-style license that can be found in the LICENSE file or at
  // https://opensource.org/licenses/MIT.
  
  import '../../interpolation_map.dart';
  import '../../parse/media_query.dart';
  import '../../utils.dart';
  
  /// A plain CSS media query, as used in `@media` and `@import`.
  final class CssMediaQuery {
    /// The modifier, probably either "not" or "only".
    ///
    /// This may be `null` if no modifier is in use.
    final String? modifier;
  
    /// The media type, for example "screen" or "print".
    ///
    /// This may be `null`. If so, [conditions] will not be empty.
    final String? type;
  
    /// Whether [conditions] is a conjunction or a disjunction.
    ///
    /// In other words, if this is `true` this query matches when _all_
    /// [conditions] are met, and if it's `false` this query matches when _any_
    /// condition in [conditions] is met.
    ///
    /// If this is `false`, [modifier] and [type] will both be `null`.
    final bool conjunction;
  
    /// Media conditions, including parentheses.
    ///
    /// This is anything that can appear in the [`<media-in-parens>`] production.
    ///
    /// [`<media-in-parens>`]: https://drafts.csswg.org/mediaqueries-4/#typedef-media-in-parens
    final List<String> conditions;
  
    /// Whether this media query matches all media types.
    bool get matchesAllTypes => type == null || equalsIgnoreCase(type, 'all');
  
    /// Parses a media query from [contents].
    ///
    /// If passed, [url] is the name of the file from which [contents] comes.
    ///
    /// Throws a [SassFormatException] if parsing fails.
    static List<CssMediaQuery> parseList(
      String contents, {
      Object? url,
      InterpolationMap? interpolationMap,
    }) =>
        MediaQueryParser(
          contents,
          url: url,
          interpolationMap: interpolationMap,
        ).parse();
  
    /// Creates a media query specifies a type and, optionally, conditions.
    ///
    /// This always sets [conjunction] to `true`.
    CssMediaQuery.type(this.type, {this.modifier, Iterable<String>? conditions})
        : conjunction = true,
          conditions =
              conditions == null ? const [] : List.unmodifiable(conditions);
  
    /// Creates a media query that matches [conditions] according to
    /// [conjunction].
    ///
    /// The [conjunction] argument may not be null if [conditions] is longer than
    /// a single element.
    CssMediaQuery.condition(Iterable<String> conditions, {bool? conjunction})
        : modifier = null,
          type = null,
          conjunction = conjunction ?? true,
          conditions = List.unmodifiable(conditions) {
      if (this.conditions.length > 1 && conjunction == null) {
        throw ArgumentError(
          "If conditions is longer than one element, conjunction may not be "
          "null.",
        );
      }
    }
  
    /// Merges this with [other] to return a query that matches the intersection
    /// of both inputs.
    MediaQueryMergeResult merge(CssMediaQuery other) {
      if (!conjunction || !other.conjunction) {
        return MediaQueryMergeResult.unrepresentable;
      }
  
      var ourModifier = this.modifier?.toLowerCase();
      var ourType = this.type?.toLowerCase();
      var theirModifier = other.modifier?.toLowerCase();
      var theirType = other.type?.toLowerCase();
  
      if (ourType == null && theirType == null) {
        return MediaQuerySuccessfulMergeResult._(
          CssMediaQuery.condition([
            ...this.conditions,
            ...other.conditions,
          ], conjunction: true),
        );
      }
  
      String? modifier;
      String? type;
      List<String> conditions;
      if ((ourModifier == 'not') != (theirModifier == 'not')) {
        if (ourType == theirType) {
          var negativeConditions =
              ourModifier == 'not' ? this.conditions : other.conditions;
          var positiveConditions =
              ourModifier == 'not' ? other.conditions : this.conditions;
  
          // If the negative conditions are a subset of the positive conditions, the
          // query is empty. For example, `not screen and (color)` has no
          // intersection with `screen and (color) and (grid)`.
          //
          // However, `not screen and (color)` *does* intersect with `screen and
          // (grid)`, because it means `not (screen and (color))` and so it allows
          // a screen with no color but with a grid.
          if (negativeConditions.every(positiveConditions.contains)) {
            return MediaQueryMergeResult.empty;
          } else {
            return MediaQueryMergeResult.unrepresentable;
          }
        } else if (matchesAllTypes || other.matchesAllTypes) {
          return MediaQueryMergeResult.unrepresentable;
        }
  
        if (ourModifier == 'not') {
          modifier = theirModifier;
          type = theirType;
          conditions = other.conditions;
        } else {
          modifier = ourModifier;
          type = ourType;
          conditions = this.conditions;
        }
      } else if (ourModifier == 'not') {
        assert(theirModifier == 'not');
        // CSS has no way of representing "neither screen nor print".
        if (ourType != theirType) return MediaQueryMergeResult.unrepresentable;
  
        var moreConditions = this.conditions.length > other.conditions.length
            ? this.conditions
            : other.conditions;
        var fewerConditions = this.conditions.length > other.conditions.length
            ? other.conditions
            : this.conditions;
  
        // If one set of conditions is a superset of the other, use those conditions
        // because they're strictly narrower.
        if (fewerConditions.every(moreConditions.contains)) {
          modifier = ourModifier; // "not"
          type = ourType;
          conditions = moreConditions;
        } else {
          // Otherwise, there's no way to represent the intersection.
          return MediaQueryMergeResult.unrepresentable;
        }
      } else if (matchesAllTypes) {
        modifier = theirModifier;
        // Omit the type if either input query did, since that indicates that they
        // aren't targeting a browser that requires "all and".
        type = (other.matchesAllTypes && ourType == null) ? null : theirType;
        conditions = [...this.conditions, ...other.conditions];
      } else if (other.matchesAllTypes) {
        modifier = ourModifier;
        type = ourType;
        conditions = [...this.conditions, ...other.conditions];
      } else if (ourType != theirType) {
        return MediaQueryMergeResult.empty;
      } else {
        modifier = ourModifier ?? theirModifier;
        type = ourType;
        conditions = [...this.conditions, ...other.conditions];
      }
  
      return MediaQuerySuccessfulMergeResult._(
        CssMediaQuery.type(
          type == ourType ? this.type : other.type,
          modifier: modifier == ourModifier ? this.modifier : other.modifier,
          conditions: conditions,
        ),
      );
    }
  
    bool operator ==(Object other) =>
        other is CssMediaQuery &&
        other.modifier == modifier &&
        other.type == type &&
        listEquals(other.conditions, conditions);
  
    int get hashCode => modifier.hashCode ^ type.hashCode ^ listHash(conditions);
  
    String toString() {
      var buffer = StringBuffer();
      if (modifier != null) buffer.write("$modifier ");
      if (type != null) {
        buffer.write(type);
        if (conditions.isNotEmpty) buffer.write(" and ");
      }
      buffer.write(conditions.join(conjunction ? " and " : " or "));
      return buffer.toString();
    }
  }
  
  /// The interface of possible return values of [CssMediaQuery.merge].
  ///
  /// This is either the singleton values [empty] or [unrepresentable], or an
  /// instance of [MediaQuerySuccessfulMergeResult].
  sealed class MediaQueryMergeResult {
    /// A singleton value indicating that there are no contexts that match both
    /// input queries.
    static const empty = _SingletonCssMediaQueryMergeResult.empty;
  
    /// A singleton value indicating that the contexts that match both input
    /// queries can't be represented by a Level 3 media query.
    static const unrepresentable =
        _SingletonCssMediaQueryMergeResult.unrepresentable;
  }
  
  /// The subclass [MediaQueryMergeResult] that represents singleton enum values.
  enum _SingletonCssMediaQueryMergeResult implements MediaQueryMergeResult {
    empty,
    unrepresentable,
  }
  
  /// A successful result of [CssMediaQuery.merge].
  class MediaQuerySuccessfulMergeResult implements MediaQueryMergeResult {
    /// The merged media query.
    final CssMediaQuery query;
  
    MediaQuerySuccessfulMergeResult._(this.query);
  
    String toString() => query.toString();
  }
  