text: 'use strict';
  
  var NG_ANIMATE_ATTR_NAME = 'data-ng-animate';
  var NG_ANIMATE_PIN_DATA = '$ngAnimatePin';
  var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
    var PRE_DIGEST_STATE = 1;
    var RUNNING_STATE = 2;
  
    var rules = this.rules = {
      skip: [],
      cancel: [],
      join: []
    };
  
    function isAllowed(ruleType, element, currentAnimation, previousAnimation) {
      return rules[ruleType].some(function(fn) {
        return fn(element, currentAnimation, previousAnimation);
      });
    }
  
    function hasAnimationClasses(options, and) {
      options = options || {};
      var a = (options.addClass || '').length > 0;
      var b = (options.removeClass || '').length > 0;
      return and ? a && b : a || b;
    }
  
    rules.join.push(function(element, newAnimation, currentAnimation) {
      // if the new animation is class-based then we can just tack that on
      return !newAnimation.structural && hasAnimationClasses(newAnimation.options);
    });
  
    rules.skip.push(function(element, newAnimation, currentAnimation) {
      // there is no need to animate anything if no classes are being added and
      // there is no structural animation that will be triggered
      return !newAnimation.structural && !hasAnimationClasses(newAnimation.options);
    });
  
    rules.skip.push(function(element, newAnimation, currentAnimation) {
      // why should we trigger a new structural animation if the element will
      // be removed from the DOM anyway?
      return currentAnimation.event == 'leave' && newAnimation.structural;
    });
  
    rules.skip.push(function(element, newAnimation, currentAnimation) {
      // if there is an ongoing current animation then don't even bother running the class-based animation
      return currentAnimation.structural && currentAnimation.state === RUNNING_STATE && !newAnimation.structural;
    });
  
    rules.cancel.push(function(element, newAnimation, currentAnimation) {
      // there can never be two structural animations running at the same time
      return currentAnimation.structural && newAnimation.structural;
    });
  
    rules.cancel.push(function(element, newAnimation, currentAnimation) {
      // if the previous animation is already running, but the new animation will
      // be triggered, but the new animation is structural
      return currentAnimation.state === RUNNING_STATE && newAnimation.structural;
    });
  
    rules.cancel.push(function(element, newAnimation, currentAnimation) {
      var nO = newAnimation.options;
      var cO = currentAnimation.options;
  
      // if the exact same CSS class is added/removed then it's safe to cancel it
      return (nO.addClass && nO.addClass === cO.removeClass) || (nO.removeClass && nO.removeClass === cO.addClass);
    });
  
    this.$get = ['$$rAF', '$rootScope', '$rootElement', '$document', '$$HashMap',
                 '$$animation', '$$AnimateRunner', '$templateRequest', '$$jqLite', '$$forceReflow',
         function($$rAF,   $rootScope,   $rootElement,   $document,   $$HashMap,
                  $$animation,   $$AnimateRunner,   $templateRequest,   $$jqLite,   $$forceReflow) {
  
      var activeAnimationsLookup = new $$HashMap();
      var disabledElementsLookup = new $$HashMap();
      var animationsEnabled = null;
  
      function postDigestTaskFactory() {
        var postDigestCalled = false;
        return function(fn) {
          // we only issue a call to postDigest before
          // it has first passed. This prevents any callbacks
          // from not firing once the animation has completed
          // since it will be out of the digest cycle.
          if (postDigestCalled) {
            fn();
          } else {
            $rootScope.$$postDigest(function() {
              postDigestCalled = true;
              fn();
            });
          }
        };
      }
  
      // Wait until all directive and route-related templates are downloaded and
      // compiled. The $templateRequest.totalPendingRequests variable keeps track of
      // all of the remote templates being currently downloaded. If there are no
      // templates currently downloading then the watcher will still fire anyway.
      var deregisterWatch = $rootScope.$watch(
        function() { return $templateRequest.totalPendingRequests === 0; },
        function(isEmpty) {
          if (!isEmpty) return;
          deregisterWatch();
  
          // Now that all templates have been downloaded, $animate will wait until
          // the post digest queue is empty before enabling animations. By having two
          // calls to $postDigest calls we can ensure that the flag is enabled at the
          // very end of the post digest queue. Since all of the animations in $animate
          // use $postDigest, it's important that the code below executes at the end.
          // This basically means that the page is fully downloaded and compiled before
          // any animations are triggered.
          $rootScope.$$postDigest(function() {
            $rootScope.$$postDigest(function() {
              // we check for null directly in the event that the application already called
              // .enabled() with whatever arguments that it provided it with
              if (animationsEnabled === null) {
                animationsEnabled = true;
              }
            });
          });
        }
      );
  
      var callbackRegistry = {};
  
      // remember that the classNameFilter is set during the provider/config
      // stage therefore we can optimize here and setup a helper function
      var classNameFilter = $animateProvider.classNameFilter();
      var isAnimatableClassName = !classNameFilter
                ? function() { return true; }
                : function(className) {
                  return classNameFilter.test(className);
                };
  
      var applyAnimationClasses = applyAnimationClassesFactory($$jqLite);
  
      function normalizeAnimationOptions(element, options) {
        return mergeAnimationOptions(element, options, {});
      }
  
      function findCallbacks(element, event) {
        var targetNode = getDomNode(element);
  
        var matches = [];
        var entries = callbackRegistry[event];
        if (entries) {
          forEach(entries, function(entry) {
            if (entry.node.contains(targetNode)) {
              matches.push(entry.callback);
            }
          });
        }
  
        return matches;
      }
  
      return {
        on: function(event, container, callback) {
          var node = extractElementNode(container);
          callbackRegistry[event] = callbackRegistry[event] || [];
          callbackRegistry[event].push({
            node: node,
            callback: callback
          });
        },
  
        off: function(event, container, callback) {
          var entries = callbackRegistry[event];
          if (!entries) return;
  
          callbackRegistry[event] = arguments.length === 1
              ? null
              : filterFromRegistry(entries, container, callback);
  
          function filterFromRegistry(list, matchContainer, matchCallback) {
            var containerNode = extractElementNode(matchContainer);
            return list.filter(function(entry) {
              var isMatch = entry.node === containerNode &&
                              (!matchCallback || entry.callback === matchCallback);
              return !isMatch;
            });
          }
        },
  
        pin: function(element, parentElement) {
          assertArg(isElement(element), 'element', 'not an element');
          assertArg(isElement(parentElement), 'parentElement', 'not an element');
          element.data(NG_ANIMATE_PIN_DATA, parentElement);
        },
  
        push: function(element, event, options, domOperation) {
          options = options || {};
          options.domOperation = domOperation;
          return queueAnimation(element, event, options);
        },
  
        // this method has four signatures:
        //  () - global getter
        //  (bool) - global setter
        //  (element) - element getter
        //  (element, bool) - element setter<F37>
        enabled: function(element, bool) {
          var argCount = arguments.length;
  
          if (argCount === 0) {
            // () - Global getter
            bool = !!animationsEnabled;
          } else {
            var hasElement = isElement(element);
  
            if (!hasElement) {
              // (bool) - Global setter
              bool = animationsEnabled = !!element;
            } else {
              var node = getDomNode(element);
              var recordExists = disabledElementsLookup.get(node);
  
              if (argCount === 1) {
                // (element) - Element getter
                bool = !recordExists;
              } else {
                // (element, bool) - Element setter
                bool = !!bool;
                if (!bool) {
                  disabledElementsLookup.put(node, true);
                } else if (recordExists) {
                  disabledElementsLookup.remove(node);
                }
              }
            }
          }
  
          return bool;
        }
      };
  
      function queueAnimation(element, event, options) {
        var node, parent;
        element = stripCommentsFromElement(element);
        if (element) {
          node = getDomNode(element);
          parent = element.parent();
        }
  
        options = prepareAnimationOptions(options);
  
        // we create a fake runner with a working promise.
        // These methods will become available after the digest has passed
        var runner = new $$AnimateRunner();
  
        // this is used to trigger callbacks in postDigest mode
        var runInNextPostDigestOrNow = postDigestTaskFactory();
  
        if (isArray(options.addClass)) {
          options.addClass = options.addClass.join(' ');
        }
  
        if (options.addClass && !isString(options.addClass)) {
          options.addClass = null;
        }
  
        if (isArray(options.removeClass)) {
          options.removeClass = options.removeClass.join(' ');
        }
  
        if (options.removeClass && !isString(options.removeClass)) {
          options.removeClass = null;
        }
  
        if (options.from && !isObject(options.from)) {
          options.from = null;
        }
  
        if (options.to && !isObject(options.to)) {
          options.to = null;
        }
  
        // there are situations where a directive issues an animation for
        // a jqLite wrapper that contains only comment nodes... If this
        // happens then there is no way we can perform an animation
        if (!node) {
          close();
          return runner;
        }
  
        var className = [node.className, options.addClass, options.removeClass].join(' ');
        if (!isAnimatableClassName(className)) {
          close();
          return runner;
        }
  
        var isStructural = ['enter', 'move', 'leave'].indexOf(event) >= 0;
  
        // this is a hard disable of all animations for the application or on
        // the element itself, therefore  there is no need to continue further
        // past this point if not enabled
        var skipAnimations = !animationsEnabled || disabledElementsLookup.get(node);
        var existingAnimation = (!skipAnimations && activeAnimationsLookup.get(node)) || {};
        var hasExistingAnimation = !!existingAnimation.state;
  
        // there is no point in traversing the same collection of parent ancestors if a followup
        // animation will be run on the same element that already did all that checking work
        if (!skipAnimations && (!hasExistingAnimation || existingAnimation.state != PRE_DIGEST_STATE)) {
          skipAnimations = !areAnimationsAllowed(element, parent, event);
        }
  
        if (skipAnimations) {
          close();
          return runner;
        }
  
        if (isStructural) {
          closeChildAnimations(element);
        }
  
        var newAnimation = {
          structural: isStructural,
          element: element,
          event: event,
          close: close,
          options: options,
          runner: runner
        };
  
        if (hasExistingAnimation) {
          var skipAnimationFlag = isAllowed('skip', element, newAnimation, existingAnimation);
          if (skipAnimationFlag) {
            if (existingAnimation.state === RUNNING_STATE) {
              close();
              return runner;
            } else {
              mergeAnimationOptions(element, existingAnimation.options, options);
              return existingAnimation.runner;
            }
          }
  
          var cancelAnimationFlag = isAllowed('cancel', element, newAnimation, existingAnimation);
          if (cancelAnimationFlag) {
            if (existingAnimation.state === RUNNING_STATE) {
              // this will end the animation right away and it is safe
              // to do so since the animation is already running and the
              // runner callback code will run in async
              existingAnimation.runner.end();
            } else if (existingAnimation.structural) {
              // this means that the animation is queued into a digest, but
              // hasn't started yet. Therefore it is safe to run the close
              // method which will call the runner methods in async.
              existingAnimation.close();
            } else {
              // this will merge the new animation options into existing animation options
              mergeAnimationOptions(element, existingAnimation.options, newAnimation.options);
              return existingAnimation.runner;
            }
          } else {
            // a joined animation means that this animation will take over the existing one
            // so an example would involve a leave animation taking over an enter. Then when
            // the postDigest kicks in the enter will be ignored.
            var joinAnimationFlag = isAllowed('join', element, newAnimation, existingAnimation);
            if (joinAnimationFlag) {
              if (existingAnimation.state === RUNNING_STATE) {
                normalizeAnimationOptions(element, options);
              } else {
                applyGeneratedPreparationClasses(element, isStructural ? event : null, options);
  
                event = newAnimation.event = existingAnimation.event;
                options = mergeAnimationOptions(element, existingAnimation.options, newAnimation.options);
  
                //we return the same runner since only the option values of this animation will
                //be fed into the `existingAnimation`.
                return existingAnimation.runner;
              }
            }
          }
        } else {
          // normalization in this case means that it removes redundant CSS classes that
          // already exist (addClass) or do not exist (removeClass) on the element
          normalizeAnimationOptions(element, options);
        }
  
        // when the options are merged and cleaned up we may end up not having to do
        // an animation at all, therefore we should check this before issuing a post
        // digest callback. Structural animations will always run no matter what.
        var isValidAnimation = newAnimation.structural;
        if (!isValidAnimation) {
          // animate (from/to) can be quickly checked first, otherwise we check if any classes are present
          isValidAnimation = (newAnimation.event === 'animate' && Object.keys(newAnimation.options.to || {}).length > 0)
                              || hasAnimationClasses(newAnimation.options);
        }
  
        if (!isValidAnimation) {
          close();
          clearElementAnimationState(element);
          return runner;
        }
  
        // the counter keeps track of cancelled animations
        var counter = (existingAnimation.counter || 0) + 1;
        newAnimation.counter = counter;
  
        markElementAnimationState(element, PRE_DIGEST_STATE, newAnimation);
  
        $rootScope.$$postDigest(function() {
          var animationDetails = activeAnimationsLookup.get(node);
          var animationCancelled = !animationDetails;
          animationDetails = animationDetails || {};
  
          // if addClass/removeClass is called before something like enter then the
          // registered parent element may not be present. The code below will ensure
          // that a final value for parent element is obtained
          var parentElement = element.parent() || [];
  
          // animate/structural/class-based animations all have requirements. Otherwise there
          // is no point in performing an animation. The parent node must also be set.
          var isValidAnimation = parentElement.length > 0
                                  && (animationDetails.event === 'animate'
                                      || animationDetails.structural
                                      || hasAnimationClasses(animationDetails.options));
  
          // this means that the previous animation was cancelled
          // even if the follow-up animation is the same event
          if (animationCancelled || animationDetails.counter !== counter || !isValidAnimation) {
            // if another animation did not take over then we need
            // to make sure that the domOperation and options are
            // handled accordingly
            if (animationCancelled) {
              applyAnimationClasses(element, options);
              applyAnimationStyles(element, options);
            }
  
            // if the event changed from something like enter to leave then we do
            // it, otherwise if it's the same then the end result will be the same too
            if (animationCancelled || (isStructural && animationDetails.event !== event)) {
              options.domOperation();
              runner.end();
            }
  
            // in the event that the element animation was not cancelled or a follow-up animation
            // isn't allowed to animate from here then we need to clear the state of the element
            // so that any future animations won't read the expired animation data.
            if (!isValidAnimation) {
              clearElementAnimationState(element);
            }
  
            return;
          }
  
          // this combined multiple class to addClass / removeClass into a setClass event
          // so long as a structural event did not take over the animation
          event = !animationDetails.structural && hasAnimationClasses(animationDetails.options, true)
              ? 'setClass'
              : animationDetails.event;
  
          markElementAnimationState(element, RUNNING_STATE);
          var realRunner = $$animation(element, event, animationDetails.options);
  
          realRunner.done(function(status) {
            close(!status);
            var animationDetails = activeAnimationsLookup.get(node);
            if (animationDetails && animationDetails.counter === counter) {
              clearElementAnimationState(getDomNode(element));
            }
            notifyProgress(runner, event, 'close', {});
          });
  
          // this will update the runner's flow-control events based on
          // the `realRunner` object.
          runner.setHost(realRunner);
          notifyProgress(runner, event, 'start', {});
        });
  
        return runner;
  
        function notifyProgress(runner, event, phase, data) {
          runInNextPostDigestOrNow(function() {
            var callbacks = findCallbacks(element, event);
            if (callbacks.length) {
              // do not optimize this call here to RAF because
              // we don't know how heavy the callback code here will
              // be and if this code is buffered then this can
              // lead to a performance regression.
              $$rAF(function() {
                forEach(callbacks, function(callback) {
                  callback(element, phase, data);
                });
              });
            }
          });
          runner.progress(event, phase, data);
        }
  
        function close(reject) { // jshint ignore:line
          clearGeneratedClasses(element, options);
          applyAnimationClasses(element, options);
          applyAnimationStyles(element, options);
          options.domOperation();
          runner.complete(!reject);
        }
      }
  
      function closeChildAnimations(element) {
        var node = getDomNode(element);
        var children = node.querySelectorAll('[' + NG_ANIMATE_ATTR_NAME + ']');
        forEach(children, function(child) {
          var state = parseInt(child.getAttribute(NG_ANIMATE_ATTR_NAME));
          var animationDetails = activeAnimationsLookup.get(child);
          switch (state) {
            case RUNNING_STATE:
              animationDetails.runner.end();
              /* falls through */
            case PRE_DIGEST_STATE:
              if (animationDetails) {
                activeAnimationsLookup.remove(child);
              }
              break;
          }
        });
      }
  
      function clearElementAnimationState(element) {
        var node = getDomNode(element);
        node.removeAttribute(NG_ANIMATE_ATTR_NAME);
        activeAnimationsLookup.remove(node);
      }
  
      function isMatchingElement(nodeOrElmA, nodeOrElmB) {
        return getDomNode(nodeOrElmA) === getDomNode(nodeOrElmB);
      }
  
      function areAnimationsAllowed(element, parentElement, event) {
        var bodyElement = jqLite($document[0].body);
        var bodyElementDetected = isMatchingElement(element, bodyElement) || element[0].nodeName === 'HTML';
        var rootElementDetected = isMatchingElement(element, $rootElement);
        var parentAnimationDetected = false;
        var animateChildren;
  
        var parentHost = element.data(NG_ANIMATE_PIN_DATA);
        if (parentHost) {
          parentElement = parentHost;
        }
  
        while (parentElement && parentElement.length) {
          if (!rootElementDetected) {
            // angular doesn't want to attempt to animate elements outside of the application
            // therefore we need to ensure that the rootElement is an ancestor of the current element
            rootElementDetected = isMatchingElement(parentElement, $rootElement);
          }
  
          var parentNode = parentElement[0];
          if (parentNode.nodeType !== ELEMENT_NODE) {
            // no point in inspecting the #document element
            break;
          }
  
          var details = activeAnimationsLookup.get(parentNode) || {};
          // either an enter, leave or move animation will commence
          // therefore we can't allow any animations to take place
          // but if a parent animation is class-based then that's ok
          if (!parentAnimationDetected) {
            parentAnimationDetected = details.structural || disabledElementsLookup.get(parentNode);
          }
  
          if (isUndefined(animateChildren) || animateChildren === true) {
            var value = parentElement.data(NG_ANIMATE_CHILDREN_DATA);
            if (isDefined(value)) {
              animateChildren = value;
            }
          }
  
          // there is no need to continue traversing at this point
          if (parentAnimationDetected && animateChildren === false) break;
  
          if (!rootElementDetected) {
            // angular doesn't want to attempt to animate elements outside of the application
            // therefore we need to ensure that the rootElement is an ancestor of the current element
            rootElementDetected = isMatchingElement(parentElement, $rootElement);
            if (!rootElementDetected) {
              parentHost = parentElement.data(NG_ANIMATE_PIN_DATA);
              if (parentHost) {
                parentElement = parentHost;
              }
            }
          }
  
          if (!bodyElementDetected) {
            // we also need to ensure that the element is or will be apart of the body element
            // otherwise it is pointless to even issue an animation to be rendered
            bodyElementDetected = isMatchingElement(parentElement, bodyElement);
          }
  
          parentElement = parentElement.parent();
        }
  
        var allowAnimation = !parentAnimationDetected || animateChildren;
        return allowAnimation && rootElementDetected && bodyElementDetected;
      }
  
      function markElementAnimationState(element, state, details) {
        details = details || {};
        details.state = state;
  
        var node = getDomNode(element);
        node.setAttribute(NG_ANIMATE_ATTR_NAME, state);
  
        var oldValue = activeAnimationsLookup.get(node);
        var newValue = oldValue
            ? extend(oldValue, details)
            : details;
        activeAnimationsLookup.put(node, newValue);
      }
    }];
  }];
  