/FileExtraction/Java_unseen/dlitz_resin/modules/quercus/src/com/caucho/quercus/env/StringValue.java
/*
 * Copyright (c) 1998-2011 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 Software Foundation, Inc.
 *   59 Temple Place, Suite 330
 *   Boston, MA 02111-1307  USA
 *
 * @author Scott Ferguson
 */

package com.caucho.quercus.env;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.util.IdentityHashMap;
import java.util.zip.CRC32;

import com.caucho.quercus.QuercusModuleException;
import com.caucho.quercus.QuercusRuntimeException;
import com.caucho.quercus.lib.file.BinaryInput;
import com.caucho.quercus.lib.i18n.Decoder;
import com.caucho.quercus.marshal.Marshal;
import com.caucho.util.ByteAppendable;
import com.caucho.vfs.ReadStream;
import com.caucho.vfs.TempBuffer;
import com.caucho.vfs.WriteStream;

/**
 * Represents a Quercus string value.
 */
abstract public class StringValue
  extends Value
  implements CharSequence, ByteAppendable
{
  public static final StringValue EMPTY = new ConstStringValue("");

  protected static final int MIN_LENGTH = 32;

  protected static final int IS_STRING = 0;
  protected static final int IS_LONG = 1;
  protected static final int IS_DOUBLE = 2;

  /**
   * Creates a string builder of the same type.
   */
  abstract public StringValue createStringBuilder();

  /**
   * Creates a string builder of the same type.
   */
  abstract public StringValue createStringBuilder(int length);

  /**
   * Creates the string.
   */
  public static Value create(String value)
  {
    // XXX: needs updating for i18n, currently php5 only

    if (value == null)
      return NullValue.NULL;
    else
      return new ConstStringValue(value);
  }

  /**
   * Creates the string.
   */
  public static StringValue create(char value)
  {
    // XXX: needs updating for i18n, currently php5 only

    return ConstStringValue.create(value);

    /*
    if (value < CHAR_STRINGS.length)
      return CHAR_STRINGS[value];
    else
      return new StringBuilderValue(String.valueOf(value));
    */
  }

  /**
   * Creates the string.
   */
  public static Value create(Object value)
  {
    // XXX: needs updating for i18n, currently php5 only

    if (value == null)
      return NullValue.NULL;
    else
      return new StringBuilderValue(value.toString());
  }

  /*
   * Decodes the Unicode str from charset.
   *
   * @param str should be a Unicode string
   * @param charset to decode string from
   */
  public StringValue create(Env env, StringValue unicodeStr, String charset)
  {
    if (! unicodeStr.isUnicode())
      return unicodeStr;

    try {
      StringValue sb = createStringBuilder();

      byte []bytes = unicodeStr.toString().getBytes(charset);

      sb.append(bytes);
      return sb;

    }
    catch (UnsupportedEncodingException e) {
      env.warning(e);

      return unicodeStr;
    }
  }

  //
  // Predicates and relations
  //

  /**
   * Returns the type.
   */
  public String getType()
  {
    return "string";
  }

  /**
   * Returns the ValueType.
   */
  @Override
  public ValueType getValueType()
  {
    return ValueType.STRING;
  }

  /**
   * Returns true for a long
   */
  public boolean isLongConvertible()
  {
    return getValueType().isLongCmp();
  }

  /**
   * Returns true for a double
   */
  public boolean isDoubleConvertible()
  {
    return getValueType().isNumberCmp();
  }

  /**
   * Returns true for a number
   */
  public boolean isNumber()
  {
    return false;
  }

  /**
   * Returns true for is_numeric
   */
  @Override
  public boolean isNumeric()
  {
    // php/120y

    return getValueType().isNumberCmp();
  }

  /**
   * Returns true for a scalar
   */
  public boolean isScalar()
  {
    return true;
  }

  /**
   * Returns true for StringValue
   */
  @Override
  public boolean isString()
  {
    return true;
  }

  /*
   * Returns true if this is a PHP5 string.
   */
  public boolean isPHP5String()
  {
    return false;
  }

  /**
   * Returns true if the value is empty
   */
  @Override
  public boolean isEmpty()
  {
    return length() == 0 || length() == 1 && charAt(0) == '0';
  }

  //
  // marshal cost
  //

  /**
   * Cost to convert to a double
   */
  @Override
  public int toDoubleMarshalCost()
  {
    ValueType valueType = getValueType();

    if (valueType.isLongCmp())
      return Marshal.COST_TO_CHAR_ARRAY + 20;
    else if (valueType.isNumberCmp())
      return Marshal.COST_TO_CHAR_ARRAY + 10;
    else
      return Marshal.COST_INCOMPATIBLE;
  }

  /**
   * Cost to convert to a float
   */
  @Override
  public int toFloatMarshalCost()
  {
    ValueType valueType = getValueType();

    if (valueType.isLongCmp())
      return Marshal.COST_TO_CHAR_ARRAY + 25;
    else if (valueType.isNumberCmp())
      return Marshal.COST_TO_CHAR_ARRAY + 15;
    else
      return Marshal.COST_INCOMPATIBLE;
  }

  /**
   * Cost to convert to a long
   */
  @Override
  public int toLongMarshalCost()
  {
    ValueType valueType = getValueType();

    if (valueType.isLongCmp())
      return Marshal.COST_TO_CHAR_ARRAY + 10;
    else if (valueType.isNumberCmp())
      return Marshal.COST_TO_CHAR_ARRAY + 40;
    else
      return Marshal.COST_INCOMPATIBLE;
  }

  /**
   * Cost to convert to an integer
   */
  @Override
  public int toIntegerMarshalCost()
  {
    ValueType valueType = getValueType();

    if (valueType.isLongCmp())
      return Marshal.COST_TO_CHAR_ARRAY + 10;
    else if (valueType.isNumberCmp())
      return Marshal.COST_TO_CHAR_ARRAY + 40;
    else
      return Marshal.COST_INCOMPATIBLE;
  }

  /**
   * Cost to convert to a short
   */
  @Override
  public int toShortMarshalCost()
  {
    ValueType valueType = getValueType();

    if (valueType.isLongCmp())
      return Marshal.COST_TO_CHAR_ARRAY + 30;
    else if (valueType.isNumberCmp())
      return Marshal.COST_TO_CHAR_ARRAY + 50;
    else
      return Marshal.COST_INCOMPATIBLE;
  }

  /**
   * Cost to convert to a byte
   */
  @Override
  public int toByteMarshalCost()
  {
    ValueType valueType = getValueType();

    if (valueType.isLongCmp())
      return Marshal.COST_TO_CHAR_ARRAY + 30;
    else if (valueType.isNumberCmp())
      return Marshal.COST_TO_CHAR_ARRAY + 50;
    else
      return Marshal.COST_STRING_TO_BYTE;
  }

  /**
   * Cost to convert to a character
   */
  @Override
  public int toCharMarshalCost()
  {
    return Marshal.COST_STRING_TO_CHAR;
  }

  /**
   * Cost to convert to a String
   */
  @Override
  public int toStringMarshalCost()
  {
    return Marshal.COST_EQUAL;
  }

  /**
   * Cost to convert to a char[]
   */
  @Override
  public int toCharArrayMarshalCost()
  {
    return Marshal.COST_STRING_TO_CHAR_ARRAY;
  }

  /**
   * Cost to convert to a StringValue
   */
  public int toStringValueMarshalCost()
  {
    return Marshal.COST_IDENTICAL;
  }

  /**
   * Cost to convert to a binary value
   */
  @Override
  public int toBinaryValueMarshalCost()
  {
    return Marshal.COST_STRING_TO_BINARY;
  }

  /**
   * Returns true for equality
   */
  public int cmp(Value rValue)
  {
    if (isNumberConvertible() || rValue.isNumberConvertible()) {
      double l = toDouble();
      double r = rValue.toDouble();

      if (l == r)
        return 0;
      else if (l < r)
        return -1;
      else
        return 1;
    }
    else {
      int result = toString().compareTo(rValue.toString());

      if (result == 0)
        return 0;
      else if (result > 0)
        return 1;
      else
        return -1;
    }
  }

  /**
   * Returns true for equality
   */
  @Override
  public boolean eq(Value rValue)
  {
    ValueType typeA = getValueType();
    ValueType typeB = rValue.getValueType();

    if (typeB.isNumber()) {
      double l = toDouble();
      double r = rValue.toDouble();

      return l == r;
    }
    else if (typeB.isBoolean()) {
      return toBoolean() == rValue.toBoolean();
    }
    else if (typeA.isNumberCmp() && typeB.isNumberCmp()) {
      double l = toDouble();
      double r = rValue.toDouble();

      return l == r;
    }
    else {
      return toString().equals(rValue.toString());
    }
  }

  /**
   * Compare two strings
   */
  public int cmpString(StringValue rValue)
  {
    return toString().compareTo(rValue.toString());
  }

  // Conversions

  /**
   * Converts to a string value.
   */
  @Override
  public StringValue toStringValue()
  {
    return this;
  }

  /**
   * Converts to a string value.
   */
  @Override
  public StringValue toStringValue(Env env)
  {
    return this;
  }

  /**
   * Converts to a long.
   */
  public static long toLong(String string)
  {
    return parseLong(string);
  }

  /**
   * String to long conversion routines used by this module
   * and other modules in this package. These methods are
   * only invoked by other implementations of a "string" object.
   * The 3 implementations should be identical except for the
   * char data source.
   */

  static long parseLong(char []buffer, int offset, int len)
  {
    if (len == 0)
      return 0;

    long value = 0;
    long sign = 1;
    boolean isResultSet = false;
    long result = 0;

    int end = offset + len;

    while (offset < end && Character.isWhitespace(buffer[offset])) {
      offset++;
    }

    int ch;

    if (offset + 1 < end && buffer[offset] == '0'
      && ((ch = buffer[offset + 1]) == 'x' || ch == 'X')) {

    for (offset += 2; offset < end; offset++) {
      ch = buffer[offset] & 0xFF;

      long oldValue = value;

      if ('0' <= ch && ch <= '9')
        value = value * 16 + ch - '0';
      else if ('a' <= ch && ch <= 'z')
        value = value * 16 + ch - 'a' + 10;
      else if ('A' <= ch && ch <= 'Z')
        value = value * 16 + ch - 'A' + 10;
      else
        return value;

      if (value < oldValue)
        return Integer.MAX_VALUE;
    }

    return value;
  }

    if (offset < end && buffer[offset] == '-') {
      sign = -1;
      offset++;
    }
    else if (offset < end && buffer[offset] == '+') {
      sign = +1;
      offset++;
    }

    while (offset < end) {
      ch = buffer[offset++];

      if ('0' <= ch && ch <= '9') {
        long newValue = 10 * value + ch - '0';
        if (newValue < value) {
          // php/0143
          // long value overflowed
          result = Integer.MAX_VALUE;
          isResultSet = true;
          break;
        }
        value = newValue;
      }
      else {
        result = sign * value;
        isResultSet = true;
        break;
      }
    }

    if (! isResultSet)
      result = sign * value;

    return result;
  }

  static long parseLong(byte []buffer, int offset, int len)
  {
    if (len == 0)
      return 0;

    long value = 0;
    long sign = 1;
    boolean isResultSet = false;
    long result = 0;

    int end = offset + len;

    while (offset < end && Character.isWhitespace(buffer[offset])) {
      offset++;
    }

    int ch;

    if (offset + 1 < end && buffer[offset] == '0'
        && ((ch = buffer[offset + 1]) == 'x' || ch == 'X')) {

      for (offset += 2; offset < end; offset++) {
        ch = buffer[offset] & 0xFF;

        long oldValue = value;

        if ('0' <= ch && ch <= '9')
          value = value * 16 + ch - '0';
        else if ('a' <= ch && ch <= 'z')
          value = value * 16 + ch - 'a' + 10;
        else if ('A' <= ch && ch <= 'Z')
          value = value * 16 + ch - 'A' + 10;
        else
          return value;

        if (value < oldValue)
          return Integer.MAX_VALUE;
      }

      return value;
    }

    if (offset < end && buffer[offset] == '-') {
      sign = -1;
      offset++;
    }
    else if (offset < end && buffer[offset] == '+') {
      sign = +1;
      offset++;
    }

    while (offset < end) {
      ch = buffer[offset++];

      if ('0' <= ch && ch <= '9') {
        long newValue = 10 * value + ch - '0';
        if (newValue < value) {
          // long value overflowed, set result to integer max
          result = Integer.MAX_VALUE;
          isResultSet = true;
          break;
        }
        value = newValue;
      }
      else {
        result = sign * value;
        isResultSet = true;
        break;
      }
    }

    if (! isResultSet)
      result = sign * value;

    return result;
  }

  static long parseLong(CharSequence string)
  {
    final int len = string.length();

    if (len == 0)
      return 0;

    long value = 0;
    long sign = 1;
    boolean isResultSet = false;
    long result = 0;

    int offset = 0;
    int end = offset + len;

    while (offset < end && Character.isWhitespace(string.charAt(offset))) {
      offset++;
    }

    if (offset < end && string.charAt(offset) == '-') {
      sign = -1;
      offset++;
    }
    else if (offset < end && string.charAt(offset) == '+') {
      sign = +1;
      offset++;
    }

    while (offset < end) {
      int ch = string.charAt(offset++);

      if ('0' <= ch && ch <= '9') {
        long newValue = 10 * value + ch - '0';
        if (newValue < value) {
          // long value overflowed, set result to integer max
          result = Integer.MAX_VALUE;
          isResultSet = true;
          break;
        }
        value = newValue;
      }
      else {
        result = sign * value;
        isResultSet = true;
        break;
      }
    }

    if (! isResultSet)
      result = sign * value;

    return result;
  }

  /**
   * Converts to a double.
   */
  public double toDouble()
  {
    return toDouble(toString());
  }

  /**
   * Converts to a double.
   */
  public static double toDouble(String s)
  {
    int len = s.length();

    int start = 0;

    int i = 0;
    int ch = 0;

    while (i < len && Character.isWhitespace(s.charAt(i))) {
      start++;
      i++;
    }

    if (i + 1 < len && s.charAt(i) == '0'
        && ((ch = s.charAt(i)) == 'x' || ch == 'X')) {

      double value = 0;

      for (i += 2; i < len; i++) {
        ch = s.charAt(i);

        if ('0' <= ch && ch <= '9')
          value = value * 16 + ch - '0';
        else if ('a' <= ch && ch <= 'z')
          value = value * 16 + ch - 'a' + 10;
        else if ('A' <= ch && ch <= 'Z')
          value = value * 16 + ch - 'A' + 10;
        else
          return value;
      }

      return value;
    }

    if (i < len && ((ch = s.charAt(i)) == '+' || ch == '-')) {
      i++;
    }

    for (; i < len && '0' <= (ch = s.charAt(i)) && ch <= '9'; i++) {
    }

    if (ch == '.') {
      for (i++; i < len && '0' <= (ch = s.charAt(i)) && ch <= '9'; i++) {
      }
    }

    if (ch == 'e' || ch == 'E') {
      int e = i++;

      if (i < len && (ch = s.charAt(i)) == '+' || ch == '-') {
        i++;
      }

      for (; i < len && '0' <= (ch = s.charAt(i)) && ch <= '9'; i++) {
      }

      if (i == e + 1)
        i = e;
    }

    if (i == 0)
      return 0;
    else if (i == len && start == 0)
      return Double.parseDouble(s);
    else
      return Double.parseDouble(s.substring(start, i));
  }

  /**
   * Converts to a boolean.
   */
  public boolean toBoolean()
  {
    int length = length();

    if (length == 0)
      return false;
    else if (length > 1)
      return true;
    else
      return charAt(0) != '0';
  }

  /**
   * Converts to a key.
   */
  public Value toKey()
  {
    int len = length();

    if (len == 0)
      return this;

    int sign = 1;
    long value = 0;

    int i = 0;
    char ch = charAt(i);
    if (ch == '-') {
      sign = -1;
      i++;
    }

    for (; i < len; i++) {
      ch = charAt(i);

      if ('0' <= ch && ch <= '9')
        value = 10 * value + ch - '0';
      else
        return this;
    }

    return LongValue.create(sign * value);
  }

  /**
   * Converts to an object.
   */
  @Override
  final public Value toAutoObject(Env env)
  {
    return env.createObject();
  }

  /**
   * Converts to an array if null.
   */
  @Override
  public Value toAutoArray()
  {
    if (length() == 0)
      return new ArrayValueImpl();
    else
      return this;
  }

  /**
   * Converts to a Java object.
   */
  public Object toJavaObject()
  {
    return toString();
  }
  
  /**
   * Takes the values of this array, unmarshalls them to objects of type
   * <i>elementType</i>, and puts them in a java array.
   */
  @Override
  public Object valuesToArray(Env env, Class elementType)
  {
    if (char.class.equals(elementType)) {
      return toUnicode(env).toCharArray();
    }
    else if (Character.class.equals(elementType)) {
      char[] chars = toUnicode(env).toCharArray();

      int length = chars.length;

      Character[] charObjects = new Character[length];

      for (int i = 0; i < length; i++) {
        charObjects[i] = Character.valueOf(chars[i]);
      }

      return charObjects;
    }
    else if (byte.class.equals(elementType)) {
      return toBinaryValue(env).toBytes();
    }
    else if (Byte.class.equals(elementType)) {
      byte[] bytes = toBinaryValue(env).toBytes();

      int length = bytes.length;

      Byte[] byteObjects = new Byte[length];

      for (int i = 0; i < length; i++) {
        byteObjects[i] = Byte.valueOf(bytes[i]);
      }

      return byteObjects;
    }
    else {
      env.error(L.l("Can't assign {0} with type {1} to {2}",
                    this,
                    this.getClass(),
                    elementType));
      return null;
    }
  }
  
  /**
   * Converts to a callable object
   */
  @Override
  public Callable toCallable(Env env)
  {
    // php/1h0o
    if (isEmpty())
      return super.toCallable(env);

    String s = toString();

    int p = s.indexOf("::");

    if (p < 0)
      return new CallbackFunction(env, s);
    else {
      String className = s.substring(0, p);
      String methodName = s.substring(p + 2);

      QuercusClass cl = env.findClass(className);

      if (cl == null) {
        env.warning(L.l("can't find class {0}",
                        className));
        
        return super.toCallable(env);
      }

      return new CallbackClassMethod(cl, env.createString(methodName));
    }
  }

  /**
   * Sets the array value, returning the new array, e.g. to handle
   * string update ($a[0] = 'A').  Creates an array automatically if
   * necessary.
   */
  @Override
  public Value append(Value index, Value value)
  {
    if (length() == 0)
      return new ArrayValueImpl().append(index, value);
    else
      return this;
  }

  // Operations

  /**
   * Returns the character at an index
   */
  public Value get(Value key)
  {
    return charValueAt(key.toLong());
  }

  /**
   * Returns the character at an index
   */
  public Value getArg(Value key, boolean isTop)
  {
    // php/03ma
    return charValueAt(key.toLong());
  }

  /**
   * Returns the character at an index
   */
  @Override
  public Value charValueAt(long index)
  {
    int len = length();

    if (index < 0 || len <= index)
      return UnsetUnicodeValue.UNSET;
    else {
      return StringValue.create(charAt((int) index));
    }
  }

  /**
   * sets the character at an index
   */
  @Override
  public Value setCharValueAt(long index, Value value)
  {
    //XXX: need to double-check this for non-string values

    int len = length();

    if (index < 0 || len <= index)
      return this;
    else {
      return (createStringBuilder()
              .append(this, 0, (int) index)
              .append(value)
              .append(this, (int) (index + 1), length()));
    }
  }

  /**
   * Increment the following value.
   */
  @Override
  public Value increment(int incr)
  {
    // php/03i6
    if (length() == 0) {
      if (incr == 1)
        return createStringBuilder().append("1");
      else
        return LongValue.MINUS_ONE;
    }

    if (incr > 0) {
      StringBuilder tail = new StringBuilder();

      for (int i = length() - 1; i >= 0; i--) {
        char ch = charAt(i);

        if (ch == 'z') {
          if (i == 0)
            return createStringBuilder().append("aa").append(tail);
          else
            tail.insert(0, 'a');
        }
        else if ('a' <= ch && ch < 'z') {
          return (createStringBuilder()
                  .append(this, 0, i)
                  .append((char) (ch + 1))
                  .append(tail));
        }
        else if (ch == 'Z') {
          if (i == 0)
            return createStringBuilder().append("AA").append(tail);
          else
            tail.insert(0, 'A');
        }
        else if ('A' <= ch && ch < 'Z') {
          return (createStringBuilder()
                  .append(this, 0, i)
                  .append((char) (ch + 1))
                  .append(tail));
        }
        else if ('0' <= ch && ch <= '9' && i == length() - 1) {
          return LongValue.create(toLong() + incr);
        }
      }

      return createStringBuilder().append(tail.toString());
    }
    else if (getValueType().isLongAdd()) {
      return LongValue.create(toLong() + incr);
    }
    else {
      return this;
    }
  }

  /**
   * Adds to the following value.
   */
  public Value add(long rValue)
  {
    if (getValueType().isLongAdd())
      return LongValue.create(toLong() + rValue);

    return DoubleValue.create(toDouble() + rValue);
  }

  /**
   * Adds to the following value.
   */
  public Value sub(long rValue)
  {
    if (getValueType().isLongAdd())
      return LongValue.create(toLong() - rValue);

    return DoubleValue.create(toDouble() - rValue);
  }

  /*
   * Bit and.
   */
  @Override
  public Value bitAnd(Value rValue)
  {
    if (rValue.isString()) {
      StringValue rStr = (StringValue) rValue;

      int len = Math.min(length(), rValue.length());
      StringValue sb = createStringBuilder();

      for (int i = 0; i < len; i++) {
        char l = charAt(i);
        char r = rStr.charAt(i);

        sb.appendByte(l & r);
      }

      return sb;
    }
    else
      return LongValue.create(toLong() & rValue.toLong());
  }

  /*
   * Bit or.
   */
  @Override
  public Value bitOr(Value rValue)
  {
    if (rValue.isString()) {
      StringValue rStr = (StringValue) rValue;

      int len = Math.min(length(), rValue.length());
      StringValue sb = createStringBuilder();

      for (int i = 0; i < len; i++) {
        char l = charAt(i);
        char r = rStr.charAt(i);

        sb.appendByte(l | r);
      }

      if (len != length())
        sb.append(substring(len));
      else if (len != rStr.length())
        sb.append(rStr.substring(len));

      return sb;
    }
    else
      return LongValue.create(toLong() | rValue.toLong());
  }

  /*
   * Bit xor.
   */
  @Override
  public Value bitXor(Value rValue)
  {
    if (rValue.isString()) {
      StringValue rStr = rValue.toStringValue();

      int len = Math.min(length(), rValue.length());
      StringValue sb = createStringBuilder();

      for (int i = 0; i < len; i++) {
        char l = charAt(i);
        char r = rStr.charAt(i);

        sb.appendByte(l ^ r);
      }

      return sb;
    }
    else
      return LongValue.create(toLong() ^ rValue.toLong());
  }

  /**
   * Serializes the value.
   */
  @Override
  public void serialize(Env env, StringBuilder sb)
  {
    sb.append("s:");
    sb.append(length());
    sb.append(":\"");
    sb.append(toString());
    sb.append("\";");
  }

  /**
   * Encodes the value in JSON.
   */
  @Override
  public void jsonEncode(Env env, StringValue sb)
  {
    sb.append('"');

    int len = length();
    for (int i = 0; i < len; i++) {
      char c = charAt(i);

      switch (c) {
      case '\b':
        sb.append('\\');
        sb.append('b');
        break;
      case '\f':
        sb.append('\\');
        sb.append('f');
        break;
      case '\n':
        sb.append('\\');
        sb.append('n');
        break;
      case '\r':
        sb.append('\\');
        sb.append('r');
        break;
      case '\t':
        sb.append('\\');
        sb.append('t');
        break;
      case '\\':
        sb.append('\\');
        sb.append('\\');
        break;
      case '"':
        sb.append('\\');
        sb.append('"');
        break;
      case '/':
        sb.append('\\');
        sb.append('/');
        break;
      default:
        if (c <= 0x1f) {
          addUnicode(sb, c);
        }
        else if (c < 0x80) {
          sb.append(c);
        }
        else if ((c & 0xe0) == 0xc0 && i + 1 < len) {
          int c1 = charAt(i + 1);
          i++;

          int ch = ((c & 0x1f) << 6) + (c1 & 0x3f);

          addUnicode(sb, ch);
        }
        else if ((c & 0xf0) == 0xe0 && i + 2 < len) {
          int c1 = charAt(i + 1);
          int c2 = charAt(i + 2);

          i += 2;

          int ch = ((c & 0x0f) << 12) + ((c1 & 0x3f) << 6) + (c2 & 0x3f);

          addUnicode(sb, ch);
        }
        else {
          // technically illegal
          addUnicode(sb, c);
        }

        break;
      }
    }

    sb.append('"');
  }

  private void addUnicode(StringValue sb, int c)
  {
    sb.append('\\');
    sb.append('u');

    int d = (c >> 12) & 0xf;
    if (d < 10)
      sb.append((char) ('0' + d));
    else
      sb.append((char) ('a' + d - 10));

    d = (c >> 8) & 0xf;
    if (d < 10)
      sb.append((char) ('0' + d));
    else
      sb.append((char) ('a' + d - 10));

    d = (c >> 4) & 0xf;
    if (d < 10)
      sb.append((char) ('0' + d));
    else
      sb.append((char) ('a' + d - 10));

    d = (c) & 0xf;
    if (d < 10)
      sb.append((char) ('0' + d));
    else
      sb.append((char) ('a' + d - 10));
  }

  /*
   * Returns a value to be used as a key for the deserialize cache.
   */
  /*
  public StringValue toSerializeKey()
  {
    if (length() <= 4096)
      return this;

    try {
      MessageDigest md = MessageDigest.getInstance("SHA1");

      byte []buffer = toBytes();

      md.update(buffer, 0, buffer.length);

      //XXX: create a special serialize type?
      return new StringBuilderValue(md.digest());

    } catch (NoSuchAlgorithmException e) {
      throw new QuercusException(e);
    }
  }
  */

  //
  // append code
  //

  /**
   * Append a Java string to the value.
   */
  public StringValue append(String s)
  {
    throw new UnsupportedOperationException(getClass().getName());
  }

  /**
   * Append a Java string to the value.
   */
  public StringValue append(String s, int start, int end)
  {
    throw new UnsupportedOperationException(getClass().getName());
  }

  /**
   * Append a Java buffer to the value.
   */
  public StringValue append(char []buf, int offset, int length)
  {
    throw new UnsupportedOperationException(getClass().getName());
  }

  /**
   * Append a Java double to the value.
   */
  public StringValue append(char []buf)
  {
    return append(buf, 0, buf.length);
  }

  /**
   * Append a Java buffer to the value.
   */
  public StringValue append(CharSequence buf, int head, int tail)
  {
    throw new UnsupportedOperationException(getClass().getName());
  }

  /**
   * Append a Java buffer to the value.
   */
  public StringValue append(StringBuilderValue sb, int head, int tail)
  {
    return append((CharSequence) sb, head, tail);
  }

  /**
   * Append a Java buffer to the value.
   */
  public StringValue append(UnicodeBuilderValue sb, int head, int tail)
  {
    return append((CharSequence) sb, head, tail);
  }

  /*
   * Appends a Unicode string to the value.
   *
   * @param str should be a Unicode string
   * @param charset to decode string from
   */
  public StringValue append(Env env, StringValue unicodeStr, String charset)
  {
    if (! unicodeStr.isUnicode())
      return append(unicodeStr);

    try {
      byte []bytes = unicodeStr.toString().getBytes(charset);

      append(bytes);
      return this;

    }
    catch (UnsupportedEncodingException e) {
      env.warning(e);

      return append(unicodeStr);
    }
  }

  /**
   * Append a Java char to the value.
   */
  public StringValue append(char v)
  {
    throw new UnsupportedOperationException(getClass().getName());
  }

  /**
   * Append a Java boolean to the value.
   */
  public StringValue append(boolean v)
  {
    return append(v ? "true" : "false");
  }

  /**
   * Append a Java long to the value.
   */
  public StringValue append(long v)
  {
    return append(String.valueOf(v));
  }

  /**
   * Append a Java double to the value.
   */
  public StringValue append(double v)
  {
    return append(String.valueOf(v));
  }

  /**
   * Append a Java value to the value.
   */
  public StringValue append(Object v)
  {
    return append(String.valueOf(v));
  }

  /**
   * Append a Java value to the value.
   */
  public StringValue append(Value v)
  {
    throw new UnsupportedOperationException(getClass().getName());
  }

  /**
   * Ensure enough append capacity.
   */
  public void ensureAppendCapacity(int size)
  {
    throw new UnsupportedOperationException(getClass().getName());
  }

  /**
   * Append a byte buffer to the value.
   */
  public StringValue append(byte []buf, int offset, int length)
  {
    throw new UnsupportedOperationException(getClass().getName());
  }

  /**
   * Append a byte buffer to the value.
   */
  public StringValue append(byte []buf)
  {
    return append(buf, 0, buf.length);
  }

  /**
   * Append a byte buffer to the value.
   */
  public StringValue appendUtf8(byte []buf, int offset, int length)
  {
    throw new UnsupportedOperationException(getClass().getName());
  }

  /**
   * Append a byte buffer to the value.
   */
  public StringValue appendUtf8(byte []buf)
  {
    return appendUtf8(buf, 0, buf.length);
  }

  /**
   * Append to a string builder.
   */
  @Override
  public StringValue appendTo(UnicodeBuilderValue sb)
  {
    int length = length();

    for (int i = 0; i < length; i++)
      sb.append(charAt(i));

    return this;
  }

  /**
   * Append a Java boolean to the value.
   */
  public StringValue appendUnicode(boolean v)
  {
    return append(v ? "true" : "false");
  }

  /**
   * Append a Java long to the value.
   */
  public StringValue appendUnicode(long v)
  {
    return append(String.valueOf(v));
  }

  /**
   * Append a Java double to the value.
   */
  public StringValue appendUnicode(double v)
  {
    return append(String.valueOf(v));
  }

  /**
   * Append a Java value to the value.
   */
  public StringValue appendUnicode(Object v)
  {
    return append(String.valueOf(v));
  }

  /**
   * Append a Java char, possibly converting to a unicode string
   */
  public StringValue appendUnicode(char v)
  {
    return append(v);
  }

  /**
   * Append a Java char buffer, possibly converting to a unicode string
   */
  public StringValue appendUnicode(char []buffer, int offset, int length)
  {
    return append(buffer, offset, length);
  }

  /**
   * Append a Java char buffer, possibly converting to a unicode string
   */
  public StringValue appendUnicode(char []buffer)
  {
    return append(buffer);
  }

  /**
   * Append a Java char buffer, possibly converting to a unicode string
   */
  public StringValue appendUnicode(String value)
  {
    return append(value);
  }

  /**
   * Append a Java char buffer, possibly converting to a unicode string
   */
  public StringValue appendUnicode(String value, int offset, int length)
  {
    return append(value, offset, length);
  }

  /**
   * Append a Java char buffer, possibly converting to a unicode string
   */
  public StringValue appendUnicode(Value value)
  {
    return append(value);
  }

  /**
   * Append a Java char buffer, possibly converting to a unicode string
   */
  public StringValue appendUnicode(Value v1, Value v2)
  {
    return append(v1).append(v2);
  }

  /**
   * Append a Java byte to the value without conversions.
   */
  public StringValue appendByte(int v)
  {
    throw new UnsupportedOperationException(getClass().getName());
  }

  /**
   * Append a Java String to the value without conversions.
   */
  public StringValue appendBytes(String s)
  {
    StringValue sb = this;

    for (int i = 0; i < s.length(); i++) {
      sb = sb.appendByte(s.charAt(i));
    }

    return sb;
  }

  /**
   * Append a Java String to the value without conversions.
   */
  public StringValue appendBytes(StringValue s)
  {
    StringValue sb = this;

    for (int i = 0; i < s.length(); i++) {
      sb = sb.appendByte(s.charAt(i));
    }

    return sb;
  }

  /**
   * Append a Java char[] to the value without conversions.
   */
  public StringValue appendBytes(char []buf, int offset, int length)
  {
    StringValue sb = this;
    int end = Math.min(buf.length, offset + length);

    while (offset < end) {
      sb = sb.appendByte(buf[offset++]);
    }

    return sb;
  }

  /**
   * Append Java bytes to the value without conversions.
   */
  public StringValue appendBytes(byte []bytes, int offset, int end)
  {
    StringValue sb = this;

    while (offset < end) {
      sb = sb.appendByte(bytes[offset++]);
    }

    return sb;
  }

  /**
   * Append from a temp buffer list
   */
  public StringValue append(TempBuffer ptr)
  {
    for (; ptr != null; ptr = ptr.getNext()) {
      append(ptr.getBuffer(), 0, ptr.getLength());
    }

    return this;
  }

  /**
   * Append from a read stream
   */
  public StringValue append(Reader reader)
    throws IOException
  {
    int ch;

    while ((ch = reader.read()) >= 0) {
      append((char) ch);
    }

    return this;
  }

  /**
   * Append from a read stream
   */
  public StringValue append(Reader reader, long length)
    throws IOException
  {
    int ch;

    while (length-- > 0 && (ch = reader.read()) >= 0) {
      append((char) ch);
    }

    return this;
  }

  /**
   * Append from an input stream, using InputStream.read semantics,
   * i.e. just call is.read once even if more data is available.
   */
  public int appendRead(InputStream is, long length)
  {
    TempBuffer tBuf = TempBuffer.allocate();

    try {
      byte []buffer = tBuf.getBuffer();
      int sublen = buffer.length;
      if (length < sublen)
        sublen = (int) length;

      sublen = is.read(buffer, 0, sublen);

      if (sublen > 0)
        append(buffer, 0, sublen);

      return sublen;
    } catch (IOException e) {
      throw new QuercusModuleException(e);
    } finally {
      TempBuffer.free(tBuf);
    }
  }

  /**
   * Append from an input stream, reading from the input stream until
   * end of file or the length is reached.
   */
  public int appendReadAll(InputStream is, long length)
  {
    TempBuffer tBuf = TempBuffer.allocate();

    try {
      byte []buffer = tBuf.getBuffer();
      int readLength = 0;

      while (length > 0) {
        int sublen = buffer.length;
        if (length < sublen)
          sublen = (int) length;

        sublen = is.read(buffer, 0, sublen);

        if (sublen > 0) {
          append(buffer, 0, sublen);
          length -= sublen;
          readLength += sublen;
        }
        else
          return readLength > 0 ? readLength : -1;
      }

      return readLength;
    } catch (IOException e) {
      throw new QuercusModuleException(e);
    } finally {
      TempBuffer.free(tBuf);
    }
  }
  
  /**
   * Append from an input stream, reading from the input stream until
   * end of file or the length is reached.
   */
  public int appendReadAll(ReadStream is, long length)
  {
    TempBuffer tBuf = TempBuffer.allocate();

    try {
      byte []buffer = tBuf.getBuffer();
      int readLength = 0;

      while (length > 0) {
        int sublen = buffer.length;
        if (length < sublen)
          sublen = (int) length;

        sublen = is.read(buffer, 0, sublen);

        if (sublen > 0) {
          append(buffer, 0, sublen);
          length -= sublen;
          readLength += sublen;
        }
        else
          return readLength > 0 ? readLength : -1;
      }

      return readLength;
    } catch (IOException e) {
      throw new QuercusModuleException(e);
    } finally {
      TempBuffer.free(tBuf);
    }
  }

  /**
   * Append from an input stream, using InputStream semantics, i.e
   * call is.read() only once.
   */
  public int appendRead(BinaryInput is, long length)
  {
    TempBuffer tBuf = TempBuffer.allocate();

    try {
      byte []buffer = tBuf.getBuffer();
      int sublen = buffer.length;
      if (length < sublen)
        sublen = (int) length;
      else if (length > sublen) {
        buffer = new byte[(int) length];
        sublen = (int) length;
      }

      sublen = is.read(buffer, 0, sublen);

      if (sublen > 0)
        append(buffer, 0, sublen);

      return sublen;
    } catch (IOException e) {
      throw new QuercusModuleException(e);
    } finally {
      TempBuffer.free(tBuf);
    }
  }

  /**
   * Append from an input stream, reading all available data from the
   * stream.
   */
  public int appendReadAll(BinaryInput is, long length)
  {
    TempBuffer tBuf = TempBuffer.allocate();

    try {
      byte []buffer = tBuf.getBuffer();
      int readLength = 0;

      while (length > 0) {
        int sublen = buffer.length;
        if (length < sublen)
          sublen = (int) length;

        sublen = is.read(buffer, 0, sublen);

        if (sublen > 0) {
          append(buffer, 0, sublen);
          length -= sublen;
          readLength += sublen;
        }
        else
          return readLength > 0 ? readLength : -1;
      }

      return readLength;
    } catch (IOException e) {
      throw new QuercusModuleException(e);
    } finally {
      TempBuffer.free(tBuf);
    }
  }

  /**
   * Exports the value.
   */
  @Override
  public void varExport(StringBuilder sb)
  {
    sb.append("'");

    String value = toString();
    int len = value.length();
    for (int i = 0; i < len; i++) {
      char ch = value.charAt(i);

      switch (ch) {
      case '\'':
        sb.append("\\'");
        break;
      case '\\':
        sb.append("\\\\");
        break;
      default:
        sb.append(ch);
      }
    }
    sb.append("'");
  }

  /**
   * Interns the string.
   */
  /*
  public StringValue intern(Quercus quercus)
  {
    return quercus.intern(toString());
  }
  */

  //
  // CharSequence
  //

  /**
   * Returns the length of the string.
   */
  public int length()
  {
    return toString().length();
  }

  /**
   * Returns the character at a particular location
   */
  public char charAt(int index)
  {
    return toString().charAt(index);
  }

  /**
   * Returns a subsequence
   */
  public CharSequence subSequence(int start, int end)
  {
    return new StringBuilderValue(toString().substring(start, end));
  }

  //
  // java.lang.String methods
  //

  /**
   * Returns the first index of the match string, starting from the head.
   */
  public final int indexOf(CharSequence match)
  {
    return indexOf(match, 0);
  }

  /**
   * Returns the first index of the match string, starting from the head.
   */
  public int indexOf(CharSequence match, int head)
  {
    int length = length();
    int matchLength = match.length();

    if (matchLength <= 0)
      return -1;
    else if (head < 0)
      return -1;

    int end = length - matchLength;
    char first = match.charAt(0);

    loop:
    for (; head <= end; head++) {
      if (charAt(head) != first)
        continue;

      for (int i = 1; i < matchLength; i++) {
        if (charAt(head + i) != match.charAt(i))
          continue loop;
      }

      return head;
    }

    return -1;
  }

  /**
   * Returns the last index of the match string, starting from the head.
   */
  public int indexOf(char match)
  {
    return indexOf(match, 0);
  }

  /**
   * Returns the last index of the match string, starting from the head.
   */
  public int indexOf(char match, int head)
  {
    int length = length();

    for (; head < length; head++) {
      if (charAt(head) == match)
        return head;
    }

    return -1;
  }

  /**
   * Returns the last index of the match string, starting from the head.
   */
  public final int lastIndexOf(char match)
  {
    return lastIndexOf(match, Integer.MAX_VALUE);
  }

  /**
   * Returns the last index of the match string, starting from the head.
   */
  public int lastIndexOf(char match, int tail)
  {
    int length = length();

    if (tail >= length)
      tail = length - 1;

    for (; tail >= 0; tail--) {
      if (charAt(tail) == match)
        return tail;
    }

    return -1;
  }

  /**
   * Returns the last index of the match string, starting from the tail.
   */
  public int lastIndexOf(CharSequence match)
  {
    return lastIndexOf(match, Integer.MAX_VALUE);
  }

  /**
   * Returns the last index of the match string, starting from the tail.
   */
  public int lastIndexOf(CharSequence match, int tail)
  {
    int length = length();
    int matchLength = match.length();

    if (matchLength <= 0)
      return -1;
    if (tail < 0)
      return -1;

    if (tail > length - matchLength)
      tail = length - matchLength;

    char first = match.charAt(0);

    loop:
    for (; tail >= 0; tail--) {
      if (charAt(tail) != first)
        continue;

      for (int i = 1; i < matchLength; i++) {
        if (charAt(tail + i) != match.charAt(i))
              continue loop;
      }

      return tail;
    }

    return -1;
  }

  /**
   * Returns true if the region matches
   */
  public boolean regionMatches(int offset,
                               char []mBuffer, int mOffset, int mLength)
  {
    int length = length();

    if (length < offset + mLength) {
      return false;
    }

    for (int i = 0; i < mLength; i++) {
      if (charAt(offset + i) != mBuffer[mOffset + i]) {
        return false;
      }
    }

    return true;
  }

  /**
   * Returns true if the region matches
   */
  public boolean regionMatches(int offset,
                               StringValue match, int mOffset, int mLength)
  {
    int length = length();

    if (length < offset + mLength)
      return false;

    for (int i = 0; i < mLength; i++) {
      if (charAt(offset + i) != match.charAt(mOffset + i))
        return false;
    }

    return true;
  }

  /**
   * Returns true if the region matches
   */
  public boolean
    regionMatchesIgnoreCase(int offset,
                            char []match, int mOffset, int mLength)
  {
    int length = length();

    if (length < offset + mLength)
      return false;

    for (int i = 0; i < mLength; i++) {
      char a = Character.toLowerCase(charAt(offset + i));
      char b = Character.toLowerCase(match[mOffset + i]);

      if (a != b)
        return false;
    }

    return true;
  }

  /**
   * Returns true if the string ends with another string.
   */
  public boolean endsWith(StringValue tail)
  {
    int len = length();
    int tailLen = tail.length();

    int offset = len - tailLen;

    if (offset < 0)
      return false;

    for (int i = 0; i < tailLen; i++) {
      if (charAt(offset + i) != tail.charAt(i))
        return false;
    }

    return true;
  }

  /**
   * Returns a StringValue substring.
   */
  public StringValue substring(int head)
  {
    return (StringValue) subSequence(head, length());
  }

  /**
   * Returns a StringValue substring.
   */
  public StringValue substring(int begin, int end)
  {
    return (StringValue) subSequence(begin, end);
  }

  /**
   * Returns a String substring
   */
  public String stringSubstring(int begin, int end)
  {
    return substring(begin, end).toString();
  }

  /**
   * Returns a character array
   */
  public char []toCharArray()
  {
    int length = length();

    char []array = new char[length()];

    getChars(0, array, 0, length);

    return array;
  }

  public char []getRawCharArray()
  {
    return toCharArray();
  }

  /**
   * Copies the chars
   */
  public void getChars(int stringOffset, char []buffer, int offset, int length)
  {
    for (int i = 0; i < length; i++)
      buffer[offset + i] = charAt(stringOffset + i);
  }

  /**
   * Convert to lower case.
   */
  public StringValue toLowerCase()
  {
    int length = length();

    UnicodeBuilderValue string = new UnicodeBuilderValue(length);

    char []buffer = string.getBuffer();
    getChars(0, buffer, 0, length);

    for (int i = 0; i < length; i++) {
      char ch = buffer[i];

      if ('A' <= ch && ch <= 'Z')
        buffer[i] = (char) (ch + 'a' - 'A');
      else if (ch < 0x80) {
      }
      else if (Character.isUpperCase(ch))
        buffer[i] = Character.toLowerCase(ch);
    }

    string.setOffset(length);

    return string;
  }

  /**
   * Convert to lower case.
   */
  public StringValue toUpperCase()
  {
    int length = length();

    UnicodeBuilderValue string = new UnicodeBuilderValue(length);

    char []buffer = string.getBuffer();
    getChars(0, buffer, 0, length);

    for (int i = 0; i < length; i++) {
      char ch = buffer[i];

      if ('a' <= ch && ch <= 'z')
        buffer[i] = (char) (ch + 'A' - 'a');
      else if (ch < 0x80) {
      }
      else if (Character.isLowerCase(ch))
        buffer[i] = Character.toUpperCase(ch);
    }

    string.setOffset(length);

    return string;
  }

  /**
   * Returns a byteArrayInputStream for the value.
   * See TempBufferStringValue for how this can be overriden
   *
   * @return InputStream
   */
  public InputStream toInputStream()
  {
    try {
      //XXX: refactor so that env is passed in
      return toInputStream(Env.getInstance().getRuntimeEncoding());
    }
    catch (UnsupportedEncodingException e) {
      throw new QuercusRuntimeException(e);
    }
    //return new StringValueInputStream();
  }

  /**
   * Returns a byte stream of chars.
   * @param charset to encode chars to
   */
  public InputStream toInputStream(String charset)
    throws UnsupportedEncodingException
  {
    return new ByteArrayInputStream(toString().getBytes(charset));
  }

  public Reader toSimpleReader()
    throws UnsupportedEncodingException
  {
    return new SimpleStringValueReader(this);
  }

  /**
   * Returns a char stream.
   * XXX: when decoding fails
   *
   * @param charset to decode bytes by
   */
  public Reader toReader(String charset)
    throws UnsupportedEncodingException
  {
    byte []bytes = toBytes();

    return new InputStreamReader(new ByteArrayInputStream(bytes), charset);
  }

  public byte []toBytes()
  {
    throw new UnsupportedOperationException();
  }

  /**
   * Converts to a unicode value.
   */
  @Override
  public StringValue toUnicode(Env env)
  {
    return this;
  }

  /**
   * Decodes from charset and returns UnicodeValue.
   *
   * @param env
   * @param charset
   */
  public StringValue toUnicodeValue(Env env, String charset)
  {
    StringValue sb = new UnicodeBuilderValue();

    Decoder decoder = Decoder.create(charset);

    sb.append(decoder.decode(env, this));

    return sb;
  }

  /**
   * Decodes from charset and returns UnicodeValue.
   *
   * @param env
   * @param charset
   */
  public StringValue convertToUnicode(Env env, String charset)
  {
    UnicodeBuilderValue sb = new UnicodeBuilderValue();

    Decoder decoder = Decoder.create(charset);
    decoder.setAllowMalformedOut(true);

    sb.append(decoder.decode(env, this));

    return sb;
  }

  /**
   * Converts to a string builder
   */
  @Override
  public StringValue toStringBuilder(Env env)
  {
    return createStringBuilder().append(this);
  }

  /**
   * Return true if the array value is set
   */
  public boolean isset(Value indexV)
  {
    int index = indexV.toInt();
    int len = length();

    return 0 <= index && index < len;
  }

  /**
   * Writes to a stream
   */
  public void writeTo(OutputStream os)
  {
    try {
      int len = length();

      for (int i = 0; i < len; i++)
        os.write(charAt(i));
    } catch (IOException e) {
      throw new QuercusModuleException(e);
    }
  }

  /**
   * Calculates CRC32 value.
   */
  public long getCrc32Value()
  {
    CRC32 crc = new CRC32();

    int length = length();

    for (int i = 0; i < length; i++) {
      crc.update((byte) charAt(i));
    }

    return crc.getValue() & 0xffffffff;
  }

  //
  // ByteAppendable methods
  //
  
  public void write(int value)
  {
    throw new UnsupportedOperationException(getClass().getName());
  }

  /**
   * Appends buffer to the ByteAppendable.
   */
  public void write(byte[] buffer, int offset, int len)
  {
    throw new UnsupportedOperationException(getClass().getName());
  }
  
  //
  // java.lang.Object methods
  //

  /**
   * Returns the hash code.
   */
  public int hashCode()
  {
    int hash = 37;

    int length = length();

    for (int i = 0; i < length; i++) {
      hash = 65521 * hash + charAt(i);
    }

    return hash;
  }
  
  /**
   * Returns the case-insensitive hash code
   */
  public int hashCodeCaseInsensitive()
  {
    int hash = 37;

    int length = length();

    for (int i = length - 1; i >= 0; i--) {
      int ch = charAt(i);
      
      if ('A' <= ch && ch <= 'Z')
        ch = ch + 'a' - 'A';
      
      hash = 65521 * hash + ch;
    }

    return hash;
  }

  /**
   * Test for equality
   */
  public boolean equals(Object o)
  {
    if (this == o)
      return true;
    else if (! (o instanceof StringValue))
      return false;

    StringValue s = (StringValue) o;

    if (s.isUnicode() != isUnicode())
      return false;

    int aLength = length();
    int bLength = s.length();

    if (aLength != bLength)
      return false;

    for (int i = aLength - 1; i >= 0; i--) {
      if (charAt(i) != s.charAt(i))
        return false;
    }

    return true;
  }

  /**
   * Test for equality
   */
  public boolean equalsIgnoreCase(Object o)
  {
    if (this == o)
      return true;
    else if (! (o instanceof StringValue))
      return false;

    StringValue s = (StringValue) o;

    if (s.isUnicode() != isUnicode())
      return false;

    int aLength = length();
    int bLength = s.length();

    if (aLength != bLength)
      return false;
    
    for (int i = aLength - 1; i >= 0; i--) {
      int chA = charAt(i);
      int chB = s.charAt(i);

      if (chA == chB) {
      }
      else {
        if ('A' <= chA && chA <= 'Z')
          chA += 'a' - 'A';
        
        if ('A' <= chB && chB <= 'Z')
          chB += 'a' - 'A';

        if (chA != chB)
          return false;
      }
    }
    
    return true;
  }

  //
  // Java generator code
  //

  /**
   * Generates code to recreate the expression.
   *
   * @param out the writer to the Java source code.
   */
  @Override
  public void generate(PrintWriter out)
    throws IOException
  {
    // max JVM constant string length
    int maxSublen = 0xFFFE;

    int len = length();

    String className = getClass().getSimpleName();

    if (len == 1) {
      out.print(className + ".create('");
      printJavaChar(out, charAt(0));
      out.print("')");
    }
    else if (len < maxSublen) {
      out.print("new " + className + "(\"");
      printJavaString(out, this);
      out.print("\")");
    }
    else {
      out.print("((" + className + ") (new " + className + "(\"");

      // php/313u
      for (int i = 0; i < len; i += maxSublen) {
        if (i != 0)
          out.print("\").append(\"");

        printJavaString(out, substring(i, Math.min(i + maxSublen, len)));
      }

      out.print("\")))");
    }
  }

  @Override
  abstract public String toDebugString();

  @Override
  abstract public void varDumpImpl(Env env,
                                   WriteStream out,
                                   int depth,
                                   IdentityHashMap<Value, String> valueSet)
    throws IOException;

  class StringValueInputStream extends java.io.InputStream {
    private final int _length;
    private int _index;

    StringValueInputStream()
    {
      _length = length();
    }

    /**
     * Reads the next byte.
     */
    public int read()
    {
      if (_index < _length)
        return charAt(_index++);
      else
        return -1;
    }

    /**
     * Reads into a buffer.
     */
    public int read(byte []buffer, int offset, int length)
    {
      int sublen = _length - _index;

      if (length < sublen)
        sublen = length;

      if (sublen <= 0)
        return -1;

      int index = _index;

      for (int i = 0; i < sublen; i++)
        buffer[offset + i] = (byte) charAt(index + i);

      _index += sublen;

      return sublen;
    }
  }

  static class SimpleStringValueReader extends Reader
  {
    StringValue _str;
    int _index;
    int _length;

    SimpleStringValueReader(StringValue s)
    {
      _str = s;
      _length = s.length();
    }

    public int read()
    {
      if (_index >= _length)
        return -1;
      else
        return _str.charAt(_index++);
    }

    public int read(char []buf, int off, int len)
    {
      if (_index >= _length)
        return -1;

      int i = 0;
      len = Math.min(_length - _index, len);

      for (; i < len; i++) {
        buf[off + i] = _str.charAt(i + _index++);
      }

      return i;
    }

    public void close()
      throws IOException
    {
    }
  }
}

