/**
 * @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 {AotCompilerHost, AotCompilerOptions, AotSummaryResolver, CompileDirectiveMetadata, CompileMetadataResolver, CompilerConfig, DirectiveNormalizer, DirectiveResolver, DomElementSchemaRegistry, HtmlParser, I18NHtmlParser, Lexer, NgModuleResolver, Parser, PipeResolver, StaticReflector, StaticSymbol, StaticSymbolCache, StaticSymbolResolver, TemplateParser, TypeScriptEmitter, analyzeNgModules, createAotUrlResolver} from '@angular/compiler';
import {ViewEncapsulation} from '@angular/core';
import * as ts from 'typescript';
import {ConstantPool} from '../../src/constant_pool';
import * as o from '../../src/output/output_ast';
import {compileComponent, compileDirective} from '../../src/render3/r3_view_compiler';
import {OutputContext} from '../../src/util';
import {MockAotCompilerHost, MockCompilerHost, MockData, MockDirectory, arrayToMockDir, settings, setup, toMockFileArray} from '../aot/test_util';
describe('r3_view_compiler', () => {
  const angularFiles = setup({
    compileAngular: true,
    compileAnimations: false,
    compileCommon: true,
  });
  describe('hello world', () => {
    it('should be able to generate the hello world component', () => {
      const files: MockDirectory = {
        app: {
          'hello.ts': `
           import {Component, NgModule} from '@angular/core';
           @Component({
             selector: 'hello-world',
             template: 'Hello, world!'
           })
           export class HelloWorldComponent {
           }
           @NgModule({
             declarations: [HelloWorldComponent]
           })
           export class HelloWorldModule {}
        `
        }
      };
      compile(files, angularFiles);
    });
  });
  it('should be able to generate the example', () => {
    const files: MockDirectory = {
      app: {
        'example.ts': `
        import {Component, OnInit, OnDestroy, ElementRef, Input, NgModule} from '@angular/core';
        import {CommonModule} from '@angular/common';
        @Component({
          selector: 'my-app',
          template: ''
        })
        export class MyApp implements OnInit {
          list: any[] = [];
          constructor(public elementRef: ElementRef) {}
          ngOnInit(): void {
          }
        }
        @Component({
          selector: 'todo',
          template: '
'
        })
        export class TodoComponent implements OnInit, OnDestroy {
          @Input()
          data: any[] = [];
          myTitle: string;
          constructor(public elementRef: ElementRef) {}
          ngOnInit(): void {}
          ngOnDestroy(): void {}
        }
        @NgModule({
          declarations: [TodoComponent, MyApp],
          imports: [CommonModule]
        })
        export class TodoModule{}
        `
      }
    };
    const result = compile(files, angularFiles);
    expect(result.source).toContain('@angular/core');
  });
  /* These tests are codified version of the tests in compiler_canonical_spec.ts. Every
   * test in compiler_canonical_spec.ts should have a corresponding test here.
   */
  describe('compiler conformance', () => {
    describe('elements', () => {
      it('should translate DOM structure', () => {
        const files = {
          app: {
            'spec.ts': `
                import {Component, NgModule} from '@angular/core';
                @Component({
                  selector: 'my-component',
                  template: \`Hello World!
\`
                })
                export class MyComponent {}
                @NgModule({declarations: [MyComponent]})
                export class MyModule {}
            `
          }
        };
        // The factory should look like this:
        const factory = 'factory: function MyComponent_Factory() { return new MyComponent(); }';
        // The template should look like this (where IDENT is a wild card for an identifier):
        const template = `
          template: function MyComponent_Template(ctx: IDENT, cm: IDENT) {
            if (cm) {
              IDENT.ɵE(0, 'div', IDENT);
              IDENT.ɵT(1, 'Hello ');
              IDENT.ɵE(2, 'b');
              IDENT.ɵT(3, 'World');
              IDENT.ɵe();
              IDENT.ɵT(4, '!');
              IDENT.ɵe();
            }
          }
        `;
        // The compiler should also emit a const array like this:
        const constants = `const IDENT = ['class', 'my-app', 'title', 'Hello'];`;
        const result = compile(files, angularFiles);
        expectEmit(result.source, factory, 'Incorrect factory');
        expectEmit(result.source, template, 'Incorrect template');
        expectEmit(result.source, constants, 'Incorrect shared constants');
      });
    });
  });
  describe('components & directives', () => {
    it('should instantiate directives', () => {
      const files = {
        app: {
          'spec.ts': `
            import {Component, Directive, NgModule} from '@angular/core';
            @Component({selector: 'child', template: 'child-view'})
            export class ChildComponent {}
            @Directive({selector: '[some-directive]'})
            export class SomeDirective {}
            @Component({selector: 'my-component', template: '!'})
            export class MyComponent {}
            @NgModule({declarations: [ChildComponent, SomeDirective, MyComponent]})
            export class MyModule{}
          `
        }
      };
      // ChildComponent definition should be:
      const ChildComponentDefinition = `
        static ngComponentDef = IDENT.ɵdefineComponent({
          tag: 'child',
          factory: function ChildComponent_Factory() { return new ChildComponent(); },
          template: function ChildComponent_Template(ctx: IDENT, cm: IDENT) {
            if (cm) {
              IDENT.ɵT(0, 'child-view');
            }
          }
        });`;
      // SomeDirective definition should be:
      const SomeDirectiveDefinition = `
        static ngDirectiveDef = IDENT.ɵdefineDirective({
          factory: function SomeDirective_Factory() {return new SomeDirective(); }
        });
      `;
      // MyComponent definition should be:
      const MyComponentDefinition = `
        static ngComponentDef = IDENT.ɵdefineComponent({
          tag: 'my-component',
          factory: function MyComponent_Factory() { return new MyComponent(); },
          template: function MyComponent_Template(ctx: IDENT, cm: IDENT) {
            if (cm) {
              IDENT.ɵE(0, ChildComponent, IDENT, IDENT);
              IDENT.ɵe();
              IDENT.ɵT(3, '!');
            }
            ChildComponent.ngComponentDef.r(1, 0);
            SomeDirective.ngDirectiveDef.r(2, 0);
          }
        });
      `;
      // The following constants should be emitted as well.
      const AttributesConstant = `const IDENT = ['some-directive', ''];`;
      const DirectivesConstant = `const IDENT = [SomeDirective];`;
      const result = compile(files, angularFiles);
      const source = result.source;
      expectEmit(source, ChildComponentDefinition, 'Incorrect ChildComponent.ngComponentDef');
      expectEmit(source, SomeDirectiveDefinition, 'Incorrect SomeDirective.ngDirectiveDef');
      expectEmit(source, MyComponentDefinition, 'Incorrect MyComponentDefinition.ngComponentDef');
      expectEmit(source, AttributesConstant, 'Incorrect shared attributes constant');
      expectEmit(source, DirectivesConstant, 'Incorrect share directives constant');
    });
    it('should support structural directives', () => {
      const files = {
        app: {
          'spec.ts': `
            import {Component, Directive, NgModule, TemplateRef} from '@angular/core';
            @Directive({selector: '[if]'})
            export class IfDirective {
              constructor(template: TemplateRef) { }
            }
            @Component({
              selector: 'my-component',
              template: ''
            })
            export class MyComponent {
              salutation = 'Hello';
            }
            @NgModule({declarations: [IfDirective, MyComponent]})
            export class MyModule {}
            `
        }
      };
      const IfDirectiveDefinition = `
        static ngDirectiveDef = IDENT.ɵdefineDirective({
          factory: function IfDirective_Factory() { return new IfDirective(IDENT.ɵinjectTemplateRef()); }
        });`;
      const MyComponentDefinition = `
        static ngComponentDef = IDENT.ɵdefineComponent({
          tag: 'my-component',
          factory: function MyComponent_Factory() { return new MyComponent(); },
          template: function MyComponent_Template(ctx: IDENT, cm: IDENT) {
            if (cm) {
              IDENT.ɵE(0, 'ul', null, null, IDENT);
              IDENT.ɵC(2, IDENT, MyComponent_IfDirective_Template_2);
              IDENT.ɵe();
            }
            const IDENT = IDENT.ɵm(1);
            IDENT.ɵcR(2);
            IfDirective.ngDirectiveDef.r(3,2);
            IDENT.ɵcr();
            function MyComponent_IfDirective_Template_2(ctx0: IDENT, cm: IDENT) {
              if (cm) {
                IDENT.ɵE(0, 'li');
                IDENT.ɵT(1);
                IDENT.ɵe();
              }
              IDENT.ɵt(1, IDENT.ɵb2('', ctx.salutation, ' ', IDENT, ''));
            }
          }
        });`;
      const locals = `const IDENT = ['foo', ''];`;
      const directives = `const IDENT = [IfDirective];`;
      const result = compile(files, angularFiles);
      const source = result.source;
      expectEmit(source, IfDirectiveDefinition, 'Incorrect IfDirective.ngDirectiveDef');
      expectEmit(source, MyComponentDefinition, 'Incorrect MyComponent.ngComponentDef');
      expectEmit(source, locals, 'Incorrect share locals constant');
      expectEmit(source, directives, 'Incorrect shared directive constant');
    });
    it('local reference', () => {
      const files = {
        app: {
          'spec.ts': `
            import {Component, NgModule} from '@angular/core';
            @Component({selector: 'my-component', template: 'Hello {{user.value}}!'})
            export class MyComponent {}
            @NgModule({declarations: [MyComponent]})
            export class MyModule {}
          `
        }
      };
      const MyComponentDefinition = `
        static ngComponentDef = IDENT.ɵdefineComponent({
          tag: 'my-component',
          factory: function MyComponent_Factory() { return new MyComponent(); },
          template: function MyComponent_Template(ctx: IDENT, cm: IDENT) {
            if (cm) {
              IDENT.ɵE(0, 'input', null, null, IDENT);
              IDENT.ɵe();
              IDENT.ɵT(2);
            }
            const IDENT = IDENT.ɵm(1);
            IDENT.ɵt(2, IDENT.ɵb1('Hello ', IDENT.value, '!'));
          }
        });
      `;
      const locals = `const IDENT = ['user', ''];`;
      const result = compile(files, angularFiles);
      const source = result.source;
      expectEmit(source, MyComponentDefinition, 'Incorrect MyComponent.ngComponentDef');
      expectEmit(source, locals, 'Incorrect locals constant definition');
    });
  });
});
const IDENTIFIER = /[A-Za-z_$ɵ][A-Za-z0-9_$]*/;
const OPERATOR =
    /!|%|\*|\/|\^|\&|\&\&\|\||\|\||\(|\)|\{|\}|\[|\]|:|;|\.|<|<=|>|>=|=|==|===|!=|!==|=>|\+|\+\+|-|--|@|,|\.|\.\.\./;
const STRING = /\'[^'\n]*\'|"[^'\n]*"|`[^`]*`/;
const NUMBER = /[0-9]+/;
const TOKEN = new RegExp(
    `^((${IDENTIFIER.source})|(${OPERATOR.source})|(${STRING.source})|${NUMBER.source})`);
const WHITESPACE = /^\s+/;
type Piece = string | RegExp;
const IDENT = /[A-Za-z$_][A-Za-z0-9$_]*/;
function tokenize(text: string): Piece[] {
  function matches(exp: RegExp): string|false {
    const m = text.match(exp);
    if (!m) return false;
    text = text.substr(m[0].length);
    return m[0];
  }
  function next(): string {
    const result = matches(TOKEN);
    if (!result) {
      throw Error(`Invalid test, no token found for '${text.substr(0, 30)}...'`);
    }
    matches(WHITESPACE);
    return result;
  }
  const pieces: Piece[] = [];
  matches(WHITESPACE);
  while (text) {
    const token = next();
    if (token === 'IDENT') {
      pieces.push(IDENT);
    } else {
      pieces.push(token);
    }
  }
  return pieces;
}
const contextWidth = 100;
function expectEmit(source: string, emitted: string, description: string) {
  const pieces = tokenize(emitted);
  const expr = r(...pieces);
  if (!expr.test(source)) {
    let last: number = 0;
    for (let i = 1; i < pieces.length; i++) {
      let t = r(...pieces.slice(0, i));
      let m = source.match(t);
      let expected = pieces[i - 1] == IDENT ? '' : pieces[i - 1];
      if (!m) {
        const contextPieceWidth = contextWidth / 2;
        fail(
            `${description}: Expected to find ${expected} '${source.substr(0,last)}[<---HERE]${source.substr(last)}'`);
        return;
      } else {
        last = (m.index || 0) + m[0].length;
      }
    }
    fail(
        'Test helper failure: Expected expression failed but the reporting logic could not find where it failed');
  }
}
const IDENT_LIKE = /^[a-z][A-Z]/;
const SPECIAL_RE_CHAR = /\/|\(|\)|\||\*|\+|\[|\]|\{|\}/g;
function r(...pieces: (string | RegExp)[]): RegExp {
  let results: string[] = [];
  let first = true;
  for (const piece of pieces) {
    if (!first)
      results.push(`\\s${typeof piece === 'string' && IDENT_LIKE.test(piece) ? '+' : '*'}`);
    first = false;
    if (typeof piece === 'string') {
      results.push(piece.replace(SPECIAL_RE_CHAR, s => '\\' + s));
    } else {
      results.push('(' + piece.source + ')');
    }
  }
  return new RegExp(results.join(''));
}
function compile(
    data: MockDirectory, angularFiles: MockData, options: AotCompilerOptions = {},
    errorCollector: (error: any, fileName?: string) => void = error => { throw error; }) {
  const testFiles = toMockFileArray(data);
  const scripts = testFiles.map(entry => entry.fileName);
  const angularFilesArray = toMockFileArray(angularFiles);
  const files = arrayToMockDir([...testFiles, ...angularFilesArray]);
  const mockCompilerHost = new MockCompilerHost(scripts, files);
  const compilerHost = new MockAotCompilerHost(mockCompilerHost);
  const program = ts.createProgram(scripts, {...settings}, mockCompilerHost);
  // TODO(chuckj): Replace with a variant of createAotCompiler() when the r3_view_compiler is
  // integrated
  const translations = options.translations || '';
  const urlResolver = createAotUrlResolver(compilerHost);
  const symbolCache = new StaticSymbolCache();
  const summaryResolver = new AotSummaryResolver(compilerHost, symbolCache);
  const symbolResolver = new StaticSymbolResolver(compilerHost, symbolCache, summaryResolver);
  const staticReflector =
      new StaticReflector(summaryResolver, symbolResolver, [], [], errorCollector);
  const htmlParser = new I18NHtmlParser(
      new HtmlParser(), translations, options.i18nFormat, options.missingTranslation, console);
  const config = new CompilerConfig({
    defaultEncapsulation: ViewEncapsulation.Emulated,
    useJit: false,
    enableLegacyTemplate: options.enableLegacyTemplate === true,
    missingTranslation: options.missingTranslation,
    preserveWhitespaces: options.preserveWhitespaces,
    strictInjectionParameters: options.strictInjectionParameters,
  });
  const normalizer = new DirectiveNormalizer(
      {get: (url: string) => compilerHost.loadResource(url)}, urlResolver, htmlParser, config);
  const expressionParser = new Parser(new Lexer());
  const elementSchemaRegistry = new DomElementSchemaRegistry();
  const templateParser = new TemplateParser(
      config, staticReflector, expressionParser, elementSchemaRegistry, htmlParser, console, []);
  const resolver = new CompileMetadataResolver(
      config, htmlParser, new NgModuleResolver(staticReflector),
      new DirectiveResolver(staticReflector), new PipeResolver(staticReflector), summaryResolver,
      elementSchemaRegistry, normalizer, console, symbolCache, staticReflector, errorCollector);
  // Create the TypeScript program
  const sourceFiles = program.getSourceFiles().map(sf => sf.fileName);
  // Analyze the modules
  // TODO(chuckj): Eventually this should not be necessary as the ts.SourceFile should be sufficient
  // to generate a template definition.
  const analyzedModules = analyzeNgModules(sourceFiles, compilerHost, symbolResolver, resolver);
  const directives = Array.from(analyzedModules.ngModuleByPipeOrDirective.keys());
  const fakeOutputContext: OutputContext = {
    genFilePath: 'fakeFactory.ts',
    statements: [],
    importExpr(symbol: StaticSymbol, typeParams: o.Type[]) {
      if (!(symbol instanceof StaticSymbol)) {
        if (!symbol) {
          throw new Error('Invalid: undefined passed to as a symbol');
        }
        throw new Error(`Invalid: ${(symbol as any).constructor.name} is not a symbol`);
      }
      return (symbol.members || [])
          .reduce(
              (expr, member) => expr.prop(member),
              o.importExpr(new o.ExternalReference(symbol.filePath, symbol.name)));
    },
    constantPool: new ConstantPool()
  };
  // Load All directives
  for (const directive of directives) {
    const module = analyzedModules.ngModuleByPipeOrDirective.get(directive) !;
    resolver.loadNgModuleDirectiveAndPipeMetadata(module.type.reference, true);
  }
  // Compile the directives.
  for (const directive of directives) {
    const module = analyzedModules.ngModuleByPipeOrDirective.get(directive);
    if (!module || !module.type.reference.filePath.startsWith('/app')) {
      continue;
    }
    if (resolver.isDirective(directive)) {
      const metadata = resolver.getDirectiveMetadata(directive);
      if (metadata.isComponent) {
        const fakeUrl = 'ng://fake-template-url.html';
        const htmlAst = htmlParser.parse(metadata.template !.template !, fakeUrl);
        const directives = module.transitiveModule.directives.map(
            dir => resolver.getDirectiveSummary(dir.reference));
        const pipes =
            module.transitiveModule.pipes.map(pipe => resolver.getPipeSummary(pipe.reference));
        const parsedTemplate = templateParser.parse(
            metadata, htmlAst, directives, pipes, module.schemas, fakeUrl, false);
        compileComponent(fakeOutputContext, metadata, parsedTemplate.template, staticReflector);
      } else {
        compileDirective(fakeOutputContext, metadata, staticReflector);
      }
    }
  }
  fakeOutputContext.statements.unshift(...fakeOutputContext.constantPool.statements);
  const emitter = new TypeScriptEmitter();
  const moduleName = compilerHost.fileNameToModuleName(
      fakeOutputContext.genFilePath, fakeOutputContext.genFilePath);
  const result = emitter.emitStatementsAndContext(
      fakeOutputContext.genFilePath, fakeOutputContext.statements, '', false,
      /* referenceFilter */ undefined,
      /* importFilter */ e => e.moduleName != null && e.moduleName.startsWith('/app'));
  return {source: result.sourceText, outputContext: fakeOutputContext};
}