// Copyright (c) 2015, 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.

/**
 * This file contains code to output a description of tasks and their
 * dependencies in ".dot" format.  Prior to running, the user should run "pub
 * get" in the analyzer directory to ensure that a "packages" folder exists.
 *
 * TODO(paulberry):
 * - Add general.dart and html.dart for completeness.
 * - Use Graphviz's "record" feature to produce more compact output
 *   (http://www.graphviz.org/content/node-shapes#record)
 * - Produce a warning if a result descriptor is found which isn't the output
 *   of exactly one task.
 * - Convert this tool to use package_config to find the package map.
 */
import 'dart:async';
import 'dart:io' hide File;
import 'dart:io' as io;

import 'package:analyzer/analyzer.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:analyzer/file_system/physical_file_system.dart';
import 'package:analyzer/src/context/builder.dart';
import 'package:analyzer/src/dart/analysis/driver.dart';
import 'package:analyzer/src/dart/analysis/file_state.dart';
import 'package:analyzer/src/dart/sdk/sdk.dart';
import 'package:analyzer/src/file_system/file_system.dart';
import 'package:analyzer/src/generated/constant.dart';
import 'package:analyzer/src/generated/engine.dart';
import 'package:analyzer/src/generated/resolver.dart';
import 'package:analyzer/src/generated/sdk.dart';
import 'package:analyzer/src/generated/source.dart';
import 'package:analyzer/src/generated/source_io.dart';
import 'package:analyzer/src/source/package_map_resolver.dart';
import 'package:analyzer/src/dart/analysis/byte_store.dart';
import 'package:analyzer/src/dart/analysis/performance_logger.dart';
import 'package:analyzer/src/codegen/tools.dart';
import 'package:front_end/src/testing/package_root.dart' as package_root;
import 'package:path/path.dart' as path;
import 'package:path/path.dart';

/**
 * Generate the target .dot file.
 */
main() async {
  String pkgPath = normalize(join(package_root.packageRoot, 'analyzer'));
  await GeneratedContent.generateAll(
      pkgPath, <GeneratedContent>[target, htmlTarget]);
}

final GeneratedFile htmlTarget = new GeneratedFile(
    'doc/tasks.html', (String pkgPath) => new Driver(pkgPath).generateHtml());

final GeneratedFile target = new GeneratedFile(
    'tool/task_dependency_graph/tasks.dot',
    (String pkgPath) => new Driver(pkgPath).generateFileContents());

typedef void GetterFinderCallback(PropertyAccessorElement element);

class Driver {
  static bool hasInitializedPlugins = false;
  PhysicalResourceProvider resourceProvider;
  AnalysisDriver driver;
  InterfaceType resultDescriptorType;
  InterfaceType listOfResultDescriptorType;
  ClassElement enginePluginClass;
  CompilationUnitElement taskUnitElement;
  InterfaceType extensionPointIdType;

  final String rootDir;

  Driver(String pkgPath) : rootDir = new Directory(pkgPath).absolute.path;

  /**
   * Get an [io.File] object corresponding to the file in which the generated
   * graph should be output.
   */
  io.File get file => new io.File(
      path.join(rootDir, 'tool', 'task_dependency_graph', 'tasks.dot'));

  /**
   * Starting at [node], find all calls to registerExtension() which refer to
   * the given [extensionIdVariable], and execute [callback] for the associated
   * result descriptors.
   */
  void findExtensions(AstNode node, TopLevelVariableElement extensionIdVariable,
      void callback(String descriptorName)) {
    Set<PropertyAccessorElement> resultDescriptors =
        new Set<PropertyAccessorElement>();
    node.accept(new ExtensionFinder(
        resultDescriptorType, extensionIdVariable, resultDescriptors.add));
    for (PropertyAccessorElement resultDescriptor in resultDescriptors) {
      callback(resultDescriptor.name);
    }
  }

  /**
   * Starting at [node], find all references to a getter of type
   * `List<ResultDescriptor>`, and execute [callback] on the getter names.
   */
  void findResultDescriptorLists(
      AstNode node, void callback(String descriptorListName)) {
    Set<PropertyAccessorElement> resultDescriptorLists =
        new Set<PropertyAccessorElement>();
    node.accept(new GetterFinder(
        listOfResultDescriptorType, resultDescriptorLists.add));
    for (PropertyAccessorElement resultDescriptorList
        in resultDescriptorLists) {
      // We only care about result descriptor lists associated with getters in
      // the engine plugin class.
      if (resultDescriptorList.enclosingElement != enginePluginClass) {
        continue;
      }
      callback(resultDescriptorList.name);
    }
  }

  void findResultDescriptors(
      AstNode node, void callback(String descriptorName)) {
    Set<PropertyAccessorElement> resultDescriptors =
        new Set<PropertyAccessorElement>();
    node.accept(new GetterFinder(resultDescriptorType, resultDescriptors.add));
    for (PropertyAccessorElement resultDescriptor in resultDescriptors) {
      callback(resultDescriptor.name);
    }
  }

  /**
   * Generate the task dependency graph and return it as a [String].
   */
  Future<String> generateFileContents() async {
    String data = await generateGraphData();
    return '''
// Copyright (c) 2015, 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.
//
// This file has been automatically generated.  Please do not edit it manually.
// To regenerate the file, use the script
// "pkg/analyzer/tool/task_dependency_graph/generate.dart".
//
// To render this graph using Graphviz (www.graphviz.org) use the command:
// "dot tasks.dot -Tpdf -O".
digraph G {
$data
}
''';
  }

  Future<String> generateGraphData() async {
    if (!hasInitializedPlugins) {
      AnalysisEngine.instance.processRequiredPlugins();
      hasInitializedPlugins = true;
    }
    List<String> lines = <String>[];
    resourceProvider = PhysicalResourceProvider.INSTANCE;
    DartSdk sdk = new FolderBasedDartSdk(resourceProvider,
        FolderBasedDartSdk.defaultSdkDirectory(resourceProvider));

    ContextBuilderOptions builderOptions = new ContextBuilderOptions();
    if (Platform.packageConfig != null) {
      builderOptions.defaultPackageFilePath =
          Uri.parse(Platform.packageConfig).toFilePath();
    } else {
      // Let the context builder use the default algorithm for package
      // resolution.
    }

    ContextBuilder builder = new ContextBuilder(resourceProvider, null, null,
        options: builderOptions);
    List<UriResolver> uriResolvers = [
      new DartUriResolver(sdk),
      new PackageMapUriResolver(resourceProvider,
          builder.convertPackagesToMap(builder.createPackageMap(''))),
      new ResourceUriResolver(resourceProvider)
    ];

    var logger = new PerformanceLog(null);
    var scheduler = new AnalysisDriverScheduler(logger);
    driver = new AnalysisDriver(
        scheduler,
        logger,
        resourceProvider,
        new MemoryByteStore(),
        new FileContentOverlay(),
        null,
        new SourceFactory(uriResolvers),
        new AnalysisOptionsImpl());
    scheduler.start();

    TypeProvider typeProvider = await driver.currentSession.typeProvider;

    String dartDartPath = path.join(rootDir, 'lib', 'src', 'task', 'dart.dart');
    String taskPath = path.join(rootDir, 'lib', 'src', 'plugin', 'task.dart');
    String modelPath =
        path.join(rootDir, 'lib', 'src', 'task', 'api', 'model.dart');
    String enginePluginPath =
        path.join(rootDir, 'lib', 'src', 'plugin', 'engine_plugin.dart');

    CompilationUnitElement modelElement = await getUnitElement(modelPath);
    InterfaceType analysisTaskType = modelElement.getType('AnalysisTask').type;
    DartType dynamicType = typeProvider.dynamicType;
    resultDescriptorType = modelElement
        .getType('ResultDescriptor')
        .type
        .instantiate([dynamicType]);
    listOfResultDescriptorType =
        typeProvider.listType.instantiate([resultDescriptorType]);
    CompilationUnit enginePluginUnit = await getUnit(enginePluginPath);
    enginePluginClass =
        enginePluginUnit.declaredElement.getType('EnginePlugin');
    extensionPointIdType =
        enginePluginUnit.declaredElement.getType('ExtensionPointId').type;
    CompilationUnit dartDartUnit = await getUnit(dartDartPath);
    CompilationUnit taskUnit = await getUnit(taskPath);
    taskUnitElement = taskUnit.declaredElement;
    Set<String> results = new Set<String>();
    Set<String> resultLists = new Set<String>();
    for (CompilationUnitMember dartUnitMember in dartDartUnit.declarations) {
      if (dartUnitMember is ClassDeclaration) {
        ClassDeclaration clazz = dartUnitMember;
        if (!clazz.isAbstract &&
            clazz.declaredElement.type.isSubtypeOf(analysisTaskType)) {
          String task = clazz.name.name;

          MethodDeclaration buildInputsAst;
          VariableDeclaration descriptorField;
          for (ClassMember classMember in clazz.members) {
            if (classMember is MethodDeclaration &&
                classMember.name.name == 'buildInputs') {
              buildInputsAst = classMember;
            }
            if (classMember is FieldDeclaration) {
              for (VariableDeclaration field in classMember.fields.variables) {
                if (field.name.name == 'DESCRIPTOR') {
                  descriptorField = field;
                }
              }
            }
          }

          findResultDescriptors(buildInputsAst, (String input) {
            results.add(input);
            lines.add('  $input -> $task');
          });
          findResultDescriptorLists(buildInputsAst, (String input) {
            resultLists.add(input);
            lines.add('  $input -> $task');
          });
          findResultDescriptors(descriptorField, (String out) {
            results.add(out);
            lines.add('  $task -> $out');
          });
        }
      }
    }
    for (String resultList in resultLists) {
      lines.add('  $resultList [shape=hexagon]');
      TopLevelVariableElement extensionIdVariable = _getExtensionId(resultList);
      findExtensions(enginePluginUnit, extensionIdVariable, (String extension) {
        results.add(extension);
        lines.add('  $extension -> $resultList');
      });
    }
    for (String result in results) {
      lines.add('  $result [shape=box]');
    }
    lines.sort();
    return lines.join('\n');
  }

  Future<String> generateHtml() async {
    var data = await generateGraphData();
    return '''
<!DOCTYPE html>
<html>
<head>
    <title>Analysis Task Dependency Graph</title>
    <link rel="stylesheet" href="support/style.css">
    <script src="support/viz.js"></script>
    <script type="application/dart" src="support/web_app.dart.js"></script>
    <script src="support/dart.js"></script>
</head>
<body>
<button id="zoomBtn">Zoom</button>
<script type="text/vnd.graphviz" id="dot">
digraph G {
  tooltip="Analysis Task Dependency Graph";
  node [fontname=Helvetica];
  edge [fontname=Helvetica, fontcolor=gray];
$data
}
</script>
</body>
</html>
''';
  }

  Future<CompilationUnit> getUnit(String path) async {
    var result = await driver.getResult(path);
    return result.unit;
  }

  Future<CompilationUnitElement> getUnitElement(String path) async {
    UnitElementResult result = await driver.getUnitElement(path);
    return result.element;
  }

  /**
   * Find the result list getter having name [resultListGetterName] in the
   * [EnginePlugin] class, and use the [ExtensionPointId] annotation on that
   * getter to find the associated [TopLevelVariableElement] which can be used
   * to register extensions for that getter.
   */
  TopLevelVariableElement _getExtensionId(String resultListGetterName) {
    PropertyAccessorElement getter =
        enginePluginClass.getGetter(resultListGetterName);
    for (ElementAnnotation annotation in getter.metadata) {
      DartObjectImpl annotationValue = annotation.constantValue;
      if (annotationValue.type.isSubtypeOf(extensionPointIdType)) {
        String extensionPointId =
            annotationValue.fields['extensionPointId'].toStringValue();
        for (TopLevelVariableElement variable
            in taskUnitElement.topLevelVariables) {
          if (variable.name == extensionPointId) {
            return variable;
          }
        }
      }
    }
    throw new Exception(
        'Could not find extension ID corresponding to $resultListGetterName');
  }
}

/**
 * Visitor that finds calls that register extension points.  Specifically, we
 * look for calls of the form `method(extensionIdVariable, resultDescriptor)`,
 * where `resultDescriptor` has type [resultDescriptorType], and we pass the
 * corresponding result descriptor names to [callback].
 */
class ExtensionFinder extends GeneralizingAstVisitor {
  final InterfaceType resultDescriptorType;
  final TopLevelVariableElement extensionIdVariable;
  final GetterFinderCallback callback;

  ExtensionFinder(
      this.resultDescriptorType, this.extensionIdVariable, this.callback);

  @override
  visitIdentifier(Identifier node) {
    Element element = node.staticElement;
    if (element is PropertyAccessorElement &&
        element.isGetter &&
        element.variable == extensionIdVariable) {
      AstNode parent = node.parent;
      if (parent is ArgumentList &&
          parent.arguments.length == 2 &&
          parent.arguments[0] == node) {
        Expression extension = parent.arguments[1];
        if (extension is Identifier) {
          Element element = extension.staticElement;
          if (element is PropertyAccessorElement &&
              element.isGetter &&
              element.returnType.isSubtypeOf(resultDescriptorType)) {
            callback(element);
            return;
          }
        }
      }
      throw new Exception('Could not decode extension setup: $parent');
    }
  }
}

/**
 * Visitor that finds references to getters having a specific type (or a
 * subtype of that type)
 */
class GetterFinder extends GeneralizingAstVisitor {
  final InterfaceType type;
  final GetterFinderCallback callback;

  GetterFinder(this.type, this.callback);

  @override
  visitIdentifier(Identifier node) {
    Element element = node.staticElement;
    if (element is PropertyAccessorElement &&
        element.isGetter &&
        element.returnType.isSubtypeOf(type)) {
      callback(element);
    }
  }
}
