import {isPresent, isBlank, FunctionWrapper, 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 {EventBinding} from './event_binding';
import {BindingRecord, BindingTarget} from './binding_record';
import {DirectiveRecord, DirectiveIndex} from './directive_record';
import {Locals} from './parser/locals';
import {ChangeDispatcher, ChangeDetectorGenConfig} from './interfaces';
import {ChangeDetectionUtil, SimpleChange} from './change_detection_util';
import {ChangeDetectionStrategy, ChangeDetectorState} from './constants';
import {ProtoRecord, RecordType} from './proto_record';
import {reflector} from 'angular2/src/core/reflection/reflection';
import {ObservableWrapper} from 'angular2/src/facade/async';

export class DynamicChangeDetector extends AbstractChangeDetector<any> {
  values: any[];
  changes: any[];
  localPipes: any[];
  prevContexts: any[];

  constructor(id: string, numberOfPropertyProtoRecords: number,
              propertyBindingTargets: BindingTarget[], directiveIndices: DirectiveIndex[],
              strategy: ChangeDetectionStrategy, private _records: ProtoRecord[],
              private _eventBindings: EventBinding[], private _directiveRecords: DirectiveRecord[],
              private _genConfig: ChangeDetectorGenConfig) {
    super(id, numberOfPropertyProtoRecords, propertyBindingTargets, directiveIndices, strategy);
    var len = _records.length + 1;
    this.values = ListWrapper.createFixedSize(len);
    this.localPipes = ListWrapper.createFixedSize(len);
    this.prevContexts = ListWrapper.createFixedSize(len);
    this.changes = ListWrapper.createFixedSize(len);

    this.dehydrateDirectives(false);
  }

  handleEventInternal(eventName: string, elIndex: number, locals: Locals): boolean {
    var preventDefault = false;

    this._matchingEventBindings(eventName, elIndex)
        .forEach(rec => {
          var res = this._processEventBinding(rec, locals);
          if (res === false) {
            preventDefault = true;
          }
        });

    return preventDefault;
  }

  /** @internal */
  _processEventBinding(eb: EventBinding, locals: Locals): any {
    var values = ListWrapper.createFixedSize(eb.records.length);
    values[0] = this.values[0];

    for (var protoIdx = 0; protoIdx < eb.records.length; ++protoIdx) {
      var proto = eb.records[protoIdx];

      if (proto.isSkipRecord()) {
        protoIdx += this._computeSkipLength(protoIdx, proto, values);
      } else {
        if (proto.lastInBinding) {
          this._markPathAsCheckOnce(proto);
        }
        var res = this._calculateCurrValue(proto, values, locals);
        if (proto.lastInBinding) {
          return res;
        } else {
          this._writeSelf(proto, res, values);
        }
      }
    }

    throw new BaseException("Cannot be reached");
  }

  private _computeSkipLength(protoIndex: number, proto: ProtoRecord, values: any[]): number {
    if (proto.mode === RecordType.SkipRecords) {
      return proto.fixedArgs[0] - protoIndex - 1;
    }

    if (proto.mode === RecordType.SkipRecordsIf) {
      let condition = this._readContext(proto, values);
      return condition ? proto.fixedArgs[0] - protoIndex - 1 : 0;
    }

    if (proto.mode === RecordType.SkipRecordsIfNot) {
      let condition = this._readContext(proto, values);
      return condition ? 0 : proto.fixedArgs[0] - protoIndex - 1;
    }

    throw new BaseException("Cannot be reached");
  }

  /** @internal */
  _markPathAsCheckOnce(proto: ProtoRecord): void {
    if (!proto.bindingRecord.isDefaultChangeDetection()) {
      var dir = proto.bindingRecord.directiveRecord;
      this._getDetectorFor(dir.directiveIndex).markPathToRootAsCheckOnce();
    }
  }

  /** @internal */
  _matchingEventBindings(eventName: string, elIndex: number): EventBinding[] {
    return this._eventBindings.filter(eb => eb.eventName == eventName && eb.elIndex === elIndex);
  }

  hydrateDirectives(dispatcher: ChangeDispatcher): void {
    this.values[0] = this.context;
    this.dispatcher = dispatcher;

    this.outputSubscriptions = [];
    for (var i = 0; i < this._directiveRecords.length; ++i) {
      var r = this._directiveRecords[i];
      if (isPresent(r.outputs)) {
        r.outputs.forEach(output => {
          var eventHandler =
              <any>this._createEventHandler(r.directiveIndex.elementIndex, output[1]);
          var directive = this._getDirectiveFor(r.directiveIndex);
          var getter = reflector.getter(output[0]);
          this.outputSubscriptions.push(
              ObservableWrapper.subscribe(getter(directive), eventHandler));
        });
      }
    }
  }

  private _createEventHandler(boundElementIndex: number, eventName: string): Function {
    return (event) => this.handleEvent(eventName, boundElementIndex, event);
  }


  dehydrateDirectives(destroyPipes: boolean) {
    if (destroyPipes) {
      this._destroyPipes();
      this._destroyDirectives();
    }
    this.values[0] = null;
    ListWrapper.fill(this.values, ChangeDetectionUtil.uninitialized, 1);
    ListWrapper.fill(this.changes, false);
    ListWrapper.fill(this.localPipes, null);
    ListWrapper.fill(this.prevContexts, ChangeDetectionUtil.uninitialized);
  }

  /** @internal */
  _destroyPipes() {
    for (var i = 0; i < this.localPipes.length; ++i) {
      if (isPresent(this.localPipes[i])) {
        ChangeDetectionUtil.callPipeOnDestroy(this.localPipes[i]);
      }
    }
  }

  /** @internal */
  _destroyDirectives() {
    for (var i = 0; i < this._directiveRecords.length; ++i) {
      var record = this._directiveRecords[i];
      if (record.callOnDestroy) {
        this._getDirectiveFor(record.directiveIndex).ngOnDestroy();
      }
    }
  }

  checkNoChanges(): void { this.runDetectChanges(true); }

  detectChangesInRecordsInternal(throwOnChange: boolean) {
    var protos = this._records;

    var changes = null;
    var isChanged = false;
    for (var protoIdx = 0; protoIdx < protos.length; ++protoIdx) {
      var proto: ProtoRecord = protos[protoIdx];
      var bindingRecord = proto.bindingRecord;
      var directiveRecord = bindingRecord.directiveRecord;

      if (this._firstInBinding(proto)) {
        this.propertyBindingIndex = proto.propertyBindingIndex;
      }

      if (proto.isLifeCycleRecord()) {
        if (proto.name === "DoCheck" && !throwOnChange) {
          this._getDirectiveFor(directiveRecord.directiveIndex).ngDoCheck();
        } else if (proto.name === "OnInit" && !throwOnChange &&
                   this.state == ChangeDetectorState.NeverChecked) {
          this._getDirectiveFor(directiveRecord.directiveIndex).ngOnInit();
        } else if (proto.name === "OnChanges" && isPresent(changes) && !throwOnChange) {
          this._getDirectiveFor(directiveRecord.directiveIndex).ngOnChanges(changes);
        }
      } else if (proto.isSkipRecord()) {
        protoIdx += this._computeSkipLength(protoIdx, proto, this.values);
      } else {
        var change = this._check(proto, throwOnChange, this.values, this.locals);
        if (isPresent(change)) {
          this._updateDirectiveOrElement(change, bindingRecord);
          isChanged = true;
          changes = this._addChange(bindingRecord, change, changes);
        }
      }

      if (proto.lastInDirective) {
        changes = null;
        if (isChanged && !bindingRecord.isDefaultChangeDetection()) {
          this._getDetectorFor(directiveRecord.directiveIndex).markAsCheckOnce();
        }

        isChanged = false;
      }
    }
  }

  /** @internal */
  _firstInBinding(r: ProtoRecord): boolean {
    var prev = ChangeDetectionUtil.protoByIndex(this._records, r.selfIndex - 1);
    return isBlank(prev) || prev.bindingRecord !== r.bindingRecord;
  }

  afterContentLifecycleCallbacksInternal() {
    var dirs = this._directiveRecords;
    for (var i = dirs.length - 1; i >= 0; --i) {
      var dir = dirs[i];
      if (dir.callAfterContentInit && this.state == ChangeDetectorState.NeverChecked) {
        this._getDirectiveFor(dir.directiveIndex).ngAfterContentInit();
      }

      if (dir.callAfterContentChecked) {
        this._getDirectiveFor(dir.directiveIndex).ngAfterContentChecked();
      }
    }
  }

  afterViewLifecycleCallbacksInternal() {
    var dirs = this._directiveRecords;
    for (var i = dirs.length - 1; i >= 0; --i) {
      var dir = dirs[i];
      if (dir.callAfterViewInit && this.state == ChangeDetectorState.NeverChecked) {
        this._getDirectiveFor(dir.directiveIndex).ngAfterViewInit();
      }
      if (dir.callAfterViewChecked) {
        this._getDirectiveFor(dir.directiveIndex).ngAfterViewChecked();
      }
    }
  }

  /** @internal */
  private _updateDirectiveOrElement(change, bindingRecord) {
    if (isBlank(bindingRecord.directiveRecord)) {
      super.notifyDispatcher(change.currentValue);
    } else {
      var directiveIndex = bindingRecord.directiveRecord.directiveIndex;
      bindingRecord.setter(this._getDirectiveFor(directiveIndex), change.currentValue);
    }

    if (this._genConfig.logBindingUpdate) {
      super.logBindingUpdate(change.currentValue);
    }
  }

  /** @internal */
  private _addChange(bindingRecord: BindingRecord, change, changes) {
    if (bindingRecord.callOnChanges()) {
      return super.addChange(changes, change.previousValue, change.currentValue);
    } else {
      return changes;
    }
  }

  /** @internal */
  private _getDirectiveFor(directiveIndex: DirectiveIndex) {
    return this.dispatcher.getDirectiveFor(directiveIndex);
  }

  /** @internal */
  private _getDetectorFor(directiveIndex: DirectiveIndex) {
    return this.dispatcher.getDetectorFor(directiveIndex);
  }

  /** @internal */
  private _check(proto: ProtoRecord, throwOnChange: boolean, values: any[],
                 locals: Locals): SimpleChange {
    if (proto.isPipeRecord()) {
      return this._pipeCheck(proto, throwOnChange, values);
    } else {
      return this._referenceCheck(proto, throwOnChange, values, locals);
    }
  }

  /** @internal */
  private _referenceCheck(proto: ProtoRecord, throwOnChange: boolean, values: any[],
                          locals: Locals) {
    if (this._pureFuncAndArgsDidNotChange(proto)) {
      this._setChanged(proto, false);
      return null;
    }

    var currValue = this._calculateCurrValue(proto, values, locals);

    if (proto.shouldBeChecked()) {
      var prevValue = this._readSelf(proto, values);
      var detectedChange = throwOnChange ?
                               !ChangeDetectionUtil.devModeEqual(prevValue, currValue) :
                               ChangeDetectionUtil.looseNotIdentical(prevValue, currValue);
      if (detectedChange) {
        if (proto.lastInBinding) {
          var change = ChangeDetectionUtil.simpleChange(prevValue, currValue);
          if (throwOnChange) this.throwOnChangeError(prevValue, currValue);

          this._writeSelf(proto, currValue, values);
          this._setChanged(proto, true);
          return change;
        } else {
          this._writeSelf(proto, currValue, values);
          this._setChanged(proto, true);
          return null;
        }
      } else {
        this._setChanged(proto, false);
        return null;
      }

    } else {
      this._writeSelf(proto, currValue, values);
      this._setChanged(proto, true);
      return null;
    }
  }

  private _calculateCurrValue(proto: ProtoRecord, values: any[], locals: Locals) {
    switch (proto.mode) {
      case RecordType.Self:
        return this._readContext(proto, values);

      case RecordType.Const:
        return proto.funcOrValue;

      case RecordType.PropertyRead:
        var context = this._readContext(proto, values);
        return proto.funcOrValue(context);

      case RecordType.SafeProperty:
        var context = this._readContext(proto, values);
        return isBlank(context) ? null : proto.funcOrValue(context);

      case RecordType.PropertyWrite:
        var context = this._readContext(proto, values);
        var value = this._readArgs(proto, values)[0];
        proto.funcOrValue(context, value);
        return value;

      case RecordType.KeyedWrite:
        var context = this._readContext(proto, values);
        var key = this._readArgs(proto, values)[0];
        var value = this._readArgs(proto, values)[1];
        context[key] = value;
        return value;

      case RecordType.Local:
        return locals.get(proto.name);

      case RecordType.InvokeMethod:
        var context = this._readContext(proto, values);
        var args = this._readArgs(proto, values);
        return proto.funcOrValue(context, args);

      case RecordType.SafeMethodInvoke:
        var context = this._readContext(proto, values);
        if (isBlank(context)) {
          return null;
        }
        var args = this._readArgs(proto, values);
        return proto.funcOrValue(context, args);

      case RecordType.KeyedRead:
        var arg = this._readArgs(proto, values)[0];
        return this._readContext(proto, values)[arg];

      case RecordType.Chain:
        var args = this._readArgs(proto, values);
        return args[args.length - 1];

      case RecordType.InvokeClosure:
        return FunctionWrapper.apply(this._readContext(proto, values),
                                     this._readArgs(proto, values));

      case RecordType.Interpolate:
      case RecordType.PrimitiveOp:
      case RecordType.CollectionLiteral:
        return FunctionWrapper.apply(proto.funcOrValue, this._readArgs(proto, values));

      default:
        throw new BaseException(`Unknown operation ${proto.mode}`);
    }
  }

  private _pipeCheck(proto: ProtoRecord, throwOnChange: boolean, values: any[]) {
    var context = this._readContext(proto, values);
    var selectedPipe = this._pipeFor(proto, context);
    if (!selectedPipe.pure || this._argsOrContextChanged(proto)) {
      var args = this._readArgs(proto, values);
      var currValue = selectedPipe.pipe.transform(context, args);

      if (proto.shouldBeChecked()) {
        var prevValue = this._readSelf(proto, values);
        var detectedChange = throwOnChange ?
                                 !ChangeDetectionUtil.devModeEqual(prevValue, currValue) :
                                 ChangeDetectionUtil.looseNotIdentical(prevValue, currValue);
        if (detectedChange) {
          currValue = ChangeDetectionUtil.unwrapValue(currValue);

          if (proto.lastInBinding) {
            var change = ChangeDetectionUtil.simpleChange(prevValue, currValue);
            if (throwOnChange) this.throwOnChangeError(prevValue, currValue);

            this._writeSelf(proto, currValue, values);
            this._setChanged(proto, true);

            return change;

          } else {
            this._writeSelf(proto, currValue, values);
            this._setChanged(proto, true);
            return null;
          }
        } else {
          this._setChanged(proto, false);
          return null;
        }
      } else {
        this._writeSelf(proto, currValue, values);
        this._setChanged(proto, true);
        return null;
      }
    }
  }

  private _pipeFor(proto: ProtoRecord, context) {
    var storedPipe = this._readPipe(proto);
    if (isPresent(storedPipe)) return storedPipe;

    var pipe = this.pipes.get(proto.name);
    this._writePipe(proto, pipe);
    return pipe;
  }

  private _readContext(proto: ProtoRecord, values: any[]) {
    if (proto.contextIndex == -1) {
      return this._getDirectiveFor(proto.directiveIndex);
    }
    return values[proto.contextIndex];
  }

  private _readSelf(proto: ProtoRecord, values: any[]) { return values[proto.selfIndex]; }

  private _writeSelf(proto: ProtoRecord, value, values: any[]) { values[proto.selfIndex] = value; }

  private _readPipe(proto: ProtoRecord) { return this.localPipes[proto.selfIndex]; }

  private _writePipe(proto: ProtoRecord, value) { this.localPipes[proto.selfIndex] = value; }

  private _setChanged(proto: ProtoRecord, value: boolean) {
    if (proto.argumentToPureFunction) this.changes[proto.selfIndex] = value;
  }

  private _pureFuncAndArgsDidNotChange(proto: ProtoRecord): boolean {
    return proto.isPureFunction() && !this._argsChanged(proto);
  }

  private _argsChanged(proto: ProtoRecord): boolean {
    var args = proto.args;
    for (var i = 0; i < args.length; ++i) {
      if (this.changes[args[i]]) {
        return true;
      }
    }
    return false;
  }

  private _argsOrContextChanged(proto: ProtoRecord): boolean {
    return this._argsChanged(proto) || this.changes[proto.contextIndex];
  }

  private _readArgs(proto: ProtoRecord, values: any[]) {
    var res = ListWrapper.createFixedSize(proto.args.length);
    var args = proto.args;
    for (var i = 0; i < args.length; ++i) {
      res[i] = values[args[i]];
    }
    return res;
  }
}
