/*
 Copyright (c) 2002, 2013, Oracle and/or its affiliates. All rights reserved.
 

  The MySQL Connector/J is licensed under the terms of the GPLv2
  <http://www.gnu.org/licenses/old-licenses/gpl-2.0.html>, like most MySQL Connectors.
  There are special exceptions to the terms and conditions of the GPLv2 as it is applied to
  this software, see the FLOSS License Exception
  <http://www.mysql.com/about/legal/licensing/foss-exception.html>.

  This program 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; version 2
  of the License.

  This program 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.
  See the GNU General Public License for more details.

  You should have received a copy of the GNU General Public License along with this
  program; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth
  Floor, Boston, MA 02110-1301  USA



 */
package com.mysql.jdbc;

import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.sql.SQLException;

/**
 * Buffer contains code to read and write packets from/to the MySQL server.
 * 
 * @version $Id$
 * @author Mark Matthews
 */
public class Buffer {
	static final int MAX_BYTES_TO_DUMP = 512;

	static final int NO_LENGTH_LIMIT = -1;

	static final long NULL_LENGTH = -1;

	private int bufLength = 0;

	private byte[] byteBuffer;

	private int position = 0;

	protected boolean wasMultiPacket = false;

	public Buffer(byte[] buf) {
		this.byteBuffer = buf;
		setBufLength(buf.length);
	}

	Buffer(int size) {
		this.byteBuffer = new byte[size];
		setBufLength(this.byteBuffer.length);
		this.position = MysqlIO.HEADER_LENGTH;
	}

	final void clear() {
		this.position = MysqlIO.HEADER_LENGTH;
	}

	final void dump() {
		dump(getBufLength());
	}

	final String dump(int numBytes) {
		return StringUtils.dumpAsHex(getBytes(0,
				numBytes > getBufLength() ? getBufLength() : numBytes),
				numBytes > getBufLength() ? getBufLength() : numBytes);
	}

	final String dumpClampedBytes(int numBytes) {
		int numBytesToDump = numBytes < MAX_BYTES_TO_DUMP ? numBytes
				: MAX_BYTES_TO_DUMP;

		String dumped = StringUtils.dumpAsHex(getBytes(0,
				numBytesToDump > getBufLength() ? getBufLength()
						: numBytesToDump),
				numBytesToDump > getBufLength() ? getBufLength()
						: numBytesToDump);

		if (numBytesToDump < numBytes) {
			return dumped + " ....(packet exceeds max. dump length)";
		}

		return dumped;
	}

	final void dumpHeader() {
		for (int i = 0; i < MysqlIO.HEADER_LENGTH; i++) {
			String hexVal = Integer.toHexString(readByte(i) & 0xff);

			if (hexVal.length() == 1) {
				hexVal = "0" + hexVal; //$NON-NLS-1$
			}

			System.out.print(hexVal + " "); //$NON-NLS-1$
		}
	}

	final void dumpNBytes(int start, int nBytes) {
		StringBuffer asciiBuf = new StringBuffer();

		for (int i = start; (i < (start + nBytes)) && (i < getBufLength()); i++) {
			String hexVal = Integer.toHexString(readByte(i) & 0xff);

			if (hexVal.length() == 1) {
				hexVal = "0" + hexVal; //$NON-NLS-1$
			}

			System.out.print(hexVal + " "); //$NON-NLS-1$

			if ((readByte(i) > 32) && (readByte(i) < 127)) {
				asciiBuf.append((char) readByte(i));
			} else {
				asciiBuf.append("."); //$NON-NLS-1$
			}

			asciiBuf.append(" "); //$NON-NLS-1$
		}

		System.out.println("    " + asciiBuf.toString()); //$NON-NLS-1$
	}

	final void ensureCapacity(int additionalData) throws SQLException {
		if ((this.position + additionalData) > getBufLength()) {
			if ((this.position + additionalData) < this.byteBuffer.length) {
				// byteBuffer.length is != getBufLength() all of the time
				// due to re-using of packets (we don't shrink them)
				//
				// If we can, don't re-alloc, just set buffer length
				// to size of current buffer
				setBufLength(this.byteBuffer.length);
			} else {
				//
				// Otherwise, re-size, and pad so we can avoid
				// allocing again in the near future
				//
				int newLength = (int) (this.byteBuffer.length * 1.25);

				if (newLength < (this.byteBuffer.length + additionalData)) {
					newLength = this.byteBuffer.length
							+ (int) (additionalData * 1.25);
				}

				if (newLength < this.byteBuffer.length) {
					newLength = this.byteBuffer.length + additionalData;
				}

				byte[] newBytes = new byte[newLength];

				System.arraycopy(this.byteBuffer, 0, newBytes, 0,
						this.byteBuffer.length);
				this.byteBuffer = newBytes;
				setBufLength(this.byteBuffer.length);
			}
		}
	}

	/**
	 * Skip over a length-encoded string
	 * 
	 * @return The position past the end of the string
	 */
	public int fastSkipLenString() {
		long len = this.readFieldLength();

		this.position += len;

		return (int) len;
	}

	public void fastSkipLenByteArray() {
		long len = this.readFieldLength();

		if (len == NULL_LENGTH || len == 0) {
			return;
		}
		
		this.position += len;
	}
	
	protected final byte[] getBufferSource() {
		return this.byteBuffer;
	}

	public int getBufLength() {
		return this.bufLength;
	}

	/**
	 * Returns the array of bytes this Buffer is using to read from.
	 * 
	 * @return byte array being read from
	 */
	public byte[] getByteBuffer() {
		return this.byteBuffer;
	}

	final byte[] getBytes(int len) {
		byte[] b = new byte[len];
		System.arraycopy(this.byteBuffer, this.position, b, 0, len);
		this.position += len; // update cursor

		return b;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.mysql.jdbc.Buffer#getBytes(int, int)
	 */
	byte[] getBytes(int offset, int len) {
		byte[] dest = new byte[len];
		System.arraycopy(this.byteBuffer, offset, dest, 0, len);

		return dest;
	}

	int getCapacity() {
		return this.byteBuffer.length;
	}

	public ByteBuffer getNioBuffer() {
		throw new IllegalArgumentException(Messages
				.getString("ByteArrayBuffer.0")); //$NON-NLS-1$
	}

	/**
	 * Returns the current position to write to/ read from
	 * 
	 * @return the current position to write to/ read from
	 */
	public int getPosition() {
		return this.position;
	}

	// 2000-06-05 Changed
	final boolean isLastDataPacket() {
		return ((getBufLength() < 9) && ((this.byteBuffer[0] & 0xff) == 254));
	}

	final boolean isAuthMethodSwitchRequestPacket() {
		return ((this.byteBuffer[0] & 0xff) == 254);
	}

	final boolean isOKPacket() {
		return ((this.byteBuffer[0] & 0xff) == 0);
	}

	final boolean isRawPacket() {
		return ((this.byteBuffer[0] & 0xff) == 1);
	}

	final long newReadLength() {
		int sw = this.byteBuffer[this.position++] & 0xff;

		switch (sw) {
		case 251:
			return 0;

		case 252:
			return readInt();

		case 253:
			return readLongInt();

		case 254: // changed for 64 bit lengths
			return readLongLong();

		default:
			return sw;
		}
	}

	final byte readByte() {
		return this.byteBuffer[this.position++];
	}

	final byte readByte(int readAt) {
		return this.byteBuffer[readAt];
	}

	final long readFieldLength() {
		int sw = this.byteBuffer[this.position++] & 0xff;

		switch (sw) {
		case 251:
			return NULL_LENGTH;

		case 252:
			return readInt();

		case 253:
			return readLongInt();

		case 254:
			return readLongLong();

		default:
			return sw;
		}
	}

	// 2000-06-05 Changed
	final int readInt() {
		byte[] b = this.byteBuffer; // a little bit optimization

		return (b[this.position++] & 0xff) | ((b[this.position++] & 0xff) << 8);
	}

	final int readIntAsLong() {
		byte[] b = this.byteBuffer;

		return (b[this.position++] & 0xff) | ((b[this.position++] & 0xff) << 8)
				| ((b[this.position++] & 0xff) << 16)
				| ((b[this.position++] & 0xff) << 24);
	}

	final byte[] readLenByteArray(int offset) {
		long len = this.readFieldLength();

		if (len == NULL_LENGTH) {
			return null;
		}

		if (len == 0) {
			return Constants.EMPTY_BYTE_ARRAY;
		}

		this.position += offset;

		return getBytes((int) len);
	}

	final long readLength() {
		int sw = this.byteBuffer[this.position++] & 0xff;

		switch (sw) {
		case 251:
			return 0;

		case 252:
			return readInt();

		case 253:
			return readLongInt();

		case 254:
			return readLong();

		default:
			return sw;
		}
	}

	// 2000-06-05 Fixed
	final long readLong() {
		byte[] b = this.byteBuffer;

		return ((long) b[this.position++] & 0xff)
				| (((long) b[this.position++] & 0xff) << 8)
				| ((long) (b[this.position++] & 0xff) << 16)
				| ((long) (b[this.position++] & 0xff) << 24);
	}

	// 2000-06-05 Changed
	final int readLongInt() {
		byte[] b = this.byteBuffer;

		return (b[this.position++] & 0xff) | ((b[this.position++] & 0xff) << 8)
				| ((b[this.position++] & 0xff) << 16);
	}

	// 2000-06-05 Fixed
	final long readLongLong() {
		byte[] b = this.byteBuffer;

		return (b[this.position++] & 0xff)
				| ((long) (b[this.position++] & 0xff) << 8)
				| ((long) (b[this.position++] & 0xff) << 16)
				| ((long) (b[this.position++] & 0xff) << 24)
				| ((long) (b[this.position++] & 0xff) << 32)
				| ((long) (b[this.position++] & 0xff) << 40)
				| ((long) (b[this.position++] & 0xff) << 48)
				| ((long) (b[this.position++] & 0xff) << 56);
	}

	final int readnBytes() {
		int sw = this.byteBuffer[this.position++] & 0xff;

		switch (sw) {
		case 1:
			return this.byteBuffer[this.position++] & 0xff;

		case 2:
			return this.readInt();

		case 3:
			return this.readLongInt();

		case 4:
			return (int) this.readLong();

		default:
			return 255;
		}
	}

	//
	// Read a null-terminated string
	//
	// To avoid alloc'ing a new byte array, we
	// do this by hand, rather than calling getNullTerminatedBytes()
	//
	public final String readString() {
		int i = this.position;
		int len = 0;
		int maxLen = getBufLength();

		while ((i < maxLen) && (this.byteBuffer[i] != 0)) {
			len++;
			i++;
		}

		String s = StringUtils.toString(this.byteBuffer, this.position, len);
		this.position += (len + 1); // update cursor

		return s;
	}

	/**
	 * Read string[NUL]
	 * @param encoding
	 * @param exceptionInterceptor
	 * @return
	 * @throws SQLException
	 */
	final String readString(String encoding, ExceptionInterceptor exceptionInterceptor) throws SQLException {
		int i = this.position;
		int len = 0;
		int maxLen = getBufLength();

		while ((i < maxLen) && (this.byteBuffer[i] != 0)) {
			len++;
			i++;
		}

		try {
			return StringUtils.toString(this.byteBuffer, this.position, len, encoding);
		} catch (UnsupportedEncodingException uEE) {
			throw SQLError.createSQLException(Messages.getString("ByteArrayBuffer.1") //$NON-NLS-1$
					+ encoding + "'", SQLError.SQL_STATE_ILLEGAL_ARGUMENT, exceptionInterceptor); //$NON-NLS-1$
		} finally {
			this.position += (len + 1); // update cursor
		}
	}

	/**
	 * Read string[$len]
	 */
	final String readString(String encoding, ExceptionInterceptor exceptionInterceptor, int expectedLength) throws SQLException {
		if (this.position + expectedLength > getBufLength()) {
			throw SQLError.createSQLException(Messages.getString("ByteArrayBuffer.2"),
					SQLError.SQL_STATE_ILLEGAL_ARGUMENT, exceptionInterceptor);
		}

		try {
			return StringUtils.toString(this.byteBuffer, this.position, expectedLength, encoding);
		} catch (UnsupportedEncodingException uEE) {
			throw SQLError.createSQLException(Messages.getString("ByteArrayBuffer.1") //$NON-NLS-1$
					+ encoding + "'", SQLError.SQL_STATE_ILLEGAL_ARGUMENT, exceptionInterceptor); //$NON-NLS-1$
		} finally {
			this.position += expectedLength; // update cursor
		}
	}

	public void setBufLength(int bufLengthToSet) {
		this.bufLength = bufLengthToSet;
	}

	/**
	 * Sets the array of bytes to use as a buffer to read from.
	 * 
	 * @param byteBuffer
	 *            the array of bytes to use as a buffer
	 */
	public void setByteBuffer(byte[] byteBufferToSet) {
		this.byteBuffer = byteBufferToSet;
	}

	/**
	 * Set the current position to write to/ read from
	 * 
	 * @param position
	 *            the position (0-based index)
	 */
	public void setPosition(int positionToSet) {
		this.position = positionToSet;
	}

	/**
	 * Sets whether this packet was part of a multipacket
	 * 
	 * @param flag
	 *            was this packet part of a multipacket?
	 */
	public void setWasMultiPacket(boolean flag) {
		this.wasMultiPacket = flag;
	}

	public String toString() {
		return dumpClampedBytes(getPosition());
	}

	public String toSuperString() {
		return super.toString();
	}

	/**
	 * Was this packet part of a multipacket?
	 * 
	 * @return was this packet part of a multipacket?
	 */
	public boolean wasMultiPacket() {
		return this.wasMultiPacket;
	}

	public final void writeByte(byte b) throws SQLException {
		ensureCapacity(1);

		this.byteBuffer[this.position++] = b;
	}

	// Write a byte array
	public final void writeBytesNoNull(byte[] bytes) throws SQLException {
		int len = bytes.length;
		ensureCapacity(len);
		System.arraycopy(bytes, 0, this.byteBuffer, this.position, len);
		this.position += len;
	}

	// Write a byte array with the given offset and length
	final void writeBytesNoNull(byte[] bytes, int offset, int length)
			throws SQLException {
		ensureCapacity(length);
		System.arraycopy(bytes, offset, this.byteBuffer, this.position, length);
		this.position += length;
	}

	final void writeDouble(double d) throws SQLException {
		long l = Double.doubleToLongBits(d);
		writeLongLong(l);
	}

	final void writeFieldLength(long length) throws SQLException {
		if (length < 251) {
			writeByte((byte) length);
		} else if (length < 65536L) {
			ensureCapacity(3);
			writeByte((byte) 252);
			writeInt((int) length);
		} else if (length < 16777216L) {
			ensureCapacity(4);
			writeByte((byte) 253);
			writeLongInt((int) length);
		} else {
			ensureCapacity(9);
			writeByte((byte) 254);
			writeLongLong(length);
		}
	}

	final void writeFloat(float f) throws SQLException {
		ensureCapacity(4);

		int i = Float.floatToIntBits(f);
		byte[] b = this.byteBuffer;
		b[this.position++] = (byte) (i & 0xff);
		b[this.position++] = (byte) (i >>> 8);
		b[this.position++] = (byte) (i >>> 16);
		b[this.position++] = (byte) (i >>> 24);
	}

	// 2000-06-05 Changed
	final void writeInt(int i) throws SQLException {
		ensureCapacity(2);

		byte[] b = this.byteBuffer;
		b[this.position++] = (byte) (i & 0xff);
		b[this.position++] = (byte) (i >>> 8);
	}

	// Write a String using the specified character
	// encoding
	final void writeLenBytes(byte[] b) throws SQLException {
		int len = b.length;
		ensureCapacity(len + 9);
		writeFieldLength(len);
		System.arraycopy(b, 0, this.byteBuffer, this.position, len);
		this.position += len;
	}

	// Write a String using the specified character
	// encoding
	final void writeLenString(String s, String encoding, String serverEncoding,
			SingleByteCharsetConverter converter, boolean parserKnowsUnicode,
			MySQLConnection conn)
			throws UnsupportedEncodingException, SQLException {
		byte[] b = null;

		if (converter != null) {
			b = converter.toBytes(s);
		} else {
			b = StringUtils.getBytes(s, encoding, serverEncoding,
					parserKnowsUnicode, conn, conn.getExceptionInterceptor());
		}

		int len = b.length;
		ensureCapacity(len + 9);
		writeFieldLength(len);
		System.arraycopy(b, 0, this.byteBuffer, this.position, len);
		this.position += len;
	}

	// 2000-06-05 Changed
	final void writeLong(long i) throws SQLException {
		ensureCapacity(4);

		byte[] b = this.byteBuffer;
		b[this.position++] = (byte) (i & 0xff);
		b[this.position++] = (byte) (i >>> 8);
		b[this.position++] = (byte) (i >>> 16);
		b[this.position++] = (byte) (i >>> 24);
	}

	// 2000-06-05 Changed
	final void writeLongInt(int i) throws SQLException {
		ensureCapacity(3);
		byte[] b = this.byteBuffer;
		b[this.position++] = (byte) (i & 0xff);
		b[this.position++] = (byte) (i >>> 8);
		b[this.position++] = (byte) (i >>> 16);
	}

	final void writeLongLong(long i) throws SQLException {
		ensureCapacity(8);
		byte[] b = this.byteBuffer;
		b[this.position++] = (byte) (i & 0xff);
		b[this.position++] = (byte) (i >>> 8);
		b[this.position++] = (byte) (i >>> 16);
		b[this.position++] = (byte) (i >>> 24);
		b[this.position++] = (byte) (i >>> 32);
		b[this.position++] = (byte) (i >>> 40);
		b[this.position++] = (byte) (i >>> 48);
		b[this.position++] = (byte) (i >>> 56);
	}

	// Write null-terminated string
	final void writeString(String s) throws SQLException {
		ensureCapacity((s.length() * 3) + 1);
		writeStringNoNull(s);
		this.byteBuffer[this.position++] = 0;
	}
	
	//	 Write null-terminated string in the given encoding
	final void writeString(String s, String encoding, MySQLConnection conn) throws SQLException {
		ensureCapacity((s.length() * 3) + 1);
		try {
			writeStringNoNull(s, encoding, encoding, false, conn);
		} catch (UnsupportedEncodingException ue) {
			throw new SQLException(ue.toString(), SQLError.SQL_STATE_GENERAL_ERROR);
		}
		
		this.byteBuffer[this.position++] = 0;
	}

	// Write string, with no termination
	final void writeStringNoNull(String s) throws SQLException {
		int len = s.length();
		ensureCapacity(len * 3);
		System.arraycopy(StringUtils.getBytes(s), 0, this.byteBuffer, this.position, len);
		this.position += len;

		// for (int i = 0; i < len; i++)
		// {
		// this.byteBuffer[this.position++] = (byte)s.charAt(i);
		// }
	}

	// Write a String using the specified character
	// encoding
	final void writeStringNoNull(String s, String encoding,
			String serverEncoding, boolean parserKnowsUnicode, MySQLConnection conn)
			throws UnsupportedEncodingException, SQLException {
		byte[] b = StringUtils.getBytes(s, encoding, serverEncoding,
				parserKnowsUnicode, conn, conn.getExceptionInterceptor());

		int len = b.length;
		ensureCapacity(len);
		System.arraycopy(b, 0, this.byteBuffer, this.position, len);
		this.position += len;
	}
}
