/**
 * @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 {GeneratedFile} from '@angular/compiler';
import * as ts from 'typescript';

import * as api from '../transformers/api';
import {nocollapseHack} from '../transformers/nocollapse_hack';

import {ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, NoopReferencesRegistry, PipeDecoratorHandler, ReferencesRegistry} from './annotations';
import {BaseDefDecoratorHandler} from './annotations/src/base_def';
import {CycleAnalyzer, ImportGraph} from './cycles';
import {ErrorCode, ngErrorCode} from './diagnostics';
import {FlatIndexGenerator, ReferenceGraph, checkForPrivateExports, findFlatIndexEntryPoint} from './entry_point';
import {AbsoluteModuleStrategy, AliasGenerator, AliasStrategy, DefaultImportTracker, FileToModuleHost, FileToModuleStrategy, ImportRewriter, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, NoopImportRewriter, R3SymbolsImportRewriter, Reference, ReferenceEmitter} from './imports';
import {IncrementalState} from './incremental';
import {CompoundMetadataReader, CompoundMetadataRegistry, DtsMetadataReader, LocalMetadataRegistry, MetadataReader} from './metadata';
import {PartialEvaluator} from './partial_evaluator';
import {AbsoluteFsPath, LogicalFileSystem} from './path';
import {NOOP_PERF_RECORDER, PerfRecorder, PerfTracker} from './perf';
import {TypeScriptReflectionHost} from './reflection';
import {HostResourceLoader} from './resource_loader';
import {NgModuleRouteAnalyzer, entryPointKeyFor} from './routing';
import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from './scope';
import {FactoryGenerator, FactoryInfo, GeneratedShimsHostWrapper, ShimGenerator, SummaryGenerator, TypeCheckShimGenerator, generatedFactoryTransform} from './shims';
import {ivySwitchTransform} from './switch';
import {IvyCompilation, declarationTransformFactory, ivyTransformFactory} from './transform';
import {aliasTransformFactory} from './transform/src/alias';
import {TypeCheckContext, TypeCheckingConfig, typeCheckFilePath} from './typecheck';
import {normalizeSeparators} from './util/src/path';
import {getRootDirs, isDtsPath} from './util/src/typescript';

export class NgtscProgram implements api.Program {
  private tsProgram: ts.Program;
  private reuseTsProgram: ts.Program;
  private resourceManager: HostResourceLoader;
  private compilation: IvyCompilation|undefined = undefined;
  private factoryToSourceInfo: Map<string, FactoryInfo>|null = null;
  private sourceToFactorySymbols: Map<string, Set<string>>|null = null;
  private host: ts.CompilerHost;
  private _coreImportsFrom: ts.SourceFile|null|undefined = undefined;
  private _importRewriter: ImportRewriter|undefined = undefined;
  private _reflector: TypeScriptReflectionHost|undefined = undefined;
  private _isCore: boolean|undefined = undefined;
  private rootDirs: AbsoluteFsPath[];
  private closureCompilerEnabled: boolean;
  private entryPoint: ts.SourceFile|null;
  private exportReferenceGraph: ReferenceGraph|null = null;
  private flatIndexGenerator: FlatIndexGenerator|null = null;
  private routeAnalyzer: NgModuleRouteAnalyzer|null = null;

  private constructionDiagnostics: ts.Diagnostic[] = [];
  private moduleResolver: ModuleResolver;
  private cycleAnalyzer: CycleAnalyzer;
  private metaReader: MetadataReader|null = null;

  private refEmitter: ReferenceEmitter|null = null;
  private fileToModuleHost: FileToModuleHost|null = null;
  private defaultImportTracker: DefaultImportTracker;
  private perfRecorder: PerfRecorder = NOOP_PERF_RECORDER;
  private perfTracker: PerfTracker|null = null;
  private incrementalState: IncrementalState;
  private typeCheckFilePath: AbsoluteFsPath;

  constructor(
      rootNames: ReadonlyArray<string>, private options: api.CompilerOptions,
      host: api.CompilerHost, oldProgram?: NgtscProgram) {
    if (shouldEnablePerfTracing(options)) {
      this.perfTracker = PerfTracker.zeroedToNow();
      this.perfRecorder = this.perfTracker;
    }

    this.rootDirs = getRootDirs(host, options);
    this.closureCompilerEnabled = !!options.annotateForClosureCompiler;
    this.resourceManager = new HostResourceLoader(host, options);
    const shouldGenerateShims = options.allowEmptyCodegenFiles || false;
    const normalizedRootNames = rootNames.map(n => AbsoluteFsPath.from(n));
    this.host = host;
    if (host.fileNameToModuleName !== undefined) {
      this.fileToModuleHost = host as FileToModuleHost;
    }
    let rootFiles = [...rootNames];

    const generators: ShimGenerator[] = [];
    if (shouldGenerateShims) {
      // Summary generation.
      const summaryGenerator = SummaryGenerator.forRootFiles(normalizedRootNames);

      // Factory generation.
      const factoryGenerator = FactoryGenerator.forRootFiles(normalizedRootNames);
      const factoryFileMap = factoryGenerator.factoryFileMap;
      this.factoryToSourceInfo = new Map<string, FactoryInfo>();
      this.sourceToFactorySymbols = new Map<string, Set<string>>();
      factoryFileMap.forEach((sourceFilePath, factoryPath) => {
        const moduleSymbolNames = new Set<string>();
        this.sourceToFactorySymbols !.set(sourceFilePath, moduleSymbolNames);
        this.factoryToSourceInfo !.set(factoryPath, {sourceFilePath, moduleSymbolNames});
      });

      const factoryFileNames = Array.from(factoryFileMap.keys());
      rootFiles.push(...factoryFileNames, ...summaryGenerator.getSummaryFileNames());
      generators.push(summaryGenerator, factoryGenerator);
    }

    this.typeCheckFilePath = typeCheckFilePath(this.rootDirs);
    generators.push(new TypeCheckShimGenerator(this.typeCheckFilePath));
    rootFiles.push(this.typeCheckFilePath);

    let entryPoint: string|null = null;
    if (options.flatModuleOutFile !== undefined) {
      entryPoint = findFlatIndexEntryPoint(normalizedRootNames);
      if (entryPoint === null) {
        // This error message talks specifically about having a single .ts file in "files". However
        // the actual logic is a bit more permissive. If a single file exists, that will be taken,
        // otherwise the highest level (shortest path) "index.ts" file will be used as the flat
        // module entry point instead. If neither of these conditions apply, the error below is
        // given.
        //
        // The user is not informed about the "index.ts" option as this behavior is deprecated -
        // an explicit entrypoint should always be specified.
        this.constructionDiagnostics.push({
          category: ts.DiagnosticCategory.Error,
          code: ngErrorCode(ErrorCode.CONFIG_FLAT_MODULE_NO_INDEX),
          file: undefined,
          start: undefined,
          length: undefined,
          messageText:
              'Angular compiler option "flatModuleOutFile" requires one and only one .ts file in the "files" field.',
        });
      } else {
        const flatModuleId = options.flatModuleId || null;
        const flatModuleOutFile = normalizeSeparators(options.flatModuleOutFile);
        this.flatIndexGenerator =
            new FlatIndexGenerator(entryPoint, flatModuleOutFile, flatModuleId);
        generators.push(this.flatIndexGenerator);
        rootFiles.push(this.flatIndexGenerator.flatIndexPath);
      }
    }

    if (generators.length > 0) {
      this.host = new GeneratedShimsHostWrapper(host, generators);
    }

    this.tsProgram =
        ts.createProgram(rootFiles, options, this.host, oldProgram && oldProgram.reuseTsProgram);
    this.reuseTsProgram = this.tsProgram;

    this.entryPoint = entryPoint !== null ? this.tsProgram.getSourceFile(entryPoint) || null : null;
    this.moduleResolver = new ModuleResolver(this.tsProgram, options, this.host);
    this.cycleAnalyzer = new CycleAnalyzer(new ImportGraph(this.moduleResolver));
    this.defaultImportTracker = new DefaultImportTracker();
    if (oldProgram === undefined) {
      this.incrementalState = IncrementalState.fresh();
    } else {
      this.incrementalState = IncrementalState.reconcile(
          oldProgram.incrementalState, oldProgram.reuseTsProgram, this.tsProgram);
    }
  }

  getTsProgram(): ts.Program { return this.tsProgram; }

  getTsOptionDiagnostics(cancellationToken?: ts.CancellationToken|
                         undefined): ReadonlyArray<ts.Diagnostic> {
    return this.tsProgram.getOptionsDiagnostics(cancellationToken);
  }

  getNgOptionDiagnostics(cancellationToken?: ts.CancellationToken|
                         undefined): ReadonlyArray<ts.Diagnostic|api.Diagnostic> {
    return this.constructionDiagnostics;
  }

  getTsSyntacticDiagnostics(
      sourceFile?: ts.SourceFile|undefined,
      cancellationToken?: ts.CancellationToken|undefined): ReadonlyArray<ts.Diagnostic> {
    return this.tsProgram.getSyntacticDiagnostics(sourceFile, cancellationToken);
  }

  getNgStructuralDiagnostics(cancellationToken?: ts.CancellationToken|
                             undefined): ReadonlyArray<api.Diagnostic> {
    return [];
  }

  getTsSemanticDiagnostics(
      sourceFile?: ts.SourceFile|undefined,
      cancellationToken?: ts.CancellationToken|undefined): ReadonlyArray<ts.Diagnostic> {
    return this.tsProgram.getSemanticDiagnostics(sourceFile, cancellationToken);
  }

  getNgSemanticDiagnostics(
      fileName?: string|undefined, cancellationToken?: ts.CancellationToken|
                                   undefined): ReadonlyArray<ts.Diagnostic|api.Diagnostic> {
    const compilation = this.ensureAnalyzed();
    const diagnostics = [...compilation.diagnostics, ...this.getTemplateDiagnostics()];
    if (this.entryPoint !== null && this.exportReferenceGraph !== null) {
      diagnostics.push(...checkForPrivateExports(
          this.entryPoint, this.tsProgram.getTypeChecker(), this.exportReferenceGraph));
    }
    return diagnostics;
  }

  async loadNgStructureAsync(): Promise<void> {
    if (this.compilation === undefined) {
      this.compilation = this.makeCompilation();
    }
    const analyzeSpan = this.perfRecorder.start('analyze');
    await Promise.all(this.tsProgram.getSourceFiles()
                          .filter(file => !file.fileName.endsWith('.d.ts'))
                          .map(file => {

                            const analyzeFileSpan = this.perfRecorder.start('analyzeFile', file);
                            let analysisPromise = this.compilation !.analyzeAsync(file);
                            if (analysisPromise === undefined) {
                              this.perfRecorder.stop(analyzeFileSpan);
                            } else if (this.perfRecorder.enabled) {
                              analysisPromise = analysisPromise.then(
                                  () => this.perfRecorder.stop(analyzeFileSpan));
                            }
                            return analysisPromise;
                          })
                          .filter((result): result is Promise<void> => result !== undefined));
    this.perfRecorder.stop(analyzeSpan);
    this.compilation.resolve();
  }

  listLazyRoutes(entryRoute?: string|undefined): api.LazyRoute[] {
    if (entryRoute) {
      // Note:
      // This resolution step is here to match the implementation of the old `AotCompilerHost` (see
      // https://github.com/angular/angular/blob/50732e156/packages/compiler-cli/src/transformers/compiler_host.ts#L175-L188).
      //
      // `@angular/cli` will always call this API with an absolute path, so the resolution step is
      // not necessary, but keeping it backwards compatible in case someone else is using the API.

      // Relative entry paths are disallowed.
      if (entryRoute.startsWith('.')) {
        throw new Error(
            `Failed to list lazy routes: Resolution of relative paths (${entryRoute}) is not supported.`);
      }

      // Non-relative entry paths fall into one of the following categories:
      // - Absolute system paths (e.g. `/foo/bar/my-project/my-module`), which are unaffected by the
      //   logic below.
      // - Paths to enternal modules (e.g. `some-lib`).
      // - Paths mapped to directories in `tsconfig.json` (e.g. `shared/my-module`).
      //   (See https://www.typescriptlang.org/docs/handbook/module-resolution.html#path-mapping.)
      //
      // In all cases above, the `containingFile` argument is ignored, so we can just take the first
      // of the root files.
      const containingFile = this.tsProgram.getRootFileNames()[0];
      const [entryPath, moduleName] = entryRoute.split('#');
      const resolved = ts.resolveModuleName(entryPath, containingFile, this.options, this.host);

      if (resolved.resolvedModule) {
        entryRoute = entryPointKeyFor(resolved.resolvedModule.resolvedFileName, moduleName);
      }
    }

    this.ensureAnalyzed();
    return this.routeAnalyzer !.listLazyRoutes(entryRoute);
  }

  getLibrarySummaries(): Map<string, api.LibrarySummary> {
    throw new Error('Method not implemented.');
  }

  getEmittedGeneratedFiles(): Map<string, GeneratedFile> {
    throw new Error('Method not implemented.');
  }

  getEmittedSourceFiles(): Map<string, ts.SourceFile> {
    throw new Error('Method not implemented.');
  }

  private ensureAnalyzed(): IvyCompilation {
    if (this.compilation === undefined) {
      const analyzeSpan = this.perfRecorder.start('analyze');
      this.compilation = this.makeCompilation();
      this.tsProgram.getSourceFiles()
          .filter(file => !file.fileName.endsWith('.d.ts'))
          .forEach(file => {
            const analyzeFileSpan = this.perfRecorder.start('analyzeFile', file);
            this.compilation !.analyzeSync(file);
            this.perfRecorder.stop(analyzeFileSpan);
          });
      this.perfRecorder.stop(analyzeSpan);
      this.compilation.resolve();
    }
    return this.compilation;
  }

  emit(opts?: {
    emitFlags?: api.EmitFlags,
    cancellationToken?: ts.CancellationToken,
    customTransformers?: api.CustomTransformers,
    emitCallback?: api.TsEmitCallback,
    mergeEmitResultsCallback?: api.TsMergeEmitResultsCallback
  }): ts.EmitResult {
    const emitCallback = opts && opts.emitCallback || defaultEmitCallback;

    const compilation = this.ensureAnalyzed();

    const writeFile: ts.WriteFileCallback =
        (fileName: string, data: string, writeByteOrderMark: boolean,
         onError: ((message: string) => void) | undefined,
         sourceFiles: ReadonlyArray<ts.SourceFile>| undefined) => {
          if (this.closureCompilerEnabled && fileName.endsWith('.js')) {
            data = nocollapseHack(data);
          }
          this.host.writeFile(fileName, data, writeByteOrderMark, onError, sourceFiles);
        };

    const customTransforms = opts && opts.customTransformers;

    const beforeTransforms = [
      ivyTransformFactory(
          compilation, this.reflector, this.importRewriter, this.defaultImportTracker, this.isCore,
          this.closureCompilerEnabled),
      aliasTransformFactory(compilation.exportStatements) as ts.TransformerFactory<ts.SourceFile>,
      this.defaultImportTracker.importPreservingTransformer(),
    ];
    const afterDeclarationsTransforms = [
      declarationTransformFactory(compilation),
    ];


    if (this.factoryToSourceInfo !== null) {
      beforeTransforms.push(
          generatedFactoryTransform(this.factoryToSourceInfo, this.importRewriter));
    }
    beforeTransforms.push(ivySwitchTransform);
    if (customTransforms && customTransforms.beforeTs) {
      beforeTransforms.push(...customTransforms.beforeTs);
    }

    const emitSpan = this.perfRecorder.start('emit');
    const emitResults: ts.EmitResult[] = [];

    const typeCheckFile = this.tsProgram.getSourceFile(this.typeCheckFilePath);

    for (const targetSourceFile of this.tsProgram.getSourceFiles()) {
      if (targetSourceFile.isDeclarationFile || targetSourceFile === typeCheckFile) {
        continue;
      }

      if (this.incrementalState.safeToSkipEmit(targetSourceFile)) {
        continue;
      }

      const fileEmitSpan = this.perfRecorder.start('emitFile', targetSourceFile);
      emitResults.push(emitCallback({
        targetSourceFile,
        program: this.tsProgram,
        host: this.host,
        options: this.options,
        emitOnlyDtsFiles: false, writeFile,
        customTransformers: {
          before: beforeTransforms,
          after: customTransforms && customTransforms.afterTs,
          afterDeclarations: afterDeclarationsTransforms,
        },
      }));
      this.perfRecorder.stop(fileEmitSpan);
    }
    this.perfRecorder.stop(emitSpan);

    if (this.perfTracker !== null && this.options.tracePerformance !== undefined) {
      this.perfTracker.serializeToFile(this.options.tracePerformance, this.host);
    }

    // Run the emit, including a custom transformer that will downlevel the Ivy decorators in code.
    return ((opts && opts.mergeEmitResultsCallback) || mergeEmitResults)(emitResults);
  }

  private getTemplateDiagnostics(): ReadonlyArray<ts.Diagnostic> {
    // Skip template type-checking if it's disabled.
    if (this.options.ivyTemplateTypeCheck === false &&
        this.options.fullTemplateTypeCheck !== true) {
      return [];
    }

    const compilation = this.ensureAnalyzed();

    // Run template type-checking.

    // First select a type-checking configuration, based on whether full template type-checking is
    // requested.
    let typeCheckingConfig: TypeCheckingConfig;
    if (this.options.fullTemplateTypeCheck) {
      typeCheckingConfig = {
        applyTemplateContextGuards: true,
        checkQueries: false,
        checkTemplateBodies: true,
        checkTypeOfBindings: true,
        checkTypeOfPipes: true,
        strictSafeNavigationTypes: true,
      };
    } else {
      typeCheckingConfig = {
        applyTemplateContextGuards: false,
        checkQueries: false,
        checkTemplateBodies: false,
        checkTypeOfBindings: false,
        checkTypeOfPipes: false,
        strictSafeNavigationTypes: false,
      };
    }

    // Execute the typeCheck phase of each decorator in the program.
    const prepSpan = this.perfRecorder.start('typeCheckPrep');
    const ctx = new TypeCheckContext(typeCheckingConfig, this.refEmitter !, this.typeCheckFilePath);
    compilation.typeCheck(ctx);
    this.perfRecorder.stop(prepSpan);

    // Get the diagnostics.
    const typeCheckSpan = this.perfRecorder.start('typeCheckDiagnostics');
    const {diagnostics, program} =
        ctx.calculateTemplateDiagnostics(this.tsProgram, this.host, this.options);
    this.perfRecorder.stop(typeCheckSpan);
    this.reuseTsProgram = program;

    return diagnostics;
  }

  private makeCompilation(): IvyCompilation {
    const checker = this.tsProgram.getTypeChecker();

    let aliasGenerator: AliasGenerator|null = null;
    // Construct the ReferenceEmitter.
    if (this.fileToModuleHost === null || !this.options._useHostForImportGeneration) {
      // The CompilerHost doesn't have fileNameToModuleName, so build an NPM-centric reference
      // resolution strategy.
      this.refEmitter = new ReferenceEmitter([
        // First, try to use local identifiers if available.
        new LocalIdentifierStrategy(),
        // Next, attempt to use an absolute import.
        new AbsoluteModuleStrategy(this.tsProgram, checker, this.options, this.host),
        // Finally, check if the reference is being written into a file within the project's logical
        // file system, and use a relative import if so. If this fails, ReferenceEmitter will throw
        // an error.
        new LogicalProjectStrategy(checker, new LogicalFileSystem(this.rootDirs)),
      ]);
    } else {
      // The CompilerHost supports fileNameToModuleName, so use that to emit imports.
      this.refEmitter = new ReferenceEmitter([
        // First, try to use local identifiers if available.
        new LocalIdentifierStrategy(),
        // Then use aliased references (this is a workaround to StrictDeps checks).
        new AliasStrategy(),
        // Then use fileNameToModuleName to emit imports.
        new FileToModuleStrategy(checker, this.fileToModuleHost),
      ]);
      aliasGenerator = new AliasGenerator(this.fileToModuleHost);
    }

    const evaluator = new PartialEvaluator(this.reflector, checker);
    const dtsReader = new DtsMetadataReader(checker, this.reflector);
    const localMetaRegistry = new LocalMetadataRegistry();
    const depScopeReader = new MetadataDtsModuleScopeResolver(dtsReader, aliasGenerator);
    const scopeRegistry = new LocalModuleScopeRegistry(
        localMetaRegistry, depScopeReader, this.refEmitter, aliasGenerator);
    const metaRegistry = new CompoundMetadataRegistry([localMetaRegistry, scopeRegistry]);

    this.metaReader = new CompoundMetadataReader([localMetaRegistry, dtsReader]);


    // If a flat module entrypoint was specified, then track references via a `ReferenceGraph`
    // in
    // order to produce proper diagnostics for incorrectly exported directives/pipes/etc. If
    // there
    // is no flat module entrypoint then don't pay the cost of tracking references.
    let referencesRegistry: ReferencesRegistry;
    if (this.entryPoint !== null) {
      this.exportReferenceGraph = new ReferenceGraph();
      referencesRegistry = new ReferenceGraphAdapter(this.exportReferenceGraph);
    } else {
      referencesRegistry = new NoopReferencesRegistry();
    }

    this.routeAnalyzer = new NgModuleRouteAnalyzer(this.moduleResolver, evaluator);

    // Set up the IvyCompilation, which manages state for the Ivy transformer.
    const handlers = [
      new BaseDefDecoratorHandler(this.reflector, evaluator, this.isCore),
      new ComponentDecoratorHandler(
          this.reflector, evaluator, metaRegistry, this.metaReader !, scopeRegistry, this.isCore,
          this.resourceManager, this.rootDirs, this.options.preserveWhitespaces || false,
          this.options.i18nUseExternalIds !== false, this.moduleResolver, this.cycleAnalyzer,
          this.refEmitter, this.defaultImportTracker),
      new DirectiveDecoratorHandler(
          this.reflector, evaluator, metaRegistry, this.defaultImportTracker, this.isCore),
      new InjectableDecoratorHandler(
          this.reflector, this.defaultImportTracker, this.isCore,
          this.options.strictInjectionParameters || false),
      new NgModuleDecoratorHandler(
          this.reflector, evaluator, metaRegistry, scopeRegistry, referencesRegistry, this.isCore,
          this.routeAnalyzer, this.refEmitter, this.defaultImportTracker),
      new PipeDecoratorHandler(
          this.reflector, evaluator, metaRegistry, this.defaultImportTracker, this.isCore),
    ];

    return new IvyCompilation(
        handlers, checker, this.reflector, this.importRewriter, this.incrementalState,
        this.perfRecorder, this.sourceToFactorySymbols);
  }

  private get reflector(): TypeScriptReflectionHost {
    if (this._reflector === undefined) {
      this._reflector = new TypeScriptReflectionHost(this.tsProgram.getTypeChecker());
    }
    return this._reflector;
  }

  private get coreImportsFrom(): ts.SourceFile|null {
    if (this._coreImportsFrom === undefined) {
      this._coreImportsFrom = this.isCore && getR3SymbolsFile(this.tsProgram) || null;
    }
    return this._coreImportsFrom;
  }

  private get isCore(): boolean {
    if (this._isCore === undefined) {
      this._isCore = isAngularCorePackage(this.tsProgram);
    }
    return this._isCore;
  }

  private get importRewriter(): ImportRewriter {
    if (this._importRewriter === undefined) {
      const coreImportsFrom = this.coreImportsFrom;
      this._importRewriter = coreImportsFrom !== null ?
          new R3SymbolsImportRewriter(coreImportsFrom.fileName) :
          new NoopImportRewriter();
    }
    return this._importRewriter;
  }
}

const defaultEmitCallback: api.TsEmitCallback =
    ({program, targetSourceFile, writeFile, cancellationToken, emitOnlyDtsFiles,
      customTransformers}) =>
        program.emit(
            targetSourceFile, writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers);

function mergeEmitResults(emitResults: ts.EmitResult[]): ts.EmitResult {
  const diagnostics: ts.Diagnostic[] = [];
  let emitSkipped = false;
  const emittedFiles: string[] = [];
  for (const er of emitResults) {
    diagnostics.push(...er.diagnostics);
    emitSkipped = emitSkipped || er.emitSkipped;
    emittedFiles.push(...(er.emittedFiles || []));
  }

  return {diagnostics, emitSkipped, emittedFiles};
}

/**
 * Find the 'r3_symbols.ts' file in the given `Program`, or return `null` if it wasn't there.
 */
function getR3SymbolsFile(program: ts.Program): ts.SourceFile|null {
  return program.getSourceFiles().find(file => file.fileName.indexOf('r3_symbols.ts') >= 0) || null;
}

/**
 * Determine if the given `Program` is @angular/core.
 */
function isAngularCorePackage(program: ts.Program): boolean {
  // Look for its_just_angular.ts somewhere in the program.
  const r3Symbols = getR3SymbolsFile(program);
  if (r3Symbols === null) {
    return false;
  }

  // Look for the constant ITS_JUST_ANGULAR in that file.
  return r3Symbols.statements.some(stmt => {
    // The statement must be a variable declaration statement.
    if (!ts.isVariableStatement(stmt)) {
      return false;
    }
    // It must be exported.
    if (stmt.modifiers === undefined ||
        !stmt.modifiers.some(mod => mod.kind === ts.SyntaxKind.ExportKeyword)) {
      return false;
    }
    // It must declare ITS_JUST_ANGULAR.
    return stmt.declarationList.declarations.some(decl => {
      // The declaration must match the name.
      if (!ts.isIdentifier(decl.name) || decl.name.text !== 'ITS_JUST_ANGULAR') {
        return false;
      }
      // It must initialize the variable to true.
      if (decl.initializer === undefined || decl.initializer.kind !== ts.SyntaxKind.TrueKeyword) {
        return false;
      }
      // This definition matches.
      return true;
    });
  });
}

export class ReferenceGraphAdapter implements ReferencesRegistry {
  constructor(private graph: ReferenceGraph) {}

  add(source: ts.Declaration, ...references: Reference<ts.Declaration>[]): void {
    for (const {node} of references) {
      let sourceFile = node.getSourceFile();
      if (sourceFile === undefined) {
        sourceFile = ts.getOriginalNode(node).getSourceFile();
      }

      // Only record local references (not references into .d.ts files).
      if (sourceFile === undefined || !isDtsPath(sourceFile.fileName)) {
        this.graph.add(source, node);
      }
    }
  }
}

function shouldEnablePerfTracing(options: api.CompilerOptions): boolean {
  return options.tracePerformance !== undefined;
}
