'use strict';

/**
 * @ngdoc function
 * @name angular.module.ng.$compile
 * @function
 *
 * @description
 * Compiles a piece of HTML string or DOM into a template and produces a template function, which
 * can then be used to link {@link angular.module.ng.$rootScope.Scope scope} and the template together.
 *
 * The compilation is a process of walking the DOM tree and trying to match DOM elements to
 * {@link angular.module.ng.$compileProvider.directive directives}. For each match it
 * executes corresponding template function and collects the
 * instance functions into a single template function which is then returned.
 *
 * The template function can then be used once to produce the view or as it is the case with
 * {@link angular.module.ng.$compileProvider.directive.ng:repeat repeater} many-times, in which
 * case each call results in a view that is a DOM clone of the original template.
 *
 <doc:example module="compile">
   <doc:source>
    <script>
      // declare a new module, and inject the $compileProvider
      angular.module('compile', [], function($compileProvider) {
        // configure new 'compile' directive by passing a directive
        // factory function. The factory function injects the '$compile'
        $compileProvider.directive('compile', function($compile) {
          // directive factory creates a link function
          return function(scope, element, attrs) {
            scope.$watch(
              function(scope) {
                 // watch the 'compile' expression for changes
                return scope.$eval(attrs.compile);
              },
              function(value) {
                // when the 'compile' expression changes
                // assign it into the current DOM
                element.html(value);

                // compile the new DOM and link it to the current
                // scope.
                // NOTE: we only compile .childNodes so that
                // we don't get into infinite loop compiling ourselves
                $compile(element.contents())(scope);
              }
            );
          };
        })
      });

      function Ctrl($scope) {
        $scope.name = 'Angular';
        $scope.html = 'Hello {{name}}';
      }
    </script>
    <div ng-controller="Ctrl">
      <input ng:model="name"> <br>
      <textarea ng:model="html"></textarea> <br>
      <div compile="html"></div>
    </div>
   </doc:source>
   <doc:scenario>
     it('should auto compile', function() {
       expect(element('div[compile]').text()).toBe('Hello Angular');
       input('html').enter('{{name}}!');
       expect(element('div[compile]').text()).toBe('Angular!');
     });
   </doc:scenario>
 </doc:example>

 *
 *
 * @param {string|DOMElement} element Element or HTML string to compile into a template function.
 * @returns {function(scope[, cloneAttachFn])} a link function which is used to bind template
 * (a DOM element/tree) to a scope. Where:
 *
 *  * `scope` - A {@link angular.module.ng.$rootScope.Scope Scope} to bind to.
 *  * `cloneAttachFn` - If `cloneAttachFn` is provided, then the link function will clone the
 *               `template` and call the `cloneAttachFn` function allowing the caller to attach the
 *               cloned elements to the DOM document at the appropriate place. The `cloneAttachFn` is
 *               called as: <br> `cloneAttachFn(clonedElement, scope)` where:
 *
 *      * `clonedElement` - is a clone of the original `element` passed into the compiler.
 *      * `scope` - is the current scope with which the linking function is working with.
 *
 * Calling the linking function returns the element of the template. It is either the original element
 * passed in, or the clone of the element if the `cloneAttachFn` is provided.
 *
 * After linking the view is not updateh until after a call to $digest which typically is done by
 * Angular automatically.
 *
 * If you need access to the bound view, there are two ways to do it:
 *
 * - If you are not asking the linking function to clone the template, create the DOM element(s)
 *   before you send them to the compiler and keep this reference around.
 *   <pre>
 *     var element = $compile('<p>{{total}}</p>')(scope);
 *   </pre>
 *
 * - if on the other hand, you need the element to be cloned, the view reference from the original
 *   example would not point to the clone, but rather to the original template that was cloned. In
 *   this case, you can access the clone via the cloneAttachFn:
 *   <pre>
 *     var templateHTML = angular.element('<p>{{total}}</p>'),
 *         scope = ....;
 *
 *     var clonedElement = $compile(templateHTML)(scope, function(clonedElement, scope) {
 *       //attach the clone to DOM document at the right place
 *     });
 *
 *     //now we have reference to the cloned DOM via `clone`
 *   </pre>
 *
 *
 * Compiler Methods For Widgets and Directives:
 *
 * The following methods are available for use when you write your own widgets, directives,
 * and markup.
 *
 *
 * For information on how the compiler works, see the
 * {@link guide/dev_guide.compiler Angular HTML Compiler} section of the Developer Guide.
 */


$CompileProvider.$inject = ['$provide'];
function $CompileProvider($provide) {
  var hasDirectives = {},
      Suffix = 'Directive',
      COMMENT_DIRECTIVE_REGEXP = /^\s*directive\:\s*([\d\w\-_]+)\s+(.*)$/,
      CLASS_DIRECTIVE_REGEXP = /(([\d\w\-_]+)(?:\:([^;]+))?;?)/,
      CONTENT_REGEXP = /\<\<content\>\>/i,
      HAS_ROOT_ELEMENT = /^\<[\s\S]*\>$/,
      SIDE_EFFECT_ATTRS = {};

  forEach('src,href,multiple,selected,checked,disabled,readonly,required'.split(','), function(name) {
    SIDE_EFFECT_ATTRS[name] = name;
    SIDE_EFFECT_ATTRS[directiveNormalize('ng_' + name)] = name;
  });


  this.directive = function registerDirective(name, directiveFactory) {
    if (isString(name)) {
      assertArg(directiveFactory, 'directive');
      if (!hasDirectives.hasOwnProperty(name)) {
        hasDirectives[name] = [];
        $provide.factory(name + Suffix, ['$injector', '$exceptionHandler',
          function($injector, $exceptionHandler) {
            var directives = [];
            forEach(hasDirectives[name], function(directiveFactory) {
              try {
                var directive = $injector.invoke(directiveFactory);
                if (isFunction(directive)) {
                  directive = { compile: valueFn(directive) };
                } else if (!directive.compile && directive.link) {
                  directive.compile = valueFn(directive.link);
                }
                directive.priority = directive.priority || 0;
                directive.name = name;
                directive.restrict = directive.restrict || 'EACM';
                directives.push(directive);
              } catch (e) {
                $exceptionHandler(e);
              }
            });
            return directives;
          }]);
      }
      hasDirectives[name].push(directiveFactory);
    } else {
      forEach(name, reverseParams(registerDirective));
    }
    return this;
  };


  this.$get = ['$injector', '$interpolate', '$exceptionHandler', '$http', '$templateCache',
       function($injector,   $interpolate,   $exceptionHandler,   $http,   $templateCache) {

    return function(templateElement) {
      templateElement = jqLite(templateElement);
      var linkingFn = compileNodes(templateElement, templateElement);
      return function(scope, cloneConnectFn){
        assertArg(scope, 'scope');
        // important!!: we must call our jqLite.clone() since the jQuery one is trying to be smart
        // and sometimes changes the structure of the DOM.
        var element = cloneConnectFn
          ? JQLitePrototype.clone.call(templateElement) // IMPORTANT!!!
          : templateElement;
        element.data('$scope', scope);
        if (cloneConnectFn) cloneConnectFn(element, scope);
        if (linkingFn) linkingFn(scope, element, element);
        return element;
      };
    };

    //================================

    /**
     * Compile function matches each node in nodeList against the directives. Once all directives
     * for a particular node are collected their compile functions are executed. The compile
     * functions return values - the linking functions - are combined into a composite linking
     * function, which is the a linking function for the node.
     *
     * @param {NodeList} nodeList an array of nodes to compile
     * @param {DOMElement=} rootElement If the nodeList is the root of the compilation tree then the
     *        rootElement must be set the jqLite collection of the compile root. This is
     *        needed so that the jqLite collection items can be replaced with widgets.
     * @returns {?function} A composite linking function of all of the matched directives or null.
     */
    function compileNodes(nodeList, rootElement) {
     var linkingFns = [],
         directiveLinkingFn, childLinkingFn, directives, attrs, linkingFnFound;

     for(var i = 0, ii = nodeList.length; i < ii; i++) {
       attrs = {
         $attr: {},
         $normalize: directiveNormalize,
         $set: attrSetter
       };
       // we must always refer to nodeList[i] since the nodes can be replaced underneath us.
       directives = collectDirectives(nodeList[i], [], attrs);

       directiveLinkingFn = (directives.length)
           ? applyDirectivesToNode(directives, nodeList[i], attrs, rootElement)
           : null;

       childLinkingFn = (directiveLinkingFn && directiveLinkingFn.terminal)
           ? null
           : compileNodes(nodeList[i].childNodes);

       linkingFns.push(directiveLinkingFn);
       linkingFns.push(childLinkingFn);
       linkingFnFound = (linkingFnFound || directiveLinkingFn || childLinkingFn);
     }

     // return a linking function if we have found anything, null otherwise
     return linkingFnFound ? linkingFn : null;

     function linkingFn(scope, nodeList, rootElement) {
       if (linkingFns.length != nodeList.length * 2) {
         throw Error('Template changed structure!');
       }

       var childLinkingFn, directiveLinkingFn, node, childScope;

       for(var i=0, n=0, ii=linkingFns.length; i<ii; n++) {
         node = nodeList[n];
         directiveLinkingFn = linkingFns[i++];
         childLinkingFn = linkingFns[i++];

         if (directiveLinkingFn) {
           if (directiveLinkingFn.scope && !rootElement) {
             childScope = scope.$new();
             jqLite(node).data('$scope', childScope);
           } else {
             childScope = scope;
           }
           directiveLinkingFn(childLinkingFn, childScope, node, rootElement);
         } else if (childLinkingFn) {
           childLinkingFn(scope, node.childNodes);
         }
       }
     }
   }


    /**
     * Looks for directives on the given node ands them to the directive collection which is sorted.
     *
     * @param node node to search
     * @param directives an array to which the directives are added to. This array is sorted before
     *        the function returns.
     * @param attrs the shared attrs object which is used to populate the normalized attributes.
     */
    function collectDirectives(node, directives, attrs) {
      var nodeType = node.nodeType,
          attrsMap = attrs.$attr,
          match,
          className;

      switch(nodeType) {
        case 1: /* Element */
          // use the node name: <directive>
          addDirective(directives, directiveNormalize(nodeName_(node).toLowerCase()), 'E');

          // iterate over the attributes
          for (var attr, name, nName, value, nAttrs = node.attributes,
                   j = 0, jj = nAttrs && nAttrs.length; j < jj; j++) {
            attr = nAttrs[j];
            name = attr.name;
            nName = directiveNormalize(name.toLowerCase());
            attrsMap[nName] = name;
            attrs[nName] = value = trim((msie && name == 'href')
                ? decodeURIComponent(node.getAttribute(name, 2))
                : attr.value);
            if (BOOLEAN_ATTR[nName]) {
              attrs[nName] = true; // presence means true
            }
            addAttrInterpolateDirective(directives, value, nName);
            addDirective(directives, nName, 'A');
          }

          // use class as directive
          className = node.className;
          while (match = CLASS_DIRECTIVE_REGEXP.exec(className)) {
            nName = directiveNormalize(match[2]);
            if (addDirective(directives, nName, 'C')) {
              attrs[nName] = trim(match[3]);
            }
            className = className.substr(match.index + match[0].length);
          }
          break;
        case 3: /* Text Node */
          addTextInterpolateDirective(directives, node.nodeValue);
          break;
        case 8: /* Comment */
          match = COMMENT_DIRECTIVE_REGEXP.exec(node.nodeValue);
          if (match) {
            nName = directiveNormalize(match[1]);
            if (addDirective(directives, nName, 'M')) {
              attrs[nName] = trim(match[2]);
            }
          }
          break;
      }

      directives.sort(byPriority);
      return directives;
    }


    /**
     * Once the directives have been collected their compile functions is executed. This method
     * is responsible for inlining widget templates as well as terminating the application
     * of the directives if the terminal directive has been reached..
     *
     * @param {Array} directives Array of collected directives to execute their compile function.
     *        this needs to be pre-sorted by priority order.
     * @param {Node} templateNode The raw DOM node to apply the compile functions to
     * @param {Object} templateAttrs The shared attribute function
     * @param {DOMElement} rootElement If we are working on the root of the compile tree then this
     *        argument has the root jqLite array so that we can replace widgets on it.
     * @returns linkingFn
     */
    function applyDirectivesToNode(directives, templateNode, templateAttrs, rootElement) {
      var terminalPriority = -Number.MAX_VALUE,
          preLinkingFns = [],
          postLinkingFns = [],
          newScopeDirective = null,
          templateDirective = null,
          delayedLinkingFn = null,
          element = templateAttrs.$element = jqLite(templateNode),
          directive, linkingFn;

      // executes all directives on the current element
      for(var i = 0, ii = directives.length; i < ii; i++) {
        directive = directives[i];

        if (terminalPriority > directive.priority) {
          break; // prevent further processing of directives
        }

        if (directive.scope) {
          assertNoDuplicate('new scope', newScopeDirective, directive, element);
          newScopeDirective = directive;
        }

        if (directive.template) {
          assertNoDuplicate('template', templateDirective, directive, element);
          templateDirective = directive;

          // include the contents of the original element into the template and replace the element
          var content = directive.template.replace(CONTENT_REGEXP, element.html());
          templateNode = jqLite(content)[0];
          if (directive.replace) {
            replaceWith(rootElement, element, templateNode);

            var newTemplateAttrs = {$attr: {}};

            // combine directives from the original node and from the template:
            // - take the array of directives for this element
            // - split it into two parts, those that were already applied and those that weren't
            // - collect directives from the template, add them to the second group and sort them
            // - append the second group with new directives to the first group
            directives = directives.concat(
                collectDirectives(
                    templateNode,
                    directives.splice(i + 1, directives.length - (i + 1)),
                    newTemplateAttrs
                )
            );
            mergeTemplateAttributes(templateAttrs, newTemplateAttrs);

            ii = directives.length;
          } else {
            element.html(content);
          }
        }

        if (directive.templateUrl) {
          assertNoDuplicate('template', templateDirective, directive, element);
          templateDirective = directive;
          delayedLinkingFn = compileTemplateUrl(directives.splice(i, directives.length - i),
              compositeLinkFn, element, templateAttrs, rootElement, directive.replace);
          ii = directives.length;
        } else if (directive.compile) {
          try {
            linkingFn = directive.compile(element, templateAttrs);
            if (isFunction(linkingFn)) {
              postLinkingFns.push(linkingFn);
            } else if (linkingFn) {
              if (linkingFn.pre) preLinkingFns.push(linkingFn.pre);
              if (linkingFn.post) postLinkingFns.push(linkingFn.post);
            }
          } catch (e) {
            $exceptionHandler(e, startingTag(element));
          }
        }

        if (directive.terminal) {
          compositeLinkFn.terminal = true;
          terminalPriority = Math.max(terminalPriority, directive.priority);
        }

      }
      compositeLinkFn.scope = !!newScopeDirective;

      // if we have templateUrl, then we have to delay linking
      return delayedLinkingFn || compositeLinkFn;

      ////////////////////


      function compositeLinkFn(childLinkingFn, scope, linkNode) {
        var attrs, element, i, ii;

        if (templateNode === linkNode) {
          attrs = templateAttrs;
        } else {
          attrs = shallowCopy(templateAttrs);
          attrs.$element = jqLite(linkNode);
        }
        element = attrs.$element;

        // PRELINKING
        for(i = 0, ii = preLinkingFns.length; i < ii; i++) {
          try {
            preLinkingFns[i](scope, element, attrs);
          } catch (e) {
            $exceptionHandler(e, startingTag(element));
          }
        }

        // RECURSION
        childLinkingFn && childLinkingFn(scope, linkNode.childNodes);

        // POSTLINKING
        for(i = 0, ii = postLinkingFns.length; i < ii; i++) {
          try {
            postLinkingFns[i](scope, element, attrs);
          } catch (e) {
            $exceptionHandler(e, startingTag(element));
          }
        }
      }
    }


    /**
     * looks up the directive and decorates it with exception handling and proper parameters. We
     * call this the boundDirective.
     *
     * @param {string} name name of the directive to look up.
     * @param {string} location The directive must be found in specific format.
     *   String containing any of theses characters:
     *
     *   * `E`: element name
     *   * `A': attribute
     *   * `C`: class
     *   * `M`: comment
     * @returns true if directive was added.
     */
    function addDirective(tDirectives, name, location) {
      var match = false;
      if (hasDirectives.hasOwnProperty(name)) {
        for(var directive, directives = $injector.get(name + Suffix),
            i=0, ii = directives.length; i<ii; i++) {
          try {
            directive = directives[i];
            if (directive.restrict.indexOf(location) != -1) {
              tDirectives.push(directive);
              match = true;
            }
          } catch(e) { $exceptionHandler(e); }
        }
      }
      return match;
    }


    /**
     * When the element is replaced with HTML template then the new attributes
     * on the template need to be merged with the existing attributes in the DOM.
     * The desired effect is to have both of the attributes present.
     *
     * @param {object} dst destination attributes (original DOM)
     * @param {object} src source attributes (from the directive template)
     */
    function mergeTemplateAttributes(dst, src) {
      var srcAttr = src.$attr,
          dstAttr = dst.$attr,
          element = dst.$element;
      // reapply the old attributes to the new element
      forEach(dst, function(value, key) {
        if (key.charAt(0) != '$') {
          dst.$set(key, value, srcAttr[key]);
        }
      });
      // copy the new attributes on the old attrs object
      forEach(src, function(value, key) {
        if (key == 'class') {
          element.addClass(value);
        } else if (key == 'style') {
          element.attr('style', element.attr('style') + ';' + value);
        } else if (key.charAt(0) != '$' && !dst.hasOwnProperty(key)) {
          dst[key] = value;
          dstAttr[key] = srcAttr[key];
        }
      });
    }


    function compileTemplateUrl(directives, beforeWidgetLinkFn, tElement, tAttrs, rootElement,
                                replace) {
      var linkQueue = [],
          afterWidgetLinkFn,
          afterWidgetChildrenLinkFn,
          originalWidgetNode = tElement[0],
          asyncWidgetDirective = directives.shift(),
          // The fact that we have to copy and patch the directive seems wrong!
          syncWidgetDirective = extend({}, asyncWidgetDirective, {templateUrl:null}),
          html = tElement.html();

      tElement.html('');

      $http.get(asyncWidgetDirective.templateUrl, {cache: $templateCache}).
        success(function(content) {
          content = trim(content).replace(CONTENT_REGEXP, html);
          if (replace && !content.match(HAS_ROOT_ELEMENT)) {
            throw Error('Template must have exactly one root element: ' + content);
          }

          var templateNode, tempTemplateAttrs;

          if (replace) {
            tempTemplateAttrs = {$attr: {}};
            templateNode = jqLite(content)[0];
            replaceWith(rootElement, tElement, templateNode);
            collectDirectives(tElement[0], directives, tempTemplateAttrs);
            mergeTemplateAttributes(tAttrs, tempTemplateAttrs);
          } else {
            templateNode = tElement[0];
            tElement.html(content);
          }

          directives.unshift(syncWidgetDirective);
          afterWidgetLinkFn = applyDirectivesToNode(directives, tElement, tAttrs);
          afterWidgetChildrenLinkFn = compileNodes(tElement.contents());


          while(linkQueue.length) {
            var linkRootElement = linkQueue.pop(),
                cLinkNode = linkQueue.pop(),
                scope = linkQueue.pop(),
                node = templateNode;

            if (cLinkNode !== originalWidgetNode) {
              // it was cloned therefore we have to clone as well.
              node = JQLiteClone(templateNode);
              replaceWith(linkRootElement, jqLite(cLinkNode), node);
            }
            afterWidgetLinkFn(function() {
              beforeWidgetLinkFn(afterWidgetChildrenLinkFn, scope, node);
            }, scope, node);
          }
          linkQueue = null;
        }).
        error(function(response, code, headers, config) {
          throw Error('Failed to load template: ' + config.url);
        });

      return function(ignoreChildLinkingFn, scope, node, rootElement) {
        if (linkQueue) {
          linkQueue.push(scope);
          linkQueue.push(node);
          linkQueue.push(rootElement);
        } else {
          afterWidgetLinkFn(function() {
            beforeWidgetLinkFn(afterWidgetChildrenLinkFn, scope, node);
          }, scope, node);
        }
      };
    }


    /**
     * Sorting function for bound directives.
     */
    function byPriority(a, b) {
      return b.priority - a.priority;
    }


    function assertNoDuplicate(what, previousDirective, directive, element) {
      if (previousDirective) {
        throw Error('Multiple directives [' + previousDirective.name + ', ' +
          directive.name + '] asking for ' + what + ' on: ' +  startingTag(element));
      }
    }


    function addTextInterpolateDirective(directives, text) {
      var interpolateFn = $interpolate(text, true);
      if (interpolateFn) {
        directives.push({
          priority: 0,
          compile: valueFn(function(scope, node) {
            var parent = node.parent(),
                bindings = parent.data('$binding') || [];
            bindings.push(interpolateFn);
            parent.data('$binding', bindings).addClass('ng-binding');
            scope.$watch(interpolateFn, function(value) {
              node[0].nodeValue = value;
            });
          })
        });
      }
    }


    function addAttrInterpolateDirective(directives, value, name) {
      var interpolateFn = $interpolate(value, true);
      if (SIDE_EFFECT_ATTRS[name]) {
        name = SIDE_EFFECT_ATTRS[name];
        if (BOOLEAN_ATTR[name]) {
          value = true;
        }
      } else if (!interpolateFn) {
        // we are not a side-effect attr, and we have no side-effects -> ignore
        return;
      }
      directives.push({
        priority: 100,
        compile: function(element, attr) {
          if (interpolateFn) {
            return function(scope, element, attr) {
              scope.$watch(interpolateFn, function(value) {
                attr.$set(name, value);
              });
            };
          } else {
            attr.$set(name, value);
          }
        }
      });
    }


    /**
     * This is a special jqLite.replaceWith, which can replace items which
     * have no parents, provided that the containing jqLite collection is provided.
     *
     * @param {JqLite=} rootElement The root of the compile tree. Used so that we can replace nodes
     *    in the root of the tree.
     * @param {JqLite} element The jqLite element which we are going to replace. We keep the shell,
     *    but replace its DOM node reference.
     * @param {Node} newNode The new DOM node.
     */
    function replaceWith(rootElement, element, newNode) {
      var oldNode = element[0],
          parent = oldNode.parentNode,
          i, ii;

      if (rootElement) {
        for(i = 0, ii = rootElement.length; i<ii; i++) {
          if (rootElement[i] == oldNode) {
            rootElement[i] = newNode;
          }
        }
      }
      if (parent) {
        parent.replaceChild(newNode, oldNode);
      }
      element[0] = newNode;
    }
  }];


  /**
   * Set a normalized attribute on the element in a way such that all directives
   * can share the attribute. This function properly handles boolean attributes.
   * @param {string} key Normalized key. (ie ngAttribute)
   * @param {string|boolean} value The value to set. If `null` attribute will be deleted.
   * @param {string=} attrName Optional none normalized name. Defaults to key.
   */
  function attrSetter(key, value, attrName) {
    var booleanKey = BOOLEAN_ATTR[key.toLowerCase()];

    if (booleanKey) {
      value = toBoolean(value);
      this.$element.prop(key, value);
      this[key] = value;
      attrName = key = booleanKey;
      value = value ? booleanKey : undefined;
    } else {
      this[key] = value;
    }

    // translate normalized key to actual key
    if (attrName) {
      this.$attr[key] = attrName;
    } else {
      attrName = this.$attr[key];
      if (!attrName) {
        this.$attr[key] = attrName = snake_case(key, '-');
      }
    }

    if (value === null || value === undefined) {
      this.$element.removeAttr(attrName);
    } else {
      this.$element.attr(attrName, value);
    }
  }
}

var PREFIX_REGEXP = /^(x[\:\-_]|data[\:\-_])/i;
/**
 * Converts all accepted directives format into proper directive name.
 * All of these will become 'myDirective':
 *   my:DiRective
 *   my-directive
 *   x-my-directive
 *   data-my:directive
 *
 * Also there is special case for Moz prefix starting with upper case letter.
 * @param name Name to normalize
 */
function directiveNormalize(name) {
  return camelCase(name.replace(PREFIX_REGEXP, ''));
}
