/**
 * @license
 * Copyright Google Inc. All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.io/license
 */

import {Rule, SchematicsException, Tree} from '@angular-devkit/schematics';
import {dirname, relative} from 'path';
import * as ts from 'typescript';

import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths';
import {parseTsconfigFile} from '../../utils/typescript/parse_tsconfig';
import {COMMON_IMPORT, DOCUMENT_TOKEN_NAME, DocumentImportVisitor, ResolvedDocumentImport} from './document_import_visitor';
import {addToImport, createImport, removeFromImport} from './move-import';


/** Entry point for the V8 move-document migration. */
export default function(): Rule {
  return (tree: Tree) => {
    const {buildPaths, testPaths} = getProjectTsConfigPaths(tree);
    const basePath = process.cwd();

    if (!buildPaths.length && !testPaths.length) {
      throw new SchematicsException(`Could not find any tsconfig file. Cannot migrate DOCUMENT 
          to new import source.`);
    }

    for (const tsconfigPath of [...buildPaths, ...testPaths]) {
      runMoveDocumentMigration(tree, tsconfigPath, basePath);
    }
  };
}

/**
 * Runs the DOCUMENT InjectionToken import migration for the given TypeScript project. The
 * schematic analyzes the imports within the project and moves the deprecated symbol to the
 * new import source.
 */
function runMoveDocumentMigration(tree: Tree, tsconfigPath: string, basePath: string) {
  const parsed = parseTsconfigFile(tsconfigPath, dirname(tsconfigPath));
  const host = ts.createCompilerHost(parsed.options, true);

  // We need to overwrite the host "readFile" method, as we want the TypeScript
  // program to be based on the file contents in the virtual file tree. Otherwise
  // if we run the migration for multiple tsconfig files which have intersecting
  // source files, it can end up updating query definitions multiple times.
  host.readFile = fileName => {
    const buffer = tree.read(relative(basePath, fileName));
    return buffer ? buffer.toString() : undefined;
  };

  const program = ts.createProgram(parsed.fileNames, parsed.options, host);
  const typeChecker = program.getTypeChecker();
  const visitor = new DocumentImportVisitor(typeChecker);
  const rootSourceFiles = program.getRootFileNames().map(f => program.getSourceFile(f) !);

  // Analyze source files by finding imports.
  rootSourceFiles.forEach(sourceFile => visitor.visitNode(sourceFile));

  const {importsMap} = visitor;

  // Walk through all source files that contain resolved queries and update
  // the source files if needed. Note that we need to update multiple queries
  // within a source file within the same recorder in order to not throw off
  // the TypeScript node offsets.
  importsMap.forEach((resolvedImport: ResolvedDocumentImport, sourceFile: ts.SourceFile) => {
    const {platformBrowserImport, commonImport, documentElement} = resolvedImport;
    if (!documentElement || !platformBrowserImport) {
      return;
    }
    const update = tree.beginUpdate(relative(basePath, sourceFile.fileName));

    const platformBrowserDeclaration = platformBrowserImport.parent.parent;
    const newPlatformBrowserText =
        removeFromImport(platformBrowserImport, sourceFile, DOCUMENT_TOKEN_NAME);
    const newCommonText = commonImport ?
        addToImport(commonImport, sourceFile, documentElement.name, documentElement.propertyName) :
        createImport(COMMON_IMPORT, sourceFile, documentElement.name, documentElement.propertyName);

    // Replace the existing query decorator call expression with the updated
    // call expression node.
    update.remove(platformBrowserDeclaration.getStart(), platformBrowserDeclaration.getWidth());
    update.insertRight(platformBrowserDeclaration.getStart(), newPlatformBrowserText);

    if (commonImport) {
      const commonDeclaration = commonImport.parent.parent;
      update.remove(commonDeclaration.getStart(), commonDeclaration.getWidth());
      update.insertRight(commonDeclaration.getStart(), newCommonText);
    } else {
      update.insertRight(platformBrowserDeclaration.getStart(), newCommonText);
    }

    tree.commitUpdate(update);
  });
}
