// 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 '../../exception.dart';
import '../../extend/functions.dart';
import '../../interpolation_map.dart';
import '../../parse/selector.dart';
import '../../utils.dart';
import '../../util/iterable.dart';
import '../../util/span.dart';
import '../../value.dart';
import '../../visitor/interface/selector.dart';
import '../../visitor/selector_search.dart';
import '../css/value.dart';
import '../selector.dart';

/// A selector list.
///
/// A selector list is composed of [ComplexSelector]s. It matches any element
/// that matches any of the component selectors.
///
/// {@category AST}
/// {@category Parsing}
final class SelectorList extends Selector {
  /// The components of this selector.
  ///
  /// This is never empty.
  final List<ComplexSelector> components;

  /// Returns a SassScript list that represents this selector.
  ///
  /// This has the same format as a list returned by `selector-parse()`.
  SassList get asSassList {
    return SassList(
      components.map((complex) {
        return SassList([
          for (var combinator in complex.leadingCombinators)
            SassString(combinator.toString(), quotes: false),
          for (var component in complex.components) ...[
            SassString(component.selector.toString(), quotes: false),
            for (var combinator in component.combinators)
              SassString(combinator.toString(), quotes: false),
          ],
        ], ListSeparator.space);
      }),
      ListSeparator.comma,
    );
  }

  SelectorList(Iterable<ComplexSelector> components, super.span)
      : components = List.unmodifiable(components) {
    if (this.components.isEmpty) {
      throw ArgumentError("components may not be empty.");
    }
  }

  /// Parses a selector list from [contents].
  ///
  /// If passed, [url] is the name of the file from which [contents] comes. If
  /// [allowParent] is false, this doesn't allow [ParentSelector]s. If
  /// [plainCss] is true, this parses the selector as plain CSS rather than
  /// unresolved Sass.
  ///
  /// If passed, [interpolationMap] maps the text of [contents] back to the
  /// original location of the selector in the source file.
  ///
  /// Throws a [SassFormatException] if parsing fails.
  factory SelectorList.parse(
    String contents, {
    Object? url,
    InterpolationMap? interpolationMap,
    bool allowParent = true,
    bool plainCss = false,
  }) =>
      SelectorParser(
        contents,
        url: url,
        interpolationMap: interpolationMap,
        allowParent: allowParent,
        plainCss: plainCss,
      ).parse();

  T accept<T>(SelectorVisitor<T> visitor) => visitor.visitSelectorList(this);

  /// Returns a [SelectorList] that matches only elements that are matched by
  /// both this and [other].
  ///
  /// If no such list can be produced, returns `null`.
  SelectorList? unify(SelectorList other) {
    var contents = [
      for (var complex1 in components)
        for (var complex2 in other.components)
          ...?unifyComplex([complex1, complex2], complex1.span),
    ];

    return contents.isEmpty ? null : SelectorList(contents, span);
  }

  /// Returns a new selector list that represents `this` nested within [parent].
  ///
  /// By default, this replaces [ParentSelector]s in `this` with [parent]. If
  /// [preserveParentSelectors] is true, this instead preserves those selectors
  /// as parent selectors.
  ///
  /// If [implicitParent] is true, this prepends [parent] to any
  /// [ComplexSelector]s in this that don't contain explicit [ParentSelector]s,
  /// or to _all_ [ComplexSelector]s if [preserveParentSelectors] is true.
  ///
  /// The given [parent] may be `null`, indicating that this has no parents. If
  /// so, this list is returned as-is if it doesn't contain any explicit
  /// [ParentSelector]s or if [preserveParentSelectors] is true. Otherwise, this
  /// throws a [SassScriptException].
  SelectorList nestWithin(
    SelectorList? parent, {
    bool implicitParent = true,
    bool preserveParentSelectors = false,
  }) {
    if (parent == null) {
      if (preserveParentSelectors) return this;
      var parentSelector = accept(const _ParentSelectorVisitor());
      if (parentSelector == null) return this;
      throw SassException(
        'Top-level selectors may not contain the parent selector "&".',
        parentSelector.span,
      );
    }

    return SelectorList(
      flattenVertically(
        components.map((complex) {
          if (preserveParentSelectors || !_containsParentSelector(complex)) {
            if (!implicitParent) return [complex];
            return parent.components.map(
              (parentComplex) =>
                  parentComplex.concatenate(complex, complex.span),
            );
          }

          var newComplexes = <ComplexSelector>[];
          for (var component in complex.components) {
            var resolved = _nestWithinCompound(component, parent);
            if (resolved == null) {
              if (newComplexes.isEmpty) {
                newComplexes.add(
                  ComplexSelector(
                    complex.leadingCombinators,
                    [component],
                    complex.span,
                    lineBreak: false,
                  ),
                );
              } else {
                for (var i = 0; i < newComplexes.length; i++) {
                  newComplexes[i] = newComplexes[i].withAdditionalComponent(
                    component,
                    complex.span,
                  );
                }
              }
            } else if (newComplexes.isEmpty) {
              newComplexes.addAll(
                complex.leadingCombinators.isEmpty
                    ? resolved
                    : resolved.map(
                        (resolvedComplex) => ComplexSelector(
                          resolvedComplex.leadingCombinators.isEmpty
                              ? complex.leadingCombinators
                              : [
                                  ...complex.leadingCombinators,
                                  ...resolvedComplex.leadingCombinators,
                                ],
                          resolvedComplex.components,
                          complex.span,
                          lineBreak: resolvedComplex.lineBreak,
                        ),
                      ),
              );
            } else {
              var previousComplexes = newComplexes;
              newComplexes = [
                for (var newComplex in previousComplexes)
                  for (var resolvedComplex in resolved)
                    newComplex.concatenate(resolvedComplex, newComplex.span),
              ];
            }
          }

          return newComplexes;
        }),
      ),
      span,
    );
  }

  /// Returns a new selector list based on [component] with all
  /// [ParentSelector]s replaced with [parent].
  ///
  /// Returns `null` if [component] doesn't contain any [ParentSelector]s.
  Iterable<ComplexSelector>? _nestWithinCompound(
    ComplexSelectorComponent component,
    SelectorList parent,
  ) {
    var simples = component.selector.components;
    var containsSelectorPseudo = simples.any((simple) {
      if (simple is! PseudoSelector) return false;
      var selector = simple.selector;
      return selector != null && _containsParentSelector(selector);
    });
    if (!containsSelectorPseudo && simples.first is! ParentSelector) {
      return null;
    }

    var resolvedSimples = containsSelectorPseudo
        ? simples.map(
            (simple) => switch (simple) {
              PseudoSelector(:var selector?)
                  when _containsParentSelector(selector) =>
                simple.withSelector(
                  selector.nestWithin(parent, implicitParent: false),
                ),
              _ => simple,
            },
          )
        : simples;

    var parentSelector = simples.first;
    try {
      if (parentSelector is! ParentSelector) {
        return [
          ComplexSelector(const [], [
            ComplexSelectorComponent(
              CompoundSelector(resolvedSimples, component.selector.span),
              component.combinators,
              component.span,
            ),
          ], component.span),
        ];
      } else if (simples.length == 1 && parentSelector.suffix == null) {
        return parent
            .withAdditionalCombinators(component.combinators)
            .components;
      }
    } on SassException catch (error, stackTrace) {
      throwWithTrace(
        error.withAdditionalSpan(parentSelector.span, "parent selector"),
        error,
        stackTrace,
      );
    }

    return parent.components.map((complex) {
      try {
        var lastComponent = complex.components.last;
        if (lastComponent.combinators.isNotEmpty) {
          throw MultiSpanSassException(
            'Selector "$complex" can\'t be used as a parent in a compound '
                'selector.',
            lastComponent.span.trimRight(),
            "outer selector",
            {parentSelector.span: "parent selector"},
          );
        }

        var suffix = parentSelector.suffix;
        var lastSimples = lastComponent.selector.components;
        var last = CompoundSelector(
          suffix == null
              ? [...lastSimples, ...resolvedSimples.skip(1)]
              : [
                  ...lastSimples.exceptLast,
                  lastSimples.last.addSuffix(suffix),
                  ...resolvedSimples.skip(1),
                ],
          component.selector.span,
        );

        return ComplexSelector(
          complex.leadingCombinators,
          [
            ...complex.components.exceptLast,
            ComplexSelectorComponent(
              last,
              component.combinators,
              component.span,
            ),
          ],
          component.span,
          lineBreak: complex.lineBreak,
        );
      } on SassException catch (error, stackTrace) {
        throwWithTrace(
          error.withAdditionalSpan(parentSelector.span, "parent selector"),
          error,
          stackTrace,
        );
      }
    });
  }

  /// Whether this is a superselector of [other].
  ///
  /// That is, whether this matches every element that [other] matches, as well
  /// as possibly additional elements.
  bool isSuperselector(SelectorList other) =>
      listIsSuperselector(components, other.components);

  /// Returns a copy of `this` with [combinators] added to the end of each
  /// complex selector in [components].
  ///
  /// @nodoc
  @internal
  SelectorList withAdditionalCombinators(
    List<CssValue<Combinator>> combinators,
  ) =>
      combinators.isEmpty
          ? this
          : SelectorList(
              components.map(
                (complex) => complex.withAdditionalCombinators(combinators),
              ),
              span,
            );

  int get hashCode => listHash(components);

  bool operator ==(Object other) =>
      other is SelectorList && listEquals(components, other.components);
}

/// Returns whether [selector] recursively contains a parent selector.
bool _containsParentSelector(Selector selector) =>
    selector.accept(const _ParentSelectorVisitor()) != null;

/// A visitor for finding the first [ParentSelector] in a given selector.
class _ParentSelectorVisitor with SelectorSearchVisitor<ParentSelector> {
  const _ParentSelectorVisitor();

  ParentSelector visitParentSelector(ParentSelector selector) => selector;
}
