// 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/analysis/results.dart';
import 'package:analyzer/dart/analysis/session.dart';
import 'package:analyzer/dart/element/element2.dart';

/// A wrapper around [AnalysisSession] that provides additional utilities.
///
/// The methods in this class that return analysis results will throw an
/// [InconsistentAnalysisException] if the result to be returned might be
/// inconsistent with any previously returned results.
class AnalysisSessionHelper {
  final AnalysisSession session;

  final Map<String, ResolvedLibraryResult> _resolvedLibraries = {};

  AnalysisSessionHelper(this.session);

  /// Returns the [ClassElement2] with the given [className] that is exported
  /// from the library with the given [libraryUri], or `null` if the library
  /// does not export a class with such name.
  Future<ClassElement2?> getClass(String libraryUri, String className) async {
    var libraryResult = await session.getLibraryByUri(libraryUri);
    if (libraryResult is LibraryElementResult) {
      var element = libraryResult.element2.exportNamespace.get2(className);
      if (element is ClassElement2) {
        return element;
      }
    }
    return null;
  }

  @Deprecated('Use [getClass] instead.')
  Future<ClassElement2?> getClass2(String libraryUri, String className) async {
    return await getClass(libraryUri, className);
  }

  /// Return the [EnumElement2] with the given [className] that is exported
  /// from the library with the given [libraryUri], or `null` if the library
  /// does not export a class with such name.
  Future<EnumElement2?> getEnum(String libraryUri, String className) async {
    var libraryResult = await session.getLibraryByUri(libraryUri);
    if (libraryResult is LibraryElementResult) {
      var element = libraryResult.element2.exportNamespace.get2(className);
      if (element is EnumElement2) {
        return element;
      }
    }
    return null;
  }

  /// Returns the [ClassElement2] with the given [className] that is exported
  /// from the Flutter widgets library, or `null` if the library does not export
  /// a class with such name.
  Future<ClassElement2?> getFlutterClass(String className) =>
      getClass('package:flutter/widgets.dart', className);

  /// Returns the declaration of the [fragment].
  ///
  /// Returns `null` if the [fragment] is synthetic, or is declared in a file
  /// that is not a part of a library.
  Future<FragmentDeclarationResult?> getFragmentDeclaration(
      Fragment fragment) async {
    var libraryPath = fragment.libraryFragment!.source.fullName;
    var resolvedLibrary = await _getResolvedLibrary(libraryPath);
    return resolvedLibrary?.getFragmentDeclaration(fragment);
  }

  /// Return the [MixinElement2] with the given [name] that is exported
  /// from the library with the given [libraryUri], or `null` if the library
  /// does not export a class with such name.
  Future<MixinElement2?> getMixin(String libraryUri, String name) async {
    var libraryResult = await session.getLibraryByUri(libraryUri);
    if (libraryResult is LibraryElementResult) {
      var element = libraryResult.element2.exportNamespace.get2(name);
      if (element is MixinElement2) {
        return element;
      }
    }
    return null;
  }

  /// Returns the resolved unit that declares the given [element].
  Future<ResolvedUnitResult?> getResolvedUnitByElement(Element2 element) async {
    var libraryPath = element.library2!.firstFragment.source.fullName;
    var resolvedLibrary = await _getResolvedLibrary(libraryPath);
    if (resolvedLibrary == null) {
      return null;
    }

    var unitPath = element.firstFragment.libraryFragment!.source.fullName;
    return resolvedLibrary.units
        .singleWhere((resolvedUnit) => resolvedUnit.path == unitPath);
  }

  /// Returns the [PropertyAccessorElement2] with the given [name] that is
  /// exported from the library with the given [uri].
  ///
  /// Returns `null` if the library does not export a top-level accessor with
  /// that name.
  Future<PropertyAccessorElement2?> getTopLevelPropertyAccessor(
      String uri, String name) async {
    var libraryResult = await session.getLibraryByUri(uri);
    if (libraryResult is LibraryElementResult) {
      var element = libraryResult.element2.exportNamespace.get2(name);
      if (element is PropertyAccessorElement2) {
        return element;
      }
    }
    return null;
  }

  /// Return a newly resolved, or cached library with the given [path].
  Future<ResolvedLibraryResult?> _getResolvedLibrary(String path) async {
    var result = _resolvedLibraries[path];
    if (result == null) {
      var unit = await session.getUnitElement(path);
      if (unit is! UnitElementResult) {
        return null;
      }
      var some =
          await session.getResolvedLibraryByElement2(unit.fragment.element);
      if (some is ResolvedLibraryResult) {
        result = _resolvedLibraries[path] = some;
      }
    }
    return result;
  }
}
