/*
 * 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 Software Foundation, Inc.
 *   59 Temple Place, Suite 330
 *   Boston, MA 02111-1307  USA
 *
 * @author Scott Ferguson
 */

package com.caucho.quercus.env;

import com.caucho.util.CharBuffer;
import com.caucho.vfs.TempCharBuffer;
import com.caucho.vfs.WriteStream;
import com.caucho.quercus.QuercusModuleException;

import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.Reader;
import java.util.IdentityHashMap;
import java.util.Locale;
import java.util.zip.CRC32;

/**
 * Represents a PHP 5 style string builder (unicode.semantics = off)
 */
public class StringBuilderValue
  extends BinaryValue
{
  public static final StringBuilderValue EMPTY = new ConstStringValue("");

  private static final StringBuilderValue []CHAR_STRINGS;
  private static final int LARGE_BUILDER_THRESHOLD
    = LargeStringBuilderValue.SIZE;

  private byte []_buffer;
  private int _length;
  private boolean _isCopy;

  private int _hashCode;

  public StringBuilderValue()
  {
    _buffer = new byte[MIN_LENGTH];
  }

  public StringBuilderValue(int capacity)
  {
    if (capacity < MIN_LENGTH)
      capacity = MIN_LENGTH;

    _buffer = new byte[capacity];
  }

  public StringBuilderValue(int capacity, boolean isAbsolute)
  {
    if (! isAbsolute && capacity < MIN_LENGTH)
      capacity = MIN_LENGTH;

    _buffer = new byte[capacity];
  }

  public StringBuilderValue(byte []buffer, int offset, int length)
  {
    _buffer = new byte[length];
    _length = length;

    System.arraycopy(buffer, offset, _buffer, 0, length);
  }

  public StringBuilderValue(char []buffer, int offset, int length)
  {
    _buffer = new byte[length];
    _length = length;

    for (int i = 0; i < length; i++) {
      _buffer[i] = (byte) buffer[offset + i];
    }
  }

  /**
   * Creates a new StringBuilderValue with the buffer without copying.
   */
  public StringBuilderValue(char []buffer, int length)
  {
    this(buffer, 0, length);
  }

  public StringBuilderValue(byte []buffer)
  {
    this(buffer, 0, buffer.length);
  }

  public StringBuilderValue(char ch)
  {
    _buffer = new byte[1];
    _length = 1;

    _buffer[0] = (byte) ch;
  }

  public StringBuilderValue(byte ch)
  {
    _buffer = new byte[1];
    _length = 1;

    _buffer[0] = ch;
  }

  public StringBuilderValue(String s)
  {
    int len = s.length();

    _buffer = new byte[len];
    _length = len;

    for (int i = 0; i < len; i++) {
      _buffer[i] = (byte) s.charAt(i);
    }
  }

  public StringBuilderValue(char []s)
  {
    this(s, 0, s.length);
  }

  public StringBuilderValue(char []s, Value v1)
  {
    int len = s.length;

    int bufferLength = MIN_LENGTH;
    while (bufferLength < len)
      bufferLength *= 2;

    _buffer = new byte[bufferLength];
    _length = len;

    for (int i = 0; i < len; i++) {
      _buffer[i] = (byte) s[i];
    }

    v1.appendTo(this);
  }

  public StringBuilderValue(byte []s, Value v1)
  {
    int len = s.length;

    int bufferLength = MIN_LENGTH;
    while (bufferLength < len)
      bufferLength *= 2;

    _buffer = new byte[bufferLength];
    _length = len;

    System.arraycopy(s, 0, _buffer, 0, len);

    v1.appendTo(this);
  }

  public StringBuilderValue(Value v1)
  {
    if (v1 instanceof StringBuilderValue)
      init((StringBuilderValue) v1);
    else {
      _buffer = new byte[MIN_LENGTH];

      v1.appendTo(this);
    }
  }

  public StringBuilderValue(StringBuilderValue v)
  {
    init(v);
  }

  private void init(StringBuilderValue v)
  {
    if (v._isCopy || v instanceof ConstStringValue) {
      _buffer = new byte[v._buffer.length];
      System.arraycopy(v._buffer, 0, _buffer, 0, v._length);
      _length = v._length;
    }
    else {
      _buffer = v._buffer;
      _length = v._length;
      v._isCopy = true;
    }
  }

  public StringBuilderValue(Value v1, Value v2)
  {
    _buffer = new byte[MIN_LENGTH];

    v1.appendTo(this);
    v2.appendTo(this);
  }

  public StringBuilderValue(Value v1, Value v2, Value v3)
  {
    _buffer = new byte[MIN_LENGTH];

    v1.appendTo(this);
    v2.appendTo(this);
    v3.appendTo(this);
  }

  /**
   * Creates the string.
   */
  public static StringValue create(byte value)
  {
    return CHAR_STRINGS[value & 0xFF];
  }

  /**
   * Creates the string.
   */
  public static StringValue create(char value)
  {
    return CHAR_STRINGS[value & 0xFF];
  }

  /**
   * Creates a PHP string from a Java String.
   * If the value is null then NullValue is returned.
   */
  public static Value create(String value)
  {
    if (value == null)
      return NullValue.NULL;
    else if (value.length() == 0)
      return StringBuilderValue.EMPTY;
    else
      return new StringBuilderValue(value);
  }

  /**
   * Returns the value.
   */
  public final String getValue()
  {
    return toString();
  }

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

  /**
   * Returns the ValueType.
   */
  @Override
  public ValueType getValueType()
  {
    return getValueType(_buffer, 0, _length);
  }

  public static final ValueType getValueType(byte []buffer,
                                             int offset,
                                             int len)
  {
    if (len == 0) {
      // php/0307
      return ValueType.LONG_ADD;
    }

    int i = offset;
    int ch = 0;

    while (i < len && Character.isWhitespace(buffer[i])) {
      i++;
    }

    if (i + 1 < len && buffer[i] == '0' && buffer[i + 1] == 'x')
      return ValueType.LONG_EQ;

    if (i < len && ((ch = buffer[i]) == '+' || ch == '-')) {
      i++;
    }

    if (len <= i)
      return ValueType.STRING;

    ch = buffer[i];

    if (ch == '.') {
      // XXX: this doesn't look right 2012-03-15
      for (i++; i < len && '0' <= (ch = buffer[i]) && ch <= '9'; i++) {
        return ValueType.DOUBLE_CMP;
      }

      return ValueType.STRING;
    }
    else if (! ('0' <= ch && ch <= '9'))
      return ValueType.STRING;

    for (; i < len && '0' <= (ch = buffer[i]) && ch <= '9'; i++) {
    }

    while (i < len && Character.isWhitespace(buffer[i])) {
      i++;
    }

    if (len <= i)
      return ValueType.LONG_EQ;
    else if (ch == '.' || ch == 'e' || ch == 'E') {
      for (i++;
           i < len
           && ('0' <= (ch = buffer[i]) && ch <= '9'
               || ch == '+'
               || ch == '-'
               || ch == 'e'
               || ch == 'E');
           i++) {
      }

      while (i < len && Character.isWhitespace(buffer[i])) {
        i++;
      }

      if (i < len)
        return ValueType.STRING;
      else
        return ValueType.DOUBLE_CMP;
    }
    else
      return ValueType.STRING;
  }

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

  /**
   * Converts to a boolean.
   */
  @Override
  public final boolean toBoolean()
  {
    if (_length == 0)
      return false;
    else if (_length == 1 && _buffer[0] == '0')
      return false;
    else
      return true;
  }

  /**
   * Converts to a long.
   */
  @Override
  public long toLong()
  {
    return parseLong(_buffer, 0, _length);
  }

  /**
   * Converts to a double.
   */
  @Override
  public double toDouble()
  {
    return toDouble(_buffer, 0, _length);
  }

  public static final double toDouble(byte []buffer, int offset, int len)
  {
    int start = offset;
    int i = offset;
    int ch = 0;

    while (i < len && Character.isWhitespace(buffer[i])) {
      start++;
      i++;
    }

    int end = offset + len;

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

      double value = 0;

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

        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 = buffer[i]) == '+' || ch == '-')) {
      i++;
    }

    for (; i < len && '0' <= (ch = buffer[i]) && ch <= '9'; i++) {
    }

    if (ch == '.') {
      for (i++; i < len && '0' <= (ch = buffer[i]) && ch <= '9'; i++) {
      }

      if (i == 1)
        return 0;
    }

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

      if (i < len && (ch = buffer[i]) == '+' || ch == '-') {
        i++;
      }

      for (; i < len && '0' <= (ch = buffer[i]) && ch <= '9'; i++) {
      }

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

    if (i == 0)
      return 0;

    try {
      return Double.parseDouble(new String(buffer, start, i - start));
    } catch (NumberFormatException e) {
      return 0;
    }
  }

  /**
   * Convert to an input stream.
   */
  @Override
  public final InputStream toInputStream()
  {
    return new BuilderInputStream();
  }

  /**
   * Converts to a string.
   */
  @Override
  public String toString()
  {
    if (_length == 1)
      return String.valueOf((char) (_buffer[0] & 0xff));
    else {
      CharBuffer buf = CharBuffer.allocate();
      buf.append(_buffer, 0, _length);

      String str = buf.toString();
      buf.free();

      return str;
    }
  }

  /**
   * Converts to a BinaryValue.
   */
  @Override
  public final StringValue toBinaryValue(Env env)
  {
    return this;
  }

  /**
   * Converts to a BinaryValue in desired charset.
   */
  @Override
  public final StringValue toBinaryValue(String charset)
  {
    return this;
  }

  /**
   * Converts to a UnicodeValue.
   */
  @Override
  public StringValue toUnicodeValue()
  {
    // php/0c94
    return new UnicodeBuilderValue().append(getBuffer(), 0, length());
  }

  /**
   * Converts to a UnicodeValue.
   */
  @Override
  public StringValue toUnicodeValue(Env env)
  {
    return toUnicodeValue();
  }

  /**
   * Converts to a UnicodeValue in desired charset.
   */
  @Override
  public StringValue toUnicodeValue(Env env, String charset)
  {
    return toUnicodeValue();
  }

  /**
   * Converts to an object.
   */
  @Override
  public final Object toJavaObject()
  {
    return toString();
  }

  /**
   * Returns true if the value is empty.
   */
  @Override
  public final boolean isEmpty()
  {
    return _length == 0 || _length == 1 && _buffer[0] == '0';
  }

  /**
   * Writes to a stream
   */
  @Override
  public final void writeTo(OutputStream os)
  {
    try {
      os.write(_buffer, 0, _length);

    } catch (IOException e) {
      throw new QuercusModuleException(e);
    }
  }

  /**
   * Append to a string builder.
   */
  @Override
  public StringValue appendTo(StringBuilderValue bb)
  {
    bb.append(_buffer, 0, _length);

    return bb;
  }

  /**
   * Append to a string builder.
   */
  @Override
  public StringValue appendTo(UnicodeBuilderValue bb)
  {
    bb.append(_buffer, 0, _length);

    return bb;
  }

  /**
   * Append to a string builder.
   */
  @Override
  public StringValue appendTo(LargeStringBuilderValue bb)
  {
    bb.append(_buffer, 0, _length);

    return bb;
  }


  /**
   * Append to a string builder.
   */
  @Override
  public StringValue appendTo(BinaryBuilderValue bb)
  {
    bb.append(_buffer, 0, _length);

    return bb;
  }

  /**
   * Converts to a key.
   */
  @Override
  public Value toKey()
  {
    byte []buffer = _buffer;
    int len = _length;

    if (len == 0)
      return this;

    int sign = 1;
    long value = 0;

    int i = 0;
    int ch = buffer[i];
    if (ch == '-') {
      if (len == 1)
        return this;

      sign = -1;
      i++;
    }

    for (; i < len; i++) {
      ch = buffer[i];

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

    return LongValue.create(sign * value);
  }

  /**
   * Converts to a byte array, with no consideration of character encoding.
   * Each character becomes one byte, characters with values above 255 are
   * not correctly preserved.
   */
  public final byte[] toBytes()
  {
    byte[] bytes = new byte[_length];

    System.arraycopy(_buffer, 0, bytes, 0, _length);

    return bytes;
  }

  //
  // Operations
  //

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

  /**
   * Sets the array ref.
   */
  @Override
  public Value put(Value index, Value value)
  {
    setCharValueAt(index.toLong(), value);

    return value;
  }

  /**
   * Sets the array ref.
   */
  @Override
  public Value append(Value index, Value value)
  {
    if (_length > 0)
      return setCharValueAt(index.toLong(), value);
    else
      return new ArrayValueImpl().append(index, value);
  }

  /**
   * sets the character at an index
   */
  /*
  public Value setCharAt(long index, String value)
  {
    int len = _length;

    if (index < 0 || len <= index)
      return this;
    else {
      StringBuilderValue sb = new StringBuilderValue(_buffer, 0, (int) index);
      sb.append(value);
      sb.append(_buffer, (int) (index + 1), (int) (len - index - 1));

      return sb;
    }
  }
  */

  //
  // CharSequence
  //

  /**
   * Returns the length of the string.
   */
  @Override
  public final int length()
  {
    return _length;
  }

  /**
   * Returns the character at a particular location
   */
  @Override
  public final char charAt(int index)
  {
    if (index < 0 || _length <= index)
      return 0;
    else
      return (char) (_buffer[index] & 0xff);
  }

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

    if (index < 0 || len <= index)
      return UnsetStringValue.UNSET;
    else {
      byte ch = _buffer[(int) index];

      return CHAR_STRINGS[ch & 0xff];
    }
  }

  /**
   * sets the character at an index
   */
  @Override
  public Value setCharValueAt(long indexL, Value value)
  {
    int len = _length;

    if (indexL < 0)
      return this;
    else if (indexL < len) {
      StringBuilderValue sb = createStringBuilder(_buffer, 0, len);

      StringValue str = value.toStringValue();

      int index = (int) indexL;

      if (value.length() == 0)
        sb._buffer[index] = 0;
      else
        sb._buffer[index] = (byte) str.charAt(0);

      return sb;
    }
    else {
      // php/03mg, #2940

      int index = (int) indexL;

      StringBuilderValue sb = (StringBuilderValue) copyStringBuilder();

      if (sb._buffer.length < index + 1)
        sb.ensureCapacity(index + 1);

      int padLen = index - len;

      for (int i = 0; i <= padLen; i++) {
         sb._buffer[sb._length++] = ' ';
      }

      StringValue str = value.toStringValue();

      if (value.length() == 0)
        sb._buffer[index] = 0;
      else
        sb._buffer[index] = (byte) str.charAt(0);

      return sb;
    }
  }

  /**
   * Returns the first index of the match string, starting from the head.
   */
  public int indexOf(char match)
  {
    final int length = _length;
    final byte []buffer = _buffer;

    for (int head = 0; head < length; head++) {
      if (buffer[head] == match)
        return head;
    }

    return -1;
  }

  /**
   * Returns the last index of the match string, starting from the head.
   */
  @Override
  public int indexOf(char match, int head)
  {
    int length = _length;
    byte []buffer = _buffer;

    for (; head < length; head++) {
      if (buffer[head] == match)
        return head;
    }

    return -1;
  }

  /**
   * Returns a subsequence
   */
  @Override
  public CharSequence subSequence(int start, int end)
  {
    if (end <= start)
      return StringBuilderValue.EMPTY;
    else if (end - start == 1)
      return CHAR_STRINGS[_buffer[start] & 0xff];

    return createStringBuilder(_buffer, start, end - start);
  }

  /**
   * Returns a subsequence
   */
  @Override
  public String stringSubstring(int start, int end)
  {
    if (end <= start)
      return "";

    CharBuffer buf = CharBuffer.allocate();
    buf.append(_buffer, start, end - start);

    String str = buf.toString();
    buf.free();

    return str;
  }

  /**
   * Convert to lower case.
   */
  @Override
  public StringValue toLowerCase(Locale locale)
  {
    int length = _length;

    boolean isUpperCase = false;

    byte []srcBuffer = _buffer;

    for (int i = 0; i < length; i++) {
      byte ch = srcBuffer[i];

      if ('a' <= ch && ch <= 'z') {
      }
      else if ('A' <= ch && ch <= 'Z') {
        isUpperCase = true;
        break;
      }
    }

    if (! isUpperCase) {
      return this;
    }

    StringBuilderValue string = createStringBuilder(length);
    byte []dstBuffer = string._buffer;

    for (int i = 0; i < length; i++) {
      byte ch = srcBuffer[i];

      if ('A' <= ch && ch <= 'Z') {
        dstBuffer[i] = (byte) (ch + 'a' - 'A');
      }
      else {
        dstBuffer[i] = ch;
      }
    }

    string._length = length;

    return string;
  }

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

    StringBuilderValue string = createStringBuilder(_length);

    byte []srcBuffer = _buffer;
    byte []dstBuffer = string._buffer;

    for (int i = 0; i < length; i++) {
      byte ch = srcBuffer[i];

      if ('a' <= ch && ch <= 'z')
        dstBuffer[i] = (byte) (ch + 'A' - 'a');
      else
        dstBuffer[i] = ch;
    }

    string._length = length;

    return string;
  }

  /**
   * 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;
    }

    byte []buffer = _buffer;

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

    return true;
  }

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

    if (length < offset + mLength)
      return false;

    byte []buffer = _buffer;

    for (int i = 0; i < mLength; i++) {
      int a = buffer[offset + i] & 0xFF;
      char b = mBuffer[mOffset + i];

      if ('A' <= a && a <= 'Z')
        a += 'a' - 'A';

      if ('A' <= b && b <= 'Z')
        b += 'a' - 'A';

      if (a != b)
        return false;
    }

    return true;
  }

  /**
   * Creates a string builder of the same type.
   */
  @Override
  public StringBuilderValue createStringBuilder()
  {
    return new StringBuilderValue();
  }

  /**
   * Creates a string builder of the same type.
   */
  @Override
  public StringBuilderValue createStringBuilder(int length)
  {
    return new StringBuilderValue(length);
  }

  /**
   * Creates a string builder of the same type.
   */
  public StringBuilderValue
    createStringBuilder(byte []buffer, int offset, int length)
  {
    return new StringBuilderValue(buffer, offset, length);
  }

  /**
   * Converts to a string builder
   */
  public StringValue copyStringBuilder()
  {
    return new StringBuilderValue(this);
  }

  /**
   * Converts to a string builder
   */
  @Override
  public StringValue toStringBuilder()
  {
    return new StringBuilderValue(this);
  }

  /**
   * Converts to a string builder
   */
  @Override
  public StringValue toStringBuilder(Env env)
  {
    if (_length >= LARGE_BUILDER_THRESHOLD)
      return new LargeStringBuilderValue(this);
    else
      return new StringBuilderValue(this);
  }

  /**
   * Converts to a string builder
   */
  @Override
  public StringValue toStringBuilder(Env env, Value value)
  {
    if (_length + value.length() >= LARGE_BUILDER_THRESHOLD) {
      LargeStringBuilderValue v = new LargeStringBuilderValue(this);

      value.appendTo(v);

      return v;
    }
    else {
      StringBuilderValue v = new StringBuilderValue(this);

      value.appendTo(v);

      return v;
    }
  }

  /**
   * Converts to a string builder
   */
  public StringValue toStringBuilder(Env env, StringValue value)
  {
    if (_length + value.length() >= LARGE_BUILDER_THRESHOLD) {
      LargeStringBuilderValue v = new LargeStringBuilderValue(this);

      value.appendTo(v);

      return v;
    }
    else {
      StringBuilderValue v = new StringBuilderValue(this);

      value.appendTo(v);

      return v;
    }
  }

  //
  // append code
  //

  /**
   * Append a Java string to the value.
   */
  @Override
  public final StringValue append(String s)
  {
    int sublen = s.length();

    if (_buffer.length < _length + sublen)
      ensureCapacity(_length + sublen);

    byte []buffer = _buffer;
    int length = _length;

    for (int i = 0; i < sublen; i++) {
      buffer[length + i] = (byte) s.charAt(i);
    }

    _length = length + sublen;

    return this;
  }

  /**
   * Append a Java string to the value.
   */
  @Override
  public final StringValue append(String s, int start, int end)
  {
    int sublen = end - start;

    if (_buffer.length < _length + sublen)
      ensureCapacity(_length + sublen);

    byte []buffer = _buffer;
    int length = _length;

    for (; start < end; start++)
      buffer[length++] = (byte) s.charAt(start);

    _length = length;

    return this;
  }

  /**
   * Append a Java char to the value.
   */
  @Override
  public final StringValue append(char ch)
  {
    if (_buffer.length < _length + 1)
      ensureCapacity(_length + 1);

    _buffer[_length++] = (byte) ch;

    return this;
  }

  @Override
  public final void write(int ch)
  {
    if (_buffer.length < _length + 1)
      ensureCapacity(_length + 1);

    _buffer[_length++] = (byte) ch;
  }


  /**
   * Append a Java buffer to the value.
   */
  @Override
  public final StringValue append(char []buf, int offset, int length)
  {
    int end = _length + length;

    if (_buffer.length < end)
      ensureCapacity(end);

    byte []buffer = _buffer;
    int bufferLength = _length;

    for (int i = 0; i < length; i++) {
      buffer[bufferLength + i] = (byte) buf[offset + i];
    }

    _length = end;

    return this;
  }

  /**
   * Append a Java buffer to the value.
   */
  @Override
  public final StringValue append(char []buf)
  {
    int length = buf.length;

    if (_buffer.length < _length + length)
      ensureCapacity(_length + length);

    byte []buffer = _buffer;
    int bufferLength = _length;

    for (int i = 0; i < length; i++)
      buffer[bufferLength++] = (byte) buf[i];

    _buffer = buffer;
    _length = bufferLength;

    return this;
  }

  /**
   * Append a Java buffer to the value.
   */
  @Override
  public StringValue appendUnicode(char []buf)
  {
    int length = buf.length;

    if (_buffer.length < _length + length)
      ensureCapacity(_length + length);

    byte []buffer = _buffer;
    int bufferLength = _length;

    for (int i = 0; i < length; i++)
      buffer[bufferLength++] = (byte) buf[i];

    _buffer = buffer;
    _length = bufferLength;

    return this;
  }

  /**
   * Append a Java buffer to the value.
   */
  @Override
  public StringValue appendUnicode(char []buf, int offset, int length)
  {
    if (_buffer.length < _length + length)
      ensureCapacity(_length + length);

    byte []buffer = _buffer;
    int bufferLength = _length;

    for (; length > 0; length--)
      buffer[bufferLength++] = (byte) buf[offset++];

    _buffer = buffer;
    _length = bufferLength;

    return this;
  }

  /**
   * Append a Java buffer to the value.
   */
  @Override
  public final StringValue append(CharSequence buf, int head, int tail)
  {
    int length = tail - head;

    if (_buffer.length < _length + length)
      ensureCapacity(_length + length);

    if (buf instanceof StringBuilderValue) {
      StringBuilderValue sb = (StringBuilderValue) buf;

      System.arraycopy(sb._buffer, head, _buffer, _length, length);

      _length += length;

      return this;
    }
    else {
      byte []buffer = _buffer;
      int bufferLength = _length;

      for (; head < tail; head++) {
        buffer[bufferLength++] = (byte) buf.charAt(head);
      }

      _length = bufferLength;

      return this;
    }
  }

  /**
   * Append a Java buffer to the value.
   */
  @Override
  public StringValue append(StringBuilderValue sb, int head, int tail)
  {
    int length = tail - head;

    if (_buffer.length < _length + length)
      ensureCapacity(_length + length);

    System.arraycopy(sb._buffer, head, _buffer, _length, length);

    _length += length;

    return this;
  }

  /**
   * Append a Java value to the value.
   */
  @Override
  public final StringValue append(Value v)
  {
    /*
    if (v.length() == 0)
      return this;
    else {
      // php/033a
      v.appendTo(this);

      return this;
    }
    */

    v.appendTo(this);

    return this;
  }

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

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

    final int length = _length;
    final int end = length - matchLength;
    final char first = match.charAt(0);
    final byte []buffer = _buffer;

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

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

      return head;
    }

    return -1;
  }

  /**
   * Append a Java value to the value.
   */
  @Override
  public StringValue appendUnicode(Value v)
  {
    v.appendTo(this);

    return this;
  }

  /**
   * Append a Java value to the value.
   */
  @Override
  public StringValue appendUnicode(Value v1, Value v2)
  {
    v1.appendTo(this);
    v2.appendTo(this);

    return this;
  }

  /**
   * Append a buffer to the value.
   */
  public final StringValue append(byte []buf, int offset, int length)
  {
    int end = _length + length;

    if (_buffer.length < end)
      ensureCapacity(end);

    System.arraycopy(buf, offset, _buffer, _length, length);

    _length = end;

    return this;
  }

  @Override
  public final void write(byte []buf, int offset, int length)
  {
    append(buf, offset, length);
  }

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

  /**
   * Append a buffer to the value.
   */
  @Override
  public final StringValue appendUtf8(byte []buf, int offset, int length)
  {
    if (_buffer.length < _length + length)
      ensureCapacity(_length + length);

    byte []charBuffer = _buffer;
    int charLength = _length;

    int end = offset + length;

    while (offset < end) {
      int ch = buf[offset++] & 0xff;

      if (ch < 0x80)
        charBuffer[charLength++] = (byte) ch;
      else if (ch < 0xe0) {
        int ch2 = buf[offset++] & 0xff;

        int v = (char) (((ch & 0x1f) << 6) + (ch2 & 0x3f));

        charBuffer[charLength++] = (byte) (v & 0xff);
      }
      else {
        int ch2 = buf[offset++] & 0xff;
        int ch3 = buf[offset++] & 0xff;

        byte v = (byte) (((ch & 0xf) << 12)
                         + ((ch2 & 0x3f) << 6)
                         + ((ch3) << 6));

        charBuffer[charLength++] = v;
      }
    }

    _length = charLength;

    return this;
  }

  /**
   * Append a Java byte to the value without conversions.
   */
  @Override
  public final StringValue appendByte(int v)
  {
    if (_buffer.length < _length + 1)
      ensureCapacity(_length + 1);

    _buffer[_length++] = (byte) v;

    return this;
  }

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

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

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

  /**
   * Append a bytes to the value.
   */
  @Override
  public StringValue appendBytes(String s)
  {
    int sublen = s.length();

    if (_buffer.length < _length + sublen)
      ensureCapacity(_length + sublen);

    for (int i = 0; i < sublen; i++) {
      _buffer[_length++] = (byte) s.charAt(i);
    }

    return this;
  }

  /**
   * Append Java bytes to the value without conversions.
   */
  @Override
  public final StringValue appendBytes(byte []bytes, int offset, int end)
  {
    int len = end - offset;

    if (_buffer.length < _length + len)
      ensureCapacity(_length + len);

    System.arraycopy(bytes, offset, _buffer, _length, len);

    _length += len;

    return this;
  }

  @Override
  public StringValue append(Reader reader, long length)
    throws IOException
  {
    // php/4407 - oracle clob callback passes very long length

    TempCharBuffer tempBuf = TempCharBuffer.allocate();

    char []buffer = tempBuf.getBuffer();

    int sublen = (int) Math.min(buffer.length, length);

    try {
      while (length > 0) {
        if (_buffer.length < _length + sublen)
          ensureCapacity(_length + sublen);

        int count = reader.read(buffer, 0, sublen);

        if (count <= 0)
          break;

        append(buffer, 0, count);

        length -= count;
      }

    } catch (IOException e) {
      throw new QuercusModuleException(e);
    } finally {
      TempCharBuffer.free(tempBuf);
    }

    return this;
  }

  /**
   * Returns the buffer.
   */
  public final byte []getBuffer()
  {
    return _buffer;
  }

  /**
   * Sets the length.
   */
  public final void setLength(int offset)
  {
    _length = offset;
  }

  /**
   * Returns the current capacity.
   */
  public final int getBufferLength()
  {
    return _buffer.length;
  }

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

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

  //
  // Java generator code
  //

  /**
   * Prints the value.
   * @param env
   */
  public void print(Env env)
  {
    env.write(_buffer, 0, _length);
  }

  /**
   * Prints the value.
   * @param env
   */
  public void print(Env env, WriteStream out)
  {
    try {
      out.write(_buffer, 0, _length);
    } catch (IOException e) {
      throw new QuercusModuleException(e);
    }
  }

  /**
   * Serializes the value.
   */
  public void serialize(Env env, StringBuilder sb)
  {
    sb.append("s:");
    sb.append(_length);
    sb.append(":\"");

    for (int i = 0; i < _length; i++) {
      sb.append((char) (_buffer[i] & 0xff));
    }

    sb.append("\";");
  }

  @Override
  public String toDebugString()
  {
    StringBuilder sb = new StringBuilder();

    int length = length();

    sb.append("binary(");
    sb.append(length);
    sb.append(") \"");

    int appendLength = length < 256 ? length : 256;

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

    if (length > 256)
      sb.append(" ...");

    sb.append('"');

    return sb.toString();
  }

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

    if (length < 0)
        length = 0;

    out.print("string(");
    out.print(length);
    out.print(") \"");

    out.write(_buffer, 0, _length);

    out.print("\"");
  }

  @Override
  public OutputStream getOutputStream()
  {
    return new BuilderOutputStream();
  }

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

    crc.update(_buffer, 0, _length);

    return crc.getValue() & 0xffffffff;
  }

  public void ensureAppendCapacity(int newCapacity)
  {
    ensureCapacity(_length + newCapacity);
  }

  protected void ensureCapacity(int newCapacity)
  {
    int bufferLength = _buffer.length;

    if (newCapacity <= bufferLength)
      return;

    if (bufferLength < MIN_LENGTH)
      bufferLength = MIN_LENGTH;

    while (bufferLength <= newCapacity)
      bufferLength = 2 * bufferLength;

    byte []buffer = new byte[bufferLength];
    System.arraycopy(_buffer, 0, buffer, 0, _length);
    _buffer = buffer;
    _isCopy = false;
  }

  /**
   * Returns the hash code.
   */
  @Override
  public int hashCode()
  {
    int hash = _hashCode;

    if (hash != 0)
      return hash;

    hash = 37;

    int length = _length;
    byte []buffer = _buffer;

    if (length > 256) {
      for (int i = 127; i >= 0; i--) {
        hash = 65521 * hash + buffer[i];
      }

      for (int i = length - 128; i < length; i++) {
        hash = 65521 * hash + buffer[i];
      }

      _hashCode = hash;

      return hash;
    }

    for (int i = length - 1; i >= 0; i--) {
      hash = 65521 * hash + buffer[i];
    }

    _hashCode = hash;

    return hash;
  }

  /**
   * Returns the hash code.
   */
  @Override
  public int hashCodeCaseInsensitive()
  {
    int hash = 0;

    if (hash != 0)
      return hash;

    hash = 37;

    int length = _length;
    byte []buffer = _buffer;

    if (length > 256) {
      for (int i = 127; i >= 0; i--) {
        hash = 65521 * hash + toLower(buffer[i]);
      }

      for (int i = length - 128; i < length; i++) {
        hash = 65521 * hash + toLower(buffer[i]);
      }

      _hashCode = hash;

      return hash;
    }

    for (int i = length - 1; i >= 0; i--) {
      int ch = toLower(buffer[i]);

      hash = 65521 * hash + ch;
    }

    return hash;
  }

  private int toLower(int ch)
  {
    if ('A' <= ch && ch <= 'Z')
      return ch + 'a' - 'A';
    else
      return ch;
  }

  @Override
  public int getHashCode()
  {
    return hashCode();
  }

  /**
   * Returns true for equality
   */
  @Override
  public boolean eq(Value rValue)
  {
    rValue = rValue.toValue();

    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();
    }

    ValueType typeA = getValueType();

    if (typeA.isNumberCmp() && typeB.isNumberCmp()) {
      double l = toDouble();
      double r = rValue.toDouble();

      return l == r;
    }
    else if (rValue instanceof StringBuilderValue) {
      StringBuilderValue value = (StringBuilderValue) rValue;

      int length = _length;

      if (length != value._length)
        return false;

      byte []bufferA = _buffer;
      byte []bufferB = value._buffer;

      for (int i = length - 1; i >= 0; i--) {
        if (bufferA[i] != bufferB[i])
          return false;
      }

      return true;
    }
    else if (rValue instanceof StringValue) {
      StringValue value = (StringValue) rValue;

      int length = _length;

      if (length != value.length()) {
        return false;
      }

      byte []buffer = _buffer;

      for (int i = length - 1; i >= 0; i--) {
        if ((buffer[i] & 0xff) != value.charAt(i)) {
          return false;
        }
      }

      return true;
    }
    else if (rValue.isObject()) {
      return super.eq(rValue);
    }
    else {
      String rString = rValue.toString();

      int len = rString.length();

      if (_length != len) {
        return false;
      }

      for (int i = len - 1; i >= 0; i--) {
        if ((_buffer[i] & 0xff) != rString.charAt(i)) {
          return false;
        }
      }

      return true;
    }
  }

  @Override
  public boolean equals(Object o)
  {
    if (o == this) {
      return true;
    }

    if (o instanceof StringBuilderValue) {
      StringBuilderValue value = (StringBuilderValue) o;

      int length = _length;

      if (length != value._length)
        return false;

      byte []bufferA = _buffer;
      byte []bufferB = value._buffer;

      for (int i = length - 1; i >= 0; i--) {
        if (bufferA[i] != bufferB[i])
          return false;
      }

      return true;
    }
    else if (o instanceof LargeStringBuilderValue) {
      StringValue value = (StringValue) o;

      int length = _length;
      int lengthB = value.length();

      if (length != lengthB)
        return false;

      byte []bufferA = _buffer;

      for (int i = length - 1; i >= 0; i--) {
        if (bufferA[i] != value.charAt(i))
          return false;
      }

      return true;
    }
    else if (o instanceof StringValue) {
      StringValue str = (StringValue) o;

      int len = _length;

      if (len != str.length()) {
        return false;
      }

      byte []buffer = _buffer;

      for (int i = len - 1; i >= 0; i--) {
        if ((buffer[i] & 0xff) != str.charAt(i)) {
          return false;
        }
      }

      return true;
    }
    else {
      return false;
    }
  }

  /**
   * Test for equality
   */
  public boolean equalsIgnoreCase(Object o)
  {
    if (this == o) {
      return true;
    }
    else if (! (o instanceof StringBuilderValue)) {
      return super.equalsIgnoreCase(o);
    }

    StringBuilderValue s = (StringBuilderValue) o;

    int len = _length;

    if (len != s._length) {
      return false;
    }

    for (int i = len - 1; i >= 0; i--) {
      int chA = _buffer[i];
      int chB = s._buffer[i];

      if (chA == chB) {
        continue;
      }

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

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

    return true;
  }

  @Override
  public boolean eql(Value o)
  {
    o = o.toValue();

    if (o == this)
      return true;

    if (o instanceof StringBuilderValue) {
      StringBuilderValue value = (StringBuilderValue) o;

      int length = _length;

      if (length != value._length)
        return false;

      byte []bufferA = _buffer;
      byte []bufferB = value._buffer;

      for (int i = length - 1; i >= 0; i--) {
        if (bufferA[i] != bufferB[i])
          return false;
      }

      return true;
    }
    else
      return false;
  }

  /**
   * Generates code to recreate the expression.
   *
   * @param out the writer to the Java source code.
   */
  public void generate(PrintWriter out)
    throws IOException
  {
    ConstStringValue.generateImpl(out, this);
  }

  //
  // Java serialization code
  //

  private void writeObject(ObjectOutputStream out)
    throws IOException
  {
    out.writeInt(_length);

    out.write(_buffer, 0, _length);
  }

  private void readObject(ObjectInputStream in)
    throws ClassNotFoundException, IOException
  {
    _length = in.readInt();
    _buffer = new byte[_length];

    in.read(_buffer, 0, _length);
  }

  class BinaryInputStream extends InputStream {
    private int _offset;

    /**
     * Reads the next byte.
     */
    @Override
    public int read()
    {
      if (_offset < _length)
        return _buffer[_offset++] & 0xff;
      else
        return -1;
    }

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

      if (sublen <= 0)
        return -1;

      System.arraycopy(_buffer, _offset, buffer, offset, sublen);

      _offset += sublen;

      return sublen;
    }
  }

  class BuilderInputStream extends InputStream {
    private int _index;

    /**
     * Reads the next byte.
     */
    @Override
    public int read()
    {
      if (_index < _length)
        return _buffer[_index++] & 0xff;
      else
        return -1;
    }

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

      if (sublen <= 0)
        return -1;

      System.arraycopy(_buffer, _index, buffer, offset, sublen);

      _index += sublen;

      return sublen;
    }
  }

  class BuilderOutputStream extends OutputStream {
    /**
     * Writes the next byte.
     */
    @Override
    public void write(int ch)
    {
      appendByte(ch);
    }

    /**
     * Reads into a buffer.
     */
    @Override
    public void write(byte []buffer, int offset, int length)
    {
      append(buffer, offset, length);
    }
  }

  static {
    CHAR_STRINGS = new ConstStringValue[256];

    for (int i = 0; i < CHAR_STRINGS.length; i++) {
      CHAR_STRINGS[i] = new ConstStringValue((char) i);
    }
  }
}

