// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'package:_fe_analyzer_shared/src/base/analyzer_public_api.dart';
import 'package:_fe_analyzer_shared/src/scanner/string_canonicalizer.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/element2.dart';
import 'package:analyzer/src/dart/ast/ast.dart';
import 'package:analyzer/src/dart/element/element.dart';
import 'package:analyzer/src/generated/engine.dart';
import 'package:analyzer/src/utilities/extensions/collection.dart';
import 'package:analyzer/src/utilities/extensions/element.dart';

/// The scope defined by a block.
class BlockScope {
  /// Return the elements that are declared directly in the given [statements].
  /// This does not include elements declared in nested blocks.
  static Iterable<Element2> elementsInStatements(
    List<Statement> statements,
  ) sync* {
    int statementCount = statements.length;
    for (int i = 0; i < statementCount; i++) {
      Statement statement = statements[i];
      if (statement is LabeledStatement) {
        statement = statement.statement;
      }
      if (statement is PatternVariableDeclarationStatementImpl) {
        for (var variable in statement.declaration.elements) {
          yield variable;
        }
      } else if (statement is VariableDeclarationStatement) {
        NodeList<VariableDeclaration> variables = statement.variables.variables;
        int variableCount = variables.length;
        for (int j = 0; j < variableCount; j++) {
          yield variables[j].declaredFragment!.element;
        }
      } else if (statement is FunctionDeclarationStatement) {
        yield statement.functionDeclaration.declaredFragment!.element;
      }
    }
  }
}

/// The scope statements that can be the target of unlabeled `break` and
/// `continue` statements.
class ImplicitLabelScope {
  /// The implicit label scope associated with the top level of a function.
  static const ImplicitLabelScope ROOT = ImplicitLabelScope._(null, null);

  /// The implicit label scope enclosing this implicit label scope.
  final ImplicitLabelScope? outerScope;

  /// The statement that acts as a target for break and/or continue statements
  /// at this scoping level.
  final Statement? statement;

  /// Initialize a newly created scope, enclosed within the [outerScope],
  /// representing the given [statement].
  const ImplicitLabelScope._(this.outerScope, this.statement);

  /// Return the statement which should be the target of an unlabeled `break` or
  /// `continue` statement, or `null` if there is no appropriate target.
  Statement? getTarget(bool isContinue) {
    if (outerScope == null) {
      // This scope represents the top-level of a function body, so it doesn't
      // match either break or continue.
      return null;
    }
    if (isContinue && statement is SwitchStatement) {
      return outerScope!.getTarget(isContinue);
    }
    return statement;
  }

  /// Initialize a newly created scope to represent a switch statement or loop
  /// nested within the current scope.  [statement] is the statement associated
  /// with the newly created scope.
  ImplicitLabelScope nest(Statement statement) =>
      ImplicitLabelScope._(this, statement);
}

/// A scope in which a single label is defined.
class LabelScope {
  /// The label scope enclosing this label scope.
  final LabelScope? _outerScope;

  /// The label defined in this scope.
  final String _label;

  /// The element to which the label resolves.
  final LabelElement2 element;

  /// The AST node to which the label resolves.
  final AstNode node;

  /// Initialize a newly created scope, enclosed within the [_outerScope],
  /// representing the label [_label]. The [node] is the AST node the label
  /// resolves to. The [element] is the element the label resolves to.
  LabelScope(this._outerScope, this._label, this.node, this.element);

  /// Return the LabelScope which defines [targetLabel], or `null` if it is not
  /// defined in this scope.
  LabelScope? lookup(String targetLabel) {
    if (_label == targetLabel) {
      return this;
    }
    return _outerScope?.lookup(targetLabel);
  }
}

/// A mapping of identifiers to the elements represented by those identifiers.
/// Namespaces are the building blocks for scopes.
@AnalyzerPublicApi(message: 'exposed from elements')
class Namespace {
  /// An empty namespace.
  static Namespace EMPTY = Namespace({});

  /// A table mapping names that are defined in this namespace to the element
  /// representing the thing declared with that name.
  final Map<String, Element2> _definedNames;

  /// Initialize a newly created namespace to have the [_definedNames].
  Namespace(this._definedNames);

  /// Return a table containing the same mappings as those defined by this
  /// namespace.
  @Deprecated('Use definedNames2 instead')
  Map<String, Element> get definedNames =>
      _definedNames.mapValue((e) => e.asElement!);

  /// Return a table containing the same mappings as those defined by this
  /// namespace.
  Map<String, Element2> get definedNames2 => _definedNames;

  /// Return the element in this namespace that is available to the containing
  /// scope using the given name, or `null` if there is no such element.
  @Deprecated('Use get2() instead')
  Element? get(String name) => _definedNames[name]?.asElement;

  /// Return the element in this namespace that is available to the containing
  /// scope using the given name, or `null` if there is no such element.
  Element2? get2(String name) => _definedNames[name];

  /// Return the element in this namespace whose name is the result of combining
  /// the [prefix] and the [name], separated by a period, or `null` if there is
  /// no such element.
  @Deprecated('Use getPrefixed2() instead')
  Element? getPrefixed(String prefix, String name) => null;

  /// Return the element in this namespace whose name is the result of combining
  /// the [prefix] and the [name], separated by a period, or `null` if there is
  /// no such element.
  Element2? getPrefixed2(String prefix, String name) => null;
}

/// The builder used to build a namespace. Namespace builders are thread-safe
/// and re-usable.
class NamespaceBuilder {
  /// Create a namespace representing the export namespace of the given
  /// [export].
  Namespace createExportNamespaceForDirective2(
      LibraryExportElementImpl export) {
    var exportedLibrary = export.exportedLibrary2;
    if (exportedLibrary == null) {
      //
      // The exported library will be null if the URI does not reference a valid
      // library.
      //
      return Namespace.EMPTY;
    }
    var exportedNames = _getExportMapping(exportedLibrary);
    exportedNames = _applyCombinators(exportedNames, export.combinators);
    return Namespace(exportedNames);
  }

  /// Create a namespace representing the export namespace of the given
  /// [library].
  Namespace createExportNamespaceForLibrary(LibraryElementImpl library) {
    var exportedNames = _getExportMapping(library);
    return Namespace(exportedNames);
  }

  /// Create a namespace representing the import namespace of the given
  /// [importedLibrary].
  Namespace createImportNamespaceForDirective({
    required LibraryElementImpl importedLibrary,
    required List<NamespaceCombinator> combinators,
    required PrefixFragment? prefix,
  }) {
    var exportedNames = _getExportMapping(importedLibrary);
    exportedNames = _applyCombinators(exportedNames, combinators);
    if (prefix?.name2 case var name?) {
      return PrefixedNamespace(name, exportedNames);
    }
    return Namespace(exportedNames);
  }

  /// Create a namespace representing the public namespace of the given
  /// [library].
  Namespace createPublicNamespaceForLibrary(LibraryElementImpl library) {
    var definedNames = <String, Element2>{};
    for (var element in library.classes) {
      _addIfPublic(definedNames, element);
    }
    for (var element in library.enums) {
      _addIfPublic(definedNames, element);
    }
    for (var element in library.extensions) {
      _addIfPublic(definedNames, element);
    }
    for (var element in library.extensionTypes) {
      _addIfPublic(definedNames, element);
    }
    for (var element in library.getters) {
      _addIfPublic(definedNames, element);
    }
    for (var element in library.mixins) {
      _addIfPublic(definedNames, element);
    }
    for (var element in library.setters) {
      _addIfPublic(definedNames, element);
    }
    for (var element in library.topLevelFunctions) {
      _addIfPublic(definedNames, element);
    }
    for (var element in library.typeAliases) {
      _addIfPublic(definedNames, element);
    }

    // For libraries that import `dart:core` with a prefix, we have to add
    // `dynamic` to the `dart:core` [Namespace] specially. Note that this is not
    // true of, for instance, `Object`, because `Object` has a source definition
    // which is not possible for `dynamic`.
    if (library.isDartCore) {
      definedNames['dynamic'] = DynamicElementImpl2.instance;
      definedNames['Never'] = NeverElementImpl2.instance;
    }

    return Namespace(definedNames);
  }

  /// Add the given [element] to the table of [definedNames] if it has a
  /// publicly visible name.
  void _addIfPublic(Map<String, Element2> definedNames, Element2 element) {
    var name = element.name3;
    if (name != null && name.isNotEmpty && !Identifier.isPrivateName(name)) {
      definedNames[name] = element;
    }
  }

  /// Apply the given [combinators] to all of the names in the given table of
  /// [definedNames].
  Map<String, Element2> _applyCombinators(Map<String, Element2> definedNames,
      List<NamespaceCombinator> combinators) {
    for (NamespaceCombinator combinator in combinators) {
      if (combinator is HideElementCombinator) {
        definedNames = _hide(definedNames, combinator.hiddenNames);
      } else if (combinator is ShowElementCombinator) {
        definedNames = _show(definedNames, combinator.shownNames);
      } else {
        // Internal error.
        AnalysisEngine.instance.instrumentationService
            .logError("Unknown type of combinator: ${combinator.runtimeType}");
      }
    }
    return definedNames;
  }

  Map<String, Element2> _getExportMapping(LibraryElementImpl library) {
    return library.exportNamespace.definedNames2;
  }

  /// Return a new map of names which has all the names from [definedNames]
  /// with exception of [hiddenNames].
  Map<String, Element2> _hide(
      Map<String, Element2> definedNames, List<String> hiddenNames) {
    var newNames = {...definedNames};
    for (String name in hiddenNames) {
      newNames.remove(name);
      newNames.remove("$name=");
    }
    return newNames;
  }

  /// Return a new map of names which has only [shownNames] from [definedNames].
  Map<String, Element2> _show(
      Map<String, Element2> definedNames, List<String> shownNames) {
    var newNames = <String, Element2>{};
    for (String name in shownNames) {
      var element = definedNames[name];
      if (element != null) {
        newNames[name] = element;
      }
      String setterName = considerCanonicalizeString("$name=");
      element = definedNames[setterName];
      if (element != null) {
        newNames[setterName] = element;
      }
    }
    return newNames;
  }
}

/// A mapping of identifiers to the elements represented by those identifiers.
/// Namespaces are the building blocks for scopes.
class PrefixedNamespace implements Namespace {
  /// The prefix that is prepended to each of the defined names.
  final String _prefix;

  /// The length of the prefix.
  final int _length;

  /// A table mapping names that are defined in this namespace to the element
  /// representing the thing declared with that name.
  @override
  final Map<String, Element2> _definedNames;

  /// Initialize a newly created namespace to have the names resulting from
  /// prefixing each of the [_definedNames] with the given [_prefix] (and a
  /// period).
  PrefixedNamespace(String prefix, this._definedNames)
      : _prefix = prefix,
        _length = prefix.length;

  @Deprecated('Use definedNames2 instead')
  @override
  Map<String, Element> get definedNames {
    var definedNames = <String, Element>{};
    _definedNames.forEach((name, element) {
      definedNames["$_prefix.$name"] = element.asElement!;
    });
    return definedNames;
  }

  @override
  Map<String, Element2> get definedNames2 {
    return _definedNames;
  }

  @Deprecated('Use get2() instead')
  @override
  Element? get(String name) {
    return get2(name)?.asElement;
  }

  @override
  Element2? get2(String name) {
    if (name.length > _length && name.startsWith(_prefix)) {
      if (name.codeUnitAt(_length) == '.'.codeUnitAt(0)) {
        return _definedNames[name.substring(_length + 1)];
      }
    }
    return null;
  }

  @Deprecated('Use getPrefixed2() instead')
  @override
  Element? getPrefixed(String prefix, String name) {
    return getPrefixed2(prefix, name)?.asElement;
  }

  @override
  Element2? getPrefixed2(String prefix, String name) {
    if (prefix == _prefix) {
      return _definedNames[name];
    }
    return null;
  }
}
