/*
 * Copyright (c) 1998-2008 Caucho Technology -- all rights reserved
 *
 * This file is part of Resin(R) Open Source
 *
 * Each copy or derived work must preserve the copyright notice and this
 * notice unmodified.
 *
 * Resin Open Source is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * Resin Open Source is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, or any warranty
 * of NON-INFRINGEMENT.  See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Resin Open Source; if not, write to the
 *   Free SoftwareFoundation, Inc.
 *   59 Temple Place, Suite 330
 *   Boston, MA 02111-1307  USA
 *
 * @author Scott Ferguson
 */

package com.caucho.xpath;

import com.caucho.util.CharBuffer;
import com.caucho.xml.XmlChar;
import com.caucho.xml.XmlUtil;
import com.caucho.xpath.expr.ObjectVar;
import com.caucho.xpath.expr.Var;
import com.caucho.xpath.pattern.AbstractPattern;
import com.caucho.xpath.pattern.FromExpr;
import com.caucho.xpath.pattern.NodeArrayListIterator;
import com.caucho.xpath.pattern.NodeIterator;
import com.caucho.xpath.pattern.NodeListIterator;
import com.caucho.xpath.pattern.SingleNodeIterator;

import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import java.util.ArrayList;
import java.util.Iterator;

/**
 * Compute values from nodes.  Because the expressions themselves are
 * untyped, the class provides methods for creating the type of the
 * desired result.
 */
abstract public class Expr {
  protected static final int CONST = 0;

  protected static final int NODE_SET = CONST + 1;
  protected static final int ID = NODE_SET + 1;

  protected static final int OR = ID + 1;
  protected static final int AND = OR + 1;

  protected static final int EQ = AND + 1;
  protected static final int NEQ = EQ + 1;
  protected static final int LT = NEQ + 1;
  protected static final int LE = LT + 1;
  protected static final int GT = LE + 1;
  protected static final int GE = GT + 1;
  
  protected static final int BOOLEAN_EQ = GE + 1;
  protected static final int BOOLEAN_NEQ = BOOLEAN_EQ + 1;
  
  protected static final int NUMBER_EQ = BOOLEAN_NEQ + 1;
  protected static final int NUMBER_NEQ = NUMBER_EQ + 1;
  protected static final int NUMBER_LT = NUMBER_NEQ + 1;
  protected static final int NUMBER_LE = NUMBER_LT + 1;
  protected static final int NUMBER_GT = NUMBER_LE + 1;
  protected static final int NUMBER_GE = NUMBER_GT + 1;
  
  protected static final int STRING_EQ = NUMBER_GE + 1;
  protected static final int STRING_NEQ = STRING_EQ + 1;

  protected static final int NEG = STRING_NEQ + 1;
  protected static final int ADD = NEG + 1;
  protected static final int SUB = ADD + 1;
  protected static final int MUL = SUB + 1;
  protected static final int DIV = MUL + 1;
  protected static final int QUO = DIV + 1;
  protected static final int MOD = QUO + 1;

  protected static final int TRUE = MOD + 1;
  protected static final int FALSE = TRUE + 1;
  protected static final int NOT = FALSE + 1;
  protected static final int BOOLEAN = NOT + 1;
  protected static final int LANG = BOOLEAN + 1;

  protected static final int NUMBER = LANG + 1;
  protected static final int SUM = NUMBER + 1;
  protected static final int FLOOR = SUM + 1;
  protected static final int CEILING = FLOOR + 1;
  protected static final int ROUND = CEILING + 1;
  public static final int POSITION = ROUND + 1;
  protected static final int COUNT = POSITION + 1;
  protected static final int LAST = COUNT + 1;

  protected static final int STRING = LAST + 1;
  protected static final int CONCAT = STRING + 1;
  protected static final int STARTS_WITH = CONCAT + 1;
  protected static final int CONTAINS = STARTS_WITH + 1;
  protected static final int SUBSTRING = CONTAINS + 1;
  protected static final int SUBSTRING_BEFORE = SUBSTRING + 1;
  protected static final int SUBSTRING_AFTER = SUBSTRING_BEFORE + 1;
  protected static final int STRING_LENGTH = SUBSTRING_AFTER + 1;
  protected static final int NORMALIZE = STRING_LENGTH + 1;
  protected static final int TRANSLATE = NORMALIZE + 1;
  protected static final int FORMAT_NUMBER = TRANSLATE + 1;

  protected static final int LOCAL_PART = FORMAT_NUMBER + 1;
  protected static final int NAMESPACE = LOCAL_PART + 1;
  protected static final int QNAME = NAMESPACE + 1;
  protected static final int GENERATE_ID = QNAME + 1;

  protected static final int FUNCTION_AVAILABLE = GENERATE_ID + 1;
  protected static final int SYSTEM_PROPERTY = FUNCTION_AVAILABLE + 1;

  protected static final int IF = SYSTEM_PROPERTY + 1;
  
  protected static final int SELF = IF + 1;
  protected static final int SELF_NAME = SELF + 1;
  protected static final int ATTRIBUTE = SELF_NAME + 1;
  protected static final int ELEMENT = ATTRIBUTE + 1;
  
  protected static final int BASE_URI = ELEMENT + 1;
  protected static final int LAST_FUN = BASE_URI + 1;

  private AbstractPattern listContext;
  
  protected Expr() {}

  public void setListContext(AbstractPattern listContext)
  {
    this.listContext = listContext;
  }

  public AbstractPattern getListContext()
  {
    return listContext;
  }
  
  /**
   * true if the expression prefers to return a number.
   */
  public boolean isNumber()
  {
    return false;
  }
  
  /**
   * Evaluates the expression as a double using the node as a context.
   *
   * @param node the node to evaluate and use as a context
   *
   * @return the numeric value.
   */
  public double evalNumber(Node node)
    throws XPathException
  {
    Env env = XPath.createEnv();
    env.setCurrentNode(node);
    env.setContextNode(node);

    double result = evalNumber(node, env);

    XPath.freeEnv(env);

    return result;
  }

  /**
   * Evaluates the expression as a number.
   *
   * @param node the current node.
   * @param env variable environment.
   *
   * @return the numeric value.
   */
  public abstract double evalNumber(Node node, ExprEnvironment env)
    throws XPathException;

  /**
   * true if the expression prefers to return a boolean.
   */
  public boolean isBoolean()
  {
    return false;
  }

  /**
   * Returns the boolean value of the node.
   *
   * @param node the node to evaluate and use as a context
   *
   * @return the boolean value
   */
  public boolean evalBoolean(Node node)
    throws XPathException
  {
    Env env = XPath.createEnv();
    env.setCurrentNode(node);
    env.setContextNode(node);

    boolean result = evalBoolean(node, env);

    XPath.freeEnv(env);

    return result;
  }

  /**
   * Returns the boolean value of the node.
   *
   * @param node the node to evaluate and use as a context
   * @param env variable environment.
   *
   * @return the boolean value.
   */
  public abstract boolean evalBoolean(Node node, ExprEnvironment env)
    throws XPathException;

  /**
   * Returns the expression evaluated as a string.
   *
   * @param node the node to evaluate and use as a context
   *
   * @return the string value of the expression.
   */
  public String evalString(Node node)
    throws XPathException
  {
    Env env = XPath.createEnv();
    env.setCurrentNode(node);
    env.setContextNode(node);

    String result = evalString(node, env);

    XPath.freeEnv(env);

    return result;
  }

  /**
   * true if the expression prefers to return a string.
   */
  public boolean isString()
  {
    return false;
  }

  /**
   * Returns the string value of the node.
   *
   * @param node the node to evaluate and use as a context
   * @param env variable environment.
   */
  public abstract String evalString(Node node, ExprEnvironment env)
    throws XPathException;
  
  /**
   * Fills a char buffer with the evaluated string results.
   *
   * @param cb the buffer containing the results.
   * @param node the node to evaluate and use as a context
   */
  public void evalString(CharBuffer cb, Node node)
    throws XPathException
  {
    Env env = XPath.createEnv();
    env.setCurrentNode(node);
    env.setContextNode(node);

    evalString(cb, node, env);

    XPath.freeEnv(env);
  }
  
  /**
   * Fills a char buffer with the evaluated string results.
   *
   * @param cb the buffer containing the results.
   * @param node the node to evaluate and use as a context
   * @param env the variable environment
   */
  public void evalString(CharBuffer cb, Node node, ExprEnvironment env)
    throws XPathException
  {
    cb.append(evalString(node, env));
  }

  /**
   * true if the expression prefers to return a node set.
   */
  public boolean isNodeSet()
  {
    return false;
  }

  /**
   * Returns an iterator of matching nodes
   *
   * @param node the node to evaluate and use as a context
   *
   * @return the value as a node iterator.
   */
  public NodeIterator evalNodeSet(Node node)
    throws XPathException
  {
    Env env = XPath.createEnv();
    env.setCurrentNode(node);
    env.setContextNode(node);

    NodeIterator result = evalNodeSet(node, env);

    XPath.freeEnv(env);

    return result;
  }

  /**
   * Returns an iterator of matching nodes
   *
   * @param node the node to evaluate and use as a context
   * @param env variable environment.
   *
   * @return the value as a node iterator.
   */
  public NodeIterator evalNodeSet(Node node, ExprEnvironment env)
    throws XPathException
  {
    Object obj = evalObject(node, env);

    if (obj instanceof Node)
      return new SingleNodeIterator(env, (Node) obj);

    else if (obj instanceof NodeList)
      return new NodeListIterator(env, (NodeList) obj);

    else if (obj instanceof NodeIterator)
      return (NodeIterator) obj;

    else if (obj instanceof ArrayList)
      return new NodeArrayListIterator(env, (ArrayList) obj);

    else {
      return new SingleNodeIterator(env, null);
    }
  }
  
  /**
   * Returns the object value of the node.
   *
   * @param node the node to evaluate and use as a context
   */
  public Object evalObject(Node node)
    throws XPathException
  {
    Env env = XPath.createEnv();
    env.setCurrentNode(node);
    env.setContextNode(node);

    Object result = evalObject(node, env);

    XPath.freeEnv(env);

    return result;
  }

  /**
   * Returns the object value of the node.
   *
   * @param node the node to evaluate and use as a context
   * @param env variable environment.
   */
  public abstract Object evalObject(Node node, ExprEnvironment env)
    throws XPathException;

  /**
   * Evaluates to a variable.
   *
   * @param node the node to evaluate and use as a context.
   * @param env the variable environment.
   *
   * @return a variable containing the value.
   */
  public Var evalVar(Node node, ExprEnvironment env)
    throws XPathException
  {
    Object obj = evalObject(node, env);

    return new ObjectVar(obj);
  }

  /**
   * Adds a variable with the expression's value.
   */
  public void addVar(Env newEnv, String name, Node node, Env env)
    throws XPathException
  {
    Var var = evalVar(node, env);

    newEnv.addVar(name, var);
  }

  /**
   * Sets a variable with the expression's value.
   */
  public void setVar(String name, Node node, Env env)
    throws XPathException
  {
    env.setVar(name, evalVar(node, env));
  }

  /**
   * Adds a param with the expression's value.
   */
  public void addParam(Env newEnv, String name,
                       Node node, Env env)
    throws XPathException
  {
    Var var = env.getVar(name);
      
    if (var == null)
      newEnv.addVar(name, evalVar(node, env));
    else
      newEnv.addVar(name, var);
  }

  /**
   * Convert a Java object to a boolean using the XPath rules.
   */
  public static boolean toBoolean(Object value)
    throws XPathException
  {
    if (value instanceof Node)
      value = XmlUtil.textValue((Node) value);
    else if (value instanceof NodeList) {
      NodeList list = (NodeList) value;

      return list.item(0) != null;
    }
    else if (value instanceof ArrayList) {
      ArrayList list = (ArrayList) value;
      return list.size() > 0;
    }
    else if (value instanceof Iterator) {
      return ((Iterator) value).hasNext();
    }

    if (value == null)
      return false;

    else if (value instanceof Double) {
      Double d = (Double) value;

      return d.doubleValue() != 0;
    }
    else if (value instanceof Boolean) {
      Boolean b = (Boolean) value;
      return b.booleanValue();
    }
    else if (value instanceof String) {
      String string = (String) value;
      return string != null && string.length() > 0;
    }
    else
      return true;
  }

  /**
   * Convert a Java object to a double using the XPath rules.
   */
  public static double toDouble(Object value)
    throws XPathException
  {
    if (value instanceof Node) {
      String string = XmlUtil.textValue((Node) value);

      if (string == null)
	return 0;
      else
	return stringToNumber(string);
    }
    else if (value instanceof NodeList) {
      NodeList list = (NodeList) value;

      value = list.item(0);
    }
    else if (value instanceof ArrayList) {
      ArrayList list = (ArrayList) value;
      if (list.size() > 0)
	value = list.get(0);
      else
	value = null;
    }
    else if (value instanceof NodeIterator) {
      value = ((NodeIterator) value).nextNode();
    }

    if (value instanceof Node)
      value = XmlUtil.textValue((Node) value);

    if (value == null)
      return 0;

    if (value instanceof Number) {
      Number d = (Number) value;

      return d.doubleValue();
    }
    else if (value instanceof Boolean) {
      Boolean b = (Boolean) value;
      return b.booleanValue() ? 1 : 0;
    }
    else if (value instanceof String) {
      return stringToNumber((String) value);
    }
    else
      return 0;
  }

  /**
   * Convert a Java object to a string using the XPath rules.
   */
  public static String toString(Object value)
    throws XPathException
  {
    if (value instanceof Node) {
      String s = XmlUtil.textValue((Node) value);

      if (s == null)
	return "";
      else
	return s;
    }
    else if (value instanceof NodeList) {
      NodeList list = (NodeList) value;

      value = list.item(0);
    }
    else if (value instanceof ArrayList) {
      ArrayList list = (ArrayList) value;
      if (list.size() > 0)
	value = list.get(0);
      else
	value = null;
    }
    else if (value instanceof Iterator) {
      value = ((Iterator) value).next();
    }

    if (value instanceof Node)
      value = XmlUtil.textValue((Node) value);
    else if (value instanceof Double) {
      double d = ((Double) value).doubleValue();

      if ((int) d == d)
	return String.valueOf((int) d);
      else
	return String.valueOf(d);
    }

    if (value == null)
      return "";
    else
      return value.toString();
  }

  /**
   * Convert a Java object to a node using the XPath rules.
   */
  public static Node toNode(Object value)
    throws XPathException
  {
    if (value instanceof Node)
      return (Node) value;
    else if (value instanceof NodeList) {
      NodeList list = (NodeList) value;
      value = list.item(0);
    }
    else if (value instanceof ArrayList) {
      ArrayList list = (ArrayList) value;
      if (list.size() > 0)
	value = list.get(0);
      else
	value = null;
    }
    else if (value instanceof NodeIterator) {
      value = ((NodeIterator) value).nextNode();
    }

    if (value instanceof Node)
      return (Node) value;
    else
      return null;
  }

  /**
   * Convert a string to a double following XPath.
   *
   * @param string string to be treated as a double.
   * @return the double value.
   */
  static protected double stringToNumber(String string)
    throws XPathException
  {
    int i = 0;
    int length = string.length();
    boolean isNumber = false;
    for (; i < length && XmlChar.isWhitespace(string.charAt(i)); i++) {
    }

    if (i >= length)
      return 0;

    int ch = string.charAt(i);;
    int sign = 1;
    if (ch == '-') {
      sign = -1;
      for (i++; i < length && XmlChar.isWhitespace(string.charAt(i)); i++) {
      }
    }
      
    double value = 0;
    double exp = 1;
    for (; i < length && (ch = string.charAt(i)) >= '0' && ch <= '9'; i++) {
      value = 10 * value + ch - '0';
      isNumber = true;
    }

    if (ch == '.') {
      for (i++;
	   i < length && (ch = string.charAt(i)) >= '0' && ch <= '9';
	   i++) {
	value = 10 * value + ch - '0';
	isNumber = true;
	exp = 10 * exp;
      }
    }

    double pexp = 1.0;
    if (ch == 'e' || ch == 'E') {
      int eSign = 1;
      i++;

      if (i >= length)
        return Double.NaN;

      if (string.charAt(i) == '-') {
        eSign = -1;
        i++;
      }
      else if (string.charAt(i) == '+') {
        i++;
      }

      int v = 0;
      for (; i < length && (ch = string.charAt(i)) >= '0' && ch <= '9'; i++) {
        v = v * 10 + ch - '0';
      }

      pexp = Math.pow(10, eSign * v);
    }

    for (; i < length && XmlChar.isWhitespace(string.charAt(i)); i++) {
    }

    if (i < length || ! isNumber)
      return Double.NaN;
    else
      return sign * value * pexp / exp;
  }

  /**
   * Convert from an expression to a pattern.
   */
  protected AbstractPattern toNodeList()
  {
    return new FromExpr(null, this);
  }
}
