// 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 Generic rich data access API.
 *
 * Abstraction for data sources that allows listening for changes at different
 * levels of the data tree and updating the data via XHR requests
 *
 */


goog.provide('goog.ds.BaseDataNode');
goog.provide('goog.ds.BasicNodeList');
goog.provide('goog.ds.DataNode');
goog.provide('goog.ds.DataNodeList');
goog.provide('goog.ds.EmptyNodeList');
goog.provide('goog.ds.LoadState');
goog.provide('goog.ds.SortedNodeList');
goog.provide('goog.ds.Util');
goog.provide('goog.ds.logger');

goog.require('goog.array');
goog.require('goog.log');



/**
 * Interface for node in rich data tree.
 *
 * Names that are reserved for system use and shouldn't be used for data node
 * names: eval, toSource, toString, unwatch, valueOf, watch. Behavior is
 * undefined if these names are used.
 *
 * @constructor
 */
goog.ds.DataNode = function() {};


/**
 * Get the value of the node
 * @param {...?} var_args Do not check arity of arguments, because
 *     some subclasses require args.
 * @return {*} The value of the node, or null if no value.
 */
goog.ds.DataNode.prototype.get = goog.abstractMethod;


/**
 * Set the value of the node
 * @param {*} value The new value of the node.
 */
goog.ds.DataNode.prototype.set = goog.abstractMethod;


/**
 * Gets all of the child nodes of the current node.
 * Should return an empty DataNode list if no child nodes.
 * @param {string=} opt_selector String selector to choose child nodes.
 * @return {!goog.ds.DataNodeList} The child nodes.
 */
goog.ds.DataNode.prototype.getChildNodes = goog.abstractMethod;


/**
 * Gets a named child node of the current node
 * @param {string} name The node name.
 * @param {boolean=} opt_canCreate Whether to create a child node if it does not
 *     exist.
 * @return {goog.ds.DataNode} The child node, or null
 * if no node of this name exists.
 */
goog.ds.DataNode.prototype.getChildNode = goog.abstractMethod;


/**
 * Gets the value of a child node
 * @param {string} name The node name.
 * @return {*} The value of the node, or null if no value or the child node
 *     doesn't exist.
 */
goog.ds.DataNode.prototype.getChildNodeValue = goog.abstractMethod;


/**
 * Sets a named child node of the current node.
 *
 * @param {string} name The node name.
 * @param {Object} value The value to set, can be DataNode, object, property,
 *     or null. If value is null, removes the child node.
 * @return {Object} The child node, if the node was set.
 */
goog.ds.DataNode.prototype.setChildNode = goog.abstractMethod;


/**
 * Get the name of the node relative to the parent node
 * @return {string} The name of the node.
 */
goog.ds.DataNode.prototype.getDataName = goog.abstractMethod;


/**
 * Set the name of the node relative to the parent node
 * @param {string} name The name of the node.
 */
goog.ds.DataNode.prototype.setDataName = goog.abstractMethod;


/**
 * Gets the a qualified data path to this node
 * @return {string} The data path.
 */
goog.ds.DataNode.prototype.getDataPath = goog.abstractMethod;


/**
 * Load or reload the backing data for this node
 */
goog.ds.DataNode.prototype.load = goog.abstractMethod;


/**
 * Gets the state of the backing data for this node
 * @return {goog.ds.LoadState} The state.
 */
goog.ds.DataNode.prototype.getLoadState = goog.abstractMethod;


/**
 * Whether the value of this node is a homogeneous list of data
 * @return {boolean} True if a list.
 */
goog.ds.DataNode.prototype.isList = goog.abstractMethod;


/**
 * Enum for load state of a DataNode.
 * @enum {string}
 */
goog.ds.LoadState = {
  LOADED: 'LOADED',
  LOADING: 'LOADING',
  FAILED: 'FAILED',
  NOT_LOADED: 'NOT_LOADED'
};



/**
 * Base class for data node functionality, has default implementations for
 * many of the functions.
 *
 * implements {goog.ds.DataNode}
 * @constructor
 */
goog.ds.BaseDataNode = function() {};


/**
 * Set the value of the node
 * @param {Object} value The new value of the node.
 */
goog.ds.BaseDataNode.prototype.set = goog.abstractMethod;


/**
 * Gets all of the child nodes of the current node.
 * Should return an empty DataNode list if no child nodes.
 * @param {string=} opt_selector String selector to choose child nodes.
 * @return {!goog.ds.DataNodeList} The child nodes.
 */
goog.ds.BaseDataNode.prototype.getChildNodes = function(opt_selector) {
  return new goog.ds.EmptyNodeList();
};


/**
 * Gets a named child node of the current node
 * @param {string} name The node name.
 * @param {boolean=} opt_canCreate Whether you can create the child node if
 *     it doesn't exist already.
 * @return {goog.ds.DataNode} The child node, or null if no node of
 *     this name exists and opt_create is false.
 */
goog.ds.BaseDataNode.prototype.getChildNode = function(name, opt_canCreate) {
  return null;
};


/**
 * Gets the value of a child node
 * @param {string} name The node name.
 * @return {Object} The value of the node, or null if no value or the
 *     child node doesn't exist.
 */
goog.ds.BaseDataNode.prototype.getChildNodeValue = function(name) {
  return null;
};


/**
 * Get the name of the node relative to the parent node
 * @return {string} The name of the node.
 */
goog.ds.BaseDataNode.prototype.getDataName = goog.abstractMethod;


/**
 * Gets the a qualified data path to this node
 * @return {string} The data path.
 */
goog.ds.BaseDataNode.prototype.getDataPath = function() {
  var parentPath = '';
  var myName = this.getDataName();
  if (this.getParent && this.getParent()) {
    parentPath = this.getParent().getDataPath() +
        (myName.indexOf(goog.ds.STR_ARRAY_START) != -1 ? '' :
        goog.ds.STR_PATH_SEPARATOR);
  }

  return parentPath + myName;
};


/**
 * Load or reload the backing data for this node
 */
goog.ds.BaseDataNode.prototype.load = goog.nullFunction;


/**
 * Gets the state of the backing data for this node
 * @return {goog.ds.LoadState} The state.
 */
goog.ds.BaseDataNode.prototype.getLoadState = function() {
  return goog.ds.LoadState.LOADED;
};


/**
 * Gets the parent node. Subclasses implement this function
 * @type {Function}
 * @protected
 */
goog.ds.BaseDataNode.prototype.getParent = null;


/**
 * Interface for node list in rich data tree.
 *
 * Has both map and list-style accessors
 *
 * @constructor
 * @extends {goog.ds.DataNode}
 */
// TODO(arv): Use interfaces when available.
goog.ds.DataNodeList = function() {};


/**
 * Add a node to the node list.
 * If the node has a dataName, uses this for the key in the map.
 *
 * @param {goog.ds.DataNode} node The node to add.
 */
goog.ds.DataNodeList.prototype.add = goog.abstractMethod;


/**
 * Get a node by string key.
 * Returns null if node doesn't exist.
 *
 * @param {string} key String lookup key.
 * @return {*} The node, or null if doesn't exist.
 * @override
 */
goog.ds.DataNodeList.prototype.get = goog.abstractMethod;


/**
 * Get a node by index
 * Returns null if the index is out of range
 *
 * @param {number} index The index of the node.
 * @return {goog.ds.DataNode} The node, or null if doesn't exist.
 */
goog.ds.DataNodeList.prototype.getByIndex = goog.abstractMethod;


/**
 * Gets the size of the node list
 *
 * @return {number} The size of the list.
 */
goog.ds.DataNodeList.prototype.getCount = goog.abstractMethod;


/**
 * Sets a node in the list of a given name
 * @param {string} name Name of the node.
 * @param {goog.ds.DataNode} node The node.
 */
goog.ds.DataNodeList.prototype.setNode = goog.abstractMethod;


/**
 * Removes a node in the list of a given name
 * @param {string} name Name of the node.
 * @return {boolean} True if node existed and was deleted.
 */
goog.ds.DataNodeList.prototype.removeNode = goog.abstractMethod;


/**
 * Simple node list implementation with underlying array and map
 * implements goog.ds.DataNodeList.
 *
 * Names that are reserved for system use and shouldn't be used for data node
 * names: eval, toSource, toString, unwatch, valueOf, watch. Behavior is
 * undefined if these names are used.
 *
 * @param {Array<goog.ds.DataNode>=} opt_nodes optional nodes to add to list.
 * @constructor
 * @extends {goog.ds.DataNodeList}
 */
// TODO(arv): Use interfaces when available.
goog.ds.BasicNodeList = function(opt_nodes) {
  this.map_ = {};
  this.list_ = [];
  this.indexMap_ = {};
  if (opt_nodes) {
    for (var i = 0, node; node = opt_nodes[i]; i++) {
      this.add(node);
    }
  }
};


/**
 * Add a node to the node list.
 * If the node has a dataName, uses this for the key in the map.
 * TODO(user) Remove function as well
 *
 * @param {goog.ds.DataNode} node The node to add.
 * @override
 */
goog.ds.BasicNodeList.prototype.add = function(node) {
  this.list_.push(node);
  var dataName = node.getDataName();
  if (dataName) {
    this.map_[dataName] = node;
    this.indexMap_[dataName] = this.list_.length - 1;
  }
};


/**
 * Get a node by string key.
 * Returns null if node doesn't exist.
 *
 * @param {string} key String lookup key.
 * @return {goog.ds.DataNode} The node, or null if doesn't exist.
 * @override
 */
goog.ds.BasicNodeList.prototype.get = function(key) {
  return this.map_[key] || null;
};


/**
 * Get a node by index
 * Returns null if the index is out of range
 *
 * @param {number} index The index of the node.
 * @return {goog.ds.DataNode} The node, or null if doesn't exist.
 * @override
 */
goog.ds.BasicNodeList.prototype.getByIndex = function(index) {
  return this.list_[index] || null;
};


/**
 * Gets the size of the node list
 *
 * @return {number} The size of the list.
 * @override
 */
goog.ds.BasicNodeList.prototype.getCount = function() {
  return this.list_.length;
};


/**
 * Sets a node in the list of a given name
 * @param {string} name Name of the node.
 * @param {goog.ds.DataNode} node The node.
 * @override
 */
goog.ds.BasicNodeList.prototype.setNode = function(name, node) {
  if (node == null) {
    this.removeNode(name);
  } else {
    var existingNode = this.indexMap_[name];
    if (existingNode != null) {
      this.map_[name] = node;
      this.list_[existingNode] = node;
    } else {
      this.add(node);
    }
  }
};


/**
 * Removes a node in the list of a given name
 * @param {string} name Name of the node.
 * @return {boolean} True if node existed and was deleted.
 * @override
 */
goog.ds.BasicNodeList.prototype.removeNode = function(name) {
  var existingNode = this.indexMap_[name];
  if (existingNode != null) {
    this.list_.splice(existingNode, 1);
    delete this.map_[name];
    delete this.indexMap_[name];
    for (var index in this.indexMap_) {
      if (this.indexMap_[index] > existingNode) {
        this.indexMap_[index]--;
      }
    }
  }
  return existingNode != null;
};


/**
 * Get the index of a named node
 * @param {string} name The name of the node to get the index of.
 * @return {number|undefined} The index.
 */
goog.ds.BasicNodeList.prototype.indexOf = function(name) {
  return this.indexMap_[name];
};


/**
 * Immulatable empty node list
 * @extends {goog.ds.BasicNodeList}
 * @constructor
 * @final
 */

goog.ds.EmptyNodeList = function() {
  goog.ds.BasicNodeList.call(this);
};
goog.inherits(goog.ds.EmptyNodeList, goog.ds.BasicNodeList);


/**
 * Add a node to the node list.
 * If the node has a dataName, uses this for the key in the map.
 *
 * @param {goog.ds.DataNode} node The node to add.
 * @override
 */
goog.ds.EmptyNodeList.prototype.add = function(node) {
  throw Error('Can\'t add to EmptyNodeList');
};



/**
 * Node list implementation which maintains sort order during insertion and
 * modification operations based on a comparison function.
 *
 * The SortedNodeList does not guarantee sort order will be maintained if
 * the underlying data nodes are modified externally.
 *
 * Names that are reserved for system use and shouldn't be used for data node
 * names: eval, toSource, toString, unwatch, valueOf, watch. Behavior is
 * undefined if these names are used.
 *
 * @param {Function} compareFn Comparison function by which the
 *     node list is sorted. Should take 2 arguments to compare, and return a
 *     negative integer, zero, or a positive integer depending on whether the
 *     first argument is less than, equal to, or greater than the second.
 * @param {Array<goog.ds.DataNode>=} opt_nodes optional nodes to add to list;
 *    these are assumed to be in sorted order.
 * @extends {goog.ds.BasicNodeList}
 * @constructor
 */
goog.ds.SortedNodeList = function(compareFn, opt_nodes) {
  this.compareFn_ = compareFn;
  goog.ds.BasicNodeList.call(this, opt_nodes);
};
goog.inherits(goog.ds.SortedNodeList, goog.ds.BasicNodeList);


/**
 * Add a node to the node list, maintaining sort order.
 * If the node has a dataName, uses this for the key in the map.
 *
 * @param {goog.ds.DataNode} node The node to add.
 * @override
 */
goog.ds.SortedNodeList.prototype.add = function(node) {
  if (!this.compareFn_) {
    this.append(node);
    return;
  }

  var searchLoc = goog.array.binarySearch(this.list_, node, this.compareFn_);

  // if there is another node that is "equal" according to the comparison
  // function, insert before that one; otherwise insert at the location
  // goog.array.binarySearch indicated
  if (searchLoc < 0) {
    searchLoc = -(searchLoc + 1);
  }

  // update any indexes that are after the insertion point
  for (var index in this.indexMap_) {
    if (this.indexMap_[index] >= searchLoc) {
      this.indexMap_[index]++;
    }
  }

  goog.array.insertAt(this.list_, node, searchLoc);
  var dataName = node.getDataName();
  if (dataName) {
    this.map_[dataName] = node;
    this.indexMap_[dataName] = searchLoc;
  }
};


/**
 * Adds the given node to the end of the SortedNodeList. This should
 * only be used when the caller can guarantee that the sort order will
 * be maintained according to this SortedNodeList's compareFn (e.g.
 * when initializing a new SortedNodeList from a list of nodes that has
 * already been sorted).
 * @param {goog.ds.DataNode} node The node to append.
 */
goog.ds.SortedNodeList.prototype.append = function(node) {
  goog.ds.SortedNodeList.superClass_.add.call(this, node);
};


/**
 * Sets a node in the list of a given name, maintaining sort order.
 * @param {string} name Name of the node.
 * @param {goog.ds.DataNode} node The node.
 * @override
 */
goog.ds.SortedNodeList.prototype.setNode = function(name, node) {
  if (node == null) {
    this.removeNode(name);
  } else {
    var existingNode = this.indexMap_[name];
    if (existingNode != null) {
      if (this.compareFn_) {
        var compareResult = this.compareFn_(this.list_[existingNode], node);
        if (compareResult == 0) {
          // the new node can just replace the old one
          this.map_[name] = node;
          this.list_[existingNode] = node;
        } else {
          // remove the old node, then add the new one
          this.removeNode(name);
          this.add(node);
        }
      }
    } else {
      this.add(node);
    }
  }
};


/**
 * The character denoting an attribute.
 * @type {string}
 */
goog.ds.STR_ATTRIBUTE_START = '@';


/**
 * The character denoting all children.
 * @type {string}
 */
goog.ds.STR_ALL_CHILDREN_SELECTOR = '*';


/**
 * The wildcard character.
 * @type {string}
 */
goog.ds.STR_WILDCARD = '*';


/**
 * The character denoting path separation.
 * @type {string}
 */
goog.ds.STR_PATH_SEPARATOR = '/';


/**
 * The character denoting the start of an array.
 * @type {string}
 */
goog.ds.STR_ARRAY_START = '[';


/**
 * Shared logger instance for data package
 * @type {goog.log.Logger}
 */
goog.ds.logger = goog.log.getLogger('goog.ds');


/**
 * Create a data node that references another data node,
 * useful for pointer-like functionality.
 * All functions will return same values as the original node except for
 * getDataName()
 * @param {!goog.ds.DataNode} node The original node.
 * @param {string} name The new name.
 * @return {!goog.ds.DataNode} The new data node.
 */
goog.ds.Util.makeReferenceNode = function(node, name) {
  /**
   * @constructor
   * @extends {goog.ds.DataNode}
   * @final
   */
  var nodeCreator = function() {};
  nodeCreator.prototype = node;
  var newNode = new nodeCreator();
  newNode.getDataName = function() {
    return name;
  };
  return newNode;
};
