import {
  AsyncTestCompleter,
  inject,
  describe,
  it,
  expect,
  beforeEach,
  createTestInjector,
  beforeEachBindings
} from "angular2/test_lib";
import {DOM} from 'angular2/src/dom/dom_adapter';
import {DomTestbed, TestRootView, elRef} from '../../render/dom/dom_testbed';
import {bind} from 'angular2/di';
import {WorkerCompiler, WorkerRenderer} from "angular2/src/web-workers/worker/renderer";
import {MessageBroker, UiArguments, FnArg} from "angular2/src/web-workers/worker/broker";
import {Serializer} from "angular2/src/web-workers/shared/serializer";
import {isPresent, isBlank, BaseException, Type} from "angular2/src/facade/lang";
import {MapWrapper, ListWrapper} from "angular2/src/facade/collection";
import {
  DirectiveMetadata,
  ProtoViewDto,
  RenderProtoViewRef,
  RenderViewWithFragments,
  ViewDefinition,
  RenderProtoViewMergeMapping,
  RenderViewRef,
  RenderFragmentRef
} from "angular2/src/render/api";
import {
  MessageBus,
  MessageBusSource,
  MessageBusSink,
  SourceListener
} from "angular2/src/web-workers/shared/message_bus";
import {
  RenderProtoViewRefStore,
  WebworkerRenderProtoViewRef
} from "angular2/src/web-workers/shared/render_proto_view_ref_store";
import {
  RenderViewWithFragmentsStore,
  WorkerRenderViewRef
} from 'angular2/src/web-workers/shared/render_view_with_fragments_store';
import {resolveInternalDomProtoView} from 'angular2/src/render/dom/view/proto_view';
import {someComponent} from '../../render/dom/dom_renderer_integration_spec';
import {WebWorkerMain} from 'angular2/src/web-workers/ui/impl';
import {AnchorBasedAppRootUrl} from 'angular2/src/services/anchor_based_app_root_url';

export function main() {
  function createBroker(workerSerializer: Serializer, uiSerializer: Serializer, tb: DomTestbed,
                        uiRenderViewStore: RenderViewWithFragmentsStore,
                        workerRenderViewStore: RenderViewWithFragmentsStore): MessageBroker {
    // set up the two message buses to pass messages to each other
    var uiMessageBus = new MockMessageBus(new MockMessageBusSink(), new MockMessageBusSource());
    var workerMessageBus = new MockMessageBus(new MockMessageBusSink(), new MockMessageBusSource());
    uiMessageBus.attachToBus(workerMessageBus);
    workerMessageBus.attachToBus(uiMessageBus);

    // set up the worker side
    var broker = new MessageBroker(workerMessageBus, workerSerializer);

    // set up the ui side
    var webWorkerMain = new WebWorkerMain(tb.compiler, tb.renderer, uiRenderViewStore, uiSerializer,
                                          new AnchorBasedAppRootUrl());
    webWorkerMain.attachToWorker(uiMessageBus);
    return broker;
  }

  function createWorkerRenderer(workerSerializer: Serializer, uiSerializer: Serializer,
                                tb: DomTestbed, uiRenderViewStore: RenderViewWithFragmentsStore,
                                workerRenderViewStore: RenderViewWithFragmentsStore):
      WorkerRenderer {
    var broker =
        createBroker(workerSerializer, uiSerializer, tb, uiRenderViewStore, workerRenderViewStore);
    return new WorkerRenderer(broker, workerRenderViewStore);
  }

  function createWorkerCompiler(workerSerializer: Serializer, uiSerializer: Serializer,
                                tb: DomTestbed): WorkerCompiler {
    var broker = createBroker(workerSerializer, uiSerializer, tb, null, null);
    return new WorkerCompiler(broker);
  }

  describe("Web Worker Compiler", function() {
    var workerSerializer: Serializer;
    var uiSerializer: Serializer;
    var workerRenderProtoViewRefStore: RenderProtoViewRefStore;
    var uiRenderProtoViewRefStore: RenderProtoViewRefStore;
    var tb: DomTestbed;

    beforeEach(() => {
      workerRenderProtoViewRefStore = new RenderProtoViewRefStore(true);
      uiRenderProtoViewRefStore = new RenderProtoViewRefStore(false);
      workerSerializer = createSerializer(workerRenderProtoViewRefStore, null);
      uiSerializer = createSerializer(uiRenderProtoViewRefStore, null);
      tb = createTestInjector([DomTestbed]).get(DomTestbed);
    });

    function resolveWebWorkerRef(ref: RenderProtoViewRef) {
      var refNumber = (<WebworkerRenderProtoViewRef>ref).refNumber;
      return resolveInternalDomProtoView(uiRenderProtoViewRefStore.deserialize(refNumber));
    }

    it('should build the proto view', inject([AsyncTestCompleter], (async) => {
         var compiler: WorkerCompiler = createWorkerCompiler(workerSerializer, uiSerializer, tb);

         var dirMetadata = DirectiveMetadata.create(
             {id: 'id', selector: 'CUSTOM', type: DirectiveMetadata.COMPONENT_TYPE});
         compiler.compileHost(dirMetadata)
             .then((protoView) => {
               expect(DOM.tagName(DOM.firstChild(
                          DOM.content(resolveWebWorkerRef(protoView.render).rootElement))))
                   .toEqual('CUSTOM');
               expect(protoView).not.toBeNull();
               async.done();
             });
       }));
  });

  describe("Web Worker Renderer", () => {
    beforeEachBindings(() => [DomTestbed]);
    var renderer: WorkerRenderer;
    var workerSerializer: Serializer;
    var workerRenderViewStore: RenderViewWithFragmentsStore;
    var uiRenderViewStore: RenderViewWithFragmentsStore;
    var uiSerializer: Serializer;
    var tb: DomTestbed;

    /**
     * Seriliazes the given obj with the uiSerializer and then returns the version that
     * the worker would deserialize
     */
    function serialize(obj: any, type: Type): any {
      var serialized = uiSerializer.serialize(obj, type);
      return workerSerializer.deserialize(serialized, type);
    }

    beforeEach(() => {
      workerRenderViewStore = new RenderViewWithFragmentsStore(true);
      tb = createTestInjector([DomTestbed]).get(DomTestbed);
      uiRenderViewStore = new RenderViewWithFragmentsStore(false);
      workerSerializer = createSerializer(new RenderProtoViewRefStore(true), workerRenderViewStore);
      uiSerializer = createSerializer(new RenderProtoViewRefStore(false), uiRenderViewStore);
      renderer = createWorkerRenderer(workerSerializer, uiSerializer, tb, uiRenderViewStore,
                                      workerRenderViewStore);
    });


    it('should create and destroy root host views while using the given elements in place',
       inject([AsyncTestCompleter], (async) => {
         tb.compiler.compileHost(someComponent)
             .then((hostProtoViewDto: any) => {
               hostProtoViewDto = serialize(hostProtoViewDto, ProtoViewDto);
               var viewWithFragments =
                   renderer.createRootHostView(hostProtoViewDto.render, 1, '#root');
               var view = new WorkerTestRootView(viewWithFragments, uiRenderViewStore);

               expect(tb.rootEl.parentNode).toBeTruthy();
               expect(view.hostElement).toBe(tb.rootEl);

               renderer.detachFragment(viewWithFragments.fragmentRefs[0]);
               renderer.destroyView(viewWithFragments.viewRef);
               expect(tb.rootEl.parentNode).toBeFalsy();

               async.done();
             });
       }));

    it('should update text nodes', inject([AsyncTestCompleter], (async) => {
         tb.compileAndMerge(
               someComponent,
               [
                 new ViewDefinition(
                     {componentId: 'someComponent', template: '{{a}}', directives: []})
               ])
             .then((protoViewMergeMappings) => {
               protoViewMergeMappings =
                   serialize(protoViewMergeMappings, RenderProtoViewMergeMapping);
               var rootView = renderer.createView(protoViewMergeMappings.mergedProtoViewRef, 1);
               renderer.hydrateView(rootView.viewRef);

               renderer.setText(rootView.viewRef, 0, 'hello');
               var view = new WorkerTestRootView(rootView, uiRenderViewStore);
               expect(view.hostElement).toHaveText('hello');
               async.done();
             });
       }));


    it('should update any element property/attributes/class/style independent of the compilation on the root element and other elements',
       inject([AsyncTestCompleter], (async) => {
         tb.compileAndMerge(someComponent,
                            [
                              new ViewDefinition({
                                componentId: 'someComponent',
                                template: '<input [title]="y" style="position:absolute">',
                                directives: []
                              })
                            ])
             .then((protoViewMergeMappings) => {
               protoViewMergeMappings =
                   serialize(protoViewMergeMappings, RenderProtoViewMergeMapping);

               var checkSetters = (elr, el) => {
                 renderer.setElementProperty(elr, 'tabIndex', 1);
                 expect((<HTMLInputElement>el).tabIndex).toEqual(1);

                 renderer.setElementClass(elr, 'a', true);
                 expect(DOM.hasClass(el, 'a')).toBe(true);
                 renderer.setElementClass(elr, 'a', false);
                 expect(DOM.hasClass(el, 'a')).toBe(false);

                 renderer.setElementStyle(elr, 'width', '10px');
                 expect(DOM.getStyle(el, 'width')).toEqual('10px');
                 renderer.setElementStyle(elr, 'width', null);
                 expect(DOM.getStyle(el, 'width')).toEqual('');

                 renderer.setElementAttribute(elr, 'someAttr', 'someValue');
                 expect(DOM.getAttribute(el, 'some-attr')).toEqual('someValue');
               };

               var rootViewWithFragments =
                   renderer.createView(protoViewMergeMappings.mergedProtoViewRef, 1);
               renderer.hydrateView(rootViewWithFragments.viewRef);

               var rootView = new WorkerTestRootView(rootViewWithFragments, uiRenderViewStore);
               // root element
               checkSetters(elRef(rootViewWithFragments.viewRef, 0), rootView.hostElement);
               // nested elements
               checkSetters(elRef(rootViewWithFragments.viewRef, 1),
                            DOM.firstChild(rootView.hostElement));

               async.done();
             });
       }));

    it('should add and remove empty fragments', inject([AsyncTestCompleter], (async) => {
         tb.compileAndMerge(someComponent,
                            [
                              new ViewDefinition({
                                componentId: 'someComponent',
                                template: '<template></template><template></template>',
                                directives: []
                              })
                            ])
             .then((protoViewMergeMappings) => {
               protoViewMergeMappings =
                   serialize(protoViewMergeMappings, RenderProtoViewMergeMapping);
               var rootViewWithFragments =
                   renderer.createView(protoViewMergeMappings.mergedProtoViewRef, 3);

               var elr = elRef(rootViewWithFragments.viewRef, 1);
               var rootView = new WorkerTestRootView(rootViewWithFragments, uiRenderViewStore);
               expect(rootView.hostElement).toHaveText('');
               var fragment = rootViewWithFragments.fragmentRefs[1];
               var fragment2 = rootViewWithFragments.fragmentRefs[2];
               renderer.attachFragmentAfterElement(elr, fragment);
               renderer.attachFragmentAfterFragment(fragment, fragment2);
               renderer.detachFragment(fragment);
               renderer.detachFragment(fragment2);
               expect(rootView.hostElement).toHaveText('');

               async.done();
             });
       }));

    if (DOM.supportsDOMEvents()) {
      it('should call actions on the element independent of the compilation',
         inject([AsyncTestCompleter], (async) => {
           tb.compileAndMerge(someComponent,
                              [
                                new ViewDefinition({
                                  componentId: 'someComponent',
                                  template: '<input [title]="y"></input>',
                                  directives: []
                                })
                              ])
               .then((protoViewMergeMappings) => {
                 protoViewMergeMappings =
                     serialize(protoViewMergeMappings, RenderProtoViewMergeMapping);
                 var rootViewWithFragments =
                     renderer.createView(protoViewMergeMappings.mergedProtoViewRef, 1);
                 var rootView = new WorkerTestRootView(rootViewWithFragments, uiRenderViewStore);

                 renderer.invokeElementMethod(elRef(rootViewWithFragments.viewRef, 1),
                                              'setAttribute', ['a', 'b']);

                 expect(DOM.getAttribute(DOM.childNodes(rootView.hostElement)[0], 'a'))
                     .toEqual('b');
                 async.done();
               });
         }));
    }
  });
}

class WorkerTestRootView extends TestRootView {
  constructor(workerViewWithFragments: RenderViewWithFragments, uiRenderViewStore) {
    super(new RenderViewWithFragments(
        uiRenderViewStore.retreive(
            (<WorkerRenderViewRef>workerViewWithFragments.viewRef).refNumber),
        ListWrapper.map(workerViewWithFragments.fragmentRefs,
                        (val) => { return uiRenderViewStore.retreive(val.refNumber); })));
  }
}

function createSerializer(protoViewRefStore: RenderProtoViewRefStore,
                          renderViewStore: RenderViewWithFragmentsStore): Serializer {
  var injector = createTestInjector([
    bind(RenderProtoViewRefStore)
        .toValue(protoViewRefStore),
    bind(RenderViewWithFragmentsStore).toValue(renderViewStore)
  ]);
  return injector.get(Serializer);
}

class MockMessageBusSource implements MessageBusSource {
  private _listenerStore: Map<int, SourceListener> = new Map<int, SourceListener>();
  private _numListeners: number = 0;

  addListener(fn: SourceListener): int {
    this._listenerStore.set(++this._numListeners, fn);
    return this._numListeners;
  }

  removeListener(index: int): void { MapWrapper.delete(this._listenerStore, index); }

  receive(message: Object): void {
    MapWrapper.forEach(this._listenerStore, (fn: SourceListener, key: int) => { fn(message); });
  }
}

class MockMessageBusSink implements MessageBusSink {
  private _sendTo: MockMessageBusSource;

  send(message: Object): void { this._sendTo.receive({'data': message}); }

  attachToSource(source: MockMessageBusSource) { this._sendTo = source; }
}

class MockMessageBus implements MessageBus {
  constructor(public sink: MockMessageBusSink, public source: MockMessageBusSource) {}
  attachToBus(bus: MockMessageBus) { this.sink.attachToSource(bus.source); }
}