// 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/src/generated/engine.dart'
    show AnalysisContext, AnalysisOptions;
import 'package:analyzer/src/generated/source.dart' show Source;
import 'package:analyzer/src/generated/utilities_general.dart';
import 'package:analyzer/src/summary/idl.dart' show PackageBundle;

/**
 * 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 analysis context used for all of the sources in this [DartSdk].
   */
  AnalysisContext get context;

  /**
   * 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 linked [PackageBundle] for this SDK, if it can be provided, or
   * `null` otherwise.
   *
   * This is a temporary API, don't use it.
   */
  PackageBundle getLinkedBundle();

  /**
   * 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);
}

/**
 * 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 flag indicating whether it is acceptable to use summaries when they are
   * available.
   */
  final bool canUseSummaries;

  /**
   * 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 = new HashMap<SdkDescription, DartSdk>();

  /**
   * Initialize a newly created manager.
   */
  DartSdkManager(this.defaultSdkDirectory, this.canUseSummaries,
      [dynamic ignored]);

  /**
   * 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 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.
   */
  Map<String, SdkLibraryImpl> _libraryMap = <String, SdkLibraryImpl>{};

  /**
   * Return a list containing all of the sdk libraries in this mapping.
   */
  List<SdkLibrary> get sdkLibraries => new 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 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:' [uri] 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 paths to the files or directories that define the SDK.
   */
  final List<String> paths;

  /**
   * The analysis options that will be used by the SDK's context.
   */
  final AnalysisOptions options;

  /**
   * Initialize a newly created SDK description to describe an SDK based on the
   * files or directories at the given [paths] that is analyzed using the given
   * [options].
   */
  SdkDescription(this.paths, this.options);

  @override
  int get hashCode {
    int hashCode = 0;
    for (int value in options.signature) {
      hashCode = JenkinsSmiHash.combine(hashCode, value);
    }
    for (String path in paths) {
      hashCode = JenkinsSmiHash.combine(hashCode, path.hashCode);
    }
    return JenkinsSmiHash.finish(hashCode);
  }

  @override
  bool operator ==(Object other) {
    if (other is SdkDescription) {
      if (!AnalysisOptions.signaturesEqual(
          options.signature, other.options.signature)) {
        return false;
      }
      int length = paths.length;
      if (other.paths.length != length) {
        return false;
      }
      for (int i = 0; i < length; i++) {
        if (other.paths[i] != paths[i]) {
          return false;
        }
      }
      return true;
    }
    return false;
  }

  @override
  String toString() {
    StringBuffer buffer = new StringBuffer();
    bool needsSeparator = false;
    void add(String optionName) {
      if (needsSeparator) {
        buffer.write(', ');
      }
      buffer.write(optionName);
      needsSeparator = true;
    }

    for (String path in paths) {
      add(path);
    }
    if (needsSeparator) {
      buffer.write(' ');
    }
    buffer.write('(');
    buffer.write(options.signature);
    buffer.write(')');
    return buffer.toString();
  }
}

class SdkLibrariesReader_LibraryBuilder extends RecursiveAstVisitor<Object> {
  /**
   * The prefix added to the name of a library to form the URI used in code to
   * reference the library.
   */
  static String _LIBRARY_PREFIX = "dart:";

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

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

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

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

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

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

  /**
   * A flag indicating whether the dart2js path should be used when it is
   * available.
   */
  final bool _useDart2jsPaths;

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

  /**
   * Initialize a newly created library builder to use the dart2js path if
   * [_useDart2jsPaths] is `true`.
   */
  SdkLibrariesReader_LibraryBuilder(this._useDart2jsPaths);

  /**
   * 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
  Object visitMapLiteralEntry(MapLiteralEntry node) {
    String libraryName = null;
    Expression key = node.key;
    if (key is SimpleStringLiteral) {
      libraryName = "$_LIBRARY_PREFIX${key.value}";
    }
    Expression value = node.value;
    if (value is InstanceCreationExpression) {
      SdkLibraryImpl library = new 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) {
            library.category =
                convertCategories((expression as StringLiteral).stringValue);
          } 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 (_useDart2jsPaths && name == _DART2JS_PATH) {
            if (expression is SimpleStringLiteral) {
              library.path = expression.value;
            }
          }
        }
      }
      _librariesMap.setLibrary(libraryName, library);
    }
    return null;
  }
}

/**
 * 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
  String path = null;

  /**
   * 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
   * [name].
   */
  SdkLibraryImpl(this.shortName);

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

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

  @override
  bool get isDocumented => _documented;

  @override
  bool get isImplementation => _implementation;

  @override
  bool get isInternal => category == "Internal";

  @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;
  }
}
