// 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.

import 'dart:async';

import 'package:analyzer/dart/analysis/results.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<LibraryElement2?> publiclyExporting(
    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 (_findElement(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;
      }

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

    return result;
  }

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

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