// Copyright (c) 2016, 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:convert';

import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:analyzer/src/dart/element/type.dart';
import 'package:analyzer/src/generated/utilities_dart.dart';
import 'package:analyzer/src/summary/base.dart';
import 'package:analyzer/src/summary/idl.dart';
import 'package:analyzer/src/summary/link.dart';
import 'package:analyzer/src/summary/package_bundle_reader.dart';

/**
 * Collect the inferred types from all the summary files listed in [args] and
 * print them in alphabetical order.
 */
main(List<String> args) {
  SummaryDataStore summaryDataStore = new SummaryDataStore(args);
  InferredTypeCollector collector = new InferredTypeCollector(
      (String uri) => summaryDataStore.linkedMap[uri],
      (String uri) => summaryDataStore.unlinkedMap[uri]);
  collector.visitSummaryDataStore(summaryDataStore);
  collector.dumpCollectedTypes();
}

/**
 * Visitor class that visits the contents of a summary file and collects the
 * inferred types in it.
 */
class InferredTypeCollector {
  UnlinkedUnit unlinkedUnit;
  LinkedUnit linkedUnit;
  CompilationUnitElementForLink unitForLink;
  final Map<String, String> inferredTypes = <String, String>{};
  List<String> typeParamsInScope = <String>[];
  final Linker _linker;

  InferredTypeCollector(
      GetDependencyCallback getDependency, GetUnitCallback getUnit)
      : _linker = new Linker({}, getDependency, getUnit, null);

  /**
   * If an inferred type exists matching the given [slot], record that it is the
   * type of the entity reachable via [path].
   */
  void collectInferredType(int slot, String path) {
    for (EntityRef type in linkedUnit.types) {
      if (type.slot == slot) {
        inferredTypes[path] = formatType(type);
        return;
      }
    }
  }

  /**
   * Collect the inferred type in summary object [obj] (if any), which is
   * reachable via [path].
   *
   * This method may modify [properties] in order to affect how sub-elements
   * are visited.
   */
  void collectInferredTypes(
      SummaryClass obj, Map<String, Object> properties, String path) {
    if (obj is UnlinkedVariable) {
      collectInferredType(obj.inferredTypeSlot, path);
      // As a temporary measure, prevent recursion into the variable's
      // initializer, since AST-based type inference doesn't infer its type
      // correctly yet.  TODO(paulberry): fix.
      properties.remove('initializer');
    } else if (obj is UnlinkedExecutable) {
      collectInferredType(obj.inferredReturnTypeSlot, path);
      // As a temporary measure, prevent recursion into the executable's local
      // variables and local functions, since AST-based type inference doesn't
      // infer locals correctly yet.  TODO(paulberry): fix if necessary.
      properties.remove('localFunctions');
      properties.remove('localVariables');
    } else if (obj is UnlinkedParam) {
      collectInferredType(obj.inferredTypeSlot, path);
      // As a temporary measure, prevent recursion into the parameter's
      // initializer, since AST-based type inference doesn't infer its type
      // correctly yet.  TODO(paulberry): fix.
      properties.remove('initializer');
    }
  }

  /**
   * Print out all the inferred types collected so far, in alphabetical order.
   */
  void dumpCollectedTypes() {
    print('Collected types (${inferredTypes.length}):');
    List<String> paths = inferredTypes.keys.toList();
    paths.sort();
    for (String path in paths) {
      print('$path -> ${inferredTypes[path]}');
    }
  }

  /**
   * Format the given [type] as a string.  Unlike the type's [toString] method,
   * this formats types using their complete URI to avoid ambiguity.
   */
  String formatDartType(DartType type) {
    if (type is FunctionType) {
      List<String> argStrings =
          type.normalParameterTypes.map(formatDartType).toList();
      List<DartType> optionalParameterTypes = type.optionalParameterTypes;
      if (optionalParameterTypes.isNotEmpty) {
        List<String> optionalArgStrings =
            optionalParameterTypes.map(formatDartType).toList();
        argStrings.add('[${optionalArgStrings.join(', ')}]');
      }
      Map<String, DartType> namedParameterTypes = type.namedParameterTypes;
      if (namedParameterTypes.isNotEmpty) {
        List<String> namedArgStrings = <String>[];
        namedParameterTypes.forEach((String name, DartType type) {
          namedArgStrings.add('$name: ${formatDartType(type)}');
        });
        argStrings.add('{${namedArgStrings.join(', ')}}');
      }
      return '(${argStrings.join(', ')}) → ${formatDartType(type.returnType)}';
    } else if (type is InterfaceType) {
      if (type.typeArguments.isNotEmpty) {
        // TODO(paulberry): implement.
        throw new UnimplementedError('type args');
      }
      return formatElement(type.element);
    } else if (type is DynamicTypeImpl) {
      return type.toString();
    } else {
      // TODO(paulberry): implement.
      throw new UnimplementedError(
          "Don't know how to format type of type ${type.runtimeType}");
    }
  }

  /**
   * Format the given [element] as a string, assuming it represents a type.
   * Unlike the element's [toString] method, this formats elements using their
   * complete URI to avoid ambiguity.
   */
  String formatElement(Element element) {
    if (element is ClassElementForLink_Class ||
        element is MethodElementForLink ||
        element is ClassElementForLink_Enum ||
        element is SpecialTypeElementForLink ||
        element is FunctionTypeAliasElementForLink ||
        element is TopLevelFunctionElementForLink) {
      return element.toString();
    } else if (element is FunctionElementForLink_Local_NonSynthetic) {
      return formatDartType(element.type);
    } else if (element is UndefinedElementForLink) {
      return '???';
    } else {
      throw new UnimplementedError(
          "Don't know how to format reference of type ${element.runtimeType}");
    }
  }

  /**
   * Interpret the given [param] as a parameter in a synthetic typedef, and
   * format it as a string.
   */
  String formatParam(UnlinkedParam param) {
    if (param.isFunctionTyped) {
      // TODO(paulberry): fix this case.
      return 'BAD(${json.encode(param)})';
    }
    String result;
    if (param.type != null) {
      result = '${formatType(param.type)} ${param.name}';
    } else {
      result = param.name;
    }
    if (param.kind == UnlinkedParamKind.named) {
      result = '{$result}';
    } else if (param.kind == UnlinkedParamKind.positional) {
      result = '[$result]';
    }
    return result;
  }

  /**
   * Convert the reference with index [index] into a string.  If [typeOf] is
   * `true`, the reference is being used in the context of naming a type, so
   * if the entity being referenced is not a type, it will be enclosed in
   * `typeof()` for clarity.
   */
  String formatReference(int index, {bool typeOf: false}) {
    ReferenceableElementForLink element = unitForLink.resolveRef(index);
    return formatElement(element);
  }

  /**
   * Interpret the given [entityRef] as a reference to a type, and format it as
   * a string.
   */
  String formatType(EntityRef entityRef) {
    List<int> implicitFunctionTypeIndices =
        entityRef.implicitFunctionTypeIndices;
    if (entityRef.syntheticReturnType != null) {
      String params = entityRef.syntheticParams.map(formatParam).join(', ');
      String retType = formatType(entityRef.syntheticReturnType);
      return '($params) -> $retType';
    }
    if (entityRef.paramReference != 0) {
      return typeParamsInScope[
          typeParamsInScope.length - entityRef.paramReference];
    }
    String result = formatReference(entityRef.reference, typeOf: true);
    List<EntityRef> typeArguments = entityRef.typeArguments.toList();
    while (typeArguments.isNotEmpty && isDynamic(typeArguments.last)) {
      typeArguments.removeLast();
    }
    if (typeArguments.isNotEmpty) {
      result += '<${typeArguments.map(formatType).join(', ')}>';
    }
    if (implicitFunctionTypeIndices.isNotEmpty) {
      result =
          'parameterOf($result, ${implicitFunctionTypeIndices.join(', ')})';
    }
    return result;
  }

  /**
   * Determine if the given [entityRef] represents the pseudo-type `dynamic`.
   */
  bool isDynamic(EntityRef entityRef) {
    if (entityRef.syntheticReturnType != null ||
        entityRef.paramReference != 0) {
      return false;
    }
    return formatReference(entityRef.reference, typeOf: true) == 'dynamic';
  }

  /**
   * Collect all the inferred types contained in [obj], which is reachable via
   * [path].  [properties] is the result of calling `obj.toMap()`, and may be
   * modified before returning.
   */
  void visit(SummaryClass obj, Map<String, Object> properties, String path) {
    List<String> oldTypeParamsInScope = typeParamsInScope;
    Object newTypeParams = properties['typeParameters'];
    if (newTypeParams is List && newTypeParams.isNotEmpty) {
      typeParamsInScope = typeParamsInScope.toList();
      for (Object typeParam in newTypeParams) {
        if (typeParam is UnlinkedTypeParam) {
          typeParamsInScope.add(typeParam.name);
        } else {
          throw new StateError(
              'Unexpected type param type: ${typeParam.runtimeType}');
        }
      }
    }
    collectInferredTypes(obj, properties, path);
    properties.forEach((String key, Object value) {
      if (value is SummaryClass) {
        visit(value, value.toMap(), '$path.$key');
      } else if (value is List) {
        for (int i = 0; i < value.length; i++) {
          Object item = value[i];
          if (item is SummaryClass) {
            Map<String, Object> itemProperties = item.toMap();
            String indexOrName = itemProperties['name'] ?? i.toString();
            visit(item, itemProperties, '$path.$key[$indexOrName]');
          }
        }
      }
    });
    typeParamsInScope = oldTypeParamsInScope;
  }

  /**
   * Collect all the inferred types contained in [summaryDataStore].
   */
  void visitSummaryDataStore(SummaryDataStore summaryDataStore) {
    // Figure out which unlinked units are a part of another library so we won't
    // visit them redundantly.
    Set<String> partOfUris = new Set<String>();
    summaryDataStore.unlinkedMap
        .forEach((String unitUriString, UnlinkedUnit unlinkedUnit) {
      Uri unitUri = Uri.parse(unitUriString);
      for (String relativePartUriString in unlinkedUnit.publicNamespace.parts) {
        partOfUris.add(
            resolveRelativeUri(unitUri, Uri.parse(relativePartUriString))
                .toString());
      }
    });
    summaryDataStore.linkedMap
        .forEach((String libraryUriString, LinkedLibrary linkedLibrary) {
      if (partOfUris.contains(libraryUriString)) {
        return;
      }
      if (libraryUriString.startsWith('dart:')) {
        // Don't bother dumping inferred types from the SDK.
        return;
      }
      Uri libraryUri = Uri.parse(libraryUriString);
      UnlinkedUnit definingUnlinkedUnit =
          summaryDataStore.unlinkedMap[libraryUriString];
      if (definingUnlinkedUnit != null) {
        visitUnit(
            definingUnlinkedUnit, linkedLibrary.units[0], libraryUriString, 0);
        for (int i = 0;
            i < definingUnlinkedUnit.publicNamespace.parts.length;
            i++) {
          Uri relativePartUri =
              Uri.parse(definingUnlinkedUnit.publicNamespace.parts[i]);
          String unitUriString =
              resolveRelativeUri(libraryUri, relativePartUri).toString();
          UnlinkedUnit unlinkedUnit =
              summaryDataStore.unlinkedMap[unitUriString];
          if (unlinkedUnit != null) {
            visitUnit(unlinkedUnit, linkedLibrary.units[i + 1],
                libraryUriString, i + 1);
          }
        }
      }
    });
  }

  /**
   * Collect all the inferred types contained in the compilation unit described
   * by [unlinkedUnit] and [linkedUnit], which has URI [libraryUriString].
   */
  void visitUnit(UnlinkedUnit unlinkedUnit, LinkedUnit linkedUnit,
      String libraryUriString, int unitNum) {
    this.unlinkedUnit = unlinkedUnit;
    this.linkedUnit = linkedUnit;
    this.unitForLink =
        _linker.getLibrary(Uri.parse(libraryUriString)).units[unitNum];
    visit(unlinkedUnit, unlinkedUnit.toMap(), libraryUriString);
    this.unlinkedUnit = null;
    this.linkedUnit = null;
    this.unitForLink = null;
  }
}
