// 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 'dart:collection';

import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/source/source.dart';
import 'package:pub_semver/pub_semver.dart';

/// A Dart SDK installed in a specified location.
abstract class DartSdk {
  /// The short name of the dart SDK 'async' library.
  static const String DART_ASYNC = "dart:async";

  /// The short name of the dart SDK 'core' library.
  static const String DART_CORE = "dart:core";

  /// The short name of the dart SDK 'html' library.
  static const String DART_HTML = "dart:html";

  /// The prefix shared by all dart library URIs.
  static const String DART_LIBRARY_PREFIX = "dart:";

  /// The version number that is returned when the real version number could not
  /// be determined.
  static const String DEFAULT_VERSION = "0";

  /// Return the content of the `allowed_experiments.json` file, or `null`
  /// if the file cannot be read, e.g. does not exist.
  String? get allowedExperimentsJson;

  /// Return the language version of this SDK, or throws an exception.
  ///
  /// The language version has only major/minor components, the patch number
  /// is always zero, because the patch number does not change the language.
  Version get languageVersion;

  /// Return a list containing all of the libraries defined in this SDK.
  List<SdkLibrary> get sdkLibraries;

  /// Return the revision number of this SDK, or `"0"` if the revision number
  /// cannot be discovered.
  String get sdkVersion;

  /// Return a list containing the library URI's for the libraries defined in
  /// this SDK.
  List<String> get uris;

  /// Return a source representing the given 'file:' [uri] if the file is in
  /// this SDK, or `null` if the file is not in this SDK.
  Source? fromFileUri(Uri uri);

  /// Return the library representing the library with the given 'dart:' [uri],
  /// or `null` if the given URI does not denote a library in this SDK.
  SdkLibrary? getSdkLibrary(String uri);

  /// Return the source representing the library with the given 'dart:' [uri],
  /// or `null` if the given URI does not denote a library in this SDK.
  Source? mapDartUri(String uri);

  /// Return the `dart` URI representing the given [path] if the file is in
  /// this SDK, or `null` if the file is not in this SDK.
  Uri? pathToUri(String path);
}

/// Manages the DartSdk's that have been created. Clients need to create
/// multiple SDKs when the analysis options associated with those SDK's contexts
/// will produce different analysis results.
class DartSdkManager {
  /// The absolute path to the directory containing the default SDK.
  final String defaultSdkDirectory;

  /// A table mapping (an encoding of) analysis options and SDK locations to the
  /// DartSdk from that location that has been configured with those options.
  Map<SdkDescription, DartSdk> sdkMap = HashMap<SdkDescription, DartSdk>();

  /// Initialize a newly created manager.
  DartSdkManager(this.defaultSdkDirectory);

  /// Return any SDK that has been created, or `null` if no SDKs have been
  /// created.
  DartSdk? get anySdk {
    if (sdkMap.isEmpty) {
      return null;
    }
    return sdkMap.values.first;
  }

  /// Return a list of the descriptors of the SDKs that are currently being
  /// managed.
  List<SdkDescription> get sdkDescriptors => sdkMap.keys.toList();

  /// Return the Dart SDK that is appropriate for the given SDK [description].
  /// If such an SDK has not yet been created, then the [ifAbsent] function will
  /// be invoked to create it.
  DartSdk getSdk(SdkDescription description, DartSdk Function() ifAbsent) {
    return sdkMap.putIfAbsent(description, ifAbsent);
  }
}

/// A map from Dart library URI's to the [SdkLibraryImpl] representing that
/// library.
class LibraryMap {
  /// A table mapping Dart library URI's to the library.
  final Map<String, SdkLibraryImpl> _libraryMap = <String, SdkLibraryImpl>{};

  /// Return a list containing all of the sdk libraries in this mapping.
  List<SdkLibraryImpl> get sdkLibraries => List.from(_libraryMap.values);

  /// Return a list containing the library URI's for which a mapping is
  /// available.
  List<String> get uris => _libraryMap.keys.toList();

  /// Return info for debugging https://github.com/dart-lang/sdk/issues/35226.
  Map<String, Object> debugInfo() {
    var map = <String, Object>{};
    for (var entry in _libraryMap.entries) {
      var uri = entry.key;
      var lib = entry.value;
      map[uri] = <String, Object>{'path': lib.path, 'shortName': lib.shortName};
    }
    return map;
  }

  /// Return the library with the given 'dart:' [uri], or `null` if the URI does
  /// not map to a library.
  SdkLibrary? getLibrary(String uri) => _libraryMap[uri];

  /// Set the library with the given 'dart:' [dartUri] to the given [library].
  void setLibrary(String dartUri, SdkLibraryImpl library) {
    _libraryMap[dartUri] = library;
  }

  /// Return the number of library URI's for which a mapping is available.
  int size() => _libraryMap.length;
}

/// A description of a [DartSdk].
class SdkDescription {
  /// The path of the SDK.
  final String path;

  SdkDescription(this.path);

  @override
  int get hashCode {
    return path.hashCode;
  }

  @override
  bool operator ==(Object other) {
    if (other is SdkDescription) {
      return other.path == path;
    }
    return false;
  }

  @override
  String toString() {
    return path;
  }
}

// Pre-existing name.
// ignore: camel_case_types
class SdkLibrariesReader_LibraryBuilder extends RecursiveAstVisitor<void> {
  /// The prefix added to the name of a library to form the URI used in code to
  /// reference the library.
  static const String _LIBRARY_PREFIX = "dart:";

  /// The name of the optional parameter used to indicate whether the library is
  /// an implementation library.
  static const String _IMPLEMENTATION = "implementation";

  /// The name of the optional parameter used to specify the path used when
  /// compiling for dart2js.
  static const String _DART2JS_PATH = "dart2jsPath";

  /// The name of the optional parameter used to indicate whether the library is
  /// documented.
  static const String _DOCUMENTED = "documented";

  /// The name of the optional parameter used to specify the category of the
  /// library.
  static const String _CATEGORIES = "categories";

  /// The name of the optional parameter used to specify the platforms on which
  /// the library can be used.
  static const String _PLATFORMS = "platforms";

  /// The value of the [_PLATFORMS] parameter used to specify that the library
  /// can be used on the VM.
  static const String _VM_PLATFORM = "VM_PLATFORM";

  /// The library map that is populated by visiting the AST structure parsed
  /// from the contents of the libraries file.
  final LibraryMap _librariesMap = LibraryMap();

  /// Return the library map that was populated by visiting the AST structure
  /// parsed from the contents of the libraries file.
  LibraryMap get librariesMap => _librariesMap;

  // To be backwards-compatible the new categories field is translated to
  // an old approximation.
  String convertCategories(String categories) {
    switch (categories) {
      case "":
        return "Internal";
      case "Client":
        return "Client";
      case "Server":
        return "Server";
      case "Client,Server":
        return "Shared";
      case "Client,Server,Embedded":
        return "Shared";
    }
    return "Shared";
  }

  @override
  void visitMapLiteralEntry(MapLiteralEntry node) {
    var key = node.key as SimpleStringLiteral;
    var libraryName = "$_LIBRARY_PREFIX${key.value}";

    Expression value = node.value;
    if (value is InstanceCreationExpression) {
      SdkLibraryImpl library = SdkLibraryImpl(libraryName);
      List<Expression> arguments = value.argumentList.arguments;
      for (Expression argument in arguments) {
        if (argument is SimpleStringLiteral) {
          library.path = argument.value;
        } else if (argument is NamedExpression) {
          String name = argument.name.label.name;
          Expression expression = argument.expression;
          if (name == _CATEGORIES) {
            var value = (expression as StringLiteral).stringValue!;
            library.category = convertCategories(value);
          } else if (name == _IMPLEMENTATION) {
            library._implementation = (expression as BooleanLiteral).value;
          } else if (name == _DOCUMENTED) {
            library.documented = (expression as BooleanLiteral).value;
          } else if (name == _PLATFORMS) {
            if (expression is SimpleIdentifier) {
              String identifier = expression.name;
              if (identifier == _VM_PLATFORM) {
                library.setVmLibrary();
              } else {
                library.setDart2JsLibrary();
              }
            }
          } else if (name == _DART2JS_PATH) {
            if (expression is SimpleStringLiteral) {
              library.path = expression.value;
            }
          }
        }
      }
      _librariesMap.setLibrary(libraryName, library);
    }
  }
}

/// Represents a single library in the SDK
abstract class SdkLibrary {
  /// Return the name of the category containing the library.
  String get category;

  /// Return `true` if this library can be compiled to JavaScript by dart2js.
  bool get isDart2JsLibrary;

  /// Return `true` if the library is documented.
  bool get isDocumented;

  /// Return `true` if the library is an implementation library.
  bool get isImplementation;

  /// Return `true` if library is internal can be used only by other SDK
  /// libraries.
  bool get isInternal;

  /// Return `true` if this library can be used for both client and server.
  bool get isShared;

  /// Return `true` if this library can be run on the VM.
  bool get isVmLibrary;

  /// Return the path to the file defining the library. The path is relative to
  /// the `lib` directory within the SDK.
  String get path;

  /// Return the short name of the library. This is the URI of the library,
  /// including `dart:`.
  String get shortName;
}

/// The information known about a single library within the SDK.
class SdkLibraryImpl implements SdkLibrary {
  /// The bit mask used to access the bit representing the flag indicating
  /// whether a library is intended to work on the dart2js platform.
  static int DART2JS_PLATFORM = 1;

  /// The bit mask used to access the bit representing the flag indicating
  /// whether a library is intended to work on the VM platform.
  static int VM_PLATFORM = 2;

  @override
  final String shortName;

  /// The path to the file defining the library. The path is relative to the
  /// 'lib' directory within the SDK.
  @override
  late String path;

  /// The name of the category containing the library. Unless otherwise
  /// specified in the libraries file all libraries are assumed to be shared
  /// between server and client.
  @override
  String category = "Shared";

  /// A flag indicating whether the library is documented.
  bool _documented = true;

  /// A flag indicating whether the library is an implementation library.
  bool _implementation = false;

  /// An encoding of which platforms this library is intended to work on.
  int _platforms = 0;

  /// Initialize a newly created library to represent the library with the given
  /// [shortName].
  SdkLibraryImpl(this.shortName);

  /// Set whether the library is documented.
  set documented(bool documented) {
    _documented = documented;
  }

  @override
  bool get isDart2JsLibrary => (_platforms & DART2JS_PLATFORM) != 0;

  @override
  bool get isDocumented => _documented;

  @override
  bool get isImplementation => _implementation;

  @override
  bool get isInternal => shortName.startsWith('dart:_');

  @override
  bool get isShared => category == "Shared";

  @override
  bool get isVmLibrary => (_platforms & VM_PLATFORM) != 0;

  /// Record that this library can be compiled to JavaScript by dart2js.
  void setDart2JsLibrary() {
    _platforms |= DART2JS_PLATFORM;
  }

  /// Record that this library can be run on the VM.
  void setVmLibrary() {
    _platforms |= VM_PLATFORM;
  }
}
