import {isPresent, isBlank, BaseException} from 'angular2/src/facade/lang';
import {ListWrapper, MapWrapper, Set, SetWrapper} from 'angular2/src/facade/collection';
import {DOM} from 'angular2/src/dom/dom_adapter';

import {
  ASTWithSource, AST, AstTransformer, AccessMember, LiteralArray, ImplicitReceiver
} from 'angular2/change_detection';
import {SetterFn} from 'angular2/src/reflection/types';

import {ProtoView} from './proto_view';
import {ElementBinder} from './element_binder';
import {setterFactory} from './property_setter_factory';

import * as api from '../../api';
import * as directDomRenderer from '../direct_dom_renderer';

import {NG_BINDING_CLASS} from '../util';

export class ProtoViewBuilder {
  rootElement;
  variableBindings: Map<string, string>;
  elements:List<ElementBinderBuilder>;
  isRootView:boolean;

  constructor(rootElement) {
    this.rootElement = rootElement;
    this.elements = [];
    this.isRootView = false;
    this.variableBindings = MapWrapper.create();
  }

  bindElement(element, description = null):ElementBinderBuilder {
    var builder = new ElementBinderBuilder(this.elements.length, element, description);
    ListWrapper.push(this.elements, builder);
    DOM.addClass(element, NG_BINDING_CLASS);

    return builder;
  }

  bindVariable(name, value) {
    // Store the variable map from value to variable, reflecting how it will be used later by
    // View. When a local is set to the view, a lookup for the variable name will take place keyed
    // by the "value", or exported identifier. For example, ng-repeat sets a view local of "index".
    // When this occurs, a lookup keyed by "index" must occur to find if there is a var referencing
    // it.
    MapWrapper.set(this.variableBindings, value, name);
  }

  setIsRootView(value) {
    this.isRootView = value;
  }

  build():api.ProtoView {
    var renderElementBinders = [];

    var apiElementBinders = [];
    ListWrapper.forEach(this.elements, (ebb) => {
      var propertySetters = MapWrapper.create();
      var eventLocalsAstSplitter = new EventLocalsAstSplitter();
      var apiDirectiveBinders = ListWrapper.map(ebb.directives, (db) => {
        return new api.DirectiveBinder({
          directiveIndex: db.directiveIndex,
          propertyBindings: db.propertyBindings,
          eventBindings: eventLocalsAstSplitter.splitEventAstIntoLocals(db.eventBindings)
        });
      });
      MapWrapper.forEach(ebb.propertySetters, (setter, propertyName) => {
        MapWrapper.set(propertySetters, propertyName, setter);
      });
      var nestedProtoView =
          isPresent(ebb.nestedProtoView) ? ebb.nestedProtoView.build() : null;
      var parentIndex = isPresent(ebb.parent) ? ebb.parent.index : -1;
      ListWrapper.push(apiElementBinders, new api.ElementBinder({
        index: ebb.index, parentIndex:parentIndex, distanceToParent:ebb.distanceToParent,
        directives: apiDirectiveBinders,
        nestedProtoView: nestedProtoView,
        propertyBindings: ebb.propertyBindings, variableBindings: ebb.variableBindings,
        eventBindings: eventLocalsAstSplitter.splitEventAstIntoLocals(ebb.eventBindings),
        textBindings: ebb.textBindings,
        readAttributes: ebb.readAttributes
      }));
      ListWrapper.push(renderElementBinders, new ElementBinder({
        textNodeIndices: ebb.textBindingIndices,
        contentTagSelector: ebb.contentTagSelector,
        parentIndex: parentIndex,
        distanceToParent: ebb.distanceToParent,
        nestedProtoView: isPresent(nestedProtoView) ? nestedProtoView.render.delegate : null,
        componentId: ebb.componentId,
        eventLocals: eventLocalsAstSplitter.buildEventLocals(),
        eventNames: eventLocalsAstSplitter.buildEventNames(),
        propertySetters: propertySetters
      }));
    });
    return new api.ProtoView({
      render: new directDomRenderer.DirectDomProtoViewRef(new ProtoView({
        element: this.rootElement,
        elementBinders: renderElementBinders,
        isRootView: this.isRootView
      })),
      elementBinders: apiElementBinders,
      variableBindings: this.variableBindings
    });
  }
}

export class ElementBinderBuilder {
  element;
  index:number;
  parent:ElementBinderBuilder;
  distanceToParent:number;
  directives:List<DirectiveBuilder>;
  nestedProtoView:ProtoViewBuilder;
  propertyBindings: Map<string, ASTWithSource>;
  variableBindings: Map<string, string>;
  eventBindings: Map<string, ASTWithSource>;
  textBindingIndices: List<number>;
  textBindings: List<ASTWithSource>;
  contentTagSelector:string;
  propertySetters: Map<string, SetterFn>;
  readAttributes: Map<string, string>;
  componentId: string;

  constructor(index, element, description) {
    this.element = element;
    this.index = index;
    this.parent = null;
    this.distanceToParent = 0;
    this.directives = [];
    this.nestedProtoView = null;
    this.propertyBindings = MapWrapper.create();
    this.variableBindings = MapWrapper.create();
    this.eventBindings = MapWrapper.create();
    this.textBindings = [];
    this.textBindingIndices = [];
    this.contentTagSelector = null;
    this.propertySetters = MapWrapper.create();
    this.componentId = null;
    this.readAttributes = MapWrapper.create();
  }

  setParent(parent:ElementBinderBuilder, distanceToParent):ElementBinderBuilder {
    this.parent = parent;
    if (isPresent(parent)) {
      this.distanceToParent = distanceToParent;
    }
    return this;
  }

  readAttribute(attrName:string) {
    if (isBlank(MapWrapper.get(this.readAttributes, attrName))) {
      MapWrapper.set(this.readAttributes, attrName, DOM.getAttribute(this.element, attrName));
    }
  }

  bindDirective(directiveIndex:number):DirectiveBuilder {
    var directive = new DirectiveBuilder(directiveIndex);
    ListWrapper.push(this.directives, directive);
    return directive;
  }

  bindNestedProtoView(rootElement):ProtoViewBuilder {
    if (isPresent(this.nestedProtoView)) {
      throw new BaseException('Only one nested view per element is allowed');
    }
    this.nestedProtoView = new ProtoViewBuilder(rootElement);
    return this.nestedProtoView;
  }

  bindProperty(name, expression) {
    MapWrapper.set(this.propertyBindings, name, expression);
    this.bindPropertySetter(name);
  }

  bindPropertySetter(name) {
    MapWrapper.set(this.propertySetters, name, setterFactory(name));
  }

  bindVariable(name, value) {
    // When current is a view root, the variable bindings are set to the *nested* proto view.
    // The root view conceptually signifies a new "block scope" (the nested view), to which
    // the variables are bound.
    if (isPresent(this.nestedProtoView)) {
      this.nestedProtoView.bindVariable(name, value);
    } else {
      // Store the variable map from value to variable, reflecting how it will be used later by
      // View. When a local is set to the view, a lookup for the variable name will take place keyed
      // by the "value", or exported identifier. For example, ng-repeat sets a view local of "index".
      // When this occurs, a lookup keyed by "index" must occur to find if there is a var referencing
      // it.
      MapWrapper.set(this.variableBindings, value, name);
    }
  }

  bindEvent(name, expression) {
    MapWrapper.set(this.eventBindings, name, expression);
  }

  bindText(index, expression) {
    ListWrapper.push(this.textBindingIndices, index);
    ListWrapper.push(this.textBindings, expression);
  }

  setContentTagSelector(value:string) {
    this.contentTagSelector = value;
  }

  setComponentId(componentId:string) {
    this.componentId = componentId;
  }
}

export class DirectiveBuilder {
  directiveIndex:number;
  propertyBindings: Map<string, ASTWithSource>;
  eventBindings: Map<string, ASTWithSource>;

  constructor(directiveIndex) {
    this.directiveIndex = directiveIndex;
    this.propertyBindings = MapWrapper.create();
    this.eventBindings = MapWrapper.create();
  }

  bindProperty(name, expression) {
    MapWrapper.set(this.propertyBindings, name, expression);
  }

  bindEvent(name, expression) {
    MapWrapper.set(this.eventBindings, name, expression);
  }
}

export class EventLocalsAstSplitter extends AstTransformer {
  locals:List<AST>;
  eventNames:List<string>;
  _implicitReceiver:AST;

  constructor() {
    super();
    this.locals = [];
    this.eventNames = [];
    this._implicitReceiver = new ImplicitReceiver();
  }

  splitEventAstIntoLocals(eventBindings:Map<string, ASTWithSource>):Map<string, ASTWithSource> {
    if (isPresent(eventBindings)) {
      var result = MapWrapper.create();
      MapWrapper.forEach(eventBindings, (astWithSource, eventName) => {
        // TODO(tbosch): reenable this when we are parsing element properties
        // out of action expressions
        // var adjustedAst = astWithSource.ast.visit(this);
        var adjustedAst = astWithSource.ast;
        MapWrapper.set(result, eventName, new ASTWithSource(adjustedAst, astWithSource.source, ''));
        ListWrapper.push(this.eventNames, eventName);
      });
      return result;
    }
    return null;
  }

  visitAccessMember(ast:AccessMember) {
    var isEventAccess = false;
    var current = ast;
    while (!isEventAccess && (current instanceof AccessMember)) {
      if (current.name == '$event') {
        isEventAccess = true;
      }
      current = current.receiver;
    }

    if (isEventAccess) {
      ListWrapper.push(this.locals, ast);
      var index = this.locals.length - 1;
      return new AccessMember(this._implicitReceiver, `${index}`, (arr) => arr[index], null);
    } else {
      return ast;
    }
  }

  buildEventLocals() {
    return new LiteralArray(this.locals);
  }

  buildEventNames() {
    return this.eventNames;
  }
}