(function () {
  "use strict";
  /**
   * Bindonce - Zero watches binding for AngularJs
   * @version v0.3.1
   * @link https://github.com/Pasvaz/bindonce
   * @author Pasquale Vazzana <pasqualevazzana@gmail.com>
   * @license MIT License, http://www.opensource.org/licenses/MIT
   */

  var bindonceModule = angular.module('pasvaz.bindonce', []);

  bindonceModule.directive('bindonce', function ()
  {
    var toBoolean = function (value)
    {
      if (value && value.length !== 0)
      {
        var v = angular.lowercase("" + value);
        value = !(v === 'f' || v === '0' || v === 'false' || v === 'no' || v === 'n' || v === '[]');
      }
      else
      {
        value = false;
      }
      return value;
    };

    var msie = parseInt((/msie (\d+)/.exec(angular.lowercase(navigator.userAgent)) || [])[1], 10);
    if (isNaN(msie))
    {
      msie = parseInt((/trident\/.*; rv:(\d+)/.exec(angular.lowercase(navigator.userAgent)) || [])[1], 10);
    }

    var bindonceDirective =
    {
      restrict: "AM",
      controller: ['$scope', '$element', '$attrs', '$interpolate', function ($scope, $element, $attrs, $interpolate)
      {
        var showHideBinder = function (elm, attr, value)
        {
          var show = (attr === 'show') ? '' : 'none';
          var hide = (attr === 'hide') ? '' : 'none';
          elm.css('display', toBoolean(value) ? show : hide);
        };
        var classBinder = function (elm, value)
        {
          if (angular.isObject(value) && !angular.isArray(value))
          {
            var results = [];
            angular.forEach(value, function (value, index)
            {
              if (value) results.push(index);
            });
            value = results;
          }
          if (value)
          {
            elm.addClass(angular.isArray(value) ? value.join(' ') : value);
          }
        };
        var transclude = function (transcluder, scope)
        {
          transcluder.transclude(scope, function (clone)
          {
            var parent = transcluder.element.parent();
            var afterNode = transcluder.element && transcluder.element[transcluder.element.length - 1];
            var parentNode = parent && parent[0] || afterNode && afterNode.parentNode;
            var afterNextSibling = (afterNode && afterNode.nextSibling) || null;
            angular.forEach(clone, function (node)
            {
              parentNode.insertBefore(node, afterNextSibling);
            });
          });
        };

        var ctrl =
        {
          watcherRemover: undefined,
          binders: [],
          group: $attrs.boName,
          element: $element,
          ran: false,

          addBinder: function (binder)
          {
            this.binders.push(binder);

            // In case of late binding (when using the directive bo-name/bo-parent)
            // it happens only when you use nested bindonce, if the bo-children
            // are not dom children the linking can follow another order
            if (this.ran)
            {
              this.runBinders();
            }
          },

          setupWatcher: function (bindonceValue)
          {
            var that = this;
            this.watcherRemover = $scope.$watch(bindonceValue, function (newValue)
            {
              if (newValue === undefined) return;
              that.removeWatcher();
              that.checkBindonce(newValue);
            }, true);
          },

          checkBindonce: function (value)
          {
            var that = this, promise = (value.$promise) ? value.$promise.then : value.then;
            // since Angular 1.2 promises are no longer
            // undefined until they don't get resolved
            if (typeof promise === 'function')
            {
              promise(function ()
              {
                that.runBinders();
              });
            }
            else
            {
              that.runBinders();
            }
          },

          removeWatcher: function ()
          {
            if (this.watcherRemover !== undefined)
            {
              this.watcherRemover();
              this.watcherRemover = undefined;
            }
          },

          runBinders: function ()
          {
            while (this.binders.length > 0)
            {
              var binder = this.binders.shift();
              if (this.group && this.group != binder.group) continue;
              var value = binder.scope.$eval((binder.interpolate) ? $interpolate(binder.value) : binder.value);
              switch (binder.attr)
              {
                case 'boIf':
                  if (toBoolean(value))
                  {
                    transclude(binder, binder.scope.$new());
                  }
                  break;
                case 'boSwitch':
                  var selectedTranscludes, switchCtrl = binder.controller[0];
                  if ((selectedTranscludes = switchCtrl.cases['!' + value] || switchCtrl.cases['?']))
                  {
                    binder.scope.$eval(binder.attrs.change);
                    angular.forEach(selectedTranscludes, function (selectedTransclude)
                    {
                      transclude(selectedTransclude, binder.scope.$new());
                    });
                  }
                  break;
                case 'boSwitchWhen':
                  var ctrl = binder.controller[0];
                  ctrl.cases['!' + binder.attrs.boSwitchWhen] = (ctrl.cases['!' + binder.attrs.boSwitchWhen] || []);
                  ctrl.cases['!' + binder.attrs.boSwitchWhen].push({ transclude: binder.transclude, element: binder.element });
                  break;
                case 'boSwitchDefault':
                  var ctrl = binder.controller[0];
                  ctrl.cases['?'] = (ctrl.cases['?'] || []);
                  ctrl.cases['?'].push({ transclude: binder.transclude, element: binder.element });
                  break;
                case 'hide':
                case 'show':
                  showHideBinder(binder.element, binder.attr, value);
                  break;
                case 'class':
                  classBinder(binder.element, value);
                  break;
                case 'text':
                  binder.element.text(value);
                  break;
                case 'html':
                  binder.element.html(value);
                  break;
                case 'style':
                  binder.element.css(value);
                  break;
                case 'src':
                  binder.element.attr(binder.attr, value);
                  if (msie) binder.element.prop('src', value);
                  break;
                case 'attr':
                  angular.forEach(binder.attrs, function (attrValue, attrKey)
                  {
                    var newAttr, newValue;
                    if (attrKey.match(/^boAttr./) && binder.attrs[attrKey])
                    {
                      newAttr = attrKey.replace(/^boAttr/, '').replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
                      newValue = binder.scope.$eval(binder.attrs[attrKey]);
                      binder.element.attr(newAttr, newValue);
                    }
                  });
                  break;
                case 'href':
                case 'alt':
                case 'title':
                case 'id':
                case 'value':
                  binder.element.attr(binder.attr, value);
                  break;
              }
            }
            this.ran = true;
          }
        };

        return ctrl;
      }],

      link: function (scope, elm, attrs, bindonceController)
      {
        var value = attrs.bindonce && scope.$eval(attrs.bindonce);
        if (value !== undefined)
        {
          bindonceController.checkBindonce(value);
        }
        else
        {
          bindonceController.setupWatcher(attrs.bindonce);
          elm.bind("$destroy", bindonceController.removeWatcher);
        }
      }
    };

    return bindonceDirective;
  });

  angular.forEach(
  [
    { directiveName: 'boShow', attribute: 'show' },
    { directiveName: 'boHide', attribute: 'hide' },
    { directiveName: 'boClass', attribute: 'class' },
    { directiveName: 'boText', attribute: 'text' },
    { directiveName: 'boBind', attribute: 'text' },
    { directiveName: 'boHtml', attribute: 'html' },
    { directiveName: 'boSrcI', attribute: 'src', interpolate: true },
    { directiveName: 'boSrc', attribute: 'src' },
    { directiveName: 'boHrefI', attribute: 'href', interpolate: true },
    { directiveName: 'boHref', attribute: 'href' },
    { directiveName: 'boAlt', attribute: 'alt' },
    { directiveName: 'boTitle', attribute: 'title' },
    { directiveName: 'boId', attribute: 'id' },
    { directiveName: 'boStyle', attribute: 'style' },
    { directiveName: 'boValue', attribute: 'value' },
    { directiveName: 'boAttr', attribute: 'attr' },

    { directiveName: 'boIf', transclude: 'element', terminal: true, priority: 1000 },
    { directiveName: 'boSwitch', require: 'boSwitch', controller: function () { this.cases = {}; } },
    { directiveName: 'boSwitchWhen', transclude: 'element', priority: 800, require: '^boSwitch' },
    { directiveName: 'boSwitchDefault', transclude: 'element', priority: 800, require: '^boSwitch' }
  ],
  function (boDirective)
  {
    var childPriority = 200;
    return bindonceModule.directive(boDirective.directiveName, function ()
    {
      var bindonceDirective =
      {
        priority: boDirective.priority || childPriority,
        transclude: boDirective.transclude || false,
        terminal: boDirective.terminal || false,
        require: ['^bindonce'].concat(boDirective.require || []),
        controller: boDirective.controller,
        compile: function (tElement, tAttrs, transclude)
        {
          return function (scope, elm, attrs, controllers)
          {
            var bindonceController = controllers[0];
            var name = attrs.boParent;
            if (name && bindonceController.group !== name)
            {
              var element = bindonceController.element.parent();
              bindonceController = undefined;
              var parentValue;

              while (element[0].nodeType !== 9 && element.length)
              {
                if ((parentValue = element.data('$bindonceController'))
                  && parentValue.group === name)
                {
                  bindonceController = parentValue;
                  break;
                }
                element = element.parent();
              }
              if (!bindonceController)
              {
                throw new Error("No bindonce controller: " + name);
              }
            }

            bindonceController.addBinder(
            {
              element: elm,
              attr: boDirective.attribute || boDirective.directiveName,
              attrs: attrs,
              value: attrs[boDirective.directiveName],
              interpolate: boDirective.interpolate,
              group: name,
              transclude: transclude,
              controller: controllers.slice(1),
              scope: scope
            });
          };
        }
      };

      return bindonceDirective;
    });
  })
})();