

import * as o from './output/output_ast';
import {OutputContext, error} from './util';

const CONSTANT_PREFIX = '_c';

// Closure variables holding messages must be named `MSG_[A-Z0-9]+`
const TRANSLATION_PREFIX = 'MSG_';

export const enum DefinitionKind {Injector, Directive, Component, Pipe}

/**
 * Closure uses `goog.getMsg(message)` to lookup translations
 */
const GOOG_GET_MSG = 'goog.getMsg';

/**
 * Context to use when producing a key.
 *
 * This ensures we see the constant not the reference variable when producing
 * a key.
 */
const KEY_CONTEXT = {};

/**
 * A node that is a place-holder that allows the node to be replaced when the actual
 * node is known.
 *
 * This allows the constant pool to change an expression from a direct reference to
 * a constant to a shared constant. It returns a fix-up node that is later allowed to
 * change the referenced expression.
 */
class FixupExpression extends o.Expression {
  private original: o.Expression;

  shared: boolean;

  constructor(public resolved: o.Expression) {
    super(resolved.type);
    this.original = resolved;
  }

  visitExpression(visitor: o.ExpressionVisitor, context: any): any {
    if (context === KEY_CONTEXT) {
      // When producing a key we want to traverse the constant not the
      // variable used to refer to it.
      return this.original.visitExpression(visitor, context);
    } else {
      return this.resolved.visitExpression(visitor, context);
    }
  }

  isEquivalent(e: o.Expression): boolean {
    return e instanceof FixupExpression && this.resolved.isEquivalent(e.resolved);
  }

  isConstant() { return true; }

  fixup(expression: o.Expression) {
    this.resolved = expression;
    this.shared = true;
  }
}

/**
 * A constant pool allows a code emitter to share constant in an output context.
 *
 * The constant pool also supports sharing access to ivy definitions references.
 */
export class ConstantPool {
  statements: o.Statement[] = [];
  private translations = new Map<string, o.Expression>();
  private literals = new Map<string, FixupExpression>();
  private literalFactories = new Map<string, o.Expression>();
  private injectorDefinitions = new Map<any, FixupExpression>();
  private directiveDefinitions = new Map<any, FixupExpression>();
  private componentDefinitions = new Map<any, FixupExpression>();
  private pipeDefinitions = new Map<any, FixupExpression>();

  private nextNameIndex = 0;

  getConstLiteral(literal: o.Expression, forceShared?: boolean): o.Expression {
    if (literal instanceof o.LiteralExpr || literal instanceof FixupExpression) {
      // Do no put simple literals into the constant pool or try to produce a constant for a
      // reference to a constant.
      return literal;
    }
    const key = this.keyOf(literal);
    let fixup = this.literals.get(key);
    let newValue = false;
    if (!fixup) {
      fixup = new FixupExpression(literal);
      this.literals.set(key, fixup);
      newValue = true;
    }

    if ((!newValue && !fixup.shared) || (newValue && forceShared)) {
      // Replace the expression with a variable
      const name = this.freshName();
      this.statements.push(
          o.variable(name).set(literal).toDeclStmt(o.INFERRED_TYPE, [o.StmtModifier.Final]));
      fixup.fixup(o.variable(name));
    }

    return fixup;
  }

  // Generates closure specific code for translation.
  //
  // ```
  // /**
  //  * @desc description?
  //  * @meaning meaning?
  //  */
  // const MSG_XYZ = goog.getMsg('message');
  // ```
  getTranslation(message: string, meta: {description?: string, meaning?: string}): o.Expression {
    // The identity of an i18n message depends on the message and its meaning
    const key = meta.meaning ? `${message}\u0000\u0000${meta.meaning}` : message;

    const exp = this.translations.get(key);

    if (exp) {
      return exp;
    }

    const docStmt = i18nMetaToDocStmt(meta);
    if (docStmt) {
      this.statements.push(docStmt);
    }

    // Call closure to get the translation
    const variable = o.variable(this.freshTranslationName());
    const fnCall = o.variable(GOOG_GET_MSG).callFn([o.literal(message)]);
    const msgStmt = variable.set(fnCall).toDeclStmt(o.INFERRED_TYPE, [o.StmtModifier.Final]);
    this.statements.push(msgStmt);

    this.translations.set(key, variable);
    return variable;
  }

  getDefinition(type: any, kind: DefinitionKind, ctx: OutputContext, forceShared: boolean = false):
      o.Expression {
    const definitions = this.definitionsOf(kind);
    let fixup = definitions.get(type);
    let newValue = false;
    if (!fixup) {
      const property = this.propertyNameOf(kind);
      fixup = new FixupExpression(ctx.importExpr(type).prop(property));
      definitions.set(type, fixup);
      newValue = true;
    }

    if ((!newValue && !fixup.shared) || (newValue && forceShared)) {
      const name = this.freshName();
      this.statements.push(
          o.variable(name).set(fixup.resolved).toDeclStmt(o.INFERRED_TYPE, [o.StmtModifier.Final]));
      fixup.fixup(o.variable(name));
    }
    return fixup;
  }

  getLiteralFactory(literal: o.LiteralArrayExpr|o.LiteralMapExpr):
      {literalFactory: o.Expression, literalFactoryArguments: o.Expression[]} {
    // Create a pure function that builds an array of a mix of constant  and variable expressions
    if (literal instanceof o.LiteralArrayExpr) {
      const argumentsForKey = literal.entries.map(e => e.isConstant() ? e : o.literal(null));
      const key = this.keyOf(o.literalArr(argumentsForKey));
      return this._getLiteralFactory(key, literal.entries, entries => o.literalArr(entries));
    } else {
      const expressionForKey = o.literalMap(
          literal.entries.map(e => ({
                                key: e.key,
                                value: e.value.isConstant() ? e.value : o.literal(null),
                                quoted: e.quoted
                              })));
      const key = this.keyOf(expressionForKey);
      return this._getLiteralFactory(
          key, literal.entries.map(e => e.value),
          entries => o.literalMap(entries.map((value, index) => ({
                                                key: literal.entries[index].key,
                                                value,
                                                quoted: literal.entries[index].quoted
                                              }))));
    }
  }

  private _getLiteralFactory(
      key: string, values: o.Expression[], resultMap: (parameters: o.Expression[]) => o.Expression):
      {literalFactory: o.Expression, literalFactoryArguments: o.Expression[]} {
    let literalFactory = this.literalFactories.get(key);
    const literalFactoryArguments = values.filter((e => !e.isConstant()));
    if (!literalFactory) {
      const resultExpressions = values.map(
          (e, index) => e.isConstant() ? this.getConstLiteral(e, true) : o.variable(`a${index}`));
      const parameters =
          resultExpressions.filter(isVariable).map(e => new o.FnParam(e.name !, o.DYNAMIC_TYPE));
      const pureFunctionDeclaration =
          o.fn(parameters, [new o.ReturnStatement(resultMap(resultExpressions))], o.INFERRED_TYPE);
      const name = this.freshName();
      this.statements.push(
          o.variable(name).set(pureFunctionDeclaration).toDeclStmt(o.INFERRED_TYPE, [
            o.StmtModifier.Final
          ]));
      literalFactory = o.variable(name);
      this.literalFactories.set(key, literalFactory);
    }
    return {literalFactory, literalFactoryArguments};
  }

  /**
   * Produce a unique name.
   *
   * The name might be unique among different prefixes if any of the prefixes end in
   * a digit so the prefix should be a constant string (not based on user input) and
   * must not end in a digit.
   */
  uniqueName(prefix: string): string { return `${prefix}${this.nextNameIndex++}`; }

  private definitionsOf(kind: DefinitionKind): Map<any, FixupExpression> {
    switch (kind) {
      case DefinitionKind.Component:
        return this.componentDefinitions;
      case DefinitionKind.Directive:
        return this.directiveDefinitions;
      case DefinitionKind.Injector:
        return this.injectorDefinitions;
      case DefinitionKind.Pipe:
        return this.pipeDefinitions;
    }
    error(`Unknown definition kind ${kind}`);
    return this.componentDefinitions;
  }

  public propertyNameOf(kind: DefinitionKind): string {
    switch (kind) {
      case DefinitionKind.Component:
        return 'ngComponentDef';
      case DefinitionKind.Directive:
        return 'ngDirectiveDef';
      case DefinitionKind.Injector:
        return 'ngInjectorDef';
      case DefinitionKind.Pipe:
        return 'ngPipeDef';
    }
    error(`Unknown definition kind ${kind}`);
    return '<unknown>';
  }

  private freshName(): string { return this.uniqueName(CONSTANT_PREFIX); }

  private freshTranslationName(): string {
    return this.uniqueName(TRANSLATION_PREFIX).toUpperCase();
  }

  private keyOf(expression: o.Expression) {
    return expression.visitExpression(new KeyVisitor(), KEY_CONTEXT);
  }
}

/**
 * Visitor used to determine if 2 expressions are equivalent and can be shared in the
 * `ConstantPool`.
 *
 * When the id (string) generated by the visitor is equal, expressions are considered equivalent.
 */
class KeyVisitor implements o.ExpressionVisitor {
  visitLiteralExpr(ast: o.LiteralExpr): string {
    return `${typeof ast.value === 'string' ? '"' + ast.value + '"' : ast.value}`;
  }

  visitLiteralArrayExpr(ast: o.LiteralArrayExpr, context: object): string {
    return `[${ast.entries.map(entry => entry.visitExpression(this, context)).join(',')}]`;
  }

  visitLiteralMapExpr(ast: o.LiteralMapExpr, context: object): string {
    const mapKey = (entry: o.LiteralMapEntry) => {
      const quote = entry.quoted ? '"' : '';
      return `${quote}${entry.key}${quote}`;
    };
    const mapEntry = (entry: o.LiteralMapEntry) =>
        `${mapKey(entry)}:${entry.value.visitExpression(this, context)}`;
    return `{${ast.entries.map(mapEntry).join(',')}`;
  }

  visitExternalExpr(ast: o.ExternalExpr): string {
    return ast.value.moduleName ? `EX:${ast.value.moduleName}:${ast.value.name}` :
                                  `EX:${ast.value.runtime.name}`;
  }

  visitReadVarExpr = invalid;
  visitWriteVarExpr = invalid;
  visitWriteKeyExpr = invalid;
  visitWritePropExpr = invalid;
  visitInvokeMethodExpr = invalid;
  visitInvokeFunctionExpr = invalid;
  visitInstantiateExpr = invalid;
  visitConditionalExpr = invalid;
  visitNotExpr = invalid;
  visitAssertNotNullExpr = invalid;
  visitCastExpr = invalid;
  visitFunctionExpr = invalid;
  visitBinaryOperatorExpr = invalid;
  visitReadPropExpr = invalid;
  visitReadKeyExpr = invalid;
  visitCommaExpr = invalid;
}

function invalid<T>(arg: o.Expression | o.Statement): never {
  throw new Error(
      `Invalid state: Visitor ${this.constructor.name} doesn't handle ${arg.constructor.name}`);
}

function isVariable(e: o.Expression): e is o.ReadVarExpr {
  return e instanceof o.ReadVarExpr;
}

// Converts i18n meta informations for a message (description, meaning) to a JsDoc statement
// formatted as expected by the Closure compiler.
function i18nMetaToDocStmt(meta: {description?: string, id?: string, meaning?: string}):
    o.JSDocCommentStmt|null {
  const tags: o.JSDocTag[] = [];

  if (meta.description) {
    tags.push({tagName: o.JSDocTagName.Desc, text: meta.description});
  }

  if (meta.meaning) {
    tags.push({tagName: o.JSDocTagName.Meaning, text: meta.meaning});
  }

  return tags.length == 0 ? null : new o.JSDocCommentStmt(tags);
}
