// Copyright (c) 2018, 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:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/element/element2.dart';
import 'package:analyzer/src/dart/element/element.dart';
import 'package:analyzer/src/test_utilities/function_ast_visitor.dart';
import 'package:analyzer/src/utilities/extensions/element.dart';
import 'package:collection/collection.dart';

/// Helper for finding elements declared in the resolved [unit].
class FindElement2 extends _FindElementBase {
  final CompilationUnit unit;

  FindElement2(this.unit);

  @override
  LibraryFragment get libraryFragment => unit.declaredFragment!;

  LibraryExportElementImpl export(String targetUri) {
    LibraryExport? result;

    for (var export in libraryFragment.libraryExports2) {
      var exportedUri = export.exportedLibrary2?.uri.toString();
      if (exportedUri == targetUri) {
        if (result != null) {
          throw StateError('Not unique: $targetUri');
        }
        result = export;
      }
    }

    if (result != null) {
      return result as LibraryExportElementImpl;
    }
    throw StateError('Not found: $targetUri');
  }

  FieldFormalParameterElement2 fieldFormalParameter(String name) {
    return parameter(name) as FieldFormalParameterElement2;
  }

  TopLevelFunctionElement function(String name) {
    for (var function in libraryElement.topLevelFunctions) {
      if (function.name3 == name) {
        return function;
      }
    }
    throw StateError('Not found: $name');
  }

  LibraryImportElementImpl import(String targetUri,
      {bool mustBeUnique = true}) {
    LibraryImport? importElement;

    for (var libraryFragment in libraryFragment.withEnclosing2) {
      for (var import in libraryFragment.libraryImports2) {
        var importedUri = import.importedLibrary2?.uri.toString();
        if (importedUri == targetUri) {
          if (importElement == null) {
            importElement = import;
          } else if (mustBeUnique) {
            throw StateError('Not unique: $targetUri');
          }
        }
      }
    }

    if (importElement != null) {
      return importElement as LibraryImportElementImpl;
    }
    throw StateError('Not found: $targetUri');
  }

  ImportFindElement importFind(String targetUri, {bool mustBeUnique = true}) {
    var import = this.import(targetUri, mustBeUnique: mustBeUnique);
    return ImportFindElement(import);
  }

  LabelElement2 label(String name) {
    LabelElement2? result;

    void updateResult(Element2 element) {
      if (element is LabelElement2 && element.name3 == name) {
        if (result != null) {
          throw StateError('Not unique: $name');
        }
        result = element;
      }
    }

    unit.accept(FunctionAstVisitor(
      label: (node) {
        updateResult(node.label.element!);
      },
    ));

    if (result == null) {
      throw StateError('Not found: $name');
    }
    return result!;
  }

  LocalFunctionElement localFunction(String name) {
    LocalFunctionElement? result;

    unit.accept(FunctionAstVisitor(
      functionDeclarationStatement: (node) {
        var element = node.functionDeclaration.declaredFragment?.element;
        if (element is LocalFunctionElement && element.name3 == name) {
          if (result != null) {
            throw StateError('Not unique: $name');
          }
          result = element;
        }
      },
    ));

    if (result == null) {
      throw StateError('Not found: $name');
    }
    return result!;
  }

  LocalVariableElement2 localVar(String name) {
    LocalVariableElement2? result;

    void updateResult(Element2 element) {
      if (element is LocalVariableElement2 && element.name3 == name) {
        if (result != null) {
          throw StateError('Not unique: $name');
        }
        result = element;
      }
    }

    unit.accept(FunctionAstVisitor(
      catchClauseParameter: (node) {
        updateResult(node.declaredElement2!);
      },
      declaredIdentifier: (node) {
        updateResult(node.declaredElement2!);
      },
      declaredVariablePattern: (node) {
        updateResult(node.declaredElement2!);
      },
      variableDeclaration: (node) {
        updateResult(node.declaredFragment!.element);
      },
    ));

    if (result == null) {
      throw StateError('Not found: $name');
    }
    return result!;
  }

  @override
  FormalParameterElement parameter(String name) {
    FormalParameterElement? result;

    void findIn(List<FormalParameterElement> formalParameters) {
      for (var formalParameter in formalParameters) {
        if (formalParameter.name3 == name) {
          if (result != null) {
            throw StateError('Not unique: $name');
          }
          result = formalParameter;
        }
      }
    }

    void findInExecutables(List<ExecutableElement2> executables) {
      for (var executable in executables) {
        findIn(executable.formalParameters);
      }
    }

    void findInClasses(List<InterfaceElement2> classes) {
      for (var class_ in classes) {
        findInExecutables(class_.getters2);
        findInExecutables(class_.setters2);
        findInExecutables(class_.constructors2);
        findInExecutables(class_.methods2);
      }
    }

    findInExecutables(libraryElement.getters);
    findInExecutables(libraryElement.setters);
    findInExecutables(libraryElement.topLevelFunctions);

    findInClasses(libraryElement.classes);
    findInClasses(libraryElement.enums);
    findInClasses(libraryElement.extensionTypes);
    findInClasses(libraryElement.mixins);

    for (var extension_ in libraryElement.extensions) {
      findInExecutables(extension_.getters2);
      findInExecutables(extension_.setters2);
      findInExecutables(extension_.methods2);
    }

    for (var alias in libraryElement.typeAliases) {
      var aliasedElement = alias.aliasedElement2;
      if (aliasedElement is GenericFunctionTypeElement2) {
        findIn(aliasedElement.formalParameters);
      }
    }

    unit.accept(
      FunctionAstVisitor(functionExpression: (node, local) {
        if (local) {
          var functionElement = node.declaredFragment!.element;
          findIn(functionElement.formalParameters);
        }
      }),
    );

    if (result != null) {
      return result!;
    }
    throw StateError('Not found: $name');
  }

  CompilationUnitElementImpl part(String targetUri) {
    LibraryFragment? result;

    for (var partElement in libraryFragment.partIncludes) {
      var uri = partElement.uri;
      if (uri is DirectiveUriWithUnit) {
        var unitElement = uri.libraryFragment;
        if ('${unitElement.source.uri}' == targetUri) {
          if (result != null) {
            throw StateError('Not unique: $targetUri');
          }
          result = unitElement;
        }
      }
    }

    if (result != null) {
      return result as CompilationUnitElementImpl;
    }
    throw StateError('Not found: $targetUri');
  }

  PartFindElement partFind(String targetUri) {
    var part = this.part(targetUri);
    return PartFindElement(part);
  }

  PrefixElement2 prefix(String name) {
    for (var libraryFragment in libraryFragment.withEnclosing2) {
      for (var importPrefix in libraryFragment.prefixes) {
        if (importPrefix.name3 == name) {
          return importPrefix;
        }
      }
    }
    throw StateError('Not found: $name');
  }

  TypeParameterElement2 typeParameter(String name) {
    TypeParameterElement2? result;

    unit.accept(FunctionAstVisitor(
      typeParameter: (node) {
        var element = node.declaredFragment!.element;
        if (element.name3 == name) {
          if (result != null) {
            throw StateError('Not unique: $name');
          }
          result = element;
        }
      },
    ));

    if (result != null) {
      return result!;
    }
    throw StateError('Not found: $name');
  }
}

/// Helper for searching imported elements.
class ImportFindElement extends _FindElementBase {
  final LibraryImport import;

  ImportFindElement(this.import);

  LibraryElement2 get importedLibrary => import.importedLibrary2!;

  @override
  LibraryFragment get libraryFragment {
    return importedLibrary.firstFragment;
  }

  PrefixElement2? get prefix => import.prefix2?.element;
}

class PartFindElement extends _FindElementBase {
  @override
  final LibraryFragment libraryFragment;

  PartFindElement(this.libraryFragment);
}

abstract class _FindElementBase {
  LibraryElement2 get libraryElement => libraryFragment.element;

  LibraryFragment get libraryFragment;

  ClassElement2 class_(String name) {
    for (var class_ in libraryElement.classes) {
      if (class_.name3 == name) {
        return class_;
      }
    }
    throw StateError('Not found: $name');
  }

  InterfaceElement2 classOrMixin(String name) {
    for (var class_ in libraryElement.classes) {
      if (class_.name3 == name) {
        return class_;
      }
    }
    for (var mixin in libraryElement.mixins) {
      if (mixin.name3 == name) {
        return mixin;
      }
    }
    throw StateError('Not found: $name');
  }

  ConstructorElement2 constructor(String name, {String? of}) {
    assert(name != '');

    ConstructorElement2? result;

    void findIn(List<ConstructorElement2> constructors) {
      for (var constructor in constructors) {
        if (constructor.name3 == name) {
          if (result != null) {
            throw StateError('Not unique: $name');
          }
          result = constructor;
        }
      }
    }

    for (var class_ in libraryElement.classes) {
      if (of == null || class_.name3 == of) {
        findIn(class_.constructors2);
      }
    }

    for (var enum_ in libraryElement.enums) {
      if (of == null || enum_.name3 == of) {
        findIn(enum_.constructors2);
      }
    }

    for (var extensionType in libraryElement.extensionTypes) {
      if (of == null || extensionType.name3 == of) {
        findIn(extensionType.constructors2);
      }
    }

    if (result != null) {
      return result!;
    }
    throw StateError('Not found: $name');
  }

  EnumElement2 enum_(String name) {
    for (var enum_ in libraryElement.enums) {
      if (enum_.name3 == name) {
        return enum_;
      }
    }
    throw StateError('Not found: $name');
  }

  ExtensionElement2 extension_(String name) {
    for (var extension_ in libraryElement.extensions) {
      if (extension_.name3 == name) {
        return extension_;
      }
    }
    throw StateError('Not found: $name');
  }

  ExtensionTypeElement2 extensionType(String name) {
    for (var element in libraryElement.extensionTypes) {
      if (element.name3 == name) {
        return element;
      }
    }
    throw StateError('Not found: $name');
  }

  FieldElement2 field(String name, {String? of}) {
    return _findInClassesLike(
      className: of,
      fromClass: (element) => element.fields2.named(name),
      fromExtension: (element) => element.fields2.named(name),
    );
  }

  GetterElement getter(String name, {String? of}) {
    return _findInClassesLike(
      className: of,
      fromClass: (element) => element.getters2.named(name),
      fromExtension: (element) => element.getters2.named(name),
    );
  }

  MethodElement2 method(String name, {String? of}) {
    return _findInClassesLike(
      className: of,
      fromClass: (element) => element.methods2.named(name),
      fromExtension: (element) => element.methods2.named(name),
    );
  }

  MixinElement2 mixin(String name) {
    for (var mixin in libraryElement.mixins) {
      if (mixin.name3 == name) {
        return mixin;
      }
    }
    throw StateError('Not found: $name');
  }

  FormalParameterElement parameter(String name) {
    FormalParameterElement? result;

    for (var class_ in libraryElement.classes) {
      for (var constructor in class_.constructors2) {
        for (var formalParameter in constructor.formalParameters) {
          if (formalParameter.name3 == name) {
            if (result != null) {
              throw StateError('Not unique: $name');
            }
            result = formalParameter;
          }
        }
      }
    }

    if (result != null) {
      return result;
    }
    throw StateError('Not found: $name');
  }

  SetterElement setter(String name, {String? of}) {
    return _findInClassesLike(
      className: of,
      fromClass: (element) => element.setters2.named(name),
      fromExtension: (element) => element.setters2.named(name),
    );
  }

  TopLevelFunctionElement topFunction(String name) {
    for (var function in libraryElement.topLevelFunctions) {
      if (function.name3 == name) {
        return function;
      }
    }
    throw StateError('Not found: $name');
  }

  GetterElement topGet(String name) {
    return topVar(name).getter2!;
  }

  SetterElement topSet(String name) {
    return topVar(name).setter2!;
  }

  TopLevelVariableElementImpl2 topVar(String name) {
    for (var variable in libraryElement.topLevelVariables) {
      if (variable.name3 == name) {
        return variable as TopLevelVariableElementImpl2;
      }
    }
    throw StateError('Not found: $name');
  }

  TypeAliasElement2 typeAlias(String name) {
    for (var element in libraryElement.typeAliases) {
      if (element.name3 == name) {
        return element;
      }
    }
    throw StateError('Not found: $name');
  }

  ConstructorElement2 unnamedConstructor(String name) {
    return _findInClassesLike(
      className: name,
      fromClass: (e) => e.constructors2.firstWhereOrNull((element) {
        return element.name3 == 'new';
      }),
      fromExtension: (_) => null,
    );
  }

  ExtensionElement2 unnamedExtension() {
    for (var extension_ in libraryElement.extensions) {
      if (extension_.name3 == null) {
        return extension_;
      }
    }
    throw StateError('Not found: an unnamed extension');
  }

  T _findInClassesLike<T extends Element2>({
    required String? className,
    required T? Function(InterfaceElement2 element) fromClass,
    required T? Function(ExtensionElement2 element) fromExtension,
  }) {
    bool filter(InstanceElement2 element) {
      return className == null || element.name3 == className;
    }

    var classes = [
      ...libraryElement.classes,
      ...libraryElement.enums,
      ...libraryElement.extensionTypes,
      ...libraryElement.mixins,
    ];

    var results = [
      ...classes.where(filter).map(fromClass),
      ...libraryElement.extensions.where(filter).map(fromExtension),
    ].nonNulls.toList();

    var result = results.singleOrNull;
    if (result != null) {
      return result;
    }

    if (results.isEmpty) {
      throw StateError('Not found');
    } else {
      throw StateError('Not unique');
    }
  }
}

extension<T extends Element2> on List<T> {
  T? named(String targetName) {
    for (var element in this) {
      if (element.name3 == targetName) {
        return element;
      }
    }
    return null;
  }
}

extension ExecutableElementExtensions on ExecutableElement2 {
  FormalParameterElement parameter(String name) {
    for (var formalParameter in formalParameters) {
      if (formalParameter.name3 == name) {
        return formalParameter;
      }
    }
    throw StateError('Not found: $name');
  }

  SuperFormalParameterElement2 superFormalParameter(String name) {
    for (var formalParameter in formalParameters) {
      if (formalParameter is SuperFormalParameterElement2 &&
          formalParameter.name3 == name) {
        return formalParameter;
      }
    }
    throw StateError('Not found: $name');
  }
}
