/*
 * Copyright (c) 1998-2008 Caucho Technology -- all rights reserved
 *
 * This file is part of Resin(R) Open Source
 *
 * Each copy or derived work must preserve the copyright notice and this
 * notice unmodified.
 *
 * Resin Open Source is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * Resin Open Source is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, or any warranty
 * of NON-INFRINGEMENT.  See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Resin Open Source; if not, write to the
 *   Free SoftwareFoundation, Inc.
 *   59 Temple Place, Suite 330
 *   Boston, MA 02111-1307  USA
 *
 * @author Scott Ferguson
 */

package com.caucho.util;

import com.caucho.vfs.ReadStream;
import com.caucho.vfs.VfsStream;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;

/**
 * A variable-length byte buffer, similar to a character buffer.
 *
 * <p>The byte buffer is unsynchronized.
 */
public final class ByteBuffer {
  private byte []_buffer;
  private int _capacity;
  private int _length;

  public ByteBuffer(int minimumCapacity)
  {
    _capacity = 32;
    if (minimumCapacity > 0x1000) {
      _capacity = (minimumCapacity + 0xfff) & ~0xfff;
    } else {
      while (_capacity < minimumCapacity) {
	_capacity += _capacity;
      }
    }

    _buffer = new byte[minimumCapacity];
    _length = 0;
  }

  public ByteBuffer()
  {
    _buffer = new byte[32];
    _capacity = _buffer.length;
    _length = 0;
  }

  /**
   * Returns the actual capacity of the buffer, i.e. how many bytes it
   * can hold.
   */
  public int capacity()
  {
    return _capacity;
  }

  public int hashCode()
  {
    int hash = 17;
    for (int i = _length - 1; i >= 0; i--) {
      hash = 65537 * hash + _buffer[i];
    }

    return hash;
  }

  /**
   * Ensure the buffer can hold at least 'minimumCapacity' bytes.
   */
  public void ensureCapacity(int minimumCapacity)
  {
    if (minimumCapacity <= _capacity)
      return;

    if (minimumCapacity > 0x1000) {
      _capacity = (minimumCapacity + 0xfff) & ~0xfff;
    } else {
      while (_capacity < minimumCapacity) {
	_capacity += _capacity;
      }
    }

    byte []bytes = new byte[_capacity];

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

    _buffer = bytes;
  }

  /**
   * Returns the buffer length
   */
  public int length()
  {
    return _length;
  }

  public int size()
  {
    return _length;
  }
  /**
   * Returns the buffer length
   */
  public int getLength()
  {
    return _length;
  }

  /**
   * Set the buffer.
   */
  public void setLength(int len)
  {
    if (len < 0)
      throw new RuntimeException("illegal argument");
    else if (len > _capacity)
      ensureCapacity(len);

    _length = len;
  }

  public void clear()
  {
    _length = 0;
  }
  
  /**
   * Returns the byte array for the buffer.
   */
  public byte []getBuffer() { return _buffer; }

  /**
   * Add a byte to the buffer.
   */
  public void append(int b)
  {
    if (_length + 1 > _capacity)
      ensureCapacity(_length + 1);

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

  /**
   * Inserts a byte array
   */
  public void add(int i, byte []buffer, int offset, int length)
  {
    if (_length + length > _capacity)
      ensureCapacity(_length + length);

    System.arraycopy(_buffer, i, _buffer, i + length, _length - i);
    System.arraycopy(buffer, offset, _buffer, i, length);

    _length += length;
  }

  public void add(byte []buffer, int offset, int length)
  {
    if (_capacity < _length + length)
      ensureCapacity(_length + length);

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

    _length += length;
  }

  /**
   * Inserts a byte array
   */
  public void add(int i, int data)
  {
    if (_length + 1 > _capacity)
      ensureCapacity(_length + 1);

    System.arraycopy(_buffer, i, _buffer, i + 1, _length - i);
    _buffer[i] = (byte) data;

    _length += 1;
  }

  public void add(int data)
  {
    if (_capacity < _length + 1)
      ensureCapacity(_length + 1);

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

  public void set(int i, byte []buffer, int offset, int length)
  {
    System.arraycopy(buffer, offset, _buffer, i, length);
  }

  public void set(int i, int data)
  {
    _buffer[i] = (byte) data;
  }

  public void insert(int i, byte []buffer, int offset, int length)
  {
    if (_length + length > _capacity)
      ensureCapacity(_length + length);

    System.arraycopy(_buffer, i, _buffer, i + length, _length - i);
    System.arraycopy(_buffer, offset, _buffer, i, length);

    _length += length;
  }

  /**
   * Inserts a byte array
   */
  public void replace(int i, byte []buffer, int offset, int length)
  {
    System.arraycopy(buffer, offset, _buffer, i, length);
  }

  /**
   * Inserts a byte array
   */
  public void append(byte []buffer, int offset, int length)
  {
    if (_length + length >= _capacity)
      ensureCapacity(_length + length);

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

    _length += length;
  }

  public void addByte(int v)
  {
    add(v);
  }

  /**
   * Inserts a short into the buffer
   */
  public void replaceShort(int i, int s)
  {
    _buffer[i]     = (byte) (s >> 8);
    _buffer[i + 1] = (byte) (s);
  }

  /**
   * Appends a short (little endian) in the buffer
   */
  public void appendShort(int s)
  {
    if (_length + 2 > _capacity)
      ensureCapacity(_length + 2);

    replaceShort(_length, s);

    _length += 2;
  }

  public void addShort(int s)
  {
    if (_length + 2 > _capacity)
      ensureCapacity(_length + 2);

    _buffer[_length++] = (byte) (s >> 8);
    _buffer[_length++] = (byte) s;
  }

  public void addShort(int i, int s)
  {
    add(i, (byte) (s >> 8));
    add(i + 1, (byte) (s));
  }

  public void setShort(int i, int s)
  {
    _buffer[i]     = (byte) (s >> 8);
    _buffer[i + 1] = (byte) (s);
  }

  /**
   * Inserts a int (little endian) into the buffer
   */
  public void replaceInt(int i, int v)
  {
    _buffer[i]     = (byte) (v >> 24);
    _buffer[i + 1] = (byte) (v >> 16);
    _buffer[i + 2] = (byte) (v >> 8);
    _buffer[i + 3] = (byte) (v);
  }

  /**
   * Appends an int (little endian) in the buffer
   */
  public void appendInt(int s)
  {
    if (_length + 4 > _capacity)
      ensureCapacity(_length + 4);

    _buffer[_length++] = (byte) (s >> 24);
    _buffer[_length++] = (byte) (s >> 16);
    _buffer[_length++] = (byte) (s >> 8);
    _buffer[_length++] = (byte) s;
  }

  public void addInt(int s)
  {
    if (_capacity < _length + 4)
      ensureCapacity(_length + 4);

    _buffer[_length++] = (byte) (s >> 24);
    _buffer[_length++] = (byte) (s >> 16);
    _buffer[_length++] = (byte) (s >> 8);
    _buffer[_length++] = (byte) s;
  }

  public void addInt(int i, int s)
  {
    add(i + 0, (byte) (s >> 24));
    add(i + 1, (byte) (s >> 16));
    add(i + 2, (byte) (s >> 8));
    add(i + 3, (byte) (s));
  }

  public void setInt(int i, int v)
  {
    _buffer[i]     = (byte) (v >> 24);
    _buffer[i + 1] = (byte) (v >> 16);
    _buffer[i + 2] = (byte) (v >> 8);
    _buffer[i + 3] = (byte) (v);
  }

  public void addLong(long v)
  {
    if (_length + 8 > _capacity)
      ensureCapacity(_length + 8);

    _buffer[_length++] = (byte) (v >> 56L);
    _buffer[_length++] = (byte) (v >> 48L);
    _buffer[_length++] = (byte) (v >> 40L);
    _buffer[_length++] = (byte) (v >> 32L);
    
    _buffer[_length++] = (byte) (v >> 24L);
    _buffer[_length++] = (byte) (v >> 16L);
    _buffer[_length++] = (byte) (v >> 8L);
    _buffer[_length++] = (byte) v;
  }

  public void addFloat(float v)
  {
    if (_length + 4 > _capacity)
      ensureCapacity(_length + 4);

    int bits = Float.floatToIntBits(v);

    _buffer[_length++] = (byte) (bits >> 24);
    _buffer[_length++] = (byte) (bits >> 16);
    _buffer[_length++] = (byte) (bits >> 8);
    _buffer[_length++] = (byte) bits;
  }

  public void addDouble(double v)
  {
    if (_length + 8 > _capacity)
      ensureCapacity(_length + 8);

    long bits = Double.doubleToLongBits(v);

    _buffer[_length++] = (byte) (bits >> 56);
    _buffer[_length++] = (byte) (bits >> 48);
    _buffer[_length++] = (byte) (bits >> 40);
    _buffer[_length++] = (byte) (bits >> 32);
    _buffer[_length++] = (byte) (bits >> 24);
    _buffer[_length++] = (byte) (bits >> 16);
    _buffer[_length++] = (byte) (bits >> 8);
    _buffer[_length++] = (byte) bits;
  }

  public void addString(String s)
  {
    int len = s.length();
    if (len + _length > _capacity)
      ensureCapacity(_length + len);

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

  /**
   * Adds a string with a specified encoding.
   */
  public void addString(String s, String encoding)
  {
    if (encoding == null || encoding.equals("ISO-8859-1")) {
      addString(s);
      return;
    }

    // XXX: special case for utf-8?

    byte []bytes = null;

    try {
      bytes = s.getBytes(encoding);
    } catch (UnsupportedEncodingException e) {
      addString(s);
      return;
    }
    
    int len = bytes.length;
    if (len + _length > _capacity)
      ensureCapacity(_length + len);

    for (int i = 0; i < len; i++)
      _buffer[_length++] = bytes[i];
  }

  public void add(String s)
  {
    int len = s.length();
    if (len + _length > _capacity)
      ensureCapacity(_length + len);

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

  public void add(char []s, int offset, int len)
  {
    if (len + _length > _capacity)
      ensureCapacity(_length + len);

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

  public void add(CharBuffer cb)
  {
    int len = cb.length();
    
    if (len + _length > _capacity)
      ensureCapacity(_length + len);

    char []s = cb.getBuffer();

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

  public void remove(int begin, int length)
  {
    System.arraycopy(_buffer, begin + length, _buffer, begin,
		     _capacity - length - begin);

    _length -= length;
  }

  /**
   * Appends an int (little endian) in the buffer
   */
  public void append(String string)
  {
    for (int i = 0; i < string.length(); i++)
      append(string.charAt(i));
  }

  /**
   * Returns the byte at the specified offset.
   */
  public byte byteAt(int i)
  {
    if (i < 0 || i > _length)
      throw new RuntimeException();

    return _buffer[i];
  }
  /**
   * Returns the byte at the specified offset.
   */
  public void setByteAt(int i, int b)
  {
    _buffer[i] = (byte) b;
  }

  public byte get(int i)
  {
    if (i < 0 || i >= _length)
      throw new RuntimeException("out of bounds: " + i + " len: " + _length);

    return _buffer[i];
  }

  public short getShort(int i)
  {
    if (i < 0 || i + 1 >= _length)
      throw new RuntimeException("out of bounds: " + i + " len: " + _length);

    return (short) (((_buffer[i] & 0xff) << 8) + 
		    (_buffer[i + 1] & 0xff));
  }

  public int getInt(int i)
  {
    if (i < 0 || i + 3 >= _length)
      throw new RuntimeException("out of bounds: " + i + " len: " + _length);

    return (((_buffer[i + 0] & 0xff) << 24) +
	    ((_buffer[i + 1] & 0xff) << 16) +
	    ((_buffer[i + 2] & 0xff) << 8) +
	    ((_buffer[i + 3] & 0xff)));
  }

  public void print(int i)
  {
    if (_length + 16 >= _capacity)
      ensureCapacity(_length + 16);

    if (i < 0) {
      _buffer[_length++] = (byte) '-';
      i = -i;
    } else if (i == 0) {
      _buffer[_length++] = (byte) '0';
      return;
    }

    int start = _length;
    while (i > 0) {
      _buffer[_length++] = (byte) ((i % 10) + '0');
      i /= 10;
    }

    for (int j = (_length - start) / 2; j > 0; j--) {
      byte temp = _buffer[_length - j];
      _buffer[_length - j] = _buffer[start + j - 1];
      _buffer[start + j - 1] = temp;
    }
  }

  public int indexOf(byte []buffer, int offset, int length)
  {
    if (length <= 0)
      return -1;
    
    int end = _length - length;
    int first = buffer[offset];

    byte []testBuffer = _buffer;

    for (int i = 0; i <= end; i++) {
      if (testBuffer[i] != first)
	continue;

      int j = length - 1;
      for (; j > 0; j--) {
	if (testBuffer[i + j] != buffer[offset + j])
	  break;
      }

      if (j == 0)
	return i;
    }

    return -1;
  }

  /**
   * Clones the buffer
   */
  public Object clone()
  {
    ByteBuffer newBuffer = new ByteBuffer(_length);

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

    return newBuffer;
  }

  public boolean equals(Object b)
  {
    if (! (b instanceof ByteBuffer))
      return false;

    ByteBuffer bb = (ByteBuffer) b;
    if (bb._length != _length)
      return false;

    for (int i = _length - 1; i >= 0; i--)
      if (bb._buffer[i] != _buffer[i])
	return false;

    return true;
  }

  public InputStream createInputStream()
  {
    return new BBInputStream(this);
  }

  public OutputStream createOutputStream()
  {
    return new BBOutputStream(this);
  }
  
  public ReadStream createReadStream()
  {
    return VfsStream.openRead(new BBInputStream(this));
  }

  /**
   * Returns the bytes
   */
  public byte []getByteArray()
  {
    byte []bytes = new byte[_length];

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

    return bytes;
  }
  

  /**
   * String representation of the buffer.
   */
  public String toString()
  {
    return new String(_buffer, 0, _length);
  }

  public String toString(String encoding)
  {
    try {
      return new String(_buffer, 0, _length, encoding);
    } catch (Exception e) {
      return new String(_buffer, 0, _length);
    }
  }

  static class BBInputStream extends InputStream {
    ByteBuffer _buf;
    int _index;

    public int available()
    {
      return _buf._length - _index;
    }
    
    public int read() throws IOException
    {
      if (_index >= _buf._length)
	return -1;
      else
	return _buf._buffer[_index++] & 0xff;
    }

    BBInputStream(ByteBuffer buf)
    {
      _buf = buf;
    }
  }

  static class BBOutputStream extends OutputStream {
    ByteBuffer _buf;

    public void write(int ch) throws IOException
    {
      _buf.append(ch);
    }

    BBOutputStream(ByteBuffer buf)
    {
      _buf = buf;
    }
  }
}


