/*
 * Copyright (c) 2001-2008 Caucho Technology, Inc.  All rights reserved.
 *
 * The Apache Software License, Version 1.1
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *
 * 3. The end-user documentation included with the redistribution, if
 *    any, must include the following acknowlegement:
 *       "This product includes software developed by the
 *        Caucho Technology (http://www.caucho.com/)."
 *    Alternately, this acknowlegement may appear in the software itself,
 *    if and wherever such third-party acknowlegements normally appear.
 *
 * 4. The names "Burlap", "Resin", and "Caucho" must not be used to
 *    endorse or promote products derived from this software without prior
 *    written permission. For written permission, please contact
 *    info@caucho.com.
 *
 * 5. Products derived from this software may not be called "Resin"
 *    nor may "Resin" appear in their names without prior written
 *    permission of Caucho Technology.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED.  IN NO EVENT SHALL CAUCHO TECHNOLOGY OR ITS CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
 * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
 * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
 * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
 * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * @author Scott Ferguson
 */

package com.caucho.hessian.io;

import com.caucho.hessian.util.IdentityIntMap;

import java.io.InputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.HashMap;

/**
 * Output stream for Hessian 2 requests.
 *
 * <p>Since HessianOutput does not depend on any classes other than
 * in the JDK, it can be extracted independently into a smaller package.
 *
 * <p>HessianOutput is unbuffered, so any client needs to provide
 * its own buffering.
 *
 * <pre>
 * OutputStream os = ...; // from http connection
 * Hessian2Output out = new Hessian2Output(os);
 * String value;
 *
 * out.startCall("hello", 1); // start hello call
 * out.writeString("arg1");   // write a string argument
 * out.completeCall();        // complete the call
 * </pre>
 */
public class Hessian2Output
  extends AbstractHessianOutput
  implements Hessian2Constants
{
  // should match Resin buffer size for perf
  public final static int SIZE = 8 * 1024;

  // the output stream/
  protected OutputStream _os;

  // map of references
  private final IdentityIntMap _refs
    = new IdentityIntMap(256);
  
  private int _refCount = 0;

  private boolean _isCloseStreamOnClose;

  // map of classes
  private final IdentityIntMap _classRefs
    = new IdentityIntMap(256);

  // map of types
  private HashMap<String,Integer> _typeRefs;

  private final byte []_buffer = new byte[SIZE];
  private int _offset;

  private boolean _isPacket;
  
  private boolean _isUnshared;

  /**
   * Creates a new Hessian output stream, initialized with an
   * underlying output stream.
   *
   * @param os the underlying output stream.
   */
  public Hessian2Output()
  {
  }

  /**
   * Creates a new Hessian output stream, initialized with an
   * underlying output stream.
   *
   * @param os the underlying output stream.
   */
  public Hessian2Output(OutputStream os)
  {
    init(os);
  }

  @Override
  public void init(OutputStream os)
  {
    reset();

    _os = os;
  }

  public void initPacket(OutputStream os)
  {
    resetReferences();

    _os = os;
  }

  public void setCloseStreamOnClose(boolean isClose)
  {
    _isCloseStreamOnClose = isClose;
  }

  public boolean isCloseStreamOnClose()
  {
    return _isCloseStreamOnClose;
  }
  
  /**
   * Sets hessian to be "unshared", meaning it will not detect 
   * duplicate or circular references.
   */
  @Override
  public boolean setUnshared(boolean isUnshared)
  {
    boolean oldIsUnshared = _isUnshared;
    
    _isUnshared = isUnshared;
    
    return oldIsUnshared;
  }

  /**
   * Writes a complete method call.
   */
  @Override
  public void call(String method, Object []args)
    throws IOException
  {
    writeVersion();

    int length = args != null ? args.length : 0;

    startCall(method, length);

    for (int i = 0; i < length; i++) {
      writeObject(args[i]);
    }

    completeCall();

    flush();
  }

  /**
   * Starts the method call.  Clients would use <code>startCall</code>
   * instead of <code>call</code> if they wanted finer control over
   * writing the arguments, or needed to write headers.
   *
   * <code><pre>
   * C
   * string # method name
   * int    # arg count
   * </pre></code>
   *
   * @param method the method name to call.
   */
  @Override
  public void startCall(String method, int length)
    throws IOException
  {
    int offset = _offset;

    if (SIZE < offset + 32) {
      flushBuffer();
      offset = _offset;
    }

    byte []buffer = _buffer;

    buffer[_offset++] = (byte) 'C';

    writeString(method);
    writeInt(length);
  }

  /**
   * Writes the call tag.  This would be followed by the
   * method and the arguments
   *
   * <code><pre>
   * C
   * </pre></code>
   *
   * @param method the method name to call.
   */
  @Override
  public void startCall()
    throws IOException
  {
    flushIfFull();

    _buffer[_offset++] = (byte) 'C';
  }

  /**
   * Starts an envelope.
   *
   * <code><pre>
   * E major minor
   * m b16 b8 method-name
   * </pre></code>
   *
   * @param method the method name to call.
   */
  public void startEnvelope(String method)
    throws IOException
  {
    int offset = _offset;

    if (SIZE < offset + 32) {
      flushBuffer();
      offset = _offset;
    }

    _buffer[_offset++] = (byte) 'E';

    writeString(method);
  }

  /**
   * Completes an envelope.
   *
   * <p>A successful completion will have a single value:
   *
   * <pre>
   * Z
   * </pre>
   */
  public void completeEnvelope()
    throws IOException
  {
    flushIfFull();

    _buffer[_offset++] = (byte) 'Z';
  }

  /**
   * Writes the method tag.
   *
   * <code><pre>
   * string
   * </pre></code>
   *
   * @param method the method name to call.
   */
  public void writeMethod(String method)
    throws IOException
  {
    writeString(method);
  }

  /**
   * Completes.
   *
   * <code><pre>
   * z
   * </pre></code>
   */
  @Override
  public void completeCall()
    throws IOException
  {
    /*
    flushIfFull();

    _buffer[_offset++] = (byte) 'Z';
    */
  }

  /**
   * Starts the reply
   *
   * <p>A successful completion will have a single value:
   *
   * <pre>
   * R
   * </pre>
   */
  @Override
  public void startReply()
    throws IOException
  {
    writeVersion();

    flushIfFull();

    _buffer[_offset++] = (byte) 'R';
  }

  public void writeVersion()
    throws IOException
  {
    flushIfFull();

    _buffer[_offset++] = (byte) 'H';
    _buffer[_offset++] = (byte) 2;
    _buffer[_offset++] = (byte) 0;
  }

  /**
   * Completes reading the reply
   *
   * <p>A successful completion will have a single value:
   *
   * <pre>
   * z
   * </pre>
   */
  @Override
  public void completeReply()
    throws IOException
  {
  }

  /**
   * Starts a packet
   *
   * <p>A message contains several objects encapsulated by a length</p>
   *
   * <pre>
   * p x02 x00
   * </pre>
   */
  public void startMessage()
    throws IOException
  {
    flushIfFull();

    _buffer[_offset++] = (byte) 'p';
    _buffer[_offset++] = (byte) 2;
    _buffer[_offset++] = (byte) 0;
  }

  /**
   * Completes reading the message
   *
   * <p>A successful completion will have a single value:
   *
   * <pre>
   * z
   * </pre>
   */
  public void completeMessage()
    throws IOException
  {
    flushIfFull();

    _buffer[_offset++] = (byte) 'z';
  }

  /**
   * Writes a fault.  The fault will be written
   * as a descriptive string followed by an object:
   *
   * <code><pre>
   * F map
   * </pre></code>
   *
   * <code><pre>
   * F H
   * \x04code
   * \x10the fault code
   *
   * \x07message
   * \x11the fault message
   *
   * \x06detail
   * M\xnnjavax.ejb.FinderException
   *     ...
   * Z
   * Z
   * </pre></code>
   *
   * @param code the fault code, a three digit
   */
  public void writeFault(String code, String message, Object detail)
    throws IOException
  {
    flushIfFull();

    writeVersion();

    _buffer[_offset++] = (byte) 'F';
    _buffer[_offset++] = (byte) 'H';

    addRef(new Object(), _refCount++, false);

    writeString("code");
    writeString(code);

    writeString("message");
    writeString(message);

    if (detail != null) {
      writeString("detail");
      writeObject(detail);
    }

    flushIfFull();
    _buffer[_offset++] = (byte) 'Z';
  }

  /**
   * Writes any object to the output stream.
   */
  @Override
  public void writeObject(Object object)
    throws IOException
  {
    if (object == null) {
      writeNull();
      return;
    }

    Serializer serializer
      = findSerializerFactory().getObjectSerializer(object.getClass());

    serializer.writeObject(object, this);
  }

  /**
   * Writes the list header to the stream.  List writers will call
   * <code>writeListBegin</code> followed by the list contents and then
   * call <code>writeListEnd</code>.
   *
   * <code><pre>
   * list ::= V type value* Z
   *      ::= v type int value*
   * </pre></code>
   *
   * @return true for variable lists, false for fixed lists
   */
  public boolean writeListBegin(int length, String type)
    throws IOException
  {
    flushIfFull();

    if (length < 0) {
      if (type != null) {
        _buffer[_offset++] = (byte) BC_LIST_VARIABLE;
        writeType(type);
      }
      else
        _buffer[_offset++] = (byte) BC_LIST_VARIABLE_UNTYPED;

      return true;
    }
    else if (length <= LIST_DIRECT_MAX) {
      if (type != null) {
        _buffer[_offset++] = (byte) (BC_LIST_DIRECT + length);
        writeType(type);
      }
      else {
        _buffer[_offset++] = (byte) (BC_LIST_DIRECT_UNTYPED + length);
      }

      return false;
    }
    else {
      if (type != null) {
        _buffer[_offset++] = (byte) BC_LIST_FIXED;
        writeType(type);
      }
      else {
        _buffer[_offset++] = (byte) BC_LIST_FIXED_UNTYPED;
      }

      writeInt(length);

      return false;
    }
  }

  /**
   * Writes the tail of the list to the stream for a variable-length list.
   */
  public void writeListEnd()
    throws IOException
  {
    flushIfFull();

    _buffer[_offset++] = (byte) BC_END;
  }

  /**
   * Writes the map header to the stream.  Map writers will call
   * <code>writeMapBegin</code> followed by the map contents and then
   * call <code>writeMapEnd</code>.
   *
   * <code><pre>
   * map ::= M type (<value> <value>)* Z
   *     ::= H (<value> <value>)* Z
   * </pre></code>
   */
  public void writeMapBegin(String type)
    throws IOException
  {
    if (SIZE < _offset + 32)
      flushBuffer();

    if (type != null) {
      _buffer[_offset++] = BC_MAP;

      writeType(type);
    }
    else
      _buffer[_offset++] = BC_MAP_UNTYPED;
  }

  /**
   * Writes the tail of the map to the stream.
   */
  public void writeMapEnd()
    throws IOException
  {
    if (SIZE < _offset + 32)
      flushBuffer();

    _buffer[_offset++] = (byte) BC_END;
  }

  /**
   * Writes the object definition
   *
   * <code><pre>
   * C &lt;string> &lt;int> &lt;string>*
   * </pre></code>
   */
  @Override
  public int writeObjectBegin(String type)
    throws IOException
  {
    int newRef = _classRefs.size();
    int ref = _classRefs.put(type, newRef, false);

    if (newRef != ref) {
      if (SIZE < _offset + 32)
        flushBuffer();

      if (ref <= OBJECT_DIRECT_MAX) {
        _buffer[_offset++] = (byte) (BC_OBJECT_DIRECT + ref);
      }
      else {
        _buffer[_offset++] = (byte) 'O';
        writeInt(ref);
      }

      return ref;
    }
    else {
      if (SIZE < _offset + 32)
        flushBuffer();

      _buffer[_offset++] = (byte) 'C';

      writeString(type);

      return -1;
    }
  }

  /**
   * Writes the tail of the class definition to the stream.
   */
  @Override
  public void writeClassFieldLength(int len)
    throws IOException
  {
    writeInt(len);
  }

  /**
   * Writes the tail of the object definition to the stream.
   */
  @Override
  public void writeObjectEnd()
    throws IOException
  {
  }

  /**
   * <code><pre>
   * type ::= string
   *      ::= int
   * </code></pre>
   */
  private void writeType(String type)
    throws IOException
  {
    flushIfFull();

    int len = type.length();
    if (len == 0) {
      throw new IllegalArgumentException("empty type is not allowed");
    }

    if (_typeRefs == null)
      _typeRefs = new HashMap<String,Integer>();

    Integer typeRefV = (Integer) _typeRefs.get(type);

    if (typeRefV != null) {
      int typeRef = typeRefV.intValue();

      writeInt(typeRef);
    }
    else {
      _typeRefs.put(type, Integer.valueOf(_typeRefs.size()));

      writeString(type);
    }
  }

  /**
   * Writes a boolean value to the stream.  The boolean will be written
   * with the following syntax:
   *
   * <code><pre>
   * T
   * F
   * </pre></code>
   *
   * @param value the boolean value to write.
   */
  @Override
  public void writeBoolean(boolean value)
    throws IOException
  {
    if (SIZE < _offset + 16)
      flushBuffer();

    if (value)
      _buffer[_offset++] = (byte) 'T';
    else
      _buffer[_offset++] = (byte) 'F';
  }

  /**
   * Writes an integer value to the stream.  The integer will be written
   * with the following syntax:
   *
   * <code><pre>
   * I b32 b24 b16 b8
   * </pre></code>
   *
   * @param value the integer value to write.
   */
  @Override
  public void writeInt(int value)
    throws IOException
  {
    int offset = _offset;
    byte []buffer = _buffer;

    if (SIZE <= offset + 16) {
      flushBuffer();
      offset = _offset;
    }

    if (INT_DIRECT_MIN <= value && value <= INT_DIRECT_MAX)
      buffer[offset++] = (byte) (value + BC_INT_ZERO);
    else if (INT_BYTE_MIN <= value && value <= INT_BYTE_MAX) {
      buffer[offset++] = (byte) (BC_INT_BYTE_ZERO + (value >> 8));
      buffer[offset++] = (byte) (value);
    }
    else if (INT_SHORT_MIN <= value && value <= INT_SHORT_MAX) {
      buffer[offset++] = (byte) (BC_INT_SHORT_ZERO + (value >> 16));
      buffer[offset++] = (byte) (value >> 8);
      buffer[offset++] = (byte) (value);
    }
    else {
      buffer[offset++] = (byte) ('I');
      buffer[offset++] = (byte) (value >> 24);
      buffer[offset++] = (byte) (value >> 16);
      buffer[offset++] = (byte) (value >> 8);
      buffer[offset++] = (byte) (value);
    }

    _offset = offset;
  }

  /**
   * Writes a long value to the stream.  The long will be written
   * with the following syntax:
   *
   * <code><pre>
   * L b64 b56 b48 b40 b32 b24 b16 b8
   * </pre></code>
   *
   * @param value the long value to write.
   */
  public void writeLong(long value)
    throws IOException
  {
    int offset = _offset;
    byte []buffer = _buffer;

    if (SIZE <= offset + 16) {
      flushBuffer();
      offset = _offset;
    }

    if (LONG_DIRECT_MIN <= value && value <= LONG_DIRECT_MAX) {
      buffer[offset++] = (byte) (value + BC_LONG_ZERO);
    }
    else if (LONG_BYTE_MIN <= value && value <= LONG_BYTE_MAX) {
      buffer[offset++] = (byte) (BC_LONG_BYTE_ZERO + (value >> 8));
      buffer[offset++] = (byte) (value);
    }
    else if (LONG_SHORT_MIN <= value && value <= LONG_SHORT_MAX) {
      buffer[offset++] = (byte) (BC_LONG_SHORT_ZERO + (value >> 16));
      buffer[offset++] = (byte) (value >> 8);
      buffer[offset++] = (byte) (value);
    }
    else if (-0x80000000L <= value && value <= 0x7fffffffL) {
      buffer[offset + 0] = (byte) BC_LONG_INT;
      buffer[offset + 1] = (byte) (value >> 24);
      buffer[offset + 2] = (byte) (value >> 16);
      buffer[offset + 3] = (byte) (value >> 8);
      buffer[offset + 4] = (byte) (value);

      offset += 5;
    }
    else {
      buffer[offset + 0] = (byte) 'L';
      buffer[offset + 1] = (byte) (value >> 56);
      buffer[offset + 2] = (byte) (value >> 48);
      buffer[offset + 3] = (byte) (value >> 40);
      buffer[offset + 4] = (byte) (value >> 32);
      buffer[offset + 5] = (byte) (value >> 24);
      buffer[offset + 6] = (byte) (value >> 16);
      buffer[offset + 7] = (byte) (value >> 8);
      buffer[offset + 8] = (byte) (value);

      offset += 9;
    }

    _offset = offset;
  }

  /**
   * Writes a double value to the stream.  The double will be written
   * with the following syntax:
   *
   * <code><pre>
   * D b64 b56 b48 b40 b32 b24 b16 b8
   * </pre></code>
   *
   * @param value the double value to write.
   */
  public void writeDouble(double value)
    throws IOException
  {
    int offset = _offset;
    byte []buffer = _buffer;

    if (SIZE <= offset + 16) {
      flushBuffer();
      offset = _offset;
    }

    int intValue = (int) value;

    if (intValue == value) {
      if (intValue == 0) {
        buffer[offset++] = (byte) BC_DOUBLE_ZERO;

        _offset = offset;

        return;
      }
      else if (intValue == 1) {
        buffer[offset++] = (byte) BC_DOUBLE_ONE;

        _offset = offset;

        return;
      }
      else if (-0x80 <= intValue && intValue < 0x80) {
        buffer[offset++] = (byte) BC_DOUBLE_BYTE;
        buffer[offset++] = (byte) intValue;

        _offset = offset;

        return;
      }
      else if (-0x8000 <= intValue && intValue < 0x8000) {
        buffer[offset + 0] = (byte) BC_DOUBLE_SHORT;
        buffer[offset + 1] = (byte) (intValue >> 8);
        buffer[offset + 2] = (byte) intValue;

        _offset = offset + 3;

        return;
      }
    }

    int mills = (int) (value * 1000);

    if (0.001 * mills == value) {
      buffer[offset + 0] = (byte) (BC_DOUBLE_MILL);
      buffer[offset + 1] = (byte) (mills >> 24);
      buffer[offset + 2] = (byte) (mills >> 16);
      buffer[offset + 3] = (byte) (mills >> 8);
      buffer[offset + 4] = (byte) (mills);

      _offset = offset + 5;

      return;
    }

    long bits = Double.doubleToLongBits(value);

    buffer[offset + 0] = (byte) 'D';
    buffer[offset + 1] = (byte) (bits >> 56);
    buffer[offset + 2] = (byte) (bits >> 48);
    buffer[offset + 3] = (byte) (bits >> 40);
    buffer[offset + 4] = (byte) (bits >> 32);
    buffer[offset + 5] = (byte) (bits >> 24);
    buffer[offset + 6] = (byte) (bits >> 16);
    buffer[offset + 7] = (byte) (bits >> 8);
    buffer[offset + 8] = (byte) (bits);

    _offset = offset + 9;
  }

  /**
   * Writes a date to the stream.
   *
   * <code><pre>
   * date ::= d   b7 b6 b5 b4 b3 b2 b1 b0
   *      ::= x65 b3 b2 b1 b0
   * </pre></code>
   *
   * @param time the date in milliseconds from the epoch in UTC
   */
  public void writeUTCDate(long time)
    throws IOException
  {
    if (SIZE < _offset + 32)
      flushBuffer();

    int offset = _offset;
    byte []buffer = _buffer;

    if (time % 60000L == 0) {
      // compact date ::= x65 b3 b2 b1 b0

      long minutes = time / 60000L;

      if ((minutes >> 31) == 0 || (minutes >> 31) == -1) {
        buffer[offset++] = (byte) BC_DATE_MINUTE;
        buffer[offset++] = ((byte) (minutes >> 24));
        buffer[offset++] = ((byte) (minutes >> 16));
        buffer[offset++] = ((byte) (minutes >> 8));
        buffer[offset++] = ((byte) (minutes >> 0));

        _offset = offset;
        return;
      }
    }

    buffer[offset++] = (byte) BC_DATE;
    buffer[offset++] = ((byte) (time >> 56));
    buffer[offset++] = ((byte) (time >> 48));
    buffer[offset++] = ((byte) (time >> 40));
    buffer[offset++] = ((byte) (time >> 32));
    buffer[offset++] = ((byte) (time >> 24));
    buffer[offset++] = ((byte) (time >> 16));
    buffer[offset++] = ((byte) (time >> 8));
    buffer[offset++] = ((byte) (time));

    _offset = offset;
  }

  /**
   * Writes a null value to the stream.
   * The null will be written with the following syntax
   *
   * <code><pre>
   * N
   * </pre></code>
   *
   * @param value the string value to write.
   */
  public void writeNull()
    throws IOException
  {
    int offset = _offset;
    byte []buffer = _buffer;

    if (SIZE <= offset + 16) {
      flushBuffer();
      offset = _offset;
    }

    buffer[offset++] = 'N';

    _offset = offset;
  }

  /**
   * Writes a string value to the stream using UTF-8 encoding.
   * The string will be written with the following syntax:
   *
   * <code><pre>
   * S b16 b8 string-value
   * </pre></code>
   *
   * If the value is null, it will be written as
   *
   * <code><pre>
   * N
   * </pre></code>
   *
   * @param value the string value to write.
   */
  public void writeString(String value)
    throws IOException
  {
    int offset = _offset;
    byte []buffer = _buffer;

    if (SIZE <= offset + 16) {
      flushBuffer();
      offset = _offset;
    }

    if (value == null) {
      buffer[offset++] = (byte) 'N';

      _offset = offset;
    }
    else {
      int length = value.length();
      int strOffset = 0;

      while (length > 0x8000) {
        int sublen = 0x8000;

        offset = _offset;

        if (SIZE <= offset + 16) {
          flushBuffer();
          offset = _offset;
        }

        // chunk can't end in high surrogate
        char tail = value.charAt(strOffset + sublen - 1);

        if (0xd800 <= tail && tail <= 0xdbff)
          sublen--;

        buffer[offset + 0] = (byte) BC_STRING_CHUNK;
        buffer[offset + 1] = (byte) (sublen >> 8);
        buffer[offset + 2] = (byte) (sublen);

        _offset = offset + 3;

        printString(value, strOffset, sublen);

        length -= sublen;
        strOffset += sublen;
      }

      offset = _offset;

      if (SIZE <= offset + 16) {
        flushBuffer();
        offset = _offset;
      }

      if (length <= STRING_DIRECT_MAX) {
        buffer[offset++] = (byte) (BC_STRING_DIRECT + length);
      }
      else if (length <= STRING_SHORT_MAX) {
        buffer[offset++] = (byte) (BC_STRING_SHORT + (length >> 8));
        buffer[offset++] = (byte) (length);
      }
      else {
        buffer[offset++] = (byte) ('S');
        buffer[offset++] = (byte) (length >> 8);
        buffer[offset++] = (byte) (length);
      }

      _offset = offset;

      printString(value, strOffset, length);
    }
  }

  /**
   * Writes a string value to the stream using UTF-8 encoding.
   * The string will be written with the following syntax:
   *
   * <code><pre>
   * S b16 b8 string-value
   * </pre></code>
   *
   * If the value is null, it will be written as
   *
   * <code><pre>
   * N
   * </pre></code>
   *
   * @param value the string value to write.
   */
  public void writeString(char []buffer, int offset, int length)
    throws IOException
  {
    if (buffer == null) {
      if (SIZE < _offset + 16)
        flushBuffer();

      _buffer[_offset++] = (byte) ('N');
    }
    else {
      while (length > 0x8000) {
        int sublen = 0x8000;

        if (SIZE < _offset + 16)
          flushBuffer();

        // chunk can't end in high surrogate
        char tail = buffer[offset + sublen - 1];

        if (0xd800 <= tail && tail <= 0xdbff)
          sublen--;

        _buffer[_offset++] = (byte) BC_STRING_CHUNK;
        _buffer[_offset++] = (byte) (sublen >> 8);
        _buffer[_offset++] = (byte) (sublen);

        printString(buffer, offset, sublen);

        length -= sublen;
        offset += sublen;
      }

      if (SIZE < _offset + 16)
        flushBuffer();

      if (length <= STRING_DIRECT_MAX) {
        _buffer[_offset++] = (byte) (BC_STRING_DIRECT + length);
      }
      else if (length <= STRING_SHORT_MAX) {
        _buffer[_offset++] = (byte) (BC_STRING_SHORT + (length >> 8));
        _buffer[_offset++] = (byte) length;
      }
      else {
        _buffer[_offset++] = (byte) ('S');
        _buffer[_offset++] = (byte) (length >> 8);
        _buffer[_offset++] = (byte) (length);
      }

      printString(buffer, offset, length);
    }
  }

  /**
   * Writes a byte array to the stream.
   * The array will be written with the following syntax:
   *
   * <code><pre>
   * B b16 b18 bytes
   * </pre></code>
   *
   * If the value is null, it will be written as
   *
   * <code><pre>
   * N
   * </pre></code>
   *
   * @param value the string value to write.
   */
  public void writeBytes(byte []buffer)
    throws IOException
  {
    if (buffer == null) {
      if (SIZE < _offset + 16)
        flushBuffer();

      _buffer[_offset++] = 'N';
    }
    else
      writeBytes(buffer, 0, buffer.length);
  }

  /**
   * Writes a byte array to the stream.
   * The array will be written with the following syntax:
   *
   * <code><pre>
   * B b16 b18 bytes
   * </pre></code>
   *
   * If the value is null, it will be written as
   *
   * <code><pre>
   * N
   * </pre></code>
   *
   * @param value the string value to write.
   */
  public void writeBytes(byte []buffer, int offset, int length)
    throws IOException
  {
    if (buffer == null) {
      if (SIZE < _offset + 16)
        flushBuffer();

      _buffer[_offset++] = (byte) 'N';
    }
    else {
      while (SIZE - _offset - 3 < length) {
        int sublen = SIZE - _offset - 3;

        if (sublen < 16) {
          flushBuffer();

          sublen = SIZE - _offset - 3;

          if (length < sublen)
            sublen = length;
        }

        _buffer[_offset++] = (byte) BC_BINARY_CHUNK;
        _buffer[_offset++] = (byte) (sublen >> 8);
        _buffer[_offset++] = (byte) sublen;

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

        length -= sublen;
        offset += sublen;

        flushBuffer();
      }

      if (SIZE < _offset + 16)
        flushBuffer();

      if (length <= BINARY_DIRECT_MAX) {
        _buffer[_offset++] = (byte) (BC_BINARY_DIRECT + length);
      }
      else if (length <= BINARY_SHORT_MAX) {
        _buffer[_offset++] = (byte) (BC_BINARY_SHORT + (length >> 8));
        _buffer[_offset++] = (byte) (length);
      }
      else {
        _buffer[_offset++] = (byte) 'B';
        _buffer[_offset++] = (byte) (length >> 8);
        _buffer[_offset++] = (byte) (length);
      }

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

      _offset += length;
    }
  }

  /**
   * Writes a byte buffer to the stream.
   *
   * <code><pre>
   * </pre></code>
   */
  public void writeByteBufferStart()
    throws IOException
  {
  }

  /**
   * Writes a byte buffer to the stream.
   *
   * <code><pre>
   * b b16 b18 bytes
   * </pre></code>
   */
  public void writeByteBufferPart(byte []buffer, int offset, int length)
    throws IOException
  {
    while (length > 0) {
      flushIfFull();

      int sublen = _buffer.length - _offset;

      if (length < sublen)
        sublen = length;

      _buffer[_offset++] = BC_BINARY_CHUNK;
      _buffer[_offset++] = (byte) (sublen >> 8);
      _buffer[_offset++] = (byte) sublen;

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

     _offset += sublen;
      length -= sublen;
      offset += sublen;
    }
  }

  /**
   * Writes a byte buffer to the stream.
   *
   * <code><pre>
   * b b16 b18 bytes
   * </pre></code>
   */
  public void writeByteBufferEnd(byte []buffer, int offset, int length)
    throws IOException
  {
    writeBytes(buffer, offset, length);
  }

  /**
   * Returns an output stream to write binary data.
   */
  public OutputStream getBytesOutputStream()
    throws IOException
  {
    return new BytesOutputStream();
  }

  /**
   * Writes a full output stream.
   */
  @Override
  public void writeByteStream(InputStream is)
    throws IOException
  {
    while (true) {
      int len = SIZE - _offset - 3;

      if (len < 16) {
        flushBuffer();
        len = SIZE - _offset - 3;
      }

      len = is.read(_buffer, _offset + 3, len);

      if (len <= 0) {
        _buffer[_offset++] = BC_BINARY_DIRECT;
        return;
      }

      _buffer[_offset + 0] = (byte) BC_BINARY_CHUNK;
      _buffer[_offset + 1] = (byte) (len >> 8);
      _buffer[_offset + 2] = (byte) (len);

      _offset += len + 3;
    }
  }

  /**
   * Writes a reference.
   *
   * <code><pre>
   * x51 &lt;int>
   * </pre></code>
   *
   * @param value the integer value to write.
   */
  @Override
  protected void writeRef(int value)
    throws IOException
  {
    if (SIZE < _offset + 16)
      flushBuffer();

    _buffer[_offset++] = (byte) BC_REF;

    writeInt(value);
  }

  /**
   * If the object has already been written, just write its ref.
   *
   * @return true if we're writing a ref.
   */
  @Override
  public boolean addRef(Object object)
    throws IOException
  {
    if (_isUnshared) {
      _refCount++;
      return false;
    }
    
    int newRef = _refCount;

    int ref = addRef(object, newRef, false);
    
    if (ref != newRef) {
      writeRef(ref);

      return true;
    }
    else {
      _refCount++;
      
      return false;
    }
  }
  
  @Override
  public int getRef(Object obj)
  {
    if (_isUnshared)
      return -1;
    
    return _refs.get(obj);
  }

  /**
   * Removes a reference.
   */
  @Override
  public boolean removeRef(Object obj)
    throws IOException
  {
    if (_isUnshared) {
      return false;
    }
    else if (_refs != null) {
      _refs.remove(obj);

      return true;
    }
    else
      return false;
  }

  /**
   * Replaces a reference from one object to another.
   */
  @Override
  public boolean replaceRef(Object oldRef, Object newRef)
    throws IOException
  {
    if (_isUnshared) {
      return false;
    }
    
    int value = _refs.get(oldRef);

    if (value >= 0) {
      addRef(newRef, value, true);
      
      _refs.remove(oldRef);
      
      return true;
    }
    else
      return false;
  }
  
  private int addRef(Object value, int newRef, boolean isReplace)
  {
    int prevRef = _refs.put(value, newRef, isReplace);
    
    return prevRef;
  }

  /**
   * Starts the streaming message
   *
   * <p>A streaming message starts with 'P'</p>
   *
   * <pre>
   * P x02 x00
   * </pre>
   */
  public void writeStreamingObject(Object obj)
    throws IOException
  {
    startPacket();

    writeObject(obj);

    endPacket();
  }

  /**
   * Starts a streaming packet
   *
   * <p>A streaming contains a set of chunks, ending with a zero chunk.
   * Each chunk is a length followed by data where the length is
   * encoded by (b1xxxxxxxx)* b0xxxxxxxx</p>
   */
  public void startPacket()
    throws IOException
  {
    if (_refs != null) {
      _refs.clear();
      _refCount = 0;
    }

    flushBuffer();

    _isPacket = true;
    _offset = 4;
    _buffer[0] = (byte) 0x05; // 0x05 = binary
    _buffer[1] = (byte) 0x55;
    _buffer[2] = (byte) 0x55;
    _buffer[3] = (byte) 0x55;
  }

  public void endPacket()
    throws IOException
  {
    int offset = _offset;

    OutputStream os = _os;

    if (os == null) {
      _offset = 0;
      return;
    }

    int len = offset - 4;

    if (len < 0x7e) {
      _buffer[2] = _buffer[0];
      _buffer[3] = (byte) (len);
    } else {
      _buffer[1] = (byte) (0x7e);
      _buffer[2] = (byte) (len >> 8);
      _buffer[3] = (byte) (len);
    }

    _isPacket = false;
    _offset = 0;

    if (os == null) {
    }
    else if (len < 0x7e) {
      os.write(_buffer, 2, offset - 2);
    }
    else {
      os.write(_buffer, 0, offset);
    }
  }

  /**
   * Prints a string to the stream, encoded as UTF-8 with preceeding length
   *
   * @param v the string to print.
   */
  public void printLenString(String v)
    throws IOException
  {
    if (SIZE < _offset + 16)
      flushBuffer();

    if (v == null) {
      _buffer[_offset++] = (byte) (0);
      _buffer[_offset++] = (byte) (0);
    }
    else {
      int len = v.length();
      _buffer[_offset++] = (byte) (len >> 8);
      _buffer[_offset++] = (byte) (len);

      printString(v, 0, len);
    }
  }

  /**
   * Prints a string to the stream, encoded as UTF-8
   *
   * @param v the string to print.
   */
  public void printString(String v)
    throws IOException
  {
    printString(v, 0, v.length());
  }

  /**
   * Prints a string to the stream, encoded as UTF-8
   *
   * @param v the string to print.
   */
  public void printString(String v, int strOffset, int length)
    throws IOException
  {
    int offset = _offset;
    byte []buffer = _buffer;

    for (int i = 0; i < length; i++) {
      if (SIZE <= offset + 16) {
        _offset = offset;
        flushBuffer();
        offset = _offset;
      }

      char ch = v.charAt(i + strOffset);

      if (ch < 0x80)
        buffer[offset++] = (byte) (ch);
      else if (ch < 0x800) {
        buffer[offset++] = (byte) (0xc0 + ((ch >> 6) & 0x1f));
        buffer[offset++] = (byte) (0x80 + (ch & 0x3f));
      }
      else {
        buffer[offset++] = (byte) (0xe0 + ((ch >> 12) & 0xf));
        buffer[offset++] = (byte) (0x80 + ((ch >> 6) & 0x3f));
        buffer[offset++] = (byte) (0x80 + (ch & 0x3f));
      }
    }

    _offset = offset;
  }

  /**
   * Prints a string to the stream, encoded as UTF-8
   *
   * @param v the string to print.
   */
  public void printString(char []v, int strOffset, int length)
    throws IOException
  {
    int offset = _offset;
    byte []buffer = _buffer;

    for (int i = 0; i < length; i++) {
      if (SIZE <= offset + 16) {
        _offset = offset;
        flushBuffer();
        offset = _offset;
      }

      char ch = v[i + strOffset];

      if (ch < 0x80)
        buffer[offset++] = (byte) (ch);
      else if (ch < 0x800) {
        buffer[offset++] = (byte) (0xc0 + ((ch >> 6) & 0x1f));
        buffer[offset++] = (byte) (0x80 + (ch & 0x3f));
      }
      else {
        buffer[offset++] = (byte) (0xe0 + ((ch >> 12) & 0xf));
        buffer[offset++] = (byte) (0x80 + ((ch >> 6) & 0x3f));
        buffer[offset++] = (byte) (0x80 + (ch & 0x3f));
      }
    }

    _offset = offset;
  }

  private final void flushIfFull()
    throws IOException
  {
    int offset = _offset;

    if (SIZE < offset + 32) {
      flushBuffer();
    }
  }

  public final void flush()
    throws IOException
  {
    flushBuffer();

    if (_os != null)
      _os.flush();
  }

  public final void flushBuffer()
    throws IOException
  {
    int offset = _offset;

    OutputStream os = _os;

    if (! _isPacket && offset > 0) {
      _offset = 0;
      if (os != null)
        os.write(_buffer, 0, offset);
    }
    else if (_isPacket && offset > 4) {
      int len = offset - 4;
      
      _buffer[0] |= (byte) 0x80;
      _buffer[1] = (byte) (0x7e);
      _buffer[2] = (byte) (len >> 8);
      _buffer[3] = (byte) (len);
      _offset = 4;

      if (os != null)
        os.write(_buffer, 0, offset);

      _buffer[0] = (byte) 0x00;
      _buffer[1] = (byte) 0x56;
      _buffer[2] = (byte) 0x56;
      _buffer[3] = (byte) 0x56;
    }
  }

  @Override
  public void close()
    throws IOException
  {
    // hessian/3a8c
    flush();

    OutputStream os = _os;
    _os = null;

    if (os != null) {
      if (_isCloseStreamOnClose)
        os.close();
    }
  }

  public void free()
  {
    reset();

    _os = null;
    _isCloseStreamOnClose = false;
  }

  /**
   * Resets the references for streaming.
   */
  @Override
  public void resetReferences()
  {
    if (_refs != null) {
      _refs.clear();
      _refCount = 0;
    }
  }

  /**
   * Resets all counters and references
   */
  public void reset()
  {
    if (_refs != null) {
      _refs.clear();
      _refCount = 0;
    }

    _classRefs.clear();
    _typeRefs = null;
    _offset = 0;
    _isPacket = false;
    _isUnshared = false;
  }

  class BytesOutputStream extends OutputStream {
    private int _startOffset;

    BytesOutputStream()
      throws IOException
    {
      if (SIZE < _offset + 16) {
        Hessian2Output.this.flushBuffer();
      }

      _startOffset = _offset;
      _offset += 3; // skip 'b' xNN xNN
    }

    @Override
    public void write(int ch)
      throws IOException
    {
      if (SIZE <= _offset) {
        int length = (_offset - _startOffset) - 3;

        _buffer[_startOffset] = (byte) BC_BINARY_CHUNK;
        _buffer[_startOffset + 1] = (byte) (length >> 8);
        _buffer[_startOffset + 2] = (byte) (length);

        Hessian2Output.this.flushBuffer();

        _startOffset = _offset;
        _offset += 3;
      }

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

    @Override
    public void write(byte []buffer, int offset, int length)
      throws IOException
    {
      while (length > 0) {
        int sublen = SIZE - _offset;

        if (length < sublen)
          sublen = length;

        if (sublen > 0) {
          System.arraycopy(buffer, offset, _buffer, _offset, sublen);
          _offset += sublen;
        }

        length -= sublen;
        offset += sublen;

        if (SIZE <= _offset) {
          int chunkLength = (_offset - _startOffset) - 3;

          _buffer[_startOffset] = (byte) BC_BINARY_CHUNK;
          _buffer[_startOffset + 1] = (byte) (chunkLength >> 8);
          _buffer[_startOffset + 2] = (byte) (chunkLength);

          Hessian2Output.this.flushBuffer();

          _startOffset = _offset;
          _offset += 3;
        }
      }
    }

    @Override
    public void close()
      throws IOException
    {
      int startOffset = _startOffset;
      _startOffset = -1;

      if (startOffset < 0)
        return;

      int length = (_offset - startOffset) - 3;

      _buffer[startOffset] = (byte) 'B';
      _buffer[startOffset + 1] = (byte) (length >> 8);
      _buffer[startOffset + 2] = (byte) (length);

      Hessian2Output.this.flushBuffer();
    }
  }
}
