import {Type, isBlank, isPresent, StringWrapper} from 'angular2/src/core/facade/lang';
import {BaseException} from 'angular2/src/core/facade/exceptions';
import {ListWrapper, MapWrapper, StringMapWrapper} from 'angular2/src/core/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} 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 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) {
    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.changeDetectionStrategy);
    this.typeName = sanitizeName(`ChangeDetector_${this.id}`);
  }

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

  generateSource(): string {
    return `
      var ${this.typeName} = function ${this.typeName}(dispatcher) {
        ${this.abstractChangeDetectorVarName}.call(
            this, ${JSON.stringify(this.id)}, dispatcher, ${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.records.map((r) => this._genRecord(r)).join("\n")}
      }

      ${this._maybeGenHandleEventInternal()}

      ${this._genCheckNoChanges()}

      ${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 {
    var recs = eb.records.map(r => this._genEventBindingEval(eb, r)).join("\n");
    return `
    if (eventName === "${eb.eventName}" && elIndex === ${eb.elIndex}) {
      ${recs}
    }`;
  }

  /** @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();
    if (destroyPipesCode) {
      destroyPipesCode = `if (destroyPipes) { ${destroyPipesCode} }`;
    }
    var dehydrateFieldsCode = this._names.genDehydrateFields();
    if (!destroyPipesCode && !dehydrateFieldsCode) return '';
    return `${this.typeName}.prototype.dehydrateDirectives = function(destroyPipes) {
        ${destroyPipesCode}
        ${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 */
  _genRecord(r: ProtoRecord): string {
    var rec;
    if (r.isLifeCycleRecord()) {
      rec = this._genDirectiveLifecycle(r);
    } else if (r.isPipeRecord()) {
      rec = this._genPipeCheck(r);
    } else {
      rec = this._genReferenceCheck(r);
    }
    return `
      ${this._maybeFirstInBinding(r)}
      ${rec}
      ${this._maybeGenLastInDirective(r)}
    `;
  }

  /** @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 = `
      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 = `
      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 oldValue = this._names.getFieldName(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 `
        ${this._genThrowOnChangeCheck(oldValue, newValue)}
        ${directiveProperty} = ${newValue};
        ${notifyDebug}
        ${IS_CHANGED_LOCAL} = true;
      `;
    } else {
      return `
        ${this._genThrowOnChangeCheck(oldValue, newValue)}
        this.notifyDispatcher(${newValue});
        ${notifyDebug}
      `;
    }
  }

  /** @internal */
  _genThrowOnChangeCheck(oldValue: string, newValue: string): string {
    if (this.genConfig.genCheckNoChanges) {
      return `
        if(throwOnChange) {
          this.throwOnChangeError(${oldValue}, ${newValue});
        }
        `;
    } else {
      return '';
    }
  }

  /** @internal */
  _genCheckNoChanges(): string {
    if (this.genConfig.genCheckNoChanges) {
      return `${this.typeName}.prototype.checkNoChanges = function() { this.runDetectChanges(true); }`;
    } 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 firstInBindng = isBlank(prev) || prev.bindingRecord !== r.bindingRecord;
    return firstInBindng && !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)}.doCheck();`;
  }

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

  /** @internal */
  _genOnChange(r: ProtoRecord): string {
    var br = r.bindingRecord;
    return `if (!throwOnChange && ${CHANGES_LOCAL}) ${this._names.getDirectiveName(br.directiveRecord.directiveIndex)}.onChanges(${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;
  }
}
