// 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 '../deprecation.dart';
import '../evaluation_context.dart';
import '../exception.dart';
import '../visitor/any_selector.dart';
import '../visitor/interface/selector.dart';
import '../visitor/serialize.dart';
import 'node.dart';
import 'selector/complex.dart';
import 'selector/list.dart';
import 'selector/parent.dart';
import 'selector/placeholder.dart';
import 'selector/pseudo.dart';

export 'selector/attribute.dart';
export 'selector/class.dart';
export 'selector/combinator.dart';
export 'selector/complex.dart';
export 'selector/complex_component.dart';
export 'selector/compound.dart';
export 'selector/id.dart';
export 'selector/list.dart';
export 'selector/parent.dart';
export 'selector/placeholder.dart';
export 'selector/pseudo.dart';
export 'selector/qualified_name.dart';
export 'selector/simple.dart';
export 'selector/type.dart';
export 'selector/universal.dart';

/// A node in the abstract syntax tree for a selector.
///
/// This selector tree is mostly plain CSS, but also may contain a
/// [ParentSelector] or a [PlaceholderSelector].
///
/// Selectors have structural equality semantics.
///
/// {@category AST}
abstract base class Selector implements AstNode {
  /// Whether this selector, and complex selectors containing it, should not be
  /// emitted.
  ///
  /// @nodoc
  @internal
  bool get isInvisible => accept(const _IsInvisibleVisitor(includeBogus: true));

  /// Whether this selector contains a [ParentSelector].
  ///
  /// @nodoc
  @internal
  bool get containsParentSelector =>
      accept(const _ContainsParentSelectorVisitor());

  // Whether this selector would be invisible even if it didn't have bogus
  // combinators.
  ///
  /// @nodoc
  @internal
  bool get isInvisibleOtherThanBogusCombinators =>
      accept(const _IsInvisibleVisitor(includeBogus: false));

  /// Whether this selector is not valid CSS.
  ///
  /// This includes both selectors that are useful exclusively for build-time
  /// nesting (`> .foo)` and selectors with invalid combiantors that are still
  /// supported for backwards-compatibility reasons (`.foo + ~ .bar`).
  bool get isBogus =>
      accept(const _IsBogusVisitor(includeLeadingCombinator: true));

  /// Whether this selector is bogus other than having a leading combinator.
  ///
  /// @nodoc
  @internal
  bool get isBogusOtherThanLeadingCombinator =>
      accept(const _IsBogusVisitor(includeLeadingCombinator: false));

  /// Whether this is a useless selector (that is, it's bogus _and_ it can't be
  /// transformed into valid CSS by `@extend` or nesting).
  ///
  /// @nodoc
  @internal
  bool get isUseless => accept(const _IsUselessVisitor());

  final FileSpan span;

  Selector(this.span);

  /// Prints a warning if `this` is a bogus selector.
  ///
  /// This may only be called from within a custom Sass function. This will
  /// throw a [SassException] in Dart Sass 2.0.0.
  void assertNotBogus({String? name}) {
    if (!isBogus) return;
    warnForDeprecation(
      (name == null ? '' : '\$$name: ') +
          '$this is not valid CSS.\n'
              'This will be an error in Dart Sass 2.0.0.\n'
              '\n'
              'More info: https://sass-lang.com/d/bogus-combinators',
      Deprecation.bogusCombinators,
    );
  }

  /// Calls the appropriate visit method on [visitor].
  T accept<T>(SelectorVisitor<T> visitor);

  String toString() => serializeSelector(this, inspect: true);
}

/// The visitor used to implement [Selector.isInvisible].
class _IsInvisibleVisitor with AnySelectorVisitor {
  /// Whether to consider selectors with bogus combinators invisible.
  final bool includeBogus;

  const _IsInvisibleVisitor({required this.includeBogus});

  bool visitSelectorList(SelectorList list) =>
      list.components.every(visitComplexSelector);

  bool visitComplexSelector(ComplexSelector complex) =>
      super.visitComplexSelector(complex) ||
      (includeBogus && complex.isBogusOtherThanLeadingCombinator);

  bool visitPlaceholderSelector(PlaceholderSelector placeholder) => true;

  bool visitPseudoSelector(PseudoSelector pseudo) {
    if (pseudo.selector case var selector?) {
      // We don't consider `:not(%foo)` to be invisible because, semantically,
      // it means "doesn't match this selector that matches nothing", so it's
      // equivalent to *. If the entire compound selector is composed of `:not`s
      // with invisible lists, the serializer emits it as `*`.
      return pseudo.name == 'not'
          ? (includeBogus && selector.isBogus)
          : selector.accept(this);
    } else {
      return false;
    }
  }
}

/// The visitor used to implement [Selector.isBogus].
class _IsBogusVisitor with AnySelectorVisitor {
  /// Whether to consider selectors with leading combinators as bogus.
  final bool includeLeadingCombinator;

  const _IsBogusVisitor({required this.includeLeadingCombinator});

  bool visitComplexSelector(ComplexSelector complex) {
    if (complex.components.isEmpty) {
      return complex.leadingCombinators.isNotEmpty;
    } else {
      return complex.leadingCombinators.length >
              (includeLeadingCombinator ? 0 : 1) ||
          complex.components.last.combinators.isNotEmpty ||
          complex.components.any(
            (component) =>
                component.combinators.length > 1 ||
                component.selector.accept(this),
          );
    }
  }

  bool visitPseudoSelector(PseudoSelector pseudo) {
    var selector = pseudo.selector;
    if (selector == null) return false;

    // The CSS spec specifically allows leading combinators in `:has()`.
    return pseudo.name == 'has'
        ? selector.isBogusOtherThanLeadingCombinator
        : selector.isBogus;
  }
}

/// The visitor used to implement [Selector.isUseless].
class _IsUselessVisitor with AnySelectorVisitor {
  const _IsUselessVisitor();

  bool visitComplexSelector(ComplexSelector complex) =>
      complex.leadingCombinators.length > 1 ||
      complex.components.any(
        (component) =>
            component.combinators.length > 1 || component.selector.accept(this),
      );

  bool visitPseudoSelector(PseudoSelector pseudo) => pseudo.isBogus;
}

/// The visitor used to implement [Selector.containsParentSelector].
class _ContainsParentSelectorVisitor with AnySelectorVisitor {
  const _ContainsParentSelectorVisitor();

  bool visitParentSelector(ParentSelector _) => true;
}
