// Copyright 2006 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
 * Expression evaluation utilities. Expression format is very similar to XPath.
 *
 * Expression details:
 * - Of format A/B/C, which will evaluate getChildNode('A').getChildNode('B').
 *    getChildNodes('C')|getChildNodeValue('C')|getChildNode('C') depending on
 *    call
 * - If expression ends with '/name()', will get the name() of the node
 *    referenced by the preceding path.
 * - If expression ends with '/count()', will get the count() of the nodes that
 *    match the expression referenced by the preceding path.
 * - If expression ends with '?', the value is OK to evaluate to null. This is
 *    not enforced by the expression evaluation functions, instead it is
 *    provided as a flag for client code which may ignore depending on usage
 * - If expression has [INDEX], will use getChildNodes().getByIndex(INDEX)
 *
 */


goog.provide('goog.ds.Expr');

goog.require('goog.ds.BasicNodeList');
goog.require('goog.ds.EmptyNodeList');
goog.require('goog.string');



/**
 * Create a new expression. An expression uses a string expression language, and
 * from this string and a passed in DataNode can evaluate to a value, DataNode,
 * or a DataNodeList.
 *
 * @param {string=} opt_expr The string expression.
 * @constructor
 * @final
 */
goog.ds.Expr = function(opt_expr) {
  if (opt_expr) {
    this.setSource_(opt_expr);
  }
};


/**
 * Set the source expression text & parse
 *
 * @param {string} expr The string expression source.
 * @param {Array=} opt_parts Array of the parts of an expression.
 * @param {goog.ds.Expr=} opt_childExpr Optional child of this expression,
 *   passed in as a hint for processing.
 * @param {goog.ds.Expr=} opt_prevExpr Optional preceding expression
 *   (i.e. $A/B/C is previous expression to B/C) passed in as a hint for
 *   processing.
 * @private
 */
goog.ds.Expr.prototype.setSource_ = function(expr, opt_parts,
    opt_childExpr, opt_prevExpr) {
  this.src_ = expr;

  if (!opt_childExpr && !opt_prevExpr) {
    // Check whether it can be empty
    if (goog.string.endsWith(expr, goog.ds.Expr.String_.CAN_BE_EMPTY)) {
      this.canBeEmpty_ = true;
      expr = expr.substring(0, expr.length - 1);
    }

    // Check whether this is an node function
    if (goog.string.endsWith(expr, '()')) {
      if (goog.string.endsWith(expr, goog.ds.Expr.String_.NAME_EXPR) ||
          goog.string.endsWith(expr, goog.ds.Expr.String_.COUNT_EXPR) ||
          goog.string.endsWith(expr, goog.ds.Expr.String_.POSITION_EXPR)) {
        var lastPos = expr.lastIndexOf(goog.ds.Expr.String_.SEPARATOR);
        if (lastPos != -1) {
          this.exprFn_ = expr.substring(lastPos + 1);
          expr = expr.substring(0, lastPos);
        } else {
          this.exprFn_ = expr;
          expr = goog.ds.Expr.String_.CURRENT_NODE_EXPR;
        }
        if (this.exprFn_ == goog.ds.Expr.String_.COUNT_EXPR) {
          this.isCount_ = true;
        }
      }
    }
  }

  // Split into component parts
  this.parts_ = opt_parts || expr.split('/');
  this.size_ = this.parts_.length;
  this.last_ = this.parts_[this.size_ - 1];
  this.root_ = this.parts_[0];

  if (this.size_ == 1) {
    this.rootExpr_ = this;
    this.isAbsolute_ = goog.string.startsWith(expr, '$');
  } else {
    this.rootExpr_ = goog.ds.Expr.createInternal_(this.root_, null,
        this, null);
    this.isAbsolute_ = this.rootExpr_.isAbsolute_;
    this.root_ = this.rootExpr_.root_;
  }

  if (this.size_ == 1 && !this.isAbsolute_) {
    // Check whether expression maps to current node, for convenience
    this.isCurrent_ = (expr == goog.ds.Expr.String_.CURRENT_NODE_EXPR ||
        expr == goog.ds.Expr.String_.EMPTY_EXPR);

    // Whether this expression is just an attribute (i.e. '@foo')
    this.isJustAttribute_ =
        goog.string.startsWith(expr, goog.ds.Expr.String_.ATTRIBUTE_START);

    // Check whether this is a common node expression
    this.isAllChildNodes_ = expr == goog.ds.Expr.String_.ALL_CHILD_NODES_EXPR;
    this.isAllAttributes_ = expr == goog.ds.Expr.String_.ALL_ATTRIBUTES_EXPR;
    this.isAllElements_ = expr == goog.ds.Expr.String_.ALL_ELEMENTS_EXPR;
  }
};


/**
 * Get the source data path for the expression
 * @return {string} The path.
 */
goog.ds.Expr.prototype.getSource = function() {
  return this.src_;
};


/**
 * Gets the last part of the expression.
 * @return {?string} Last part of the expression.
 */
goog.ds.Expr.prototype.getLast = function() {
  return this.last_;
};


/**
 * Gets the parent expression of this expression, or null if this is top level
 * @return {goog.ds.Expr} The parent.
 */
goog.ds.Expr.prototype.getParent = function() {
  if (!this.parentExprSet_) {
    if (this.size_ > 1) {
      this.parentExpr_ = goog.ds.Expr.createInternal_(null,
          this.parts_.slice(0, this.parts_.length - 1), this, null);
    }
    this.parentExprSet_ = true;
  }
  return this.parentExpr_;
};


/**
 * Gets the parent expression of this expression, or null if this is top level
 * @return {goog.ds.Expr} The parent.
 */
goog.ds.Expr.prototype.getNext = function() {
  if (!this.nextExprSet_) {
    if (this.size_ > 1) {
      this.nextExpr_ = goog.ds.Expr.createInternal_(null, this.parts_.slice(1),
          null, this);
    }
    this.nextExprSet_ = true;
  }
  return this.nextExpr_;
};


/**
 * Evaluate an expression on a data node, and return a value
 * Recursively walks through child nodes to evaluate
 * TODO(user) Support other expression functions
 *
 * @param {goog.ds.DataNode=} opt_ds Optional datasource to evaluate against.
 *     If not provided, evaluates against DataManager global root.
 * @return {*} Value of the node, or null if doesn't exist.
 */
goog.ds.Expr.prototype.getValue = function(opt_ds) {
  if (opt_ds == null) {
    opt_ds = goog.ds.DataManager.getInstance();
  } else if (this.isAbsolute_) {
    opt_ds = opt_ds.getDataRoot ? opt_ds.getDataRoot() :
        goog.ds.DataManager.getInstance();
  }

  if (this.isCount_) {
    var nodes = this.getNodes(opt_ds);
    return nodes.getCount();
  }

  if (this.size_ == 1) {
    return opt_ds.getChildNodeValue(this.root_);
  } else if (this.size_ == 0) {
    return opt_ds.get();
  }

  var nextDs = opt_ds.getChildNode(this.root_);

  if (nextDs == null) {
    return null;
  } else {
    return this.getNext().getValue(nextDs);
  }
};


/**
 * Evaluate an expression on a data node, and return matching nodes
 * Recursively walks through child nodes to evaluate
 *
 * @param {goog.ds.DataNode=} opt_ds Optional datasource to evaluate against.
 *     If not provided, evaluates against data root.
 * @param {boolean=} opt_canCreate If true, will try to create new nodes.
 * @return {goog.ds.DataNodeList} Matching nodes.
 */
goog.ds.Expr.prototype.getNodes = function(opt_ds, opt_canCreate) {
  return /** @type {goog.ds.DataNodeList} */(this.getNodes_(opt_ds,
      false, opt_canCreate));
};


/**
 * Evaluate an expression on a data node, and return the first matching node
 * Recursively walks through child nodes to evaluate
 *
 * @param {goog.ds.DataNode=} opt_ds Optional datasource to evaluate against.
 *     If not provided, evaluates against DataManager global root.
 * @param {boolean=} opt_canCreate If true, will try to create new nodes.
 * @return {goog.ds.DataNode} Matching nodes, or null if doesn't exist.
 */
goog.ds.Expr.prototype.getNode = function(opt_ds, opt_canCreate) {
  return /** @type {goog.ds.DataNode} */(this.getNodes_(opt_ds,
      true, opt_canCreate));
};


/**
 * Evaluate an expression on a data node, and return the first matching node
 * Recursively walks through child nodes to evaluate
 *
 * @param {goog.ds.DataNode=} opt_ds Optional datasource to evaluate against.
 *     If not provided, evaluates against DataManager global root.
 * @param {boolean=} opt_selectOne Whether to return single matching DataNode
 *     or matching nodes in DataNodeList.
 * @param {boolean=} opt_canCreate If true, will try to create new nodes.
 * @return {goog.ds.DataNode|goog.ds.DataNodeList} Matching node or nodes,
 *     depending on value of opt_selectOne.
 * @private
 */
goog.ds.Expr.prototype.getNodes_ = function(opt_ds, opt_selectOne,
    opt_canCreate) {
  if (opt_ds == null) {
    opt_ds = goog.ds.DataManager.getInstance();
  } else if (this.isAbsolute_) {
    opt_ds = opt_ds.getDataRoot ? opt_ds.getDataRoot() :
        goog.ds.DataManager.getInstance();
  }

  if (this.size_ == 0 && opt_selectOne) {
      return opt_ds;
  } else if (this.size_ == 0 && !opt_selectOne) {
    return new goog.ds.BasicNodeList([opt_ds]);
  } else if (this.size_ == 1) {
    if (opt_selectOne) {
      return opt_ds.getChildNode(this.root_, opt_canCreate);
    }
    else {
      var possibleListChild = opt_ds.getChildNode(this.root_);
      if (possibleListChild && possibleListChild.isList()) {
        return possibleListChild.getChildNodes();
      } else {
        return opt_ds.getChildNodes(this.root_);
      }
    }
  } else {
    var nextDs = opt_ds.getChildNode(this.root_, opt_canCreate);
    if (nextDs == null && opt_selectOne) {
      return null;
    } else if (nextDs == null && !opt_selectOne) {
      return new goog.ds.EmptyNodeList();
    }
    return this.getNext().getNodes_(nextDs, opt_selectOne, opt_canCreate);
  }
};


/**
 * Whether the expression can be null.
 *
 * @type {boolean}
 * @private
 */
goog.ds.Expr.prototype.canBeEmpty_ = false;


/**
 * The parsed paths in the expression
 *
 * @type {Array<string>}
 * @private
 */
goog.ds.Expr.prototype.parts_ = [];


/**
 * Number of paths in the expression
 *
 * @type {?number}
 * @private
 */
goog.ds.Expr.prototype.size_ = null;


/**
 * The root node path in the expression
 *
 * @type {string}
 * @private
 */
goog.ds.Expr.prototype.root_;


/**
 * The last path in the expression
 *
 * @type {?string}
 * @private
 */
goog.ds.Expr.prototype.last_ = null;


/**
 * Whether the expression evaluates to current node
 *
 * @type {boolean}
 * @private
 */
goog.ds.Expr.prototype.isCurrent_ = false;


/**
 * Whether the expression is just an attribute
 *
 * @type {boolean}
 * @private
 */
goog.ds.Expr.prototype.isJustAttribute_ = false;


/**
 * Does this expression select all DOM-style child nodes (element and text)
 *
 * @type {boolean}
 * @private
 */
goog.ds.Expr.prototype.isAllChildNodes_ = false;


/**
 * Does this expression select all DOM-style attribute nodes (starts with '@')
 *
 * @type {boolean}
 * @private
 */
goog.ds.Expr.prototype.isAllAttributes_ = false;


/**
 * Does this expression select all DOM-style element child nodes
 *
 * @type {boolean}
 * @private
 */
goog.ds.Expr.prototype.isAllElements_ = false;


/**
 * The function used by this expression
 *
 * @type {?string}
 * @private
 */
goog.ds.Expr.prototype.exprFn_ = null;


/**
 * Cached value for the parent expression.
 * @type {goog.ds.Expr?}
 * @private
 */
goog.ds.Expr.prototype.parentExpr_ = null;


/**
 * Cached value for the next expression.
 * @type {goog.ds.Expr?}
 * @private
 */
goog.ds.Expr.prototype.nextExpr_ = null;


/**
 * Create an expression from a string, can use cached values
 *
 * @param {string} expr The expression string.
 * @return {goog.ds.Expr} The expression object.
 */
goog.ds.Expr.create = function(expr) {
  var result = goog.ds.Expr.cache_[expr];

  if (result == null) {
    result = new goog.ds.Expr(expr);
    goog.ds.Expr.cache_[expr] = result;
  }
  return result;
};


/**
 * Create an expression from a string, can use cached values
 * Uses hints from related expressions to help in creation
 *
 * @param {?string=} opt_expr The string expression source.
 * @param {Array=} opt_parts Array of the parts of an expression.
 * @param {goog.ds.Expr=} opt_childExpr Optional child of this expression,
 *   passed in as a hint for processing.
 * @param {goog.ds.Expr=} opt_prevExpr Optional preceding expression
 *   (i.e. $A/B/C is previous expression to B/C) passed in as a hint for
 *   processing.
 * @return {goog.ds.Expr} The expression object.
 * @private
 */
goog.ds.Expr.createInternal_ = function(opt_expr, opt_parts, opt_childExpr,
    opt_prevExpr) {
  var expr = opt_expr || opt_parts.join('/');
  var result = goog.ds.Expr.cache_[expr];

  if (result == null) {
    result = new goog.ds.Expr();
    result.setSource_(expr, opt_parts, opt_childExpr, opt_prevExpr);
    goog.ds.Expr.cache_[expr] = result;
  }
  return result;
};


/**
 * Cache of pre-parsed expressions
 * @private
 */
goog.ds.Expr.cache_ = {};


/**
 * Commonly used strings in expressions.
 * @enum {string}
 * @private
 */
goog.ds.Expr.String_ = {
  SEPARATOR: '/',
  CURRENT_NODE_EXPR: '.',
  EMPTY_EXPR: '',
  ATTRIBUTE_START: '@',
  ALL_CHILD_NODES_EXPR: '*|text()',
  ALL_ATTRIBUTES_EXPR: '@*',
  ALL_ELEMENTS_EXPR: '*',
  NAME_EXPR: 'name()',
  COUNT_EXPR: 'count()',
  POSITION_EXPR: 'position()',
  INDEX_START: '[',
  INDEX_END: ']',
  CAN_BE_EMPTY: '?'
};


/**
 * Standard expressions
 */


/**
 * The current node
 */
goog.ds.Expr.CURRENT = goog.ds.Expr.create(
    goog.ds.Expr.String_.CURRENT_NODE_EXPR);


/**
 * For DOM interop - all DOM child nodes (text + element).
 * Text nodes have dataName #text
 */
goog.ds.Expr.ALL_CHILD_NODES =
    goog.ds.Expr.create(goog.ds.Expr.String_.ALL_CHILD_NODES_EXPR);


/**
 * For DOM interop - all DOM element child nodes
 */
goog.ds.Expr.ALL_ELEMENTS =
    goog.ds.Expr.create(goog.ds.Expr.String_.ALL_ELEMENTS_EXPR);


/**
 * For DOM interop - all DOM attribute nodes
 * Attribute nodes have dataName starting with "@"
 */
goog.ds.Expr.ALL_ATTRIBUTES =
    goog.ds.Expr.create(goog.ds.Expr.String_.ALL_ATTRIBUTES_EXPR);


/**
 * Get the dataName of a node
 */
goog.ds.Expr.NAME = goog.ds.Expr.create(goog.ds.Expr.String_.NAME_EXPR);


/**
 * Get the count of nodes matching an expression
 */
goog.ds.Expr.COUNT = goog.ds.Expr.create(goog.ds.Expr.String_.COUNT_EXPR);


/**
 * Get the position of the "current" node in the current node list
 * This will only apply for datasources that support the concept of a current
 * node (none exist yet). This is similar to XPath position() and concept of
 * current node
 */
goog.ds.Expr.POSITION = goog.ds.Expr.create(goog.ds.Expr.String_.POSITION_EXPR);
