import {reflector} from 'angular2/src/core/reflection/reflection';
import {isPresent} from 'angular2/src/core/facade/lang';
import {getIntParameter, bindAction, microBenchmark} from 'angular2/src/test_lib/benchmark_util';
import {BrowserDomAdapter} from 'angular2/src/core/dom/browser_adapter';

import {
  Lexer,
  Parser,
  ChangeDispatcher,
  ChangeDetection,
  DebugContext,
  DynamicChangeDetection,
  JitChangeDetection,
  ChangeDetectorDefinition,
  ChangeDetectorGenConfig,
  BindingRecord,
  DirectiveRecord,
  DirectiveIndex
} from 'angular2/src/core/change_detection/change_detection';


// ---- SHARED

class Obj {
  field0;
  field1;
  field2;
  field3;
  field4;
  field5;
  field6;
  field7;
  field8;
  field9;


  setField(index, value) {
    switch (index) {
      case 0:
        this.field0 = value;
        break;
      case 1:
        this.field1 = value;
        break;
      case 2:
        this.field2 = value;
        break;
      case 3:
        this.field3 = value;
        break;
      case 4:
        this.field4 = value;
        break;
      case 5:
        this.field5 = value;
        break;
      case 6:
        this.field6 = value;
        break;
      case 7:
        this.field7 = value;
        break;
      case 8:
        this.field8 = value;
        break;
      case 9:
        this.field9 = value;
        break;
    }
  }

  getField(index) {
    switch (index) {
      case 0:
        return this.field0;
      case 1:
        return this.field1;
      case 2:
        return this.field2;
      case 3:
        return this.field3;
      case 4:
        return this.field4;
      case 5:
        return this.field5;
      case 6:
        return this.field6;
      case 7:
        return this.field7;
      case 8:
        return this.field8;
      case 9:
        return this.field9;
    }
  }
}

class Row {
  obj;
  targetObj;
  field0;
  field1;
  field2;
  field3;
  field4;
  field5;
  field6;
  field7;
  field8;
  field9;

  next;
}

function createObject() {
  var obj = new Obj();
  for (var i = 0; i < 10; ++i) {
    obj.setField(i, i);
  }
  return obj;
}

function changeObject(object) {
  for (var i = 0; i < 10; ++i) {
    object.setField(i, object.getField(i) + 1);
  }
}

function setUpReflector() {
  reflector.registerGetters({
    'field0': function(obj) { return obj.field0 },
    'field1': function(obj) { return obj.field1 },
    'field2': function(obj) { return obj.field2 },
    'field3': function(obj) { return obj.field3 },
    'field4': function(obj) { return obj.field4 },
    'field5': function(obj) { return obj.field5 },
    'field6': function(obj) { return obj.field6 },
    'field7': function(obj) { return obj.field7 },
    'field8': function(obj) { return obj.field8 },
    'field9': function(obj) { return obj.field9 }
  });
  reflector.registerSetters({
    'field0': function(obj, v) { return obj.field0 = v },
    'field1': function(obj, v) { return obj.field1 = v },
    'field2': function(obj, v) { return obj.field2 = v },
    'field3': function(obj, v) { return obj.field3 = v },
    'field4': function(obj, v) { return obj.field4 = v },
    'field5': function(obj, v) { return obj.field5 = v },
    'field6': function(obj, v) { return obj.field6 = v },
    'field7': function(obj, v) { return obj.field7 = v },
    'field8': function(obj, v) { return obj.field8 = v },
    'field9': function(obj, v) { return obj.field9 = v }
  });
}



// ---- BASELINE

function setUpBaseline(iterations, object) {
  function createRow(i) {
    var r = new Row();
    r.obj = object;
    r.targetObj = new Obj();
    return r;
  }

  var head = createRow(0);
  var current = head;
  for (var i = 1; i < iterations; i++) {
    var newRow = createRow(i);
    current.next = newRow;
    current = newRow;
  }
  return head;
}

function checkBaselineRow(r) {
  var obj = r.obj;

  if (obj.field0 !== r.field0) {
    r.field0 = obj.field0;
    r.targetObj.field0 = obj.field0;
  }
  if (obj.field1 !== r.field1) {
    r.field1 = obj.field1;
    r.targetObj.field1 = obj.field1;
  }
  if (obj.field2 !== r.field2) {
    r.field2 = obj.field2;
    r.targetObj.field2 = obj.field2;
  }
  if (obj.field3 !== r.field3) {
    r.field3 = obj.field3;
    r.targetObj.field3 = obj.field3;
  }
  if (obj.field4 !== r.field4) {
    r.field4 = obj.field4;
    r.targetObj.field4 = obj.field4;
  }
  if (obj.field5 !== r.field5) {
    r.field5 = obj.field5;
    r.targetObj.field5 = obj.field5;
  }
  if (obj.field6 !== r.field6) {
    r.field6 = obj.field6;
    r.targetObj.field6 = obj.field6;
  }
  if (obj.field7 !== r.field7) {
    r.field7 = obj.field7;
    r.targetObj.field7 = obj.field7;
  }
  if (obj.field8 !== r.field8) {
    r.field8 = obj.field8;
    r.targetObj.field8 = obj.field8;
  }
  if (obj.field9 !== r.field9) {
    r.field9 = obj.field9;
    r.targetObj.field9 = obj.field9;
  }
}

function runBaselineChangeDetection(baselineHead) {
  var current = baselineHead;
  while (isPresent(current)) {
    checkBaselineRow(current);
    current = current.next;
  }
}

function runBaselineReads(baselineHead, numberOfRuns) {
  for (var i = 0; i < numberOfRuns; ++i) {
    runBaselineChangeDetection(baselineHead);
  }
}

function runBaselineWrites(baselineHead, numberOfRuns, object) {
  for (var i = 0; i < numberOfRuns; ++i) {
    changeObject(object);
    runBaselineChangeDetection(baselineHead);
  }
}



// ---- CHANGE DETECTION

function setUpChangeDetection(changeDetection: ChangeDetection, iterations, object) {
  var dispatcher = new DummyDispatcher();
  var parser = new Parser(new Lexer());

  var genConfig = new ChangeDetectorGenConfig(false, false, false);
  var parentProto = changeDetection.getProtoChangeDetector(
      "id", new ChangeDetectorDefinition('parent', null, [], [], [], [], genConfig));
  var parentCd = parentProto.instantiate(dispatcher);

  var directiveRecord = new DirectiveRecord({directiveIndex: new DirectiveIndex(0, 0)});
  var bindings = [
    BindingRecord.createForDirective(parser.parseBinding('field0', null), "field0",
                                     reflector.setter("field0"), directiveRecord),
    BindingRecord.createForDirective(parser.parseBinding('field1', null), "field1",
                                     reflector.setter("field1"), directiveRecord),
    BindingRecord.createForDirective(parser.parseBinding('field2', null), "field2",
                                     reflector.setter("field2"), directiveRecord),
    BindingRecord.createForDirective(parser.parseBinding('field3', null), "field3",
                                     reflector.setter("field3"), directiveRecord),
    BindingRecord.createForDirective(parser.parseBinding('field4', null), "field4",
                                     reflector.setter("field4"), directiveRecord),
    BindingRecord.createForDirective(parser.parseBinding('field5', null), "field5",
                                     reflector.setter("field5"), directiveRecord),
    BindingRecord.createForDirective(parser.parseBinding('field6', null), "field6",
                                     reflector.setter("field6"), directiveRecord),
    BindingRecord.createForDirective(parser.parseBinding('field7', null), "field7",
                                     reflector.setter("field7"), directiveRecord),
    BindingRecord.createForDirective(parser.parseBinding('field8', null), "field8",
                                     reflector.setter("field8"), directiveRecord),
    BindingRecord.createForDirective(parser.parseBinding('field9', null), "field9",
                                     reflector.setter("field9"), directiveRecord)
  ];

  var proto = changeDetection.getProtoChangeDetector(
      "id",
      new ChangeDetectorDefinition("proto", null, [], bindings, [], [directiveRecord], genConfig));

  var targetObj = new Obj();
  parentCd.hydrate(object, null, new FakeDirectives(targetObj), null);
  for (var i = 0; i < iterations; ++i) {
    var cd = proto.instantiate(dispatcher);
    cd.hydrate(object, null, new FakeDirectives(targetObj), null);
    parentCd.addChild(cd);
  }
  return parentCd;
}

function runChangeDetectionReads(changeDetector, numberOfRuns) {
  for (var i = 0; i < numberOfRuns; ++i) {
    changeDetector.detectChanges();
  }
}

function runChangeDetectionWrites(changeDetector, numberOfRuns, object) {
  for (var i = 0; i < numberOfRuns; ++i) {
    changeObject(object);
    changeDetector.detectChanges();
  }
}



export function main() {
  BrowserDomAdapter.makeCurrent();
  var numberOfChecks = getIntParameter('numberOfChecks');
  var numberOfRuns = getIntParameter('iterations');

  var numberOfChecksPerDetector = 10;
  var numberOfDetectors = numberOfChecks / numberOfChecksPerDetector / numberOfRuns;

  setUpReflector();
  var object = createObject()

      // -- BASELINE
      var baselineHead = setUpBaseline(numberOfDetectors, object);

  runBaselineReads(baselineHead, 1);  // warmup

  bindAction('#baselineChangeDetectionReads',
             () => microBenchmark('detectChangesAvg', numberOfRuns,
                                  () => runBaselineReads(baselineHead, numberOfRuns)));

  bindAction('#baselineChangeDetectionWrites',
             () => microBenchmark('detectChangesAvg', numberOfRuns,
                                  () => runBaselineWrites(baselineHead, numberOfRuns, object)));



  // -- DYNAMIC
  var ng2DynamicChangeDetector =
      setUpChangeDetection(new DynamicChangeDetection(), numberOfDetectors, object);

  runChangeDetectionReads(ng2DynamicChangeDetector, 1);  // warmup

  bindAction(
      '#ng2ChangeDetectionDynamicReads',
      () => microBenchmark('detectChangesAvg', numberOfRuns,
                           () => runChangeDetectionReads(ng2DynamicChangeDetector, numberOfRuns)));

  bindAction('#ng2ChangeDetectionDynamicWrites',
             () => microBenchmark(
                 'detectChangesAvg', numberOfRuns,
                 () => runChangeDetectionWrites(ng2DynamicChangeDetector, numberOfRuns, object)));



  // -- JIT
  // Reenable when we have transformers for Dart
  if (JitChangeDetection.isSupported()) {
    var ng2JitChangeDetector =
        setUpChangeDetection(new JitChangeDetection(), numberOfDetectors, object);

    runChangeDetectionReads(ng2JitChangeDetector, 1);  // warmup

    bindAction(
        '#ng2ChangeDetectionJitReads',
        () => microBenchmark('detectChangesAvg', numberOfRuns,
                             () => runChangeDetectionReads(ng2JitChangeDetector, numberOfRuns)));

    bindAction('#ng2ChangeDetectionJitWrites',
               () => microBenchmark(
                   'detectChangesAvg', numberOfRuns,
                   () => runChangeDetectionWrites(ng2JitChangeDetector, numberOfRuns, object)));
  } else {
    bindAction('#ng2ChangeDetectionJitReads', () => {});
    bindAction('#ng2ChangeDetectionJitWrites', () => {});
  }
}

class FakeDirectives {
  targetObj: Obj;

  constructor(targetObj) { this.targetObj = targetObj; }

  getDirectiveFor(record) { return this.targetObj; }
}

class DummyDispatcher implements ChangeDispatcher {
  getDebugContext(elementIndex: number, directiveIndex: DirectiveIndex): DebugContext {
    throw "getDebugContext not implemented.";
  }
  notifyOnBinding(bindingTarget, newValue) { throw "Should not be used"; }
  logBindingUpdate(bindingTarget, newValue) { throw "Should not be used"; }
  notifyAfterContentChecked() {}
  notifyAfterViewChecked() {}
}
