import {Type, assertionsEnabled, isBlank, isPresent, StringWrapper} from 'angular2/src/facade/lang';
import {BaseException} from 'angular2/src/facade/exceptions';
import {ListWrapper, MapWrapper, StringMapWrapper} from 'angular2/src/facade/collection';

import {AbstractChangeDetector} from './abstract_change_detector';
import {ChangeDetectionUtil} from './change_detection_util';
import {DirectiveIndex, DirectiveRecord} from './directive_record';

import {ProtoRecord, RecordType} from './proto_record';
import {CodegenNameUtil, sanitizeName} from './codegen_name_util';
import {CodegenLogicUtil} from './codegen_logic_util';
import {codify} from './codegen_facade';
import {EventBinding} from './event_binding';
import {BindingTarget} from './binding_record';
import {ChangeDetectorGenConfig, ChangeDetectorDefinition} from './interfaces';
import {ChangeDetectionStrategy, ChangeDetectorState} from './constants';
import {createPropertyRecords, createEventRecords} from './proto_change_detector';

/**
 * The code generator takes a list of proto records and creates a function/class
 * that "emulates" what the developer would write by hand to implement the same
 * kind of behaviour.
 *
 * This code should be kept in sync with the Dart transformer's
 * `angular2.transform.template_compiler.change_detector_codegen` library. If you make updates
 * here, please make equivalent changes there.
*/
const IS_CHANGED_LOCAL = "isChanged";
const CHANGES_LOCAL = "changes";

export class ChangeDetectorJITGenerator {
  private _logic: CodegenLogicUtil;
  private _names: CodegenNameUtil;
  private _endOfBlockIdxs: number[];
  private id: string;
  private changeDetectionStrategy: ChangeDetectionStrategy;
  private records: ProtoRecord[];
  private propertyBindingTargets: BindingTarget[];
  private eventBindings: EventBinding[];
  private directiveRecords: any[];
  private genConfig: ChangeDetectorGenConfig;
  typeName: string;

  constructor(definition: ChangeDetectorDefinition, private changeDetectionUtilVarName: string,
              private abstractChangeDetectorVarName: string,
              private changeDetectorStateVarName: string) {
    var propertyBindingRecords = createPropertyRecords(definition);
    var eventBindingRecords = createEventRecords(definition);
    var propertyBindingTargets = definition.bindingRecords.map(b => b.target);
    this.id = definition.id;
    this.changeDetectionStrategy = definition.strategy;
    this.genConfig = definition.genConfig;

    this.records = propertyBindingRecords;
    this.propertyBindingTargets = propertyBindingTargets;
    this.eventBindings = eventBindingRecords;
    this.directiveRecords = definition.directiveRecords;
    this._names = new CodegenNameUtil(this.records, this.eventBindings, this.directiveRecords,
                                      this.changeDetectionUtilVarName);
    this._logic =
        new CodegenLogicUtil(this._names, this.changeDetectionUtilVarName,
                             this.changeDetectorStateVarName, this.changeDetectionStrategy);
    this.typeName = sanitizeName(`ChangeDetector_${this.id}`);
  }

  generate(): Function {
    var factorySource = `
      ${this.generateSource()}
      return function() {
        return new ${this.typeName}();
      }
    `;
    return new Function(this.abstractChangeDetectorVarName, this.changeDetectionUtilVarName,
                        this.changeDetectorStateVarName, factorySource)(
        AbstractChangeDetector, ChangeDetectionUtil, ChangeDetectorState);
  }

  generateSource(): string {
    return `
      var ${this.typeName} = function ${this.typeName}() {
        ${this.abstractChangeDetectorVarName}.call(
            this, ${JSON.stringify(this.id)}, ${this.records.length},
            ${this.typeName}.gen_propertyBindingTargets, ${this.typeName}.gen_directiveIndices,
            ${codify(this.changeDetectionStrategy)});
        this.dehydrateDirectives(false);
      }

      ${this.typeName}.prototype = Object.create(${this.abstractChangeDetectorVarName}.prototype);

      ${this.typeName}.prototype.detectChangesInRecordsInternal = function(throwOnChange) {
        ${this._names.genInitLocals()}
        var ${IS_CHANGED_LOCAL} = false;
        var ${CHANGES_LOCAL} = null;

        ${this._genAllRecords(this.records)}
      }

      ${this._maybeGenHandleEventInternal()}

      ${this._maybeGenAfterContentLifecycleCallbacks()}

      ${this._maybeGenAfterViewLifecycleCallbacks()}

      ${this._maybeGenHydrateDirectives()}

      ${this._maybeGenDehydrateDirectives()}

      ${this._genPropertyBindingTargets()}

      ${this._genDirectiveIndices()}
    `;
  }

  /** @internal */
  _genPropertyBindingTargets(): string {
    var targets = this._logic.genPropertyBindingTargets(this.propertyBindingTargets,
                                                        this.genConfig.genDebugInfo);
    return `${this.typeName}.gen_propertyBindingTargets = ${targets};`;
  }

  /** @internal */
  _genDirectiveIndices(): string {
    var indices = this._logic.genDirectiveIndices(this.directiveRecords);
    return `${this.typeName}.gen_directiveIndices = ${indices};`;
  }

  /** @internal */
  _maybeGenHandleEventInternal(): string {
    if (this.eventBindings.length > 0) {
      var handlers = this.eventBindings.map(eb => this._genEventBinding(eb)).join("\n");
      return `
        ${this.typeName}.prototype.handleEventInternal = function(eventName, elIndex, locals) {
          var ${this._names.getPreventDefaultAccesor()} = false;
          ${this._names.genInitEventLocals()}
          ${handlers}
          return ${this._names.getPreventDefaultAccesor()};
        }
      `;
    } else {
      return '';
    }
  }

  /** @internal */
  _genEventBinding(eb: EventBinding): string {
    let codes: String[] = [];
    this._endOfBlockIdxs = [];

    ListWrapper.forEachWithIndex(eb.records, (r, i) => {
      let code;

      if (r.isConditionalSkipRecord()) {
        code = this._genConditionalSkip(r, this._names.getEventLocalName(eb, i));
      } else if (r.isUnconditionalSkipRecord()) {
        code = this._genUnconditionalSkip(r);
      } else {
        code = this._genEventBindingEval(eb, r);
      }

      code += this._genEndOfSkipBlock(i);

      codes.push(code);
    });

    return `
    if (eventName === "${eb.eventName}" && elIndex === ${eb.elIndex}) {
      ${codes.join("\n")}
    }`;
  }

  /** @internal */
  _genEventBindingEval(eb: EventBinding, r: ProtoRecord): string {
    if (r.lastInBinding) {
      var evalRecord = this._logic.genEventBindingEvalValue(eb, r);
      var markPath = this._genMarkPathToRootAsCheckOnce(r);
      var prevDefault = this._genUpdatePreventDefault(eb, r);
      return `${evalRecord}\n${markPath}\n${prevDefault}`;
    } else {
      return this._logic.genEventBindingEvalValue(eb, r);
    }
  }

  /** @internal */
  _genMarkPathToRootAsCheckOnce(r: ProtoRecord): string {
    var br = r.bindingRecord;
    if (br.isDefaultChangeDetection()) {
      return "";
    } else {
      return `${this._names.getDetectorName(br.directiveRecord.directiveIndex)}.markPathToRootAsCheckOnce();`;
    }
  }

  /** @internal */
  _genUpdatePreventDefault(eb: EventBinding, r: ProtoRecord): string {
    var local = this._names.getEventLocalName(eb, r.selfIndex);
    return `if (${local} === false) { ${this._names.getPreventDefaultAccesor()} = true};`;
  }

  /** @internal */
  _maybeGenDehydrateDirectives(): string {
    var destroyPipesCode = this._names.genPipeOnDestroy();
    var destroyDirectivesCode = this._logic.genDirectivesOnDestroy(this.directiveRecords);
    var dehydrateFieldsCode = this._names.genDehydrateFields();
    if (!destroyPipesCode && !destroyDirectivesCode && !dehydrateFieldsCode) return '';
    return `${this.typeName}.prototype.dehydrateDirectives = function(destroyPipes) {
        if (destroyPipes) {
          ${destroyPipesCode}
          ${destroyDirectivesCode}
        }
        ${dehydrateFieldsCode}
    }`;
  }

  /** @internal */
  _maybeGenHydrateDirectives(): string {
    var hydrateDirectivesCode = this._logic.genHydrateDirectives(this.directiveRecords);
    var hydrateDetectorsCode = this._logic.genHydrateDetectors(this.directiveRecords);
    if (!hydrateDirectivesCode && !hydrateDetectorsCode) return '';
    return `${this.typeName}.prototype.hydrateDirectives = function(directives) {
      ${hydrateDirectivesCode}
      ${hydrateDetectorsCode}
    }`;
  }

  /** @internal */
  _maybeGenAfterContentLifecycleCallbacks(): string {
    var notifications = this._logic.genContentLifecycleCallbacks(this.directiveRecords);
    if (notifications.length > 0) {
      var directiveNotifications = notifications.join("\n");
      return `
        ${this.typeName}.prototype.afterContentLifecycleCallbacksInternal = function() {
          ${directiveNotifications}
        }
      `;
    } else {
      return '';
    }
  }

  /** @internal */
  _maybeGenAfterViewLifecycleCallbacks(): string {
    var notifications = this._logic.genViewLifecycleCallbacks(this.directiveRecords);
    if (notifications.length > 0) {
      var directiveNotifications = notifications.join("\n");
      return `
        ${this.typeName}.prototype.afterViewLifecycleCallbacksInternal = function() {
          ${directiveNotifications}
        }
      `;
    } else {
      return '';
    }
  }

  /** @internal */
  _genAllRecords(rs: ProtoRecord[]): string {
    var codes: String[] = [];
    this._endOfBlockIdxs = [];

    for (let i = 0; i < rs.length; i++) {
      let code;
      let r = rs[i];

      if (r.isLifeCycleRecord()) {
        code = this._genDirectiveLifecycle(r);
      } else if (r.isPipeRecord()) {
        code = this._genPipeCheck(r);
      } else if (r.isConditionalSkipRecord()) {
        code = this._genConditionalSkip(r, this._names.getLocalName(r.contextIndex));
      } else if (r.isUnconditionalSkipRecord()) {
        code = this._genUnconditionalSkip(r);
      } else {
        code = this._genReferenceCheck(r);
      }

      code = `
        ${this._maybeFirstInBinding(r)}
        ${code}
        ${this._maybeGenLastInDirective(r)}
        ${this._genEndOfSkipBlock(i)}
      `;

      codes.push(code);
    }

    return codes.join("\n");
  }

  /** @internal */
  _genConditionalSkip(r: ProtoRecord, condition: string): string {
    let maybeNegate = r.mode === RecordType.SkipRecordsIf ? '!' : '';
    this._endOfBlockIdxs.push(r.fixedArgs[0] - 1);

    return `if (${maybeNegate}${condition}) {`;
  }

  /** @internal */
  _genUnconditionalSkip(r: ProtoRecord): string {
    this._endOfBlockIdxs.pop();
    this._endOfBlockIdxs.push(r.fixedArgs[0] - 1);
    return `} else {`;
  }

  /** @internal */
  _genEndOfSkipBlock(protoIndex: number): string {
    if (!ListWrapper.isEmpty(this._endOfBlockIdxs)) {
      let endOfBlock = ListWrapper.last(this._endOfBlockIdxs);
      if (protoIndex === endOfBlock) {
        this._endOfBlockIdxs.pop();
        return '}';
      }
    }

    return '';
  }

  /** @internal */
  _genDirectiveLifecycle(r: ProtoRecord): string {
    if (r.name === "DoCheck") {
      return this._genOnCheck(r);
    } else if (r.name === "OnInit") {
      return this._genOnInit(r);
    } else if (r.name === "OnChanges") {
      return this._genOnChange(r);
    } else {
      throw new BaseException(`Unknown lifecycle event '${r.name}'`);
    }
  }

  /** @internal */
  _genPipeCheck(r: ProtoRecord): string {
    var context = this._names.getLocalName(r.contextIndex);
    var argString = r.args.map((arg) => this._names.getLocalName(arg)).join(", ");

    var oldValue = this._names.getFieldName(r.selfIndex);
    var newValue = this._names.getLocalName(r.selfIndex);

    var pipe = this._names.getPipeName(r.selfIndex);
    var pipeName = r.name;

    var init = `
      if (${pipe} === ${this.changeDetectionUtilVarName}.uninitialized) {
        ${pipe} = ${this._names.getPipesAccessorName()}.get('${pipeName}');
      }
    `;
    var read = `${newValue} = ${pipe}.pipe.transform(${context}, [${argString}]);`;

    var contexOrArgCheck = r.args.map((a) => this._names.getChangeName(a));
    contexOrArgCheck.push(this._names.getChangeName(r.contextIndex));
    var condition = `!${pipe}.pure || (${contexOrArgCheck.join(" || ")})`;

    var check = `
      ${this._genThrowOnChangeCheck(oldValue, newValue)}
      if (${this.changeDetectionUtilVarName}.looseNotIdentical(${oldValue}, ${newValue})) {
        ${newValue} = ${this.changeDetectionUtilVarName}.unwrapValue(${newValue})
        ${this._genChangeMarker(r)}
        ${this._genUpdateDirectiveOrElement(r)}
        ${this._genAddToChanges(r)}
        ${oldValue} = ${newValue};
      }
    `;

    var genCode = r.shouldBeChecked() ? `${read}${check}` : read;

    if (r.isUsedByOtherRecord()) {
      return `${init} if (${condition}) { ${genCode} } else { ${newValue} = ${oldValue}; }`;
    } else {
      return `${init} if (${condition}) { ${genCode} }`;
    }
  }

  /** @internal */
  _genReferenceCheck(r: ProtoRecord): string {
    var oldValue = this._names.getFieldName(r.selfIndex);
    var newValue = this._names.getLocalName(r.selfIndex);
    var read = `
      ${this._logic.genPropertyBindingEvalValue(r)}
    `;

    var check = `
      ${this._genThrowOnChangeCheck(oldValue, newValue)}
      if (${this.changeDetectionUtilVarName}.looseNotIdentical(${oldValue}, ${newValue})) {
        ${this._genChangeMarker(r)}
        ${this._genUpdateDirectiveOrElement(r)}
        ${this._genAddToChanges(r)}
        ${oldValue} = ${newValue};
      }
    `;

    var genCode = r.shouldBeChecked() ? `${read}${check}` : read;

    if (r.isPureFunction()) {
      var condition = r.args.map((a) => this._names.getChangeName(a)).join(" || ");
      if (r.isUsedByOtherRecord()) {
        return `if (${condition}) { ${genCode} } else { ${newValue} = ${oldValue}; }`;
      } else {
        return `if (${condition}) { ${genCode} }`;
      }
    } else {
      return genCode;
    }
  }

  /** @internal */
  _genChangeMarker(r: ProtoRecord): string {
    return r.argumentToPureFunction ? `${this._names.getChangeName(r.selfIndex)} = true` : ``;
  }

  /** @internal */
  _genUpdateDirectiveOrElement(r: ProtoRecord): string {
    if (!r.lastInBinding) return "";

    var newValue = this._names.getLocalName(r.selfIndex);
    var notifyDebug = this.genConfig.logBindingUpdate ? `this.logBindingUpdate(${newValue});` : "";

    var br = r.bindingRecord;
    if (br.target.isDirective()) {
      var directiveProperty =
          `${this._names.getDirectiveName(br.directiveRecord.directiveIndex)}.${br.target.name}`;
      return `
        ${directiveProperty} = ${newValue};
        ${notifyDebug}
        ${IS_CHANGED_LOCAL} = true;
      `;
    } else {
      return `
        this.notifyDispatcher(${newValue});
        ${notifyDebug}
      `;
    }
  }

  /** @internal */
  _genThrowOnChangeCheck(oldValue: string, newValue: string): string {
    if (assertionsEnabled()) {
      return `
        if (throwOnChange && !${this.changeDetectionUtilVarName}.devModeEqual(${oldValue}, ${newValue})) {
          this.throwOnChangeError(${oldValue}, ${newValue});
        }
        `;
    } else {
      return '';
    }
  }

  /** @internal */
  _genAddToChanges(r: ProtoRecord): string {
    var newValue = this._names.getLocalName(r.selfIndex);
    var oldValue = this._names.getFieldName(r.selfIndex);
    if (!r.bindingRecord.callOnChanges()) return "";
    return `${CHANGES_LOCAL} = this.addChange(${CHANGES_LOCAL}, ${oldValue}, ${newValue});`;
  }

  /** @internal */
  _maybeFirstInBinding(r: ProtoRecord): string {
    var prev = ChangeDetectionUtil.protoByIndex(this.records, r.selfIndex - 1);
    var firstInBinding = isBlank(prev) || prev.bindingRecord !== r.bindingRecord;
    return firstInBinding && !r.bindingRecord.isDirectiveLifecycle() ?
               `${this._names.getPropertyBindingIndex()} = ${r.propertyBindingIndex};` :
               '';
  }

  /** @internal */
  _maybeGenLastInDirective(r: ProtoRecord): string {
    if (!r.lastInDirective) return "";
    return `
      ${CHANGES_LOCAL} = null;
      ${this._genNotifyOnPushDetectors(r)}
      ${IS_CHANGED_LOCAL} = false;
    `;
  }

  /** @internal */
  _genOnCheck(r: ProtoRecord): string {
    var br = r.bindingRecord;
    return `if (!throwOnChange) ${this._names.getDirectiveName(br.directiveRecord.directiveIndex)}.ngDoCheck();`;
  }

  /** @internal */
  _genOnInit(r: ProtoRecord): string {
    var br = r.bindingRecord;
    return `if (!throwOnChange && ${this._names.getStateName()} === ${this.changeDetectorStateVarName}.NeverChecked) ${this._names.getDirectiveName(br.directiveRecord.directiveIndex)}.ngOnInit();`;
  }

  /** @internal */
  _genOnChange(r: ProtoRecord): string {
    var br = r.bindingRecord;
    return `if (!throwOnChange && ${CHANGES_LOCAL}) ${this._names.getDirectiveName(br.directiveRecord.directiveIndex)}.ngOnChanges(${CHANGES_LOCAL});`;
  }

  /** @internal */
  _genNotifyOnPushDetectors(r: ProtoRecord): string {
    var br = r.bindingRecord;
    if (!r.lastInDirective || br.isDefaultChangeDetection()) return "";
    var retVal = `
      if(${IS_CHANGED_LOCAL}) {
        ${this._names.getDetectorName(br.directiveRecord.directiveIndex)}.markAsCheckOnce();
      }
    `;
    return retVal;
  }
}
