text: 'use strict';
  /* jshint newcap:false */
  
  var domInsert = require('../runtime/dom-insert');
  var marko = require('../');
  var componentsUtil = require('./util');
  var componentLookup = componentsUtil.$__componentLookup;
  var emitLifecycleEvent = componentsUtil.$__emitLifecycleEvent;
  var destroyComponentForEl = componentsUtil.$__destroyComponentForEl;
  var destroyElRecursive = componentsUtil.$__destroyElRecursive;
  var getElementById = componentsUtil.$__getElementById;
  var EventEmitter = require('events-light');
  var RenderResult = require('../runtime/RenderResult');
  var SubscriptionTracker = require('listener-tracker');
  var inherit = require('raptor-util/inherit');
  var updateManager = require('./update-manager');
  var morphdom = require('../morphdom');
  var eventDelegation = require('./event-delegation');
  
  var slice = Array.prototype.slice;
  
  var MORPHDOM_SKIP = true;
  
  var COMPONENT_SUBSCRIBE_TO_OPTIONS;
  var NON_COMPONENT_SUBSCRIBE_TO_OPTIONS = {
      addDestroyListener: false
  };
  
  var emit = EventEmitter.prototype.emit;
  
  function removeListener(removeEventListenerHandle) {
      removeEventListenerHandle();
  }
  
  function checkCompatibleComponent(globalComponentsContext, el) {
      var component = el._w;
      while(component) {
          var id = component.id;
          var newComponentDef = globalComponentsContext.$__componentsById[id];
          if (newComponentDef && component.$__type == newComponentDef.$__component.$__type) {
              break;
          }
  
          var rootFor = component.$__rootFor;
          if (rootFor)  {
              component = rootFor;
          } else {
              component.$__destroyShallow();
              break;
          }
      }
  }
  
  function handleCustomEventWithMethodListener(component, targetMethodName, args, extraArgs) {
      // Remove the "eventType" argument
      args.push(component);
  
      if (extraArgs) {
          args = extraArgs.concat(args);
      }
  
  
      var targetComponent = componentLookup[component.$__scope];
      var targetMethod = targetComponent[targetMethodName];
      if (!targetMethod) {
          throw Error('Method not found: ' + targetMethodName);
      }
  
      targetMethod.apply(targetComponent, args);
  }
  
  function getElIdHelper(component, componentElId, index) {
      var id = component.id;
  
      var elId = componentElId != null ? id + '-' + componentElId : id;
  
      if (index != null) {
          elId += '[' + index + ']';
      }
  
      return elId;
  }
  
  /**
   * This method is used to process "update_<stateName>" handler functions.
   * If all of the modified state properties have a user provided update handler
   * then a rerender will be bypassed and, instead, the DOM will be updated
   * looping over and invoking the custom update handlers.
   * @return {boolean} Returns true if if the DOM was updated. False, otherwise.
   */
  function processUpdateHandlers(component, stateChanges, oldState) {
      var handlerMethod;
      var handlers;
  
      for (var propName in stateChanges) {
          if (stateChanges.hasOwnProperty(propName)) {
              var handlerMethodName = 'update_' + propName;
  
              handlerMethod = component[handlerMethodName];
              if (handlerMethod) {
                  (handlers || (handlers=[])).push([propName, handlerMethod]);
              } else {
                  // This state change does not have a state handler so return false
                  // to force a rerender
                  return;
              }
          }
      }
  
      // If we got here then all of the changed state properties have
      // an update handler or there are no state properties that actually
      // changed.
      if (handlers) {
          // Otherwise, there are handlers for all of the changed properties
          // so apply the updates using those handlers
  
          handlers.forEach(function(handler, i) {
              var propertyName = handler[0];
              handlerMethod = handler[1];
  
              var newValue = stateChanges[propertyName];
              var oldValue = oldState[propertyName];
              handlerMethod.call(component, newValue, oldValue);
          });
  
          emitLifecycleEvent(component, 'update');
  
          component.$__reset();
      }
  
      return true;
  }
  
  function checkInputChanged(existingComponent, oldInput, newInput) {
      if (oldInput != newInput) {
          if (oldInput == null || newInput == null) {
              return true;
          }
  
          var oldKeys = Object.keys(oldInput);
          var newKeys = Object.keys(newInput);
          var len = oldKeys.length;
          if (len !== newKeys.length) {
              return true;
          }
  
          for (var i=0; i<len; i++) {
              var key = oldKeys[i];
              if (oldInput[key] !== newInput[key]) {
                  return true;
              }
          }
      }
  
      return false;
  }
  
  function onNodeDiscarded(node) {
      if (node.nodeType === 1) {
          destroyComponentForEl(node);
      }
  }
  
  function onBeforeNodeDiscarded(node) {
      return eventDelegation.$__handleNodeDetach(node);
  }
  
  function onBeforeElUpdated(fromEl, key, globalComponentsContext) {
      if (key) {
          var preserved = globalComponentsContext.$__preserved[key];
  
          if (preserved === true) {
              // Don't morph elements that are associated with components that are being
              // reused or elements that are being preserved. For components being reused,
              // the morphing will take place when the reused component updates.
              return MORPHDOM_SKIP;
          } else {
              // We may need to destroy a Component associated with the current element
              // if a new UI component was rendered to the same element and the types
              // do not match
              checkCompatibleComponent(globalComponentsContext, fromEl);
          }
      }
  }
  
  function onBeforeElChildrenUpdated(el, key, globalComponentsContext) {
      if (key) {
          var preserved = globalComponentsContext.$__preservedBodies[key];
          if (preserved === true) {
              // Don't morph the children since they are preserved
              return MORPHDOM_SKIP;
          }
      }
  }
  
  function onNodeAdded(node, globalComponentsContext) {
      eventDelegation.$__handleNodeAttach(node, globalComponentsContext.$__out);
  }
  
  var componentProto;
  
  /**
   * Base component type.
   *
   * NOTE: Any methods that are prefixed with an underscore should be considered private!
   */
  function Component(id) {
      EventEmitter.call(this);
      this.id = id;
      this.el = null;
      this.$__state = null;
      this.$__roots = null;
      this.$__subscriptions = null;
      this.$__domEventListenerHandles = null;
      this.$__bubblingDomEvents = null;
      this.$__customEvents = null;
      this.$__scope = null;
      this.$__renderInput = null;
      this.$__input = undefined;
  
      this.$__destroyed = false;
      this.$__updateQueued = false;
      this.$__dirty = false;
      this.$__settingInput = false;
  
      this.$__document = undefined;
  }
  
  Component.prototype = componentProto = {
      $__isComponent: true,
  
      subscribeTo: function(target) {
          if (!target) {
              throw TypeError();
          }
  
          var subscriptions = this.$__subscriptions || (this.$__subscriptions = new SubscriptionTracker());
  
          var subscribeToOptions = target.$__isComponent ?
              COMPONENT_SUBSCRIBE_TO_OPTIONS :
              NON_COMPONENT_SUBSCRIBE_TO_OPTIONS;
  
          return subscriptions.subscribeTo(target, subscribeToOptions);
      },
  
      emit: function(eventType) {
          var customEvents = this.$__customEvents;
          var target;
  
          if (customEvents && (target = customEvents[eventType])) {
              var targetMethodName = target[0];
              var extraArgs = target[1];
              var args = slice.call(arguments, 1);
  
              handleCustomEventWithMethodListener(this, targetMethodName, args, extraArgs);
          }
  
          if (this.listenerCount(eventType)) {
              return emit.apply(this, arguments);
          }
      },
      getElId: function (componentElId, index) {
          return getElIdHelper(this, componentElId, index);
      },
      getEl: function (componentElId, index) {
          var doc = this.$__document;
  
          if (componentElId != null) {
              return getElementById(doc, getElIdHelper(this, componentElId, index));
          } else {
              return this.el || getElementById(doc, getElIdHelper(this));
          }
      },
      getEls: function(id) {
          var els = [];
          var i = 0;
          var el;
          while((el = this.getEl(id, i))) {
              els.push(el);
              i++;
          }
          return els;
      },
      getComponent: function(id, index) {
          return componentLookup[getElIdHelper(this, id, index)];
      },
      getComponents: function(id) {
          var components = [];
          var i = 0;
          var component;
          while((component = componentLookup[getElIdHelper(this, id, i)])) {
              components.push(component);
              i++;
          }
          return components;
      },
      destroy: function() {
          if (this.$__destroyed) {
              return;
          }
  
          var els = this.els;
  
          this.$__destroyShallow();
  
          var rootComponents = this.$__rootComponents;
          if (rootComponents) {
              rootComponents.forEach(function(rootComponent) {
                  rootComponent.$__destroy();
              });
          }
  
          els.forEach(function(el) {
              destroyElRecursive(el);
  
              var parentNode = el.parentNode;
              if (parentNode) {
                  parentNode.removeChild(el);
              }
          });
      },
  
      $__destroyShallow: function() {
          if (this.$__destroyed) {
              return;
          }
  
          emitLifecycleEvent(this, 'destroy');
          this.$__destroyed = true;
  
          this.el = null;
  
          // Unsubscribe from all DOM events
          this.$__removeDOMEventListeners();
  
          var subscriptions = this.$__subscriptions;
          if (subscriptions) {
              subscriptions.removeAllListeners();
              this.$__subscriptions = null;
          }
  
          delete componentLookup[this.id];
      },
  
      isDestroyed: function() {
          return this.$__destroyed;
      },
      get state() {
          return this.$__state;
      },
      set state(newState) {
          var state = this.$__state;
          if (!state && !newState) {
              return;
          }
  
          if (!state) {
              state = this.$__state = new this.$__State(this);
          }
  
          state.$__replace(newState || {});
  
          if (state.$__dirty) {
              this.$__queueUpdate();
          }
  
          if (!newState) {
              this.$__state = null;
          }
      },
      setState: function(name, value) {
          var state = this.$__state;
  
          if (typeof name == 'object') {
              // Merge in the new state with the old state
              var newState = name;
              for (var k in newState) {
                  if (newState.hasOwnProperty(k)) {
                      state.$__set(k, newState[k], true /* ensure:true */);
                  }
              }
          } else {
              state.$__set(name, value, true /* ensure:true */);
          }
      },
  
      setStateDirty: function(name, value) {
          var state = this.$__state;
  
          if (arguments.length == 1) {
              value = state[name];
          }
  
          state.$__set(name, value, true /* ensure:true */, true /* forceDirty:true */);
      },
  
      replaceState: function(newState) {
          this.$__state.$__replace(newState);
      },
  
      get input() {
          return this.$__input;
      },
      set input(newInput) {
          if (this.$__settingInput) {
              this.$__input = newInput;
          } else {
              this.$__setInput(newInput);
          }
      },
  
      $__setInput: function(newInput, onInput, out) {
          onInput = onInput || this.onInput;
          var updatedInput;
  
          var oldInput = this.$__input;
          this.$__input = undefined;
  
          if (onInput) {
              // We need to set a flag to preview `this.input = foo` inside
              // onInput causing infinite recursion
              this.$__settingInput = true;
              updatedInput = onInput.call(this, newInput || {}, out);
              this.$__settingInput = false;
          }
  
          newInput = this.$__renderInput = updatedInput || newInput;
  
          if ((this.$__dirty = checkInputChanged(this, oldInput, newInput))) {
              this.$__queueUpdate();
          }
  
          if (this.$__input === undefined) {
              this.$__input = newInput;
          }
  
          return newInput;
      },
  
      forceUpdate: function() {
          this.$__dirty = true;
          this.$__queueUpdate();
      },
  
      $__queueUpdate: function() {
          if (!this.$__updateQueued) {
              updateManager.$__queueComponentUpdate(this);
          }
      },
  
      update: function() {
          if (this.$__destroyed === true || this.$__isDirty === false) {
              return;
          }
  
          var input = this.$__input;
          var state = this.$__state;
  
          if (this.$__dirty === false && state !== null && state.$__dirty === true) {
              if (processUpdateHandlers(this, state.$__changes, state.$__old, state)) {
                  state.$__dirty = false;
              }
          }
  
          if (this.$__isDirty === true) {
              // The UI component is still dirty after process state handlers
              // then we should rerender
  
              if (this.shouldUpdate(input, state) !== false) {
                  this.$__rerender();
              }
          }
  
          this.$__reset();
      },
  
  
      get $__isDirty() {
          return this.$__dirty === true || (this.$__state !== null && this.$__state.$__dirty === true);
      },
  
      $__reset: function() {
          this.$__dirty = false;
          this.$__updateQueued = false;
          this.$__renderInput = null;
          var state = this.$__state;
          if (state) {
              state.$__reset();
          }
      },
  
      shouldUpdate: function(newState, newProps) {
          return true;
      },
  
      $__emitLifecycleEvent: function(eventType, eventArg1, eventArg2) {
          emitLifecycleEvent(this, eventType, eventArg1, eventArg2);
      },
  
      $__rerender: function(input) {
          if (input) {
              this.input = input;
          }
  
          var self = this;
          var renderer = self.$__renderer;
  
          if (!renderer) {
              throw TypeError();
          }
  
          var globalData = {
              $w: self
          };
  
          var fromEls = self.$__getRootEls({});
          var doc = self.$__document;
          input = this.$__renderInput || this.$__input;
  
          updateManager.$__batchUpdate(function() {
              var createOut = renderer.createOut || marko.createOut;
              var out = createOut(globalData);
              out.sync();
              out.$__document = self.$__document;
              renderer(input, out);
              var result = new RenderResult(out);
              var targetNode = out.$__getOutput();
  
              var globalComponentsContext = out.global.components;
  
              var fromEl;
  
              var targetEl = targetNode.firstChild;
              while(targetEl) {
                  var id = targetEl.id;
  
                  if (id) {
                      fromEl = fromEls[id];
                      if (fromEl) {
                          morphdom(
                              fromEl,
                              targetEl,
                              globalComponentsContext,
                              onNodeAdded,
                              onBeforeElUpdated,
                              onBeforeNodeDiscarded,
                              onNodeDiscarded,
                              onBeforeElChildrenUpdated);
                      }
                  }
  
                  targetEl = targetEl.nextSibling;
              }
  
              result.afterInsert(doc);
  
              out.emit('$__componentsInitialized');
          });
  
          this.$__reset();
      },
  
      $__getRootEls: function(rootEls) {
          var i, len;
  
          var componentEls = this.els;
  
          for (i=0, len=componentEls.length; i<len; i++) {
              var componentEl = componentEls[i];
              rootEls[componentEl.id] = componentEl;
          }
  
          var rootComponents = this.$__rootComponents;
          if (rootComponents) {
              for (i=0, len=rootComponents.length; i<len; i++) {
                  var rootComponent = rootComponents[i];
                  rootComponent.$__getRootEls(rootEls);
              }
          }
  
          return rootEls;
      },
  
      $__removeDOMEventListeners: function() {
          var eventListenerHandles = this.$__domEventListenerHandles;
          if (eventListenerHandles) {
              eventListenerHandles.forEach(removeListener);
              this.$__domEventListenerHandles = null;
          }
      },
  
      get $__rawState() {
          var state = this.$__state;
          return state && state.$__raw;
      },
  
      $__setCustomEvents: function(customEvents, scope) {
          var finalCustomEvents = this.$__customEvents = {};
          this.$__scope = scope;
  
          customEvents.forEach(function(customEvent) {
              var eventType = customEvent[0];
              var targetMethodName = customEvent[1];
              var extraArgs = customEvent[2];
  
              finalCustomEvents[eventType] = [targetMethodName, extraArgs];
          });
      }
  };
  
  componentProto.elId = componentProto.getElId;
  componentProto.$__update = componentProto.update;
  componentProto.$__destroy = componentProto.destroy;
  
  // Add all of the following DOM methods to Component.prototype:
  // - appendTo(referenceEl)
  // - replace(referenceEl)
  // - replaceChildrenOf(referenceEl)
  // - insertBefore(referenceEl)
  // - insertAfter(referenceEl)
  // - prependTo(referenceEl)
  domInsert(
      componentProto,
      function getEl(component) {
          var els = this.els;
          var elCount = els.length;
          if (elCount > 1) {
              var fragment = component.$__document.createDocumentFragment();
              els.forEach(function(el) {
                  fragment.appendChild(el);
              });
              return fragment;
          } else {
              return els[0];
          }
      },
      function afterInsert(component) {
          return component;
      });
  
  inherit(Component, EventEmitter);
  
  module.exports = Component;
  