/**
 * @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 {InjectablePipeVisitor} from './angular/injectable_pipe_visitor';
import {INJECTABLE_DECORATOR_NAME, addImport, getNamedImports} from './util';

/**
 * Runs a migration over a TypeScript project that adds an `@Injectable`
 * annotation to all classes that have `@Pipe`.
 */
export default function(): Rule {
  return (tree: Tree) => {
    const {buildPaths, testPaths} = getProjectTsConfigPaths(tree);
    const basePath = process.cwd();
    const allPaths = [...buildPaths, ...testPaths];

    if (!allPaths.length) {
      throw new SchematicsException(
          'Could not find any tsconfig file. Cannot add Injectable annotation to pipes.');
    }

    for (const tsconfigPath of allPaths) {
      runInjectablePipeMigration(tree, tsconfigPath, basePath);
    }
  };
}

function runInjectablePipeMigration(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 InjectablePipeVisitor(typeChecker);
  const rootSourceFiles = program.getRootFileNames().map(f => program.getSourceFile(f) !);
  const printer = ts.createPrinter();

  rootSourceFiles.forEach(sourceFile => visitor.visitNode(sourceFile));

  visitor.missingInjectablePipes.forEach(data => {
    const {classDeclaration, importDeclarationMissingImport} = data;
    const sourceFile = classDeclaration.getSourceFile();
    const update = tree.beginUpdate(relative(basePath, sourceFile.fileName));

    // Note that we don't need to go through the AST to insert the decorator, because the change
    // is pretty basic. Also this has a better chance of preserving the user's formatting.
    update.insertLeft(classDeclaration.getStart(), `@${INJECTABLE_DECORATOR_NAME}()\n`);

    // Add @Injectable to the imports if it isn't imported already. Note that this doesn't deal with
    // the case where there aren't any imports for `@angular/core` at all. We don't need to handle
    // it because the Pipe decorator won't be recognized if it hasn't been imported from Angular.
    if (importDeclarationMissingImport) {
      const namedImports = getNamedImports(importDeclarationMissingImport);

      if (namedImports) {
        update.remove(namedImports.getStart(), namedImports.getWidth());
        update.insertRight(
            namedImports.getStart(),
            printer.printNode(
                ts.EmitHint.Unspecified, addImport(namedImports, INJECTABLE_DECORATOR_NAME),
                sourceFile));
      }
    }

    tree.commitUpdate(update);
  });
}
