// Copyright 2007 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
 * @fileoverview DOM pattern matcher.  Allows for simple searching of DOM
 * using patterns descended from {@link goog.dom.pattern.AbstractPattern}.
 *
 * @author robbyw@google.com (Robby Walker)
 */

goog.provide('goog.dom.pattern.Matcher');

goog.require('goog.dom.TagIterator');
goog.require('goog.dom.pattern.MatchType');
goog.require('goog.iter');


// TODO(robbyw): Allow for backtracks of size > 1.



/**
 * Given a set of patterns and a root node, this class tests the patterns in
 * parallel.
 *
 * It is not (yet) a smart matcher - it doesn't do any advanced backtracking.
 * Given the pattern <code>DIV, SPAN</code> the matcher will not match
 * <code>DIV, DIV, SPAN</code> because it starts matching at the first
 * <code>DIV</code>, fails to match <code>SPAN</code> at the second, and never
 * backtracks to try again.
 *
 * It is also possible to have a set of complex patterns that when matched in
 * parallel will miss some possible matches.  Running multiple times will catch
 * all matches eventually.
 *
 * @constructor
 * @final
 */
goog.dom.pattern.Matcher = function() {
  this.patterns_ = [];
  this.callbacks_ = [];
};


/**
 * Array of patterns to attempt to match in parallel.
 *
 * @type {Array.<goog.dom.pattern.AbstractPattern>}
 * @private
 */
goog.dom.pattern.Matcher.prototype.patterns_;


/**
 * Array of callbacks to call when a pattern is matched.  The indexing is the
 * same as the {@link #patterns_} array.
 *
 * @type {Array.<Function>}
 * @private
 */
goog.dom.pattern.Matcher.prototype.callbacks_;


/**
 * Adds a pattern to be matched.  The callback can return an object whose keys
 * are processing instructions.
 *
 * @param {goog.dom.pattern.AbstractPattern} pattern The pattern to add.
 * @param {Function} callback Function to call when a match is found.  Uses
 *     the above semantics.
 */
goog.dom.pattern.Matcher.prototype.addPattern = function(pattern, callback) {
  this.patterns_.push(pattern);
  this.callbacks_.push(callback);
};


/**
 * Resets all the patterns.
 *
 * @private
 */
goog.dom.pattern.Matcher.prototype.reset_ = function() {
  for (var i = 0, len = this.patterns_.length; i < len; i++) {
    this.patterns_[i].reset();
  }
};


/**
 * Test the given node against all patterns.
 *
 * @param {goog.dom.TagIterator} position A position in a node walk that is
 *     located at the token to process.
 * @return {boolean} Whether a pattern modified the position or tree
 *     and its callback resulted in DOM structure or position modification.
 * @private
 */
goog.dom.pattern.Matcher.prototype.matchToken_ = function(position) {
  for (var i = 0, len = this.patterns_.length; i < len; i++) {
    var pattern = this.patterns_[i];
    switch (pattern.matchToken(position.node, position.tagType)) {
      case goog.dom.pattern.MatchType.MATCH:
      case goog.dom.pattern.MatchType.BACKTRACK_MATCH:
        var callback = this.callbacks_[i];

        // Callbacks are allowed to modify the current position, but must
        // return true if the do.
        if (callback(pattern.matchedNode, position, pattern)) {
          return true;
        }

      default:
        // Do nothing.
        break;
    }
  }

  return false;
};


/**
 * Match the set of patterns against a match tree.
 *
 * @param {Node} node The root node of the tree to match.
 */
goog.dom.pattern.Matcher.prototype.match = function(node) {
  var position = new goog.dom.TagIterator(node);

  this.reset_();

  goog.iter.forEach(position, function() {
    while (this.matchToken_(position)) {
      // Since we've moved, our old pattern statuses don't make sense any more.
      // Reset them.
      this.reset_();
    }
  }, this);
};
