/**
 * @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 {AbstractEmitterVisitor, CATCH_ERROR_VAR, CATCH_STACK_VAR, EmitterVisitorContext} from './abstract_emitter';
import * as o from './output_ast';

export abstract class AbstractJsEmitterVisitor extends AbstractEmitterVisitor {
  constructor() { super(false); }
  visitDeclareClassStmt(stmt: o.ClassStmt, ctx: EmitterVisitorContext): any {
    ctx.pushClass(stmt);
    this._visitClassConstructor(stmt, ctx);

    if (stmt.parent != null) {
      ctx.print(stmt, `${stmt.name}.prototype = Object.create(`);
      stmt.parent.visitExpression(this, ctx);
      ctx.println(stmt, `.prototype);`);
    }
    stmt.getters.forEach((getter) => this._visitClassGetter(stmt, getter, ctx));
    stmt.methods.forEach((method) => this._visitClassMethod(stmt, method, ctx));
    ctx.popClass();
    return null;
  }

  private _visitClassConstructor(stmt: o.ClassStmt, ctx: EmitterVisitorContext) {
    ctx.print(stmt, `function ${stmt.name}(`);
    if (stmt.constructorMethod != null) {
      this._visitParams(stmt.constructorMethod.params, ctx);
    }
    ctx.println(stmt, `) {`);
    ctx.incIndent();
    if (stmt.constructorMethod != null) {
      if (stmt.constructorMethod.body.length > 0) {
        ctx.println(stmt, `var self = this;`);
        this.visitAllStatements(stmt.constructorMethod.body, ctx);
      }
    }
    ctx.decIndent();
    ctx.println(stmt, `}`);
  }

  private _visitClassGetter(stmt: o.ClassStmt, getter: o.ClassGetter, ctx: EmitterVisitorContext) {
    ctx.println(
        stmt,
        `Object.defineProperty(${stmt.name}.prototype, '${getter.name}', { get: function() {`);
    ctx.incIndent();
    if (getter.body.length > 0) {
      ctx.println(stmt, `var self = this;`);
      this.visitAllStatements(getter.body, ctx);
    }
    ctx.decIndent();
    ctx.println(stmt, `}});`);
  }

  private _visitClassMethod(stmt: o.ClassStmt, method: o.ClassMethod, ctx: EmitterVisitorContext) {
    ctx.print(stmt, `${stmt.name}.prototype.${method.name} = function(`);
    this._visitParams(method.params, ctx);
    ctx.println(stmt, `) {`);
    ctx.incIndent();
    if (method.body.length > 0) {
      ctx.println(stmt, `var self = this;`);
      this.visitAllStatements(method.body, ctx);
    }
    ctx.decIndent();
    ctx.println(stmt, `};`);
  }

  visitWrappedNodeExpr(ast: o.WrappedNodeExpr<any>, ctx: EmitterVisitorContext): any {
    throw new Error('Cannot emit a WrappedNodeExpr in Javascript.');
  }

  visitReadVarExpr(ast: o.ReadVarExpr, ctx: EmitterVisitorContext): string|null {
    if (ast.builtin === o.BuiltinVar.This) {
      ctx.print(ast, 'self');
    } else if (ast.builtin === o.BuiltinVar.Super) {
      throw new Error(
          `'super' needs to be handled at a parent ast node, not at the variable level!`);
    } else {
      super.visitReadVarExpr(ast, ctx);
    }
    return null;
  }
  visitDeclareVarStmt(stmt: o.DeclareVarStmt, ctx: EmitterVisitorContext): any {
    ctx.print(stmt, `var ${stmt.name}`);
    if (stmt.value) {
      ctx.print(stmt, ' = ');
      stmt.value.visitExpression(this, ctx);
    }
    ctx.println(stmt, `;`);
    return null;
  }
  visitCastExpr(ast: o.CastExpr, ctx: EmitterVisitorContext): any {
    ast.value.visitExpression(this, ctx);
    return null;
  }
  visitInvokeFunctionExpr(expr: o.InvokeFunctionExpr, ctx: EmitterVisitorContext): string|null {
    const fnExpr = expr.fn;
    if (fnExpr instanceof o.ReadVarExpr && fnExpr.builtin === o.BuiltinVar.Super) {
      ctx.currentClass !.parent !.visitExpression(this, ctx);
      ctx.print(expr, `.call(this`);
      if (expr.args.length > 0) {
        ctx.print(expr, `, `);
        this.visitAllExpressions(expr.args, ctx, ',');
      }
      ctx.print(expr, `)`);
    } else {
      super.visitInvokeFunctionExpr(expr, ctx);
    }
    return null;
  }
  visitFunctionExpr(ast: o.FunctionExpr, ctx: EmitterVisitorContext): any {
    ctx.print(ast, `function${ast.name ? ' ' + ast.name : ''}(`);
    this._visitParams(ast.params, ctx);
    ctx.println(ast, `) {`);
    ctx.incIndent();
    this.visitAllStatements(ast.statements, ctx);
    ctx.decIndent();
    ctx.print(ast, `}`);
    return null;
  }
  visitDeclareFunctionStmt(stmt: o.DeclareFunctionStmt, ctx: EmitterVisitorContext): any {
    ctx.print(stmt, `function ${stmt.name}(`);
    this._visitParams(stmt.params, ctx);
    ctx.println(stmt, `) {`);
    ctx.incIndent();
    this.visitAllStatements(stmt.statements, ctx);
    ctx.decIndent();
    ctx.println(stmt, `}`);
    return null;
  }
  visitTryCatchStmt(stmt: o.TryCatchStmt, ctx: EmitterVisitorContext): any {
    ctx.println(stmt, `try {`);
    ctx.incIndent();
    this.visitAllStatements(stmt.bodyStmts, ctx);
    ctx.decIndent();
    ctx.println(stmt, `} catch (${CATCH_ERROR_VAR.name}) {`);
    ctx.incIndent();
    const catchStmts =
        [<o.Statement>CATCH_STACK_VAR.set(CATCH_ERROR_VAR.prop('stack')).toDeclStmt(null, [
          o.StmtModifier.Final
        ])].concat(stmt.catchStmts);
    this.visitAllStatements(catchStmts, ctx);
    ctx.decIndent();
    ctx.println(stmt, `}`);
    return null;
  }

  private _visitParams(params: o.FnParam[], ctx: EmitterVisitorContext): void {
    this.visitAllObjects(param => ctx.print(null, param.name), params, ctx, ',');
  }

  getBuiltinMethodName(method: o.BuiltinMethod): string {
    let name: string;
    switch (method) {
      case o.BuiltinMethod.ConcatArray:
        name = 'concat';
        break;
      case o.BuiltinMethod.SubscribeObservable:
        name = 'subscribe';
        break;
      case o.BuiltinMethod.Bind:
        name = 'bind';
        break;
      default:
        throw new Error(`Unknown builtin method: ${method}`);
    }
    return name;
  }
}
