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 'package:meta/meta.dart';
  import 'package:source_span/source_span.dart';
  
  import '../../extend/functions.dart';
  import '../../parse/selector.dart';
  import '../../utils.dart';
  import '../../visitor/interface/selector.dart';
  import '../css/value.dart';
  import '../selector.dart';
  
  /// A complex selector.
  ///
  /// A complex selector is composed of [CompoundSelector]s separated by
  /// [Combinator]s. It selects elements based on their parent selectors.
  ///
  /// {@category AST}
  /// {@category Parsing}
  final class ComplexSelector extends Selector {
    /// This selector's leading combinators.
    ///
    /// If this is empty, that indicates that it has no leading combinator. If
    /// it's more than one element, that means it's invalid CSS; however, we still
    /// support this for backwards-compatibility purposes.
    final List<CssValue<Combinator>> leadingCombinators;
  
    /// The components of this selector.
    ///
    /// This is only empty if [leadingCombinators] is not empty.
    ///
    /// Descendant combinators aren't explicitly represented here. If two
    /// [CompoundSelector]s are adjacent to one another, there's an implicit
    /// descendant combinator between them.
    ///
    /// It's possible for multiple [Combinator]s to be adjacent to one another.
    /// This isn't valid CSS, but Sass supports it for CSS hack purposes.
    final List<ComplexSelectorComponent> components;
  
    /// Whether a line break should be emitted *before* this selector.
    ///
    /// @nodoc
    @internal
    final bool lineBreak;
  
    /// This selector's specificity.
    ///
    /// Specificity is represented in base 1000. The spec says this should be
    /// "sufficiently high"; it's extremely unlikely that any single selector
    /// sequence will contain 1000 simple selectors.
    late final int specificity = components.fold(
      0,
      (sum, component) => sum + component.selector.specificity,
    );
  
    /// If this compound selector is composed of a single compound selector with
    /// no combinators, returns it.
    ///
    /// Otherwise, returns null.
    ///
    /// @nodoc
    @internal
    CompoundSelector? get singleCompound {
      if (leadingCombinators.isNotEmpty) return null;
      return switch (components) {
        [ComplexSelectorComponent(:var selector, combinators: [])] => selector,
        _ => null,
      };
    }
  
    ComplexSelector(
      Iterable<CssValue<Combinator>> leadingCombinators,
      Iterable<ComplexSelectorComponent> components,
      super.span, {
      this.lineBreak = false,
    })  : leadingCombinators = List.unmodifiable(leadingCombinators),
          components = List.unmodifiable(components) {
      if (this.leadingCombinators.isEmpty && this.components.isEmpty) {
        throw ArgumentError(
          "leadingCombinators and components may not both be empty.",
        );
      }
    }
  
    /// Parses a complex selector from [contents].
    ///
    /// If passed, [url] is the name of the file from which [contents] comes.
    /// [allowParent] controls whether a [ParentSelector] is allowed in this
    /// selector.
    ///
    /// Throws a [SassFormatException] if parsing fails.
    factory ComplexSelector.parse(
      String contents, {
      Object? url,
      bool allowParent = true,
    }) =>
        SelectorParser(
          contents,
          url: url,
          allowParent: allowParent,
        ).parseComplexSelector();
  
    T accept<T>(SelectorVisitor<T> visitor) => visitor.visitComplexSelector(this);
  
    /// Whether this is a superselector of [other].
    ///
    /// That is, whether this matches every element that [other] matches, as well
    /// as possibly matching more.
    bool isSuperselector(ComplexSelector other) =>
        leadingCombinators.isEmpty &&
        other.leadingCombinators.isEmpty &&
        complexIsSuperselector(components, other.components);
  
    /// Returns a copy of `this` with [combinators] added to the end of the final
    /// component in [components].
    ///
    /// If [forceLineBreak] is `true`, this will mark the new complex selector as
    /// having a line break.
    ///
    /// @nodoc
    @internal
    ComplexSelector withAdditionalCombinators(
      List<CssValue<Combinator>> combinators, {
      bool forceLineBreak = false,
    }) {
      if (combinators.isEmpty) return this;
      return switch (components) {
        [...var initial, var last] => ComplexSelector(
            leadingCombinators,
            [...initial, last.withAdditionalCombinators(combinators)],
            span,
            lineBreak: lineBreak || forceLineBreak,
          ),
        [] => ComplexSelector(
            [...leadingCombinators, ...combinators],
            const [],
            span,
            lineBreak: lineBreak || forceLineBreak,
          ),
      };
    }
  
    /// Returns a copy of `this` with an additional [component] added to the end.
    ///
    /// If [forceLineBreak] is `true`, this will mark the new complex selector as
    /// having a line break.
    ///
    /// The [span] is used for the new selector.
    ///
    /// @nodoc
    @internal
    ComplexSelector withAdditionalComponent(
      ComplexSelectorComponent component,
      FileSpan span, {
      bool forceLineBreak = false,
    }) =>
        ComplexSelector(
          leadingCombinators,
          [...components, component],
          span,
          lineBreak: lineBreak || forceLineBreak,
        );
  
    /// Returns a copy of `this` with [child]'s combinators added to the end.
    ///
    /// If [child] has [leadingCombinators], they're appended to `this`'s last
    /// combinator. This does _not_ resolve parent selectors.
    ///
    /// The [span] is used for the new selector.
    ///
    /// If [forceLineBreak] is `true`, this will mark the new complex selector as
    /// having a line break.
    ///
    /// @nodoc
    @internal
    ComplexSelector concatenate(
      ComplexSelector child,
      FileSpan span, {
      bool forceLineBreak = false,
    }) {
      if (child.leadingCombinators.isEmpty) {
        return ComplexSelector(
          leadingCombinators,
          [...components, ...child.components],
          span,
          lineBreak: lineBreak || child.lineBreak || forceLineBreak,
        );
      } else if (components case [...var initial, var last]) {
        return ComplexSelector(
          leadingCombinators,
          [
            ...initial,
            last.withAdditionalCombinators(child.leadingCombinators),
            ...child.components,
          ],
          span,
          lineBreak: lineBreak || child.lineBreak || forceLineBreak,
        );
      } else {
        return ComplexSelector(
          [...leadingCombinators, ...child.leadingCombinators],
          child.components,
          span,
          lineBreak: lineBreak || child.lineBreak || forceLineBreak,
        );
      }
    }
  
    int get hashCode => listHash(leadingCombinators) ^ listHash(components);
  
    bool operator ==(Object other) =>
        other is ComplexSelector &&
        listEquals(leadingCombinators, other.leadingCombinators) &&
        listEquals(components, other.components);
  }
  