// Copyright (c) 2019, 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.

// ignore_for_file: analyzer_use_new_elements

import 'dart:async';

import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/element2.dart';
import 'package:analyzer/src/dart/analysis/driver_based_analysis_context.dart';
import 'package:analyzer/src/dart/analysis/file_state_filter.dart';

class TopLevelDeclarations {
  final ResolvedUnitResult resolvedUnit;

  TopLevelDeclarations(this.resolvedUnit);

  DriverBasedAnalysisContext get _analysisContext {
    var analysisContext = resolvedUnit.session.analysisContext;
    return analysisContext as DriverBasedAnalysisContext;
  }

  /// Return the first public library that exports (but does not necessary
  /// declare) [element].
  Future<LibraryElement?> publiclyExporting(Element element,
      {Map<Element, LibraryElement?>? resultCache}) async {
    if (resultCache?.containsKey(element) ?? false) {
      return resultCache![element];
    }

    var declarationFilePath = element.source?.fullName;
    if (declarationFilePath == null) {
      return null;
    }

    var analysisDriver = _analysisContext.driver;
    var fsState = analysisDriver.fsState;
    await analysisDriver.discoverAvailableFiles();

    var declarationFile = fsState.getFileForPath(declarationFilePath);
    var declarationPackage = declarationFile.uriProperties.packageName;

    for (var file in fsState.knownFiles.toList()) {
      var uri = file.uriProperties;
      // Only search the package that contains the declaration and its public
      // libraries.
      if (uri.packageName != declarationPackage || uri.isSrc) {
        continue;
      }

      var elementResult = await analysisDriver.getLibraryByUri(file.uriStr);
      if (elementResult is! LibraryElementResult) {
        continue;
      }

      if (_findElement(elementResult.element, element.displayName)
              ?.nonSynthetic ==
          element.nonSynthetic) {
        resultCache?[element] = elementResult.element;
        return elementResult.element;
      }
    }

    return null;
  }

  /// Return the first public library that exports (but does not necessary
  /// declare) [element].
  Future<LibraryElement2?> publiclyExporting2(Element2 element,
      {Map<Element2, LibraryElement2?>? resultCache}) async {
    if (resultCache?.containsKey(element) ?? false) {
      return resultCache![element];
    }

    var declarationFilePath = element.library2?.firstFragment.source.fullName;
    if (declarationFilePath == null) {
      return null;
    }

    var analysisDriver = _analysisContext.driver;
    var fsState = analysisDriver.fsState;
    await analysisDriver.discoverAvailableFiles();

    var declarationFile = fsState.getFileForPath(declarationFilePath);
    var declarationPackage = declarationFile.uriProperties.packageName;

    for (var file in fsState.knownFiles.toList()) {
      var uri = file.uriProperties;
      // Only search the package that contains the declaration and its public
      // libraries.
      if (uri.packageName != declarationPackage || uri.isSrc) {
        continue;
      }

      var elementResult = await analysisDriver.getLibraryByUri(file.uriStr);
      if (elementResult is! LibraryElementResult) {
        continue;
      }

      var elementLibrary = elementResult.element2;
      if (_findElement2(elementLibrary, element.displayName)?.nonSynthetic2 ==
          element.nonSynthetic2) {
        resultCache?[element] = elementLibrary;
        return elementLibrary;
      }
    }

    return null;
  }

  /// Return the mapping from a library (that is available to this context) to
  /// a top-level declaration that is exported (not necessary declared) by this
  /// library, and has the requested base name. For getters and setters the
  /// corresponding top-level variable is returned.
  Future<Map<LibraryElement2, Element2>> withName(String baseName) async {
    var analysisDriver = _analysisContext.driver;
    await analysisDriver.discoverAvailableFiles();

    var fsState = analysisDriver.fsState;
    var filter = FileStateFilter(
      fsState.getFileForPath(resolvedUnit.path),
    );

    var result = <LibraryElement2, Element2>{};

    for (var file in fsState.knownFiles.toList()) {
      if (!filter.shouldInclude(file)) {
        continue;
      }

      var elementResult = await analysisDriver.getLibraryByUri(file.uriStr);
      if (elementResult is! LibraryElementResult) {
        continue;
      }

      addElement2(result, elementResult.element2, baseName);
    }

    return result;
  }

  static void addElement(
    Map<LibraryElement, Element> result,
    LibraryElement libraryElement,
    String baseName,
  ) {
    var element = _findElement(libraryElement, baseName);
    if (element != null) {
      result[libraryElement] = element;
    }
  }

  static void addElement2(
    Map<LibraryElement2, Element2> result,
    LibraryElement2 libraryElement,
    String baseName,
  ) {
    var element = _findElement2(libraryElement, baseName);
    if (element != null) {
      result[libraryElement] = element;
    }
  }

  static Element? _findElement(LibraryElement libraryElement, String name) {
    var element = libraryElement.exportNamespace.get(name) ??
        libraryElement.exportNamespace.get('$name=');
    if (element is PropertyAccessorElement) {
      var variable = element.variable2;
      if (variable != null) {
        return variable;
      }
    }
    return element;
  }

  static Element2? _findElement2(LibraryElement2 libraryElement, String name) {
    var element = libraryElement.exportNamespace.get2(name) ??
        libraryElement.exportNamespace.get2('$name=');
    if (element is PropertyAccessorElement2) {
      var variable = element.variable3;
      if (variable != null) {
        return variable;
      }
    }
    return element;
  }
}
