/*
 * Copyright (c) 2001-2004 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 "Hessian", "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.hessian4.io;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;

/**
 * Input stream for Hessian requests.
 *
 * <p>HessianInput is unbuffered, so any client needs to provide
 * its own buffering.
 *
 * <pre>
 * InputStream is = ...; // from http connection
 * HessianInput in = new HessianInput(is);
 * String value;
 *
 * in.startReply();         // read reply header
 * value = in.readString(); // read string value
 * in.completeReply();      // read reply footer
 * </pre>
 */
public class HessianInput extends AbstractHessianInput {
  private static int END_OF_DATA = -2;

  private static Field _detailMessageField;
  
  // factory for deserializing objects in the input stream
  protected SerializerFactory _serializerFactory;
  
  protected ArrayList _refs;
  
  // the underlying input stream
  private InputStream _is;
  // a peek character
  protected int _peek = -1;
  
  // the method for a call
  private String _method;

  private Reader _chunkReader;
  private InputStream _chunkInputStream;

  private Throwable _replyFault;

  private StringBuffer _sbuf = new StringBuffer();
  
  // true if this is the last chunk
  private boolean _isLastChunk;
  // the chunk length
  private int _chunkLength;

  /**
   * Creates an uninitialized Hessian input stream.
   */
  public HessianInput()
  {
  }
  
  /**
   * Creates a new Hessian input stream, initialized with an
   * underlying input stream.
   *
   * @param is the underlying input stream.
   */
  public HessianInput(InputStream is)
  {
    init(is);
  }

  /**
   * Sets the serializer factory.
   */
  public void setSerializerFactory(SerializerFactory factory)
  {
    _serializerFactory = factory;
  }

  /**
   * Gets the serializer factory.
   */
  public SerializerFactory getSerializerFactory()
  {
    return _serializerFactory;
  }

  /**
   * Initialize the hessian stream with the underlying input stream.
   */
  public void init(InputStream is)
  {
    _is = is;
    _method = null;
    _isLastChunk = true;
    _chunkLength = 0;
    _peek = -1;
    _refs = null;
    _replyFault = null;

    if (_serializerFactory == null)
      _serializerFactory = new SerializerFactory();
  }

  /**
   * Returns the calls method
   */
  public String getMethod()
  {
    return _method;
  }

  /**
   * Returns any reply fault.
   */
  public Throwable getReplyFault()
  {
    return _replyFault;
  }

  /**
   * Starts reading the call
   *
   * <pre>
   * c major minor
   * </pre>
   */
  public int readCall()
    throws IOException
  {
    int tag = read();
    
    if (tag != 'c')
      throw error("expected hessian call ('c') at " + codeName(tag));

    int major = read();
    int minor = read();

    return (major << 16) + minor;
  }

  /**
   * For backward compatibility with HessianSkeleton
   */
  public void skipOptionalCall()
    throws IOException
  {
    int tag = read();

    if (tag == 'c') {
      read();
      read();
    }
    else
      _peek = tag;
  }

  /**
   * Starts reading the call
   *
   * <p>A successful completion will have a single value:
   *
   * <pre>
   * m b16 b8 method
   * </pre>
   */
  public String readMethod()
    throws IOException
  {
    int tag = read();
    
    if (tag != 'm')
      throw error("expected hessian method ('m') at " + codeName(tag));
    int d1 = read();
    int d2 = read();

    _isLastChunk = true;
    _chunkLength = d1 * 256 + d2;
    _sbuf.setLength(0);
    int ch;
    while ((ch = parseChar()) >= 0)
      _sbuf.append((char) ch);
    
    _method = _sbuf.toString();

    return _method;
  }

  /**
   * Starts reading the call, including the headers.
   *
   * <p>The call expects the following protocol data
   *
   * <pre>
   * c major minor
   * m b16 b8 method
   * </pre>
   */
  public void startCall()
    throws IOException
  {
    readCall();

    while (readHeader() != null) {
      readObject();
    }

    readMethod();
  }

  /**
   * Completes reading the call
   *
   * <p>A successful completion will have a single value:
   *
   * <pre>
   * z
   * </pre>
   */
  public void completeCall()
    throws IOException
  {
    int tag = read();

    if (tag == 'z') {
    }
    else
      throw error("expected end of call ('z') at " + codeName(tag) + ".  Check method arguments and ensure method overloading is enabled if necessary");
  }

  /**
   * Reads a reply as an object.
   * If the reply has a fault, throws the exception.
   */
  public Object readReply(Class expectedClass)
    throws Throwable
  {
    int tag = read();
    
    if (tag != 'r')
      error("expected hessian reply at " + codeName(tag));

    int major = read();
    int minor = read();

    tag = read();
    if (tag == 'f')
      throw prepareFault();
    else {
      _peek = tag;
    
      Object value = readObject(expectedClass);

      completeValueReply();

      return value;
    }
  }

  /**
   * Starts reading the reply
   *
   * <p>A successful completion will have a single value:
   *
   * <pre>
   * r
   * </pre>
   */
  public void startReply()
    throws Throwable
  {
    int tag = read();
    
    if (tag != 'r')
      error("expected hessian reply at " + codeName(tag));

    int major = read();
    int minor = read();

    startReplyBody();
  }

  public void startReplyBody()
    throws Throwable
  {
    int tag = read();
    
    if (tag == 'f')
      throw prepareFault();
    else
      _peek = tag;
  }

  /**
   * Prepares the fault.
   */
  private Throwable prepareFault()
    throws IOException
  {
    HashMap fault = readFault();

    Object detail = fault.get("detail");
    String message = (String) fault.get("message");

    if (detail instanceof Throwable) {
      _replyFault = (Throwable) detail;
      
      if (message != null && _detailMessageField != null) {
        try {
          _detailMessageField.set(_replyFault, message);
        } catch (Throwable e) {
        }
      }

      return _replyFault;
    }

    else {
      String code = (String) fault.get("code");
        
      _replyFault = new HessianServiceException(message, code, detail);

      return _replyFault;
    }
  }

  /**
   * Completes reading the call
   *
   * <p>A successful completion will have a single value:
   *
   * <pre>
   * z
   * </pre>
   */
  public void completeReply()
    throws IOException
  {
    int tag = read();
    
    if (tag != 'z')
      error("expected end of reply at " + codeName(tag));
  }

  /**
   * Completes reading the call
   *
   * <p>A successful completion will have a single value:
   *
   * <pre>
   * z
   * </pre>
   */
  public void completeValueReply()
    throws IOException
  {
    int tag = read();
    
    if (tag != 'z')
      error("expected end of reply at " + codeName(tag));
  }

  /**
   * Reads a header, returning null if there are no headers.
   *
   * <pre>
   * H b16 b8 value
   * </pre>
   */
  public String readHeader()
    throws IOException
  {
    int tag = read();

    if (tag == 'H') {
      _isLastChunk = true;
      _chunkLength = (read() << 8) + read();

      _sbuf.setLength(0);
      int ch;
      while ((ch = parseChar()) >= 0)
        _sbuf.append((char) ch);

      return _sbuf.toString();
    }

    _peek = tag;

    return null;
  }

  /**
   * Reads a null
   *
   * <pre>
   * N
   * </pre>
   */
  public void readNull()
    throws IOException
  {
    int tag = read();

    switch (tag) {
    case 'N': return;
      
    default:
      throw expect("null", tag);
    }
  }

  /**
   * Reads a boolean
   *
   * <pre>
   * T
   * F
   * </pre>
   */
  public boolean readBoolean()
    throws IOException
  {
    int tag = read();

    switch (tag) {
    case 'T': return true;
    case 'F': return false;
    case 'I': return parseInt() == 0;
    case 'L': return parseLong() == 0;
    case 'D': return parseDouble() == 0.0;
    case 'N': return false;
      
    default:
      throw expect("boolean", tag);
    }
  }

  /**
   * Reads a byte
   *
   * <pre>
   * I b32 b24 b16 b8
   * </pre>
   */
  /*
  public byte readByte()
    throws IOException
  {
    return (byte) readInt();
  }
  */

  /**
   * Reads a short
   *
   * <pre>
   * I b32 b24 b16 b8
   * </pre>
   */
  public short readShort()
    throws IOException
  {
    return (short) readInt();
  }

  /**
   * Reads an integer
   *
   * <pre>
   * I b32 b24 b16 b8
   * </pre>
   */
  public int readInt()
    throws IOException
  {
    int tag = read();

    switch (tag) {
    case 'T': return 1;
    case 'F': return 0;
    case 'I': return parseInt();
    case 'L': return (int) parseLong();
    case 'D': return (int) parseDouble();
      
    default:
      throw expect("int", tag);
    }
  }

  /**
   * Reads a long
   *
   * <pre>
   * L b64 b56 b48 b40 b32 b24 b16 b8
   * </pre>
   */
  public long readLong()
    throws IOException
  {
    int tag = read();

    switch (tag) {
    case 'T': return 1;
    case 'F': return 0;
    case 'I': return parseInt();
    case 'L': return parseLong();
    case 'D': return (long) parseDouble();
      
    default:
      throw expect("long", tag);
    }
  }

  /**
   * Reads a float
   *
   * <pre>
   * D b64 b56 b48 b40 b32 b24 b16 b8
   * </pre>
   */
  public float readFloat()
    throws IOException
  {
    return (float) readDouble();
  }

  /**
   * Reads a double
   *
   * <pre>
   * D b64 b56 b48 b40 b32 b24 b16 b8
   * </pre>
   */
  public double readDouble()
    throws IOException
  {
    int tag = read();

    switch (tag) {
    case 'T': return 1;
    case 'F': return 0;
    case 'I': return parseInt();
    case 'L': return (double) parseLong();
    case 'D': return parseDouble();
      
    default:
      throw expect("long", tag);
    }
  }

  /**
   * Reads a date.
   *
   * <pre>
   * T b64 b56 b48 b40 b32 b24 b16 b8
   * </pre>
   */
  public long readUTCDate()
    throws IOException
  {
    int tag = read();

    if (tag != 'd')
      throw error("expected date at " + codeName(tag));

    long b64 = read();
    long b56 = read();
    long b48 = read();
    long b40 = read();
    long b32 = read();
    long b24 = read();
    long b16 = read();
    long b8 = read();

    return ((b64 << 56) +
            (b56 << 48) +
            (b48 << 40) +
            (b40 << 32) +
            (b32 << 24) +
            (b24 << 16) +
            (b16 << 8) +
            b8);
  }

  /**
   * Reads a byte from the stream.
   */
  public int readChar()
    throws IOException
  {
    if (_chunkLength > 0) {
      _chunkLength--;
      if (_chunkLength == 0 && _isLastChunk)
        _chunkLength = END_OF_DATA;

      int ch = parseUTF8Char();
      return ch;
    }
    else if (_chunkLength == END_OF_DATA) {
      _chunkLength = 0;
      return -1;
    }
    
    int tag = read();

    switch (tag) {
    case 'N':
      return -1;

    case 'S':
    case 's':
    case 'X':
    case 'x':
      _isLastChunk = tag == 'S' || tag == 'X';
      _chunkLength = (read() << 8) + read();

      _chunkLength--;
      int value = parseUTF8Char();

      // special code so successive read byte won't
      // be read as a single object.
      if (_chunkLength == 0 && _isLastChunk)
        _chunkLength = END_OF_DATA;

      return value;
      
    default:
      throw new IOException("expected 'S' at " + (char) tag);
    }
  }

  /**
   * Reads a byte array from the stream.
   */
  public int readString(char []buffer, int offset, int length)
    throws IOException
  {
    int readLength = 0;

    if (_chunkLength == END_OF_DATA) {
      _chunkLength = 0;
      return -1;
    }
    else if (_chunkLength == 0) {
      int tag = read();

      switch (tag) {
      case 'N':
        return -1;
      
      case 'S':
      case 's':
      case 'X':
      case 'x':
        _isLastChunk = tag == 'S' || tag == 'X';
        _chunkLength = (read() << 8) + read();
        break;

      default:
        throw new IOException("expected 'S' at " + (char) tag);
      }
    }

    while (length > 0) {
      if (_chunkLength > 0) {
        buffer[offset++] = (char) parseUTF8Char();
        _chunkLength--;
        length--;
        readLength++;
      }
      else if (_isLastChunk) {
        if (readLength == 0)
          return -1;
        else {
          _chunkLength = END_OF_DATA;
          return readLength;
        }
      }
      else {
        int tag = read();

        switch (tag) {
        case 'S':
        case 's':
        case 'X':
        case 'x':
          _isLastChunk = tag == 'S' || tag == 'X';
          _chunkLength = (read() << 8) + read();
          break;
      
        default:
          throw new IOException("expected 'S' at " + (char) tag);
        }
      }
    }
    
    if (readLength == 0)
      return -1;
    else if (_chunkLength > 0 || ! _isLastChunk)
      return readLength;
    else {
      _chunkLength = END_OF_DATA;
      return readLength;
    }
  }

  /**
   * Reads a string
   *
   * <pre>
   * S b16 b8 string value
   * </pre>
   */
  public String readString()
    throws IOException
  {
    int tag = read();

    switch (tag) {
    case 'N':
      return null;

    case 'I':
      return String.valueOf(parseInt());
    case 'L':
      return String.valueOf(parseLong());
    case 'D':
      return String.valueOf(parseDouble());

    case 'S':
    case 's':
    case 'X':
    case 'x':
      _isLastChunk = tag == 'S' || tag == 'X';
      _chunkLength = (read() << 8) + read();

      _sbuf.setLength(0);
      int ch;

      while ((ch = parseChar()) >= 0)
        _sbuf.append((char) ch);

      return _sbuf.toString();

    default:
      throw expect("string", tag);
    }
  }

  /**
   * Reads an XML node.
   *
   * <pre>
   * S b16 b8 string value
   * </pre>
   */
  public org.w3c.dom.Node readNode()
    throws IOException
  {
    int tag = read();

    switch (tag) {
    case 'N':
      return null;

    case 'S':
    case 's':
    case 'X':
    case 'x':
      _isLastChunk = tag == 'S' || tag == 'X';
      _chunkLength = (read() << 8) + read();

      throw error("Can't handle string in this context");

    default:
      throw expect("string", tag);
    }
  }

  /**
   * Reads a byte array
   *
   * <pre>
   * B b16 b8 data value
   * </pre>
   */
  public byte []readBytes()
    throws IOException
  {
    int tag = read();

    switch (tag) {
    case 'N':
      return null;

    case 'B':
    case 'b':
      _isLastChunk = tag == 'B';
      _chunkLength = (read() << 8) + read();

      ByteArrayOutputStream bos = new ByteArrayOutputStream();

      int data;
      while ((data = parseByte()) >= 0)
        bos.write(data);

      return bos.toByteArray();
      
    default:
      throw expect("bytes", tag);
    }
  }

  /**
   * Reads a byte from the stream.
   */
  public int readByte()
    throws IOException
  {
    if (_chunkLength > 0) {
      _chunkLength--;
      if (_chunkLength == 0 && _isLastChunk)
        _chunkLength = END_OF_DATA;

      return read();
    }
    else if (_chunkLength == END_OF_DATA) {
      _chunkLength = 0;
      return -1;
    }
    
    int tag = read();

    switch (tag) {
    case 'N':
      return -1;

    case 'B':
    case 'b':
      _isLastChunk = tag == 'B';
      _chunkLength = (read() << 8) + read();

      int value = parseByte();

      // special code so successive read byte won't
      // be read as a single object.
      if (_chunkLength == 0 && _isLastChunk)
        _chunkLength = END_OF_DATA;

      return value;
      
    default:
      throw new IOException("expected 'B' at " + (char) tag);
    }
  }

  /**
   * Reads a byte array from the stream.
   */
  public int readBytes(byte []buffer, int offset, int length)
    throws IOException
  {
    int readLength = 0;

    if (_chunkLength == END_OF_DATA) {
      _chunkLength = 0;
      return -1;
    }
    else if (_chunkLength == 0) {
      int tag = read();

      switch (tag) {
      case 'N':
        return -1;
      
      case 'B':
      case 'b':
        _isLastChunk = tag == 'B';
        _chunkLength = (read() << 8) + read();
        break;
      
      default:
        throw new IOException("expected 'B' at " + (char) tag);
      }
    }

    while (length > 0) {
      if (_chunkLength > 0) {
        buffer[offset++] = (byte) read();
        _chunkLength--;
        length--;
        readLength++;
      }
      else if (_isLastChunk) {
        if (readLength == 0)
          return -1;
        else {
          _chunkLength = END_OF_DATA;
          return readLength;
        }
      }
      else {
        int tag = read();

        switch (tag) {
        case 'B':
        case 'b':
          _isLastChunk = tag == 'B';
          _chunkLength = (read() << 8) + read();
          break;
      
        default:
          throw new IOException("expected 'B' at " + (char) tag);
        }
      }
    }
    
    if (readLength == 0)
      return -1;
    else if (_chunkLength > 0 || ! _isLastChunk)
      return readLength;
    else {
      _chunkLength = END_OF_DATA;
      return readLength;
    }
  }

  /**
   * Reads a fault.
   */
  private HashMap readFault()
    throws IOException
  {
    HashMap map = new HashMap();

    int code = read();
    for (; code > 0 && code != 'z'; code = read()) {
      _peek = code;
      
      Object key = readObject();
      Object value = readObject();

      if (key != null && value != null)
        map.put(key, value);
    }

    if (code != 'z')
      throw expect("fault", code);

    return map;
  }

  /**
   * Reads an object from the input stream with an expected type.
   */
  public Object readObject(Class cl)
    throws IOException
  {
    if (cl == null || cl == Object.class)
      return readObject();
    
    int tag = read();
    
    switch (tag) {
    case 'N':
      return null;

    case 'M':
    {
      String type = readType();

      // hessian/3386
      if ("".equals(type)) {
        Deserializer reader;
        reader = _serializerFactory.getDeserializer(cl);

        return reader.readMap(this);
      }
      else {
        Deserializer reader;
        reader = _serializerFactory.getObjectDeserializer(type);

        return reader.readMap(this);
      }
    }

    case 'V':
    {
      String type = readType();
      int length = readLength();
      
      Deserializer reader;
      reader = _serializerFactory.getObjectDeserializer(type);
      
      if (cl != reader.getType() && cl.isAssignableFrom(reader.getType()))
        return reader.readList(this, length);

      reader = _serializerFactory.getDeserializer(cl);

      Object v = reader.readList(this, length);

      return v;
    }

    case 'R':
    {
      int ref = parseInt();

      return _refs.get(ref);
    }

    case 'r':
    {
      String type = readType();
      String url = readString();

      return resolveRemote(type, url);
    }
    }

    _peek = tag;

    // hessian/332i vs hessian/3406
    //return readObject();
    
    Object value = _serializerFactory.getDeserializer(cl).readObject(this);

    return value;
  }
  
  /**
   * Reads an arbitrary object from the input stream when the type
   * is unknown.
   */
  public Object readObject()
    throws IOException
  {
    int tag = read();

    switch (tag) {
    case 'N':
      return null;
      
    case 'T':
      return Boolean.valueOf(true);
      
    case 'F':
      return Boolean.valueOf(false);
      
    case 'I':
      return Integer.valueOf(parseInt());
    
    case 'L':
      return Long.valueOf(parseLong());
    
    case 'D':
      return Double.valueOf(parseDouble());
    
    case 'd':
      return new Date(parseLong());
    
    case 'x':
    case 'X': {
      _isLastChunk = tag == 'X';
      _chunkLength = (read() << 8) + read();

      return parseXML();
    }

    case 's':
    case 'S': {
      _isLastChunk = tag == 'S';
      _chunkLength = (read() << 8) + read();

      int data;
      _sbuf.setLength(0);
      
      while ((data = parseChar()) >= 0)
        _sbuf.append((char) data);

      return _sbuf.toString();
    }

    case 'b':
    case 'B': {
      _isLastChunk = tag == 'B';
      _chunkLength = (read() << 8) + read();

      int data;
      ByteArrayOutputStream bos = new ByteArrayOutputStream();
      
      while ((data = parseByte()) >= 0)
        bos.write(data);

      return bos.toByteArray();
    }

    case 'V': {
      String type = readType();
      int length = readLength();

      return _serializerFactory.readList(this, length, type);
    }

    case 'M': {
      String type = readType();

      return _serializerFactory.readMap(this, type);
    }

    case 'R': {
      int ref = parseInt();

      return _refs.get(ref);
    }

    case 'r': {
      String type = readType();
      String url = readString();

      return resolveRemote(type, url);
    }

    default:
      throw error("unknown code for readObject at " + codeName(tag));
    }
  }

  /**
   * Reads a remote object.
   */
  public Object readRemote()
    throws IOException
  {
    String type = readType();
    String url = readString();

    return resolveRemote(type, url);
  }

  /**
   * Reads a reference.
   */
  public Object readRef()
    throws IOException
  {
    return _refs.get(parseInt());
  }

  /**
   * Reads the start of a list.
   */
  public int readListStart()
    throws IOException
  {
    return read();
  }

  /**
   * Reads the start of a list.
   */
  public int readMapStart()
    throws IOException
  {
    return read();
  }

  /**
   * Returns true if this is the end of a list or a map.
   */
  public boolean isEnd()
    throws IOException
  {
    int code = read();

    _peek = code;

    return (code < 0 || code == 'z');
  }

  /**
   * Reads the end byte.
   */
  public void readEnd()
    throws IOException
  {
    int code = read();

    if (code != 'z')
      throw error("unknown code at " + codeName(code));
  }

  /**
   * Reads the end byte.
   */
  public void readMapEnd()
    throws IOException
  {
    int code = read();

    if (code != 'z')
      throw error("expected end of map ('z') at " + codeName(code));
  }

  /**
   * Reads the end byte.
   */
  public void readListEnd()
    throws IOException
  {
    int code = read();

    if (code != 'z')
      throw error("expected end of list ('z') at " + codeName(code));
  }

  /**
   * Adds a list/map reference.
   */
  public int addRef(Object ref)
  {
    if (_refs == null)
      _refs = new ArrayList();
    
    _refs.add(ref);

    return _refs.size() - 1;
  }

  /**
   * Adds a list/map reference.
   */
  public void setRef(int i, Object ref)
  {
    _refs.set(i, ref);
  }

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

  /**
   * Resolves a remote object.
   */
  public Object resolveRemote(String type, String url)
    throws IOException
  {
    HessianRemoteResolver resolver = getRemoteResolver();

    if (resolver != null)
      return resolver.lookup(type, url);
    else
      return new HessianRemote(type, url);
  }

  /**
   * Parses a type from the stream.
   *
   * <pre>
   * t b16 b8
   * </pre>
   */
  public String readType()
    throws IOException
  {
    int code = read();

    if (code != 't') {
      _peek = code;
      return "";
    }

    _isLastChunk = true;
    _chunkLength = (read() << 8) + read();

    _sbuf.setLength(0);
    int ch;
    while ((ch = parseChar()) >= 0)
      _sbuf.append((char) ch);

    return _sbuf.toString();
  }

  /**
   * Parses the length for an array
   *
   * <pre>
   * l b32 b24 b16 b8
   * </pre>
   */
  public int readLength()
    throws IOException
  {
    int code = read();

    if (code != 'l') {
      _peek = code;
      return -1;
    }

    return parseInt();
  }

  /**
   * Parses a 32-bit integer value from the stream.
   *
   * <pre>
   * b32 b24 b16 b8
   * </pre>
   */
  private int parseInt()
    throws IOException
  {
    int b32 = read();
    int b24 = read();
    int b16 = read();
    int b8 = read();

    return (b32 << 24) + (b24 << 16) + (b16 << 8) + b8;
  }

  /**
   * Parses a 64-bit long value from the stream.
   *
   * <pre>
   * b64 b56 b48 b40 b32 b24 b16 b8
   * </pre>
   */
  private long parseLong()
    throws IOException
  {
    long b64 = read();
    long b56 = read();
    long b48 = read();
    long b40 = read();
    long b32 = read();
    long b24 = read();
    long b16 = read();
    long b8 = read();

    return ((b64 << 56) +
            (b56 << 48) +
            (b48 << 40) +
            (b40 << 32) +
            (b32 << 24) +
            (b24 << 16) +
            (b16 << 8) +
            b8);
  }
  
  /**
   * Parses a 64-bit double value from the stream.
   *
   * <pre>
   * b64 b56 b48 b40 b32 b24 b16 b8
   * </pre>
   */
  private double parseDouble()
    throws IOException
  {
    long b64 = read();
    long b56 = read();
    long b48 = read();
    long b40 = read();
    long b32 = read();
    long b24 = read();
    long b16 = read();
    long b8 = read();

    long bits = ((b64 << 56) +
                 (b56 << 48) +
                 (b48 << 40) +
                 (b40 << 32) +
                 (b32 << 24) +
                 (b24 << 16) +
                 (b16 << 8) +
                 b8);
  
    return Double.longBitsToDouble(bits);
  }

  org.w3c.dom.Node parseXML()
    throws IOException
  {
    throw new UnsupportedOperationException();
  }
  
  /**
   * Reads a character from the underlying stream.
   */
  private int parseChar()
    throws IOException
  {
    while (_chunkLength <= 0) {
      if (_isLastChunk)
        return -1;

      int code = read();

      switch (code) {
      case 's':
      case 'x':
        _isLastChunk = false;

        _chunkLength = (read() << 8) + read();
        break;
        
      case 'S':
      case 'X':
        _isLastChunk = true;

        _chunkLength = (read() << 8) + read();
        break;

      default:
        throw expect("string", code);
      }

    }

    _chunkLength--;

    return parseUTF8Char();
  }

  /**
   * Parses a single UTF8 character.
   */
  private int parseUTF8Char()
    throws IOException
  {
    int ch = read();

    if (ch < 0x80)
      return ch;
    else if ((ch & 0xe0) == 0xc0) {
      int ch1 = read();
      int v = ((ch & 0x1f) << 6) + (ch1 & 0x3f);

      return v;
    }
    else if ((ch & 0xf0) == 0xe0) {
      int ch1 = read();
      int ch2 = read();
      int v = ((ch & 0x0f) << 12) + ((ch1 & 0x3f) << 6) + (ch2 & 0x3f);

      return v;
    }
    else
      throw error("bad utf-8 encoding at " + codeName(ch));
  }
  
  /**
   * Reads a byte from the underlying stream.
   */
  private int parseByte()
    throws IOException
  {
    while (_chunkLength <= 0) {
      if (_isLastChunk) {
        return -1;
      }

      int code = read();

      switch (code) {
      case 'b':
        _isLastChunk = false;

        _chunkLength = (read() << 8) + read();
        break;
        
      case 'B':
        _isLastChunk = true;

        _chunkLength = (read() << 8) + read();
        break;

      default:
        throw expect("byte[]", code);
      }
    }

    _chunkLength--;

    return read();
  }

  /**
   * Reads bytes based on an input stream.
   */
  public InputStream readInputStream()
    throws IOException
  {
    int tag = read();

    switch (tag) {
    case 'N':
      return null;

    case 'B':
    case 'b':
      _isLastChunk = tag == 'B';
      _chunkLength = (read() << 8) + read();
      break;
      
    default:
      throw expect("inputStream", tag);
    }
    
    return new InputStream() {
        boolean _isClosed = false;

        public int read()
          throws IOException
        {
          if (_isClosed || _is == null)
            return -1;

          int ch = parseByte();
          if (ch < 0)
            _isClosed = true;

          return ch;
        }

        public int read(byte []buffer, int offset, int length)
          throws IOException
        {
          if (_isClosed || _is == null)
            return -1;

          int len = HessianInput.this.read(buffer, offset, length);
          if (len < 0)
            _isClosed = true;

          return len;
        }

        public void close()
          throws IOException
        {
          while (read() >= 0) {
          }

          _isClosed = true;
        }
      };
  }
  
  /**
   * Reads bytes from the underlying stream.
   */
  int read(byte []buffer, int offset, int length)
    throws IOException
  {
    int readLength = 0;
    
    while (length > 0) {
      while (_chunkLength <= 0) {
        if (_isLastChunk)
          return readLength == 0 ? -1 : readLength;

        int code = read();

        switch (code) {
        case 'b':
          _isLastChunk = false;

          _chunkLength = (read() << 8) + read();
          break;
        
        case 'B':
          _isLastChunk = true;

          _chunkLength = (read() << 8) + read();
          break;

        default:
          throw expect("byte[]", code);
        }
      }

      int sublen = _chunkLength;
      if (length < sublen)
        sublen = length;

      sublen = _is.read(buffer, offset, sublen);
      offset += sublen;
      readLength += sublen;
      length -= sublen;
      _chunkLength -= sublen;
    }

    return readLength;
  }

  final int read()
    throws IOException
  {
    if (_peek >= 0) {
      int value = _peek;
      _peek = -1;
      return value;
    }

    int ch = _is.read();
      
    return ch;
  }

  public void close()
  {
    _is = null;
  }

  public Reader getReader()
  {
    return null;
  }

  protected IOException expect(String expect, int ch)
  {
    return error("expected " + expect + " at " + codeName(ch));
  }

  protected String codeName(int ch)
  {
    if (ch < 0)
      return "end of file";
    else
      return "0x" + Integer.toHexString(ch & 0xff) + " (" + (char) + ch + ")";
  }
  
  protected IOException error(String message)
  {
    if (_method != null)
      return new HessianProtocolException(_method + ": " + message);
    else
      return new HessianProtocolException(message);
  }

  static {
    try {
      _detailMessageField = Throwable.class.getDeclaredField("detailMessage");
      _detailMessageField.setAccessible(true);
    } catch (Throwable e) {
    }
  }
}
