/**
 * @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
 */

/**
 * Transform template html and css into executable code.
 * Intended to be used in a build step.
 */
import * as compiler from '@angular/compiler';
import {AppModuleMetadata, ComponentMetadata, ViewEncapsulation} from '@angular/core';
import {AngularCompilerOptions} from '@angular/tsc-wrapped';
import * as path from 'path';
import * as ts from 'typescript';

import {AppModuleCompiler, CompileMetadataResolver, DirectiveNormalizer, DomElementSchemaRegistry, HtmlParser, Lexer, Parser, StyleCompiler, TemplateParser, TypeScriptEmitter, ViewCompiler} from './compiler_private';
import {ReflectorHost, ReflectorHostContext} from './reflector_host';
import {StaticAndDynamicReflectionCapabilities} from './static_reflection_capabilities';
import {StaticReflector, StaticSymbol} from './static_reflector';

const GENERATED_FILES = /\.ngfactory\.ts$|\.css\.ts$|\.css\.shim\.ts$/;

const PREAMBLE = `/**
 * This file is generated by the Angular 2 template compiler.
 * Do not edit.
 */
 /* tslint:disable */

`;

export class CodeGenerator {
  constructor(
      private options: AngularCompilerOptions, private program: ts.Program,
      public host: ts.CompilerHost, private staticReflector: StaticReflector,
      private compiler: compiler.OfflineCompiler, private reflectorHost: ReflectorHost) {}

  private readFileMetadata(absSourcePath: string): FileMetadata {
    const moduleMetadata = this.staticReflector.getModuleMetadata(absSourcePath);
    const result: FileMetadata = {components: [], appModules: [], fileUrl: absSourcePath};
    if (!moduleMetadata) {
      console.log(`WARNING: no metadata found for ${absSourcePath}`);
      return result;
    }
    const metadata = moduleMetadata['metadata'];
    const symbols = metadata && Object.keys(metadata);
    if (!symbols || !symbols.length) {
      return result;
    }
    for (const symbol of symbols) {
      if (metadata[symbol] && metadata[symbol].__symbolic == 'error') {
        // Ignore symbols that are only included to record error information.
        continue;
      }
      const staticType = this.reflectorHost.findDeclaration(absSourcePath, symbol, absSourcePath);
      const annotations = this.staticReflector.annotations(staticType);
      annotations.forEach((annotation) => {
        if (annotation instanceof AppModuleMetadata) {
          result.appModules.push(staticType);
        } else if (annotation instanceof ComponentMetadata) {
          result.components.push(staticType);
        }
      });
    }
    return result;
  }

  // Write codegen in a directory structure matching the sources.
  private calculateEmitPath(filePath: string) {
    let root = this.options.basePath;
    for (let eachRootDir of this.options.rootDirs || []) {
      if (this.options.trace) {
        console.log(`Check if ${filePath} is under rootDirs element ${eachRootDir}`);
      }
      if (path.relative(eachRootDir, filePath).indexOf('.') !== 0) {
        root = eachRootDir;
      }
    }

    return path.join(this.options.genDir, path.relative(root, filePath));
  }

  codegen(): Promise<any> {
    let filePaths =
        this.program.getSourceFiles().map(sf => sf.fileName).filter(f => !GENERATED_FILES.test(f));
    let fileMetas = filePaths.map((filePath) => this.readFileMetadata(filePath));
    let appModules = fileMetas.reduce((appModules, fileMeta) => {
      appModules.push(...fileMeta.appModules);
      return appModules;
    }, <StaticSymbol[]>[]);
    let analyzedAppModules = this.compiler.analyzeModules(appModules);
    return Promise
        .all(fileMetas.map(
            (fileMeta) => this.compiler
                              .compile(
                                  fileMeta.fileUrl, analyzedAppModules, fileMeta.components,
                                  fileMeta.appModules)
                              .then((generatedModules) => {
                                generatedModules.forEach((generatedModule) => {
                                  const sourceFile = this.program.getSourceFile(fileMeta.fileUrl);
                                  const emitPath =
                                      this.calculateEmitPath(generatedModule.moduleUrl);
                                  this.host.writeFile(
                                      emitPath, PREAMBLE + generatedModule.source, false, () => {},
                                      [sourceFile]);
                                });
                              })))
        .catch((e) => { console.error(e.stack); });
  }

  static create(
      options: AngularCompilerOptions, program: ts.Program, compilerHost: ts.CompilerHost,
      reflectorHostContext?: ReflectorHostContext): CodeGenerator {
    const xhr: compiler.XHR = {
      get: (s: string) => {
        if (!compilerHost.fileExists(s)) {
          // TODO: We should really have a test for error cases like this!
          throw new Error(`Compilation failed. Resource file not found: ${s}`);
        }
        return Promise.resolve(compilerHost.readFile(s));
      }
    };
    const urlResolver: compiler.UrlResolver = compiler.createOfflineCompileUrlResolver();
    const reflectorHost = new ReflectorHost(program, compilerHost, options, reflectorHostContext);
    const staticReflector = new StaticReflector(reflectorHost);
    StaticAndDynamicReflectionCapabilities.install(staticReflector);
    const htmlParser = new HtmlParser();
    const config = new compiler.CompilerConfig({
      genDebugInfo: options.debug === true,
      defaultEncapsulation: ViewEncapsulation.Emulated,
      logBindingUpdate: false,
      useJit: false
    });
    const normalizer = new DirectiveNormalizer(xhr, urlResolver, htmlParser, config);
    const expressionParser = new Parser(new Lexer());
    const tmplParser = new TemplateParser(
        expressionParser, new DomElementSchemaRegistry(), htmlParser,
        /*console*/ null, []);
    const resolver = new CompileMetadataResolver(
        new compiler.DirectiveResolver(staticReflector), new compiler.PipeResolver(staticReflector),
        new compiler.ViewResolver(staticReflector), config, staticReflector);
    const offlineCompiler = new compiler.OfflineCompiler(
        resolver, normalizer, tmplParser, new StyleCompiler(urlResolver), new ViewCompiler(config),
        new AppModuleCompiler(), new TypeScriptEmitter(reflectorHost));

    return new CodeGenerator(
        options, program, compilerHost, staticReflector, offlineCompiler, reflectorHost);
  }
}

interface FileMetadata {
  fileUrl: string;
  components: StaticSymbol[];
  appModules: StaticSymbol[];
}