/*
 * @(#)ByteBuffer.java
 *
 * Copyright (c) 2001-2002 JScape
 * 1147 S. 53rd Pl., Mesa, Arizona, 85206, U.S.A.
 * All rights reserved.
 *
 * This software is the confidential and proprietary information of
 * JScape. ("Confidential Information").  You shall not disclose such
 * Confidential Information and shall use it only in accordance with
 * the terms of the license agreement you entered into with JScape.
 */
package com.jscape.util;

import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.util.Arrays;


/**
 * Class for managing an array of bytes.
 */
public class ByteBuffer {

    private static final int DEFAULT_BUFFER_SIZE = 8096;

    private byte[] buffer;
    private int bytes;

    /**
     * Creates a new instance of ByteBuffer
     */
    public ByteBuffer() {
        this(DEFAULT_BUFFER_SIZE);
    }

    /**
     * Creates a new ByteBuffer object.
     *
     * @param size int initial size of buffer
     */
    public ByteBuffer(int size) {
        this.buffer = new byte[size];
        this.bytes = 0;
    }

    /**
     * Creates a new instance of ByteBuffer
     *
     * @param buffer the data to initialize this ByteBuffer with
     */
    public ByteBuffer(byte[] buffer) {
        this.buffer = buffer;
        this.bytes += buffer.length;
    }

    /**
     * Appends data to this ByteBuffer
     *
     * @param buf the <code>ByteBuffer</code> to appending from
     */
    public void append(ByteBuffer buf) {
        byte[] data = buf.toByteArray();
        append(data);
    }

    /**
     * Appends data to this <code>ByteBuffer</code>
     *
     * @param buf    a byte array
     * @param offset the beginning index of array
     * @param len    the total number of bytes to appending, starting from offset
     */
    public void append(byte[] buf, int offset, int len) {
        if (buf != null) {
            expand(bytes + len);

            for (int i = 0;
                 i < len;
                 i++) {
                buffer[bytes++] = buf[offset + i];
            }
        }
    }

    /**
     * Appends data to this <code>ByteBuffer</code>
     *
     * @param buf a <code>byte[]</code>
     */
    public void append(byte[] buf) {
        if (buf != null) {
            append(buf, 0, buf.length);
        }
    }

    /**
     * Appends data to this <code>ByteBuffer</code>
     *
     * @param b a single <code>byte</code>
     */
    public void append(byte b) {
        byte[] buf = new byte[1];
        buf[0] = b;
        append(buf, 0, buf.length);
    }

    /**
     * Appends data to this <code>ByteBuffer</code>
     *
     * @param data     the data to appending
     * @param encoding the character encoding to use
     */
    public void append(String data, Charset encoding) {
        byte[] d = null;

        if (data != null) {
            if (encoding != null) {
                d = data.getBytes(encoding);
            } else {
                d = data.getBytes(Charset.defaultCharset());
            }

            append(d);
        }
    }

    /**
     * Appends data to this <code>ByteBuffer</code>
     *
     * @param data the data to appending
     */
    public void append(char[] data) {
        for (int i = 0;
             i < data.length;
             ++i) {
            append(data[i]);
        }
    }

    /**
     * Appends a character to this ByteBuffer.
     *
     * @param c the character to appending
     */
    public void append(char c) {
        byte b = (byte) c;
        append(b);
    }

    /**
     * Expands size of buffer.
     *
     * @param size the new size of buffer
     */
    private void expand(int size) {
        if (size > buffer.length) {
            byte[] tmp = new byte[(int) (size * 1.1)];
            System.arraycopy(buffer, 0, tmp, 0, bytes);
            buffer = tmp;
        }
    }

    /**
     * Gets buffer as a byte array.
     *
     * @return a <code>byte[]</code>
     */
    public byte[] toByteArray() {
        byte[] data = new byte[bytes];
        System.arraycopy(buffer, 0, data, 0, bytes);

        return data;

        //return buffer;
    }

    /**
     * Gets buffer as a <code>String</code> using system default character
     * encoding.
     *
     * @return the buffer contents as a <code>String</code>
     */
    public String toString() {
        return new String(buffer, 0, bytes, Charset.defaultCharset());
    }

    /**
     * Gets buffer as a <code>String</code> using provided character encoding.
     *
     * @param encoding the character encoding to use
     *
     * @return the buffer encoded using specified encoding
     */
    public String toString(Charset encoding) {
        return new String(this.buffer, 0, this.bytes, encoding);
    }

    /**
     * Gets the length of this <code>ByteBuffer</code> in bytes.
     *
     * @return length in bytes
     */
    public int length() {
        return this.bytes;
    }

    /**
     * Gets a portion of this ByteBuffer data.
     *
     * @param start the beginning index
     * @param end   the end index
     *
     * @return the buffer portion
     */
    public ByteBuffer getBuffer(int start, int end) {
        int len = end - start;
        try {
            byte[] data = new byte[len];
            System.arraycopy(buffer, start, data, 0, len);
            return new ByteBuffer(data);
        } catch (Exception e) {
            return new ByteBuffer();
        }
    }

    /**
     * Gets a portion of this ByteBuffer data.
     *
     * @param start the beginning index
     *
     * @return the buffer portion
     */
    public ByteBuffer getBuffer(int start) {
        return getBuffer(start, bytes);
    }

    /**
     * Locates the index of the specified data.
     *
     * @param data  the byte to search for
     * @param begin the beginning index to start searching from
     *
     * @return the index of specified data or -1 if not found
     */
    public int indexOf(byte data, int begin) {
        byte[] b = new byte[1];
        b[0] = data;

        return indexOf(b, begin);
    }

    /**
     * Locates the index of the specified data.
     *
     * @param data the byte[] to search for
     *
     * @return the index of specified data or -1 if not found
     */
    public int indexOf(byte data) {
        return indexOf(data, 0);
    }

    /**
     * Locates the index of the specified data.
     *
     * @param data  the byte[] to search for
     * @param begin the beginning index to start searching from
     *
     * @return the index of specified data or -1 if not found
     */
    public synchronized int indexOf(byte[] data, int begin) {
        int index = -1;
        int matches = 0;

        // loop thru all valid bytes in buffer
        for (int i = begin;
             i < bytes;
             ++i) {
            byte b = buffer[i];

            if (data[matches] == b) {
                matches++;
            } else {
                // reset counter
                if (matches != 0) {
                    // reset matches
                    matches = 0;
                }

                // check against new
                if (data[matches] == b) {
                    matches++;
                }
            }

            // check if all matches found
            if (matches == data.length) {
                // all matches found
                index = i - data.length + 1;
                break;
            }
        }
        return index;
    }

    /**
     * Locates the index of the specified data.
     *
     * @param data the byte[] to search for
     *
     * @return the index of specified data or -1 if not found
     */
    public int indexOf(byte[] data) {
        return indexOf(data, 0);
    }

    /**
     * Locates the index of the specified data.
     *
     * @param str      the String to search for
     * @param encoding the character encoding to use when performing search
     * @param begin    begin the beginning index
     *
     * @return the index of specified data or -1 if not found
     *
     * @throws UnsupportedEncodingException if encoding is invalid
     */
    public int indexOf(String str, Charset encoding, int begin)
            throws UnsupportedEncodingException {
        byte[] data = str.getBytes(encoding);

        return indexOf(data, begin);
    }

    /**
     * Checks if data is contained in this buffer.
     *
     * @param data     the data to check for
     * @param matchAll if true then all data must be found in sequence, if false then only one byte of data
     *                 must be found.
     */
    public boolean contains(byte[] data, boolean matchAll) {
        boolean contains = false;

        if (matchAll) {
            contains = indexOf(data) != -1;
        } else {
            for (int i = 0;
                 i < data.length;
                 ++i) {
                if (indexOf(data[i]) != -1) {
                    contains = true;
                    break;
                }
            }
        }
        return contains;
    }

    /**
     * Checks if data is contained in this buffer.
     *
     * @param data the data to check for
     */
    public boolean contains(byte[] data) {
        return contains(data, false);
    }

    /**
     * Locates the index of the specified data.
     *
     * @param str      the String to search for
     * @param encoding the character encoding to use when performing search
     *
     * @return the index of the data found or -1 if not found
     *
     * @throws UnsupportedEncodingException if encoding is invalid
     */
    public int indexOf(String str, Charset encoding)
            throws UnsupportedEncodingException {
        return indexOf(str, encoding, 0);
    }

    /**
     * Checks if buffer starts with specified String
     *
     * @param str      the String to check for
     * @param encoding the character encoding to use
     *
     * @return <code>true</code> if buffer starts with specified String, <code>false</code> otherwise
     *
     * @throws UnsupportedEncodingException if encoding is invalid
     */
    public boolean startsWith(String str, Charset encoding)
            throws UnsupportedEncodingException {
        String s = toString(encoding);

        return s.startsWith(str);
    }

    /**
     * Checks if buffer ends with specified String
     *
     * @param str      the String to check for
     * @param encoding the character encoding to use
     *
     * @return <code>true</code> if data ends with specified String, <code>false</code> otherwise
     *
     * @throws UnsupportedEncodingException if encoding is invalid
     */
    public boolean endsWith(String str, Charset encoding)
            throws UnsupportedEncodingException {
        String s = toString(encoding);
        return s.endsWith(str);
    }

    /**
     * Checks if buffer equals specified <code>String</code>
     *
     * @param str      the <code>String</code> to check for
     * @param encoding the character encoding to use
     *
     * @return <code>true</code> if data equals specified String, <code>false</code> otherwise
     *
     * @throws UnsupportedEncodingException if encoding is invalid
     */
    public boolean equals(String str, Charset encoding)
            throws UnsupportedEncodingException {
        String s = toString(encoding);

        return s.equals(str);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }

        ByteBuffer that = (ByteBuffer) o;

        if (this.bytes != that.bytes) {
            return false;
        }
        if (!Arrays.equals(this.buffer, that.buffer)) {
            return false;
        }

        return true;
    }

    @Override
    public int hashCode() {
        int result = Arrays.hashCode(this.buffer);
        result = 31 * result + this.bytes;
        return result;
    }
}
