// Copyright 2019 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 'dart:collection';

import 'package:collection/collection.dart';

import '../ast/selector.dart';
import '../callable.dart';
import '../evaluation_context.dart';
import '../exception.dart';
import '../extend/extension_store.dart';
import '../module/built_in.dart';
import '../value.dart';

/// The global definitions of Sass selector functions.
final global = UnmodifiableListView([
  _isSuperselector.withDeprecationWarning('selector'),
  _simpleSelectors.withDeprecationWarning('selector'),
  _parse.withDeprecationWarning('selector').withName("selector-parse"),
  _nest.withDeprecationWarning('selector').withName("selector-nest"),
  _append.withDeprecationWarning('selector').withName("selector-append"),
  _extend.withDeprecationWarning('selector').withName("selector-extend"),
  _replace.withDeprecationWarning('selector').withName("selector-replace"),
  _unify.withDeprecationWarning('selector').withName("selector-unify"),
]);

/// The Sass selector module.
final module = BuiltInModule(
  "selector",
  functions: <Callable>[
    _isSuperselector,
    _simpleSelectors,
    _parse,
    _nest,
    _append,
    _extend,
    _replace,
    _unify,
  ],
);

final _nest = _function("nest", r"$selectors...", (arguments) {
  var selectors = arguments[0].asList;
  if (selectors.isEmpty) {
    throw SassScriptException(
      "\$selectors: At least one selector must be passed.",
    );
  }

  var first = true;
  return selectors
      .map((selector) {
        var result = selector.assertSelector(allowParent: !first);
        first = false;
        return result;
      })
      .reduce((parent, child) => child.nestWithin(parent))
      .asSassList;
});

final _append = _function("append", r"$selectors...", (arguments) {
  var selectors = arguments[0].asList;
  if (selectors.isEmpty) {
    throw SassScriptException(
      "\$selectors: At least one selector must be passed.",
    );
  }

  var span = EvaluationContext.current.currentCallableSpan;
  return selectors.map((selector) => selector.assertSelector()).reduce((
    parent,
    child,
  ) {
    return SelectorList(
      child.components.map((complex) {
        if (complex.leadingCombinators.isNotEmpty) {
          throw SassScriptException("Can't append $complex to $parent.");
        }

        var [component, ...rest] = complex.components;
        var newCompound = _prependParent(component.selector);
        if (newCompound == null) {
          throw SassScriptException("Can't append $complex to $parent.");
        }

        return ComplexSelector(const [], [
          ComplexSelectorComponent(newCompound, component.combinators, span),
          ...rest,
        ], span);
      }),
      span,
    ).nestWithin(parent);
  }).asSassList;
});

final _extend = _function("extend", r"$selector, $extendee, $extender", (
  arguments,
) {
  var selector = arguments[0].assertSelector(name: "selector")
    ..assertNotBogus(name: "selector");
  var target = arguments[1].assertSelector(name: "extendee")
    ..assertNotBogus(name: "extendee");
  var source = arguments[2].assertSelector(name: "extender")
    ..assertNotBogus(name: "extender");

  return ExtensionStore.extend(
    selector,
    source,
    target,
    EvaluationContext.current.currentCallableSpan,
  ).asSassList;
});

final _replace = _function("replace", r"$selector, $original, $replacement", (
  arguments,
) {
  var selector = arguments[0].assertSelector(name: "selector")
    ..assertNotBogus(name: "selector");
  var target = arguments[1].assertSelector(name: "original")
    ..assertNotBogus(name: "original");
  var source = arguments[2].assertSelector(name: "replacement")
    ..assertNotBogus(name: "replacement");

  return ExtensionStore.replace(
    selector,
    source,
    target,
    EvaluationContext.current.currentCallableSpan,
  ).asSassList;
});

final _unify = _function("unify", r"$selector1, $selector2", (arguments) {
  var selector1 = arguments[0].assertSelector(name: "selector1")
    ..assertNotBogus(name: "selector1");
  var selector2 = arguments[1].assertSelector(name: "selector2")
    ..assertNotBogus(name: "selector2");

  return selector1.unify(selector2)?.asSassList ?? sassNull;
});

final _isSuperselector = _function("is-superselector", r"$super, $sub", (
  arguments,
) {
  var selector1 = arguments[0].assertSelector(name: "super")
    ..assertNotBogus(name: "super");
  var selector2 = arguments[1].assertSelector(name: "sub")
    ..assertNotBogus(name: "sub");

  return SassBoolean(selector1.isSuperselector(selector2));
});

final _simpleSelectors = _function("simple-selectors", r"$selector", (
  arguments,
) {
  var selector = arguments[0].assertCompoundSelector(name: "selector");

  return SassList(
    selector.components.map(
      (simple) => SassString(simple.toString(), quotes: false),
    ),
    ListSeparator.comma,
  );
});

final _parse = _function(
  "parse",
  r"$selector",
  (arguments) => arguments[0].assertSelector(name: "selector").asSassList,
);

/// Adds a [ParentSelector] to the beginning of [compound], or returns `null` if
/// that wouldn't produce a valid selector.
CompoundSelector? _prependParent(CompoundSelector compound) {
  var span = EvaluationContext.current.currentCallableSpan;
  return switch (compound.components) {
    [UniversalSelector(), ...] => null,
    [TypeSelector type, ...] when type.name.namespace != null => null,
    [TypeSelector type, ...var rest] => CompoundSelector([
        ParentSelector(span, suffix: type.name.name),
        ...rest,
      ], span),
    var components => CompoundSelector([
        ParentSelector(span),
        ...components,
      ], span),
  };
}

/// Like [BuiltInCallable.function], but always sets the URL to
/// `sass:selector`.
BuiltInCallable _function(
  String name,
  String arguments,
  Value callback(List<Value> arguments),
) =>
    BuiltInCallable.function(name, arguments, callback, url: "sass:selector");
