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

/// <reference types="node" />

import {Expression, ExternalExpr, ExternalReference, WrappedNodeExpr} from '@angular/compiler';
import * as path from 'path';
import * as ts from 'typescript';


const TS_DTS_JS_EXTENSION = /(?:\.d)?\.ts$|\.js$/;

export enum ImportMode {
  UseExistingImport,
  ForceNewImport,
}

/**
 * A reference to a `ts.Node`.
 *
 * For example, if an expression evaluates to a function or class definition, it will be returned
 * as a `Reference` (assuming references are allowed in evaluation).
 */
export abstract class Reference<T extends ts.Node = ts.Node> {
  constructor(readonly node: T) {}

  /**
   * Whether an `Expression` can be generated which references the node.
   */
  // TODO(issue/24571): remove '!'.
  readonly expressable !: boolean;

  /**
   * Generate an `Expression` representing this type, in the context of the given SourceFile.
   *
   * This could be a local variable reference, if the symbol is imported, or it could be a new
   * import if needed.
   */
  abstract toExpression(context: ts.SourceFile, importMode?: ImportMode): Expression|null;

  abstract addIdentifier(identifier: ts.Identifier): void;
}

/**
 * A reference to a node only, without any ability to get an `Expression` representing that node.
 *
 * This is used for returning references to things like method declarations, which are not directly
 * referenceable.
 */
export class NodeReference<T extends ts.Node = ts.Node> extends Reference<T> {
  constructor(node: T, readonly moduleName: string|null) { super(node); }

  toExpression(context: ts.SourceFile): null { return null; }

  addIdentifier(identifier: ts.Identifier): void {}
}

/**
 * A reference to a node which has a `ts.Identifier` and can be resolved to an `Expression`.
 *
 * Imports generated by `ResolvedReference`s are always relative.
 */
export class ResolvedReference<T extends ts.Node = ts.Node> extends Reference<T> {
  protected identifiers: ts.Identifier[] = [];

  constructor(node: T, protected primaryIdentifier: ts.Identifier) { super(node); }

  readonly expressable = true;

  toExpression(context: ts.SourceFile, importMode: ImportMode = ImportMode.UseExistingImport):
      Expression {
    const localIdentifier =
        pickIdentifier(context, this.primaryIdentifier, this.identifiers, importMode);
    if (localIdentifier !== null) {
      return new WrappedNodeExpr(localIdentifier);
    } else {
      // Relative import from context -> this.node.getSourceFile().
      // TODO(alxhub): investigate the impact of multiple source roots here.
      // TODO(alxhub): investigate the need to map such paths via the Host for proper g3 support.
      let relative =
          path.posix.relative(path.dirname(context.fileName), this.node.getSourceFile().fileName)
              .replace(TS_DTS_JS_EXTENSION, '');

      // path.relative() does not include the leading './'.
      if (!relative.startsWith('.')) {
        relative = `./${relative}`;
      }

      // path.relative() returns the empty string (converted to './' above) if the two paths are the
      // same.
      if (relative === './') {
        // Same file after all.
        return new WrappedNodeExpr(this.primaryIdentifier);
      } else {
        return new ExternalExpr(new ExternalReference(relative, this.primaryIdentifier.text));
      }
    }
  }

  addIdentifier(identifier: ts.Identifier): void { this.identifiers.push(identifier); }
}

/**
 * A reference to a node which has a `ts.Identifer` and an expected absolute module name.
 *
 * An `AbsoluteReference` can be resolved to an `Expression`, and if that expression is an import
 * the module specifier will be an absolute module name, not a relative path.
 */
export class AbsoluteReference<T extends ts.Node> extends Reference<T> {
  private identifiers: ts.Identifier[] = [];
  constructor(
      node: T, private primaryIdentifier: ts.Identifier, readonly moduleName: string,
      readonly symbolName: string) {
    super(node);
  }

  readonly expressable = true;

  toExpression(context: ts.SourceFile, importMode: ImportMode = ImportMode.UseExistingImport):
      Expression {
    const localIdentifier =
        pickIdentifier(context, this.primaryIdentifier, this.identifiers, importMode);
    if (localIdentifier !== null) {
      return new WrappedNodeExpr(localIdentifier);
    } else {
      return new ExternalExpr(new ExternalReference(this.moduleName, this.symbolName));
    }
  }

  addIdentifier(identifier: ts.Identifier): void { this.identifiers.push(identifier); }
}

function pickIdentifier(
    context: ts.SourceFile, primary: ts.Identifier, secondaries: ts.Identifier[],
    mode: ImportMode): ts.Identifier|null {
  context = ts.getOriginalNode(context) as ts.SourceFile;

  if (ts.getOriginalNode(primary).getSourceFile() === context) {
    return primary;
  } else if (mode === ImportMode.UseExistingImport) {
    return secondaries.find(id => ts.getOriginalNode(id).getSourceFile() === context) || null;
  } else {
    return null;
  }
}