// 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 '../callable.dart';
import '../exception.dart';
import '../extend/extension_store.dart';
import '../module.dart';
import '../util/limited_map_view.dart';
import '../utils.dart';
import '../value.dart';

/// A [Module] that only exposes members that aren't shadowed by a given
/// blocklist of member names.
final class ShadowedModuleView<T extends AsyncCallable> implements Module<T> {
  /// The wrapped module.
  final Module<T> _inner;

  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;

  /// Returns whether this module exposes no members or CSS.
  bool get isEmpty =>
      variables.isEmpty &&
      functions.isEmpty &&
      mixins.isEmpty &&
      css.children.isEmpty;

  /// Like [ShadowedModuleView], but returns `null` if [inner] would be unchanged.
  static ShadowedModuleView<T>? ifNecessary<T extends AsyncCallable>(
    Module<T> inner, {
    Set<String>? variables,
    Set<String>? functions,
    Set<String>? mixins,
  }) =>
      _needsBlocklist(inner.variables, variables) ||
              _needsBlocklist(inner.functions, functions) ||
              _needsBlocklist(inner.mixins, mixins)
          ? ShadowedModuleView(
              inner,
              variables: variables,
              functions: functions,
              mixins: mixins,
            )
          : null;

  /// Returns a view of [inner] that doesn't include the given [variables],
  /// [functions], or [mixins].
  ShadowedModuleView(
    this._inner, {
    Set<String>? variables,
    Set<String>? functions,
    Set<String>? mixins,
  })  : variables = _shadowedMap(_inner.variables, variables),
        variableNodes = _shadowedMap(_inner.variableNodes, variables),
        functions = _shadowedMap(_inner.functions, functions),
        mixins = _shadowedMap(_inner.mixins, mixins);

  ShadowedModuleView._(
    this._inner,
    this.variables,
    this.variableNodes,
    this.functions,
    this.mixins,
  );

  /// Returns a view of [map] with all keys in [blocklist] omitted.
  static Map<String, V> _shadowedMap<V>(
    Map<String, V> map,
    Set<String>? blocklist,
  ) =>
      blocklist == null || !_needsBlocklist(map, blocklist)
          ? map
          : LimitedMapView.blocklist(map, blocklist);

  /// Returns whether any of [map]'s keys are in [blocklist].
  static bool _needsBlocklist(
    Map<String, Object?> map,
    Set<String>? blocklist,
  ) =>
      blocklist != null && map.isNotEmpty && blocklist.any(map.containsKey);

  void setVariable(String name, Value value, AstNode nodeWithSpan) {
    if (!variables.containsKey(name)) {
      throw SassScriptException("Undefined variable.");
    } else {
      _inner.setVariable(name, value, nodeWithSpan);
    }
  }

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

  bool couldHaveBeenConfigured(Set<String> variables) =>
      this.variables == _inner.variables
          ? _inner.couldHaveBeenConfigured(variables)
          : _inner.couldHaveBeenConfigured({
              for (var name in this.variables.keys)
                if (variables.contains(name)) name
            });

  bool operator ==(Object other) =>
      other is ShadowedModuleView &&
      _inner == other._inner &&
      iterableEquals(variables.keys, other.variables.keys) &&
      iterableEquals(functions.keys, other.functions.keys) &&
      iterableEquals(mixins.keys, other.mixins.keys);

  int get hashCode => _inner.hashCode;

  Module<T> cloneCss() => ShadowedModuleView._(
        _inner.cloneCss(),
        variables,
        variableNodes,
        functions,
        mixins,
      );

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