/*
 * Copyright (c) 1998-2012 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.expr;

import com.caucho.util.CharBuffer;
import com.caucho.xml.XmlChar;
import com.caucho.xpath.Expr;
import com.caucho.xpath.ExprEnvironment;
import com.caucho.xpath.XPathException;

import org.w3c.dom.Node;

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

/**
 * Implements the builtin XPath string expressions.
 */
public class StringExpr extends Expr {
  // Code of the expression defined in Expr
  private int _code;
  // First argument
  private Expr _left;
  // Second argument
  private Expr _right;
  // Third
  private Expr _third;
  private String _value;
  // Arguments for more than 3.
  private ArrayList _args;

  /**
   * Create a StringExpression with three arguments.
   *
   * @param code the Expr code of the expression.
   * @param left the first argument.
   * @param right the second argument.
   * @param third the third argument.
   */
  public StringExpr(int code, Expr left, Expr right, Expr third)
  {
    _code = code;
    _left = left;
    _right = right;
    _third = third;
  }

  public StringExpr(int code, Expr left, Expr right)
  {
    _code = code;
    _left = left;
    _right = right;
  }

  public StringExpr(int code, Expr expr)
  {
    _code = code;
    _left = expr;
  }

  public StringExpr(String value)
  {
    _code = CONST;
    _value = value;
  }

  /**
   * Creates a string expression from a list of arguments.
   *
   * @param code Expr code for the function.
   * @param args array list of the arguments.
   */
  public StringExpr(int code, ArrayList args)
  {
    _code = code;
    _args = args;

    if (args.size() > 0)
      _left = (Expr) args.get(0);
    if (args.size() > 1)
      _right = (Expr) args.get(1);
    if (args.size() > 2)
      _third = (Expr) args.get(2);
  }

  /**
   * The StringExpr returns a string value.
   */
  public boolean isString()
  {
    return true;
  }

  public String getValue()
  {
    return _value;
  }


  /**
   * Evaluates the expression as an string.
   *
   * @param node the current node
   * @param env the variable environment.
   *
   * @return the string representation of the expression.
   */
  public String evalString(Node node, ExprEnvironment env)
    throws XPathException
  {
    switch (_code) {
    case CONST:
      return _value;

    case STRING:
      return _left.evalString(node, env);

    case CONCAT: 
      CharBuffer cb = CharBuffer.allocate();
      for (int i = 0; i < _args.size(); i++)
        ((Expr) _args.get(i)).evalString(cb, node, env);
      return cb.close();

    case SUBSTRING_BEFORE:
      String lstr = _left.evalString(node, env);
      String rstr = _right.evalString(node, env);
      int index = lstr.indexOf(rstr);
      return index > 0 ? lstr.substring(0, index) : "";

    case SUBSTRING_AFTER:
      lstr = _left.evalString(node, env);
      rstr = _right.evalString(node, env);
      index = lstr.indexOf(rstr);
      return index >= 0 ? lstr.substring(index + rstr.length()) : "";

    case NORMALIZE:
      lstr = _left.evalString(node, env);
      return normalize(lstr);

    case TRANSLATE:
      lstr = _left.evalString(node, env);
      rstr = _right.evalString(node, env);
      String tstr = _third.evalString(node, env);
      return translate(lstr, rstr, tstr);

    case FORMAT_NUMBER:
      return _left.evalString(node, env);

    case LOCAL_PART:
      Object lobj = _left.evalObject(node, env);
      Node nodeValue = toNode(lobj);
      if (nodeValue != null)
        return nodeValue.getLocalName();
      else
        return "";

    case NAMESPACE:
      lobj = _left.evalObject(node, env);
      nodeValue = toNode(lobj);
      if (nodeValue != null) {
        String uri = nodeValue.getNamespaceURI();
        return uri != null ? uri : "";
      }
      else
        return "";

    case QNAME:
      lobj = _left.evalObject(node, env);
      nodeValue = toNode(lobj);
      if (nodeValue != null)
        return nodeValue.getNodeName();
      else
        return "";

    case GENERATE_ID:
      Iterator iter = _left.evalNodeSet(node, env);
      return "G" + String.valueOf(System.identityHashCode(iter.next()));

    case SYSTEM_PROPERTY:
      lstr = _left.evalString(node, env);
      if (lstr == null)
        return "";
      else if (lstr.equals("xsl:version"))
        return "1.0";
      else
        return "";

    case SUBSTRING:
      lstr = _left.evalString(node, env);
      if (lstr == null)
        lstr = "";
      double start = _right.evalNumber(node, env) - 1;
      
      double end = lstr.length();
      if (_third != null)
        end = Math.round(start) + _third.evalNumber(node, env);

      if (Double.isNaN(end) || Double.isNaN(start)) {
        start = 0;
        end = 0;
      }

      if (start < 0)
        start = 0;
      else if (lstr.length() < start)
        start = lstr.length();
      
      if (end < 0)
        end = 0;
      else if (end < start)
        end = start;
      else if (lstr.length() < end)
        end = lstr.length();

      return lstr.substring((int) (start + 0.5), (int) (end + 0.5));

    default:
      throw new RuntimeException("unknown code: " + _code);
    }
  }

  /**
   * Evaluate the expression as a boolean, i.e. evaluate it as a string
   * and then convert it to a boolean.
   *
   * @param node the current node
   * @param env the variable environment.
   *
   * @return the boolean representation of the expression.
   */
  public boolean evalBoolean(Node node, ExprEnvironment env)
    throws XPathException
  {
    String string = evalString(node, env);

    return string != null && string.length() > 0;
  }

  /**
   * Evaluate the expression as a double, i.e. evaluate it as a string
   * and then convert it to a double.
   *
   * @param node the current node
   * @param env the variable environment.
   *
   * @return the numeric representation of the expression.
   */
  public double evalNumber(Node node, ExprEnvironment env)
    throws XPathException
  {
    return stringToNumber(evalString(node, env));
  }

  /**
   * Evaluate the expression as an object, i.e. return the string value.
   *
   * @param node the current node
   * @param env the variable environment.
   *
   * @return the boolean representation of the expression.
   */
  public Object evalObject(Node node, ExprEnvironment env)
    throws XPathException
  {
    return evalString(node, env);
  }

  /**
   * Normalize the string, converting all whitespace to a space and
   * eliminating consecutive spaces.
   */
  private String normalize(String string)
  {
    CharBuffer result = new CharBuffer();

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

    boolean lastIsWhitespace = false;
    for (; i < len; i++) {
      if (XmlChar.isWhitespace(string.charAt(i))) {
        lastIsWhitespace = true;
      }
      else if (lastIsWhitespace) {
        result.append(' ');
        result.append(string.charAt(i));
        lastIsWhitespace = false;
      }
      else
        result.append(string.charAt(i));
    }

    return result.toString();
  }

  /**
   * Translate the string, converting characters.  translate("foo", "f", "b")
   * returns "boo".
   *
   * @param string the string to translate.
   * @param from characters to convert from.
   * @param to the replacement characters.
   */
  private String translate(String string, String from, String to)
  {
    CharBuffer result = new CharBuffer();

  loop:
    for (int i = 0; i < string.length(); i++) {
      char ch = string.charAt(i);

      for (int j = 0; j < from.length(); j++) {
        if (ch == from.charAt(j)) {
          if (to.length() > j)
            result.append(to.charAt(j));
          continue loop;
        }
      }

      result.append(ch);
    }

    return result.toString();
  }

  /**
   * Return the expression as a string.  toString() returns a valid
   * XPath expression.  This lets applications like XSLT use toString()
   * to print the string in the generated Java.
   */
  public String toString()
  {
    switch (_code) {
    case CONST:
      CharBuffer cb = CharBuffer.allocate();
      cb.append("'");
      for (int i = 0; i < _value.length(); i++) {
        char ch = _value.charAt(i);
        switch (ch) {
        case '\n':
          cb.append("\\n");
          break;
        case '\r':
          cb.append("\\r");
          break;
        case '\\':
          cb.append("\\\\");
          break;
        case '\'':
          cb.append("\\'\\'");
          break;
        case '"':
          cb.append("\\\"");
          break;
        default:
          cb.append(ch);
        }
      }
      cb.append("'");
      return cb.toString();
      
    case STRING: return "string(" + _left + ")";

    case CONCAT: 
      String result = "concat(";
      for (int i = 0; i < _args.size(); i++) {
        if (i > 0)
          result = result + ", ";
        result = result + _args.get(i);
      }
      return result + ")";

    case SUBSTRING_BEFORE:
      return "substring-before(" + _left + ", " + _right + ")"; 

    case SUBSTRING_AFTER:
      return "substring-after(" + _left + ", " + _right + ")"; 

    case NORMALIZE:
      return "normalize-space(" + _left + ")";

    case TRANSLATE:
      return "translate(" + _left + ", " + _right + ", " + _third + ")";

    case FORMAT_NUMBER:
      return "format-number(" + _left + ")";

    case LOCAL_PART:
      return "local-part(" + _left + ")";

    case NAMESPACE:
      return "namespace-uri(" + _left + ")";

    case QNAME:
      return "name(" + _left + ")";

    case GENERATE_ID:
      return "generate-id(" + _left + ")";

    case SYSTEM_PROPERTY:
      return "system-property(" + _left + ")";

    case SUBSTRING:
      return "substring(" + _left + "," + _right + 
        (_third == null ? "" : ("," + _third)) + ")";

    case BASE_URI:
      return "fn:base-uri(" + _left + ")";

    default:
      return super.toString();
    }
  }
}
