// 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 '../ast/css.dart';
import '../ast/node.dart';
import '../ast/sass.dart';
import '../callable.dart';
import '../exception.dart';
import '../extend/extension_store.dart';
import '../module.dart';
import '../util/limited_map_view.dart';
import '../util/prefixed_map_view.dart';
import '../value.dart';

/// A [Module] that exposes members according to a [ForwardRule].
class ForwardedModuleView<T extends AsyncCallable> implements Module<T> {
  /// The wrapped module.
  final Module<T> _inner;

  /// The rule that determines how this module's members should be exposed.
  final ForwardRule _rule;

  Uri? get url => _inner.url;
  List<Module<T>> get upstream => _inner.upstream;
  ExtensionStore get extensionStore => _inner.extensionStore;
  CssStylesheet get css => _inner.css;
  Map<Module<T>, List<CssComment>> get preModuleComments =>
      _inner.preModuleComments;
  bool get transitivelyContainsCss => _inner.transitivelyContainsCss;
  bool get transitivelyContainsExtensions =>
      _inner.transitivelyContainsExtensions;

  final Map<String, Value> variables;
  final Map<String, AstNode> variableNodes;
  final Map<String, T> functions;
  final Map<String, T> mixins;

  /// Like [ForwardedModuleView], but returns `inner` as-is if it doesn't need
  /// any modification.
  static Module<T> ifNecessary<T extends AsyncCallable>(
    Module<T> inner,
    ForwardRule rule,
  ) {
    if (rule.prefix == null &&
        rule.shownMixinsAndFunctions == null &&
        rule.shownVariables == null &&
        (rule.hiddenMixinsAndFunctions?.isEmpty ?? false) &&
        (rule.hiddenVariables?.isEmpty ?? false)) {
      return inner;
    } else {
      return ForwardedModuleView(inner, rule);
    }
  }

  ForwardedModuleView(this._inner, this._rule)
      : variables = _forwardedMap(
          _inner.variables,
          _rule.prefix,
          _rule.shownVariables,
          _rule.hiddenVariables,
        ),
        variableNodes = _forwardedMap(
          _inner.variableNodes,
          _rule.prefix,
          _rule.shownVariables,
          _rule.hiddenVariables,
        ),
        functions = _forwardedMap(
          _inner.functions,
          _rule.prefix,
          _rule.shownMixinsAndFunctions,
          _rule.hiddenMixinsAndFunctions,
        ),
        mixins = _forwardedMap(
          _inner.mixins,
          _rule.prefix,
          _rule.shownMixinsAndFunctions,
          _rule.hiddenMixinsAndFunctions,
        );

  /// Wraps [map] so that it only shows members allowed by [blocklist] or
  /// [safelist], with the given [prefix], if given.
  ///
  /// Only one of [blocklist] or [safelist] may be non-`null`.
  static Map<String, V> _forwardedMap<V>(
    Map<String, V> map,
    String? prefix,
    Set<String>? safelist,
    Set<String>? blocklist,
  ) {
    assert(safelist == null || blocklist == null);
    if (prefix == null &&
        safelist == null &&
        (blocklist == null || blocklist.isEmpty)) {
      return map;
    }

    if (prefix != null) {
      map = PrefixedMapView(map, prefix);
    }

    if (safelist != null) {
      map = LimitedMapView.safelist(map, safelist);
    } else if (blocklist != null && blocklist.isNotEmpty) {
      map = LimitedMapView.blocklist(map, blocklist);
    }

    return map;
  }

  void setVariable(String name, Value value, AstNode nodeWithSpan) {
    if (_rule.shownVariables case var shownVariables?
        when !shownVariables.contains(name)) {
      throw SassScriptException("Undefined variable.");
    } else if (_rule.hiddenVariables case var hiddenVariables?
        when hiddenVariables.contains(name)) {
      throw SassScriptException("Undefined variable.");
    }

    if (_rule.prefix case var prefix?) {
      if (!name.startsWith(prefix)) {
        throw SassScriptException("Undefined variable.");
      }

      name = name.substring(prefix.length);
    }

    return _inner.setVariable(name, value, nodeWithSpan);
  }

  Object variableIdentity(String name) {
    assert(variables.containsKey(name));

    if (_rule.prefix case var prefix?) {
      assert(name.startsWith(prefix));
      name = name.substring(prefix.length);
    }

    return _inner.variableIdentity(name);
  }

  bool couldHaveBeenConfigured(Set<String> variables) {
    assert(_rule.shownVariables == null || _rule.hiddenVariables == null);
    if (_rule.prefix == null &&
        _rule.shownVariables == null &&
        (_rule.hiddenVariables?.isEmpty ?? true)) {
      return _inner.couldHaveBeenConfigured(variables);
    }

    if (_rule.prefix case var prefix?) {
      variables = {
        for (var name in variables)
          if (name.startsWith(prefix)) name.substring(prefix.length)
      };
    }

    if (_rule.shownVariables case var safelist?) {
      return _inner.couldHaveBeenConfigured(variables.intersection(safelist));
    } else if (_rule.hiddenVariables case var blocklist?
        when blocklist.isNotEmpty) {
      return _inner.couldHaveBeenConfigured(variables.difference(blocklist));
    } else {
      return _inner.couldHaveBeenConfigured(variables);
    }
  }

  bool operator ==(Object other) =>
      other is ForwardedModuleView &&
      _inner == other._inner &&
      _rule == other._rule;

  int get hashCode => _inner.hashCode ^ _rule.hashCode;

  Module<T> cloneCss() => ForwardedModuleView(_inner.cloneCss(), _rule);

  String toString() => "forwarded $_inner";
}
