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

import 'errors.dart';
import 'package_config_impl.dart';
import 'package_config_json.dart';

/// A package configuration.
///
/// Associates configuration data to packages and files in packages.
///
/// More members may be added to this class in the future,
/// so classes outside of this package must not implement [PackageConfig]
/// or any subclass of it.
abstract class PackageConfig {
  /// The largest configuration version currently recognized.
  static const int maxVersion = 2;

  /// An empty package configuration.
  ///
  /// A package configuration with no available packages.
  /// Is used as a default value where a package configuration
  /// is expected, but none have been specified or found.
  static const PackageConfig empty = SimplePackageConfig.empty();

  /// Creates a package configuration with the provided available [packages].
  ///
  /// The packages must be valid packages (valid package name, valid
  /// absolute directory URIs, valid language version, if any),
  /// and there must not be two packages with the same name.
  ///
  /// The package's root ([Package.root]) and package-root
  /// ([Package.packageUriRoot]) paths must satisfy a number of constraints
  /// We say that one path (which we know ends with a `/` character)
  /// is inside another path, if the latter path is a prefix of the former path,
  /// including the two paths being the same.
  ///
  /// * No package's root must be the same as another package's root.
  /// * The package-root of a package must be inside the package's root.
  /// * If one package's package-root is inside another package's root,
  ///   then the latter package's package root must not be inside the former
  ///   package's root. (No getting between a package and its package root!)
  ///   This also disallows a package's root being the same as another
  ///   package's package root.
  ///
  /// If supplied, the [extraData] will be available as the
  /// [PackageConfig.extraData] of the created configuration.
  ///
  /// The version of the resulting configuration is always [maxVersion].
  factory PackageConfig(Iterable<Package> packages, {Object? extraData}) =>
      SimplePackageConfig(maxVersion, packages, extraData);

  /// Parses a package configuration file.
  ///
  /// The [bytes] must be an UTF-8 encoded JSON object
  /// containing a valid package configuration.
  ///
  /// The [baseUri] is used as the base for resolving relative
  /// URI references in the configuration file. If the configuration
  /// has been read from a file, the [baseUri] can be the URI of that
  /// file, or of the directory it occurs in.
  ///
  /// If [onError] is provided, errors found during parsing or building
  /// the configuration are reported by calling [onError] instead of
  /// throwing, and parser makes a *best effort* attempt to continue
  /// despite the error. The input must still be valid JSON.
  /// The result may be [PackageConfig.empty] if there is no way to
  /// extract useful information from the bytes.
  static PackageConfig parseBytes(Uint8List bytes, Uri baseUri,
          {void Function(Object error)? onError}) =>
      parsePackageConfigBytes(bytes, baseUri, onError ?? throwError);

  /// Parses a package configuration file.
  ///
  /// The [configuration] must be a JSON object
  /// containing a valid package configuration.
  ///
  /// The [baseUri] is used as the base for resolving relative
  /// URI references in the configuration file. If the configuration
  /// has been read from a file, the [baseUri] can be the URI of that
  /// file, or of the directory it occurs in.
  ///
  /// If [onError] is provided, errors found during parsing or building
  /// the configuration are reported by calling [onError] instead of
  /// throwing, and parser makes a *best effort* attempt to continue
  /// despite the error. The input must still be valid JSON.
  /// The result may be [PackageConfig.empty] if there is no way to
  /// extract useful information from the bytes.
  static PackageConfig parseString(String configuration, Uri baseUri,
          {void Function(Object error)? onError}) =>
      parsePackageConfigString(configuration, baseUri, onError ?? throwError);

  /// Parses the JSON data of a package configuration file.
  ///
  /// The [jsonData] must be a JSON-like Dart data structure,
  /// like the one provided by parsing JSON text using `dart:convert`,
  /// containing a valid package configuration.
  ///
  /// The [baseUri] is used as the base for resolving relative
  /// URI references in the configuration file. If the configuration
  /// has been read from a file, the [baseUri] can be the URI of that
  /// file, or of the directory it occurs in.
  ///
  /// If [onError] is provided, errors found during parsing or building
  /// the configuration are reported by calling [onError] instead of
  /// throwing, and parser makes a *best effort* attempt to continue
  /// despite the error. The input must still be valid JSON.
  /// The result may be [PackageConfig.empty] if there is no way to
  /// extract useful information from the bytes.
  static PackageConfig parseJson(Object? jsonData, Uri baseUri,
          {void Function(Object error)? onError}) =>
      parsePackageConfigJson(jsonData, baseUri, onError ?? throwError);

  /// Writes a configuration file for this configuration on [output].
  ///
  /// If [baseUri] is provided, URI references in the generated file
  /// will be made relative to [baseUri] where possible.
  static void writeBytes(PackageConfig configuration, Sink<Uint8List> output,
      [Uri? baseUri]) {
    writePackageConfigJsonUtf8(configuration, baseUri, output);
  }

  /// Writes a configuration JSON text for this configuration on [output].
  ///
  /// If [baseUri] is provided, URI references in the generated file
  /// will be made relative to [baseUri] where possible.
  static void writeString(PackageConfig configuration, StringSink output,
      [Uri? baseUri]) {
    writePackageConfigJsonString(configuration, baseUri, output);
  }

  /// Converts a configuration to a JSON-like data structure.
  ///
  /// If [baseUri] is provided, URI references in the generated data
  /// will be made relative to [baseUri] where possible.
  static Map<String, Object?> toJson(PackageConfig configuration,
          [Uri? baseUri]) =>
      packageConfigToJson(configuration, baseUri);

  /// The configuration version number.
  ///
  /// Currently this is 1 or 2, where
  /// * Version one is the `.packages` file format and
  /// * Version two is the first `package_config.json` format.
  ///
  /// Instances of this class supports both, and the version
  /// is only useful for detecting which kind of file the configuration
  /// was read from.
  int get version;

  /// All the available packages of this configuration.
  ///
  /// No two of these packages have the same name,
  /// and no two [Package.root] directories overlap.
  Iterable<Package> get packages;

  /// Look up a package by name.
  ///
  /// Returns the [Package] from [packages] with [packageName] as
  /// [Package.name]. Returns `null` if the package is not available in the
  /// current configuration.
  Package? operator [](String packageName);

  /// Provides the associated package for a specific [file] (or directory).
  ///
  /// Returns a [Package] which contains the [file]'s path, if any.
  /// That is, the [Package.root] directory is a parent directory
  /// of the [file]'s location.
  ///
  /// Returns `null` if the file does not belong to any package.
  Package? packageOf(Uri file);

  /// Resolves a `package:` URI to a non-package URI
  ///
  /// The [packageUri] must be a valid package URI. That means:
  /// * A URI with `package` as scheme,
  /// * with no authority part (`package://...`),
  /// * with a path starting with a valid package name followed by a slash, and
  /// * with no query or fragment part.
  ///
  /// Throws an [ArgumentError] (which also implements [PackageConfigError])
  /// if the package URI is not valid.
  ///
  /// Returns `null` if the package name of [packageUri] is not available
  /// in this package configuration.
  /// Returns the remaining path of the package URI resolved relative to the
  /// [Package.packageUriRoot] of the corresponding package.
  Uri? resolve(Uri packageUri);

  /// The package URI which resolves to [nonPackageUri].
  ///
  /// The [nonPackageUri] must not have any query or fragment part,
  /// and it must not have `package` as scheme.
  /// Throws an [ArgumentError] (which also implements [PackageConfigError])
  /// if the non-package URI is not valid.
  ///
  /// Returns a package URI which [resolve] will convert to [nonPackageUri],
  /// if any such URI exists. Returns `null` if no such package URI exists.
  Uri? toPackageUri(Uri nonPackageUri);

  /// Extra data associated with the package configuration.
  ///
  /// The data may be in any format, depending on who introduced it.
  /// The standard `package_config.json` file storage will only store
  /// JSON-like list/map data structures.
  Object? get extraData;
}

/// Configuration data for a single package.
abstract class Package {
  /// Creates a package with the provided properties.
  ///
  /// The [name] must be a valid package name.
  /// The [root] must be an absolute directory URI, meaning an absolute URI
  /// with no query or fragment path and a path starting and ending with `/`.
  /// The [packageUriRoot], if provided, must be either an absolute
  /// directory URI or a relative URI reference which is then resolved
  /// relative to [root]. It must then also be a subdirectory of [root],
  /// or the same directory, and must end with `/`.
  /// If [languageVersion] is supplied, it must be a valid Dart language
  /// version, which means two decimal integer literals separated by a `.`,
  /// where the integer literals have no leading zeros unless they are
  /// a single zero digit.
  ///
  /// The [relativeRoot] controls whether the [root] is written as
  /// relative to the `package_config.json` file when the package
  /// configuration is written to a file. It defaults to being relative.
  ///
  /// If [extraData] is supplied, it will be available as the
  /// [Package.extraData] of the created package.
  factory Package(String name, Uri root,
          {Uri? packageUriRoot,
          LanguageVersion? languageVersion,
          Object? extraData,
          bool relativeRoot = true}) =>
      SimplePackage.validate(name, root, packageUriRoot, languageVersion,
          extraData, relativeRoot, throwError)!;

  /// The package-name of the package.
  String get name;

  /// The location of the root of the package.
  ///
  /// Is always an absolute URI with no query or fragment parts,
  /// and with a path ending in `/`.
  ///
  /// All files in the [root] directory are considered
  /// part of the package for purposes where that that matters.
  Uri get root;

  /// The root of the files available through `package:` URIs.
  ///
  /// A `package:` URI with [name] as the package name is
  /// resolved relative to this location.
  ///
  /// Is always an absolute URI with no query or fragment part
  /// with a path ending in `/`,
  /// and with a location which is a subdirectory
  /// of the [root], or the same as the [root].
  Uri get packageUriRoot;

  /// The default language version associated with this package.
  ///
  /// Each package may have a default language version associated,
  /// which is the language version used to parse and compile
  /// Dart files in the package.
  /// A package version is defined by two non-negative numbers,
  /// the *major* and *minor* version numbers.
  ///
  /// A package may have no language version associated with it
  /// in the package configuration, in which case tools should
  /// use a default behavior for the package.
  LanguageVersion? get languageVersion;

  /// Extra data associated with the specific package.
  ///
  /// The data may be in any format, depending on who introduced it.
  /// The standard `package_config.json` file storage will only store
  /// JSON-like list/map data structures.
  Object? get extraData;

  /// Whether the [root] URI should be written as relative.
  ///
  /// When the configuration is written to a `package_config.json`
  /// file, the [root] URI can be either relative to the file
  /// location or absolute, controller by this value.
  bool get relativeRoot;
}

/// A language version.
///
/// A language version is represented by two non-negative integers,
/// the [major] and [minor] version numbers.
///
/// If errors during parsing are handled using an `onError` handler,
/// then an *invalid* language version may be represented by an
/// [InvalidLanguageVersion] object.
abstract class LanguageVersion implements Comparable<LanguageVersion> {
  /// The maximal value allowed by [major] and [minor] values;
  static const int maxValue = 0x7FFFFFFF;

  /// Constructs a [LanguageVersion] with the specified
  /// [major] and [minor] version numbers.
  ///
  /// Both [major] and [minor] must be greater than or equal to 0
  /// and less than or equal to [maxValue].
  factory LanguageVersion(int major, int minor) {
    RangeError.checkValueInInterval(major, 0, maxValue, 'major');
    RangeError.checkValueInInterval(minor, 0, maxValue, 'minor');
    return SimpleLanguageVersion(major, minor, null);
  }

  /// Parses a language version string.
  ///
  /// A valid language version string has the form
  ///
  /// > *decimalNumber* `.` *decimalNumber*
  ///
  /// where a *decimalNumber* is a non-empty sequence of decimal digits
  /// with no unnecessary leading zeros (the decimal number only starts
  /// with a zero digit if that digit is the entire number).
  /// No spaces are allowed in the string.
  ///
  /// If the [source] is valid then it is parsed into a valid
  /// [LanguageVersion] object.
  /// If not, then the [onError] is called with a [FormatException].
  /// If [onError] is not supplied, it defaults to throwing the exception.
  /// If the call does not throw, then an [InvalidLanguageVersion] is returned
  /// containing the original [source].
  static LanguageVersion parse(String source,
          {void Function(Object error)? onError}) =>
      parseLanguageVersion(source, onError ?? throwError);

  /// The major language version.
  ///
  /// A non-negative integer less than 2<sup>31</sup>.
  ///
  /// The value is negative for objects representing *invalid* language
  /// versions ([InvalidLanguageVersion]).
  int get major;

  /// The minor language version.
  ///
  /// A non-negative integer less than 2<sup>31</sup>.
  ///
  /// The value is negative for objects representing *invalid* language
  /// versions ([InvalidLanguageVersion]).
  int get minor;

  /// Compares language versions.
  ///
  /// Two language versions are considered equal if they have the
  /// same major and minor version numbers.
  ///
  /// A language version is greater than another if the former's major version
  /// is greater than the latter's major version, or if they have
  /// the same major version and the former's minor version is greater than
  /// the latter's.
  @override
  int compareTo(LanguageVersion other);

  /// Valid language versions with the same [major] and [minor] values are
  /// equal.
  ///
  /// Invalid language versions ([InvalidLanguageVersion]) are not equal to
  /// any other object.
  @override
  bool operator ==(Object other);

  @override
  int get hashCode;

  /// A string representation of the language version.
  ///
  /// A valid language version is represented as
  /// `"${version.major}.${version.minor}"`.
  @override
  String toString();
}

/// An *invalid* language version.
///
/// Stored in a [Package] when the original language version string
/// was invalid and a `onError` handler was passed to the parser
/// which did not throw on an error.
abstract class InvalidLanguageVersion implements LanguageVersion {
  /// The value -1 for an invalid language version.
  @override
  int get major;

  /// The value -1 for an invalid language version.
  @override
  int get minor;

  /// An invalid language version is only equal to itself.
  @override
  bool operator ==(Object other);

  @override
  int get hashCode;

  /// The original invalid version string.
  @override
  String toString();
}

/// Relational operators for [LanguageVersion] that
/// compare valid versions with [LanguageVersion.compareTo].
///
/// If either operand is an [InvalidLanguageVersion], a [StateError] is thrown.
/// Versions should be verified as valid after parsing and before using them.
extension LanguageVersionRelationalOperators on LanguageVersion {
  /// Whether this language version is less than [other].
  ///
  /// If either version being compared is an [InvalidLanguageVersion],
  /// a [StateError] is thrown. Verify versions are valid before comparing them.
  ///
  /// For details on how valid language versions are compared,
  /// check out [LanguageVersion.compareTo].
  bool operator <(LanguageVersion other) {
    // Throw an error if comparing as or with an invalid language version.
    if (this is InvalidLanguageVersion) {
      _throwThisInvalid();
    } else if (other is InvalidLanguageVersion) {
      _throwOtherInvalid();
    }

    return compareTo(other) < 0;
  }

  /// Whether this language version is less than or equal to [other].
  ///
  /// If either version being compared is an [InvalidLanguageVersion],
  /// a [StateError] is thrown. Verify versions are valid before comparing them.
  ///
  /// For details on how valid language versions are compared,
  /// check out [LanguageVersion.compareTo].
  bool operator <=(LanguageVersion other) {
    // Throw an error if comparing as or with an invalid language version.
    if (this is InvalidLanguageVersion) {
      _throwThisInvalid();
    } else if (other is InvalidLanguageVersion) {
      _throwOtherInvalid();
    }

    return compareTo(other) <= 0;
  }

  /// Whether this language version is greater than [other].
  ///
  /// If either version being compared is an [InvalidLanguageVersion],
  /// a [StateError] is thrown. Verify versions are valid before comparing them.
  ///
  /// For details on how valid language versions are compared,
  /// check out [LanguageVersion.compareTo].
  bool operator >(LanguageVersion other) {
    // Throw an error if comparing as or with an invalid language version.
    if (this is InvalidLanguageVersion) {
      _throwThisInvalid();
    } else if (other is InvalidLanguageVersion) {
      _throwOtherInvalid();
    }

    return compareTo(other) > 0;
  }

  /// Whether this language version is greater than or equal to [other].
  ///
  /// If either version being compared is an [InvalidLanguageVersion],
  /// a [StateError] is thrown. Verify versions are valid before comparing them.
  ///
  /// For details on how valid language versions are compared,
  /// check out [LanguageVersion.compareTo].
  bool operator >=(LanguageVersion other) {
    // Throw an error if comparing as or with an invalid language version.
    if (this is InvalidLanguageVersion) {
      _throwThisInvalid();
    } else if (other is InvalidLanguageVersion) {
      _throwOtherInvalid();
    }

    return compareTo(other) >= 0;
  }

  static Never _throwThisInvalid() => throw StateError(
      'Can\'t compare an invalid language version to another language version. '
      'Verify language versions are valid after parsing.');

  static Never _throwOtherInvalid() => throw StateError(
      'Can\'t compare a language version to an invalid language version. '
      'Verify language versions are valid after parsing.');
}
