/*
 *
 * MariaDB Client for Java
 *
 * Copyright (c) 2012-2014 Monty Program Ab.
 * Copyright (c) 2015-2017 MariaDB Ab.
 *
 * This library is free software; you can redistribute it and/or modify it under
 * the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation; either version 2.1 of the License, or (at your option)
 * any later version.
 *
 * This library 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 Lesser General Public License
 * for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License along
 * with this library; if not, write to Monty Program Ab info@montyprogram.com.
 *
 * This particular MariaDB Client for Java file is work
 * derived from a Drizzle-JDBC. Drizzle-JDBC file which is covered by subject to
 * the following copyright and notice provisions:
 *
 * Copyright (c) 2009-2011, Marcus Eriksson
 *
 * Redistribution and use in source and binary forms, with or without modification,
 * are permitted provided that the following conditions are met:
 * Redistributions of source code must retain the above copyright notice, this list
 * of conditions and the following disclaimer.
 *
 * 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.
 *
 * Neither the name of the driver nor the names of its contributors may not be
 * used to endorse or promote products derived from this software without specific
 * prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS  AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS 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 THE COPYRIGHT HOLDER OR 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.
 *
 */

package org.mariadb.jdbc.internal.com.read;


import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;

public class Buffer {

    public byte[] buf;
    public int position;
    public int limit;

    public Buffer(final byte[] buf, int limit) {
        this.buf = buf;
        this.limit = limit;
    }

    /**
     * Constructor with default limit and offset.
     *
     * @param buf byte array
     */
    public Buffer(final byte[] buf) {
        this.buf = buf;
        this.limit = this.buf.length;
    }

    public int remaining() {
        return limit - position;
    }

    /**
     * Reads a string from the buffer, looks for a 0 to end the string.
     *
     * @param charset the charset to use, for example ASCII
     * @return the read string
     */
    public String readStringNullEnd(final Charset charset) {
        int initialPosition = position;
        int cnt = 0;
        while (remaining() > 0 && (buf[position++] != 0)) {
            cnt++;
        }
        return new String(buf, initialPosition, cnt, charset);
    }

    /**
     * Reads a byte array from the buffer, looks for a 0 to end the array.
     *
     * @return the read array
     */
    public byte[] readBytesNullEnd() {
        int initialPosition = position;
        int cnt = 0;
        while (remaining() > 0 && (buf[position++] != 0)) {
            cnt++;
        }
        final byte[] tmpArr = new byte[cnt];
        System.arraycopy(buf, initialPosition, tmpArr, 0, cnt);
        return tmpArr;
    }

    /**
     * Reads length-encoded string.
     *
     * @param charset the charset to use, for example ASCII
     * @return the read string
     */
    public String readStringLengthEncoded(final Charset charset) {
        int length = (int) getLengthEncodedNumeric();
        String string = new String(buf, position, length, charset);
        position += length;
        return string;
    }

    /**
     * Read String with defined length
     *
     * @param numberOfBytes raw data length.
     * @return String value
     */
    public String readString(final int numberOfBytes) {
        position += numberOfBytes;
        return new String(buf, position - numberOfBytes, numberOfBytes);
    }

    /**
     * Read a short (2 bytes) from the buffer.
     *
     * @return an short
     */
    public short readShort() {
        return (short) ((buf[position++] & 0xff)
                + ((buf[position++] & 0xff) << 8));
    }

    /**
     * Read 24 bit integer.
     *
     * @return length
     */
    public int read24bitword() {
        return (buf[position++] & 0xff)
                + ((buf[position++] & 0xff) << 8)
                + ((buf[position++] & 0xff) << 16);
    }

    /**
     * Read a int (4 bytes) from the buffer.
     *
     * @return a int
     */
    public int readInt() {
        return ((buf[position++] & 0xff)
                + ((buf[position++] & 0xff) << 8)
                + ((buf[position++] & 0xff) << 16)
                + ((buf[position++] & 0xff) << 24));
    }

    /**
     * Read a long (8 bytes) from the buffer.
     *
     * @return a long
     */
    public long readLong() {
        return ((buf[position++] & 0xff)
                + ((long) (buf[position++] & 0xff) << 8)
                + ((long) (buf[position++] & 0xff) << 16)
                + ((long) (buf[position++] & 0xff) << 24)
                + ((long) (buf[position++] & 0xff) << 32)
                + ((long) (buf[position++] & 0xff) << 40)
                + ((long) (buf[position++] & 0xff) << 48)
                + ((long) (buf[position++] & 0xff) << 56));
    }

    /**
     * Reads a byte from the buffer.
     *
     * @return the byte
     */
    public byte readByte() {
        return buf[position++];
    }

    /**
     * Read raw data.
     *
     * @param numberOfBytes raw data length.
     * @return raw data
     */
    public byte[] readRawBytes(final int numberOfBytes) {
        final byte[] tmpArr = new byte[numberOfBytes];
        System.arraycopy(buf, position, tmpArr, 0, numberOfBytes);
        position += numberOfBytes;
        return tmpArr;
    }

    public void skipByte() {
        position++;
    }

    public void skipBytes(final int bytesToSkip) {
        position += bytesToSkip;
    }

    /**
     * Skip next length encode binary data.
     */
    public void skipLengthEncodedBytes() {
        int type = this.buf[this.position++] & 0xff;
        switch (type) {
            case 251:
                break;

            case 252:
                position += 2 + (0xffff & (((buf[position] & 0xff) + ((buf[position + 1] & 0xff) << 8))));
                break;

            case 253:
                position += 3 + (0xffffff & ((buf[position] & 0xff)
                        + ((buf[position + 1] & 0xff) << 8)
                        + ((buf[position + 2] & 0xff) << 16)));
                break;

            case 254:
                position += 8 + ((buf[position] & 0xff)
                        + ((long) (buf[position + 1] & 0xff) << 8)
                        + ((long) (buf[position + 2] & 0xff) << 16)
                        + ((long) (buf[position + 3] & 0xff) << 24)
                        + ((long) (buf[position + 4] & 0xff) << 32)
                        + ((long) (buf[position + 5] & 0xff) << 40)
                        + ((long) (buf[position + 6] & 0xff) << 48)
                        + ((long) (buf[position + 7] & 0xff) << 56));
                break;

            default:
                position += type;
                break;
        }
    }

    /**
     * Get next binary data length.
     *
     * @return length of next binary data
     */
    public long getLengthEncodedNumeric() {
        int type = this.buf[this.position++] & 0xff;
        switch (type) {
            case 251:
                return -1;
            case 252:
                return 0xffff & readShort();
            case 253:
                return 0xffffff & read24bitword();
            case 254:
                return readLong();
            default:
                return type;
        }
    }

    /**
     * Get next data bytes from length encoded prefix.
     *
     * @return buffer
     */
    public Buffer getLengthEncodedBuffer() {
        return new Buffer(getLengthEncodedBytes());
    }

    /**
     * Get next data bytes with length encoded prefix.
     *
     * @return the raw binary data
     */
    public byte[] getLengthEncodedBytes() {
        int type = this.buf[this.position++] & 0xff;
        int length;
        switch (type) {
            case 251:
                return null;
            case 252:
                length = 0xffff & readShort();
                break;
            case 253:
                length = 0xffffff & read24bitword();
                break;
            case 254:
                length = (int) ((buf[position++] & 0xff)
                        + ((long) (buf[position++] & 0xff) << 8)
                        + ((long) (buf[position++] & 0xff) << 16)
                        + ((long) (buf[position++] & 0xff) << 24)
                        + ((long) (buf[position++] & 0xff) << 32)
                        + ((long) (buf[position++] & 0xff) << 40)
                        + ((long) (buf[position++] & 0xff) << 48)
                        + ((long) (buf[position++] & 0xff) << 56));
                break;
            default:
                length = type;
                break;
        }

        byte[] tmpBuf = new byte[length];
        System.arraycopy(buf, position, tmpBuf, 0, length);
        position += length;
        return tmpBuf;
    }

    public byte getByteAt(final int position) {
        return buf[position];
    }

    /**
     * Write value with length encoded prefix.
     *
     * @param value value to write
     */
    public void writeStringLength(String value) {
        byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
        int length = bytes.length;
        while (remaining() < length + 9) grow();
        writeLength(length);
        System.arraycopy(bytes, 0, buf, position, length);
        position += length;
    }

    /**
     * Write value with length encoded prefix.
     *
     * @param bytes value to write
     */
    public void writeStringLength(byte[] bytes) {
        int length = bytes.length;
        while (remaining() < length + 9) grow();
        writeLength(length);
        System.arraycopy(bytes, 0, buf, position, length);
        position += length;
    }

    /**
     * Write value with length encoded prefix.
     * value length MUST be less than 251 char
     * @param value value to write
     */
    public void writeStringSmallLength(byte[] value) {
        int length = value.length;
        while (remaining() < length + 1) grow();
        buf[position++] = (byte) length;
        System.arraycopy(value, 0, buf, position, length);
        position += length;
    }


    /**
     * Write bytes.
     *
     * @param header header byte
     * @param bytes  command bytes
     */
    public void writeBytes(byte header, byte[] bytes) {
        int length = bytes.length;
        while (remaining() < length + 10) grow();
        writeLength(length + 1);
        buf[position++] = header;
        System.arraycopy(bytes, 0, buf, position, length);
        position += length;
    }

    /**
     * Write length.
     *
     * @param length length
     */
    public void writeLength(long length) {
        if (length < 251) {
            buf[position++] = (byte) length;
        } else if (length < 65536) {
            buf[position++] = (byte) 0xfc;
            buf[position++] = (byte) length;
            buf[position++] = (byte) (length >>> 8);
        } else if (length < 16777216) {
            buf[position++] = (byte) 0xfd;
            buf[position++] = (byte) length;
            buf[position++] = (byte) (length >>> 8);
            buf[position++] = (byte) (length >>> 16);
        } else {
            buf[position++] = (byte) 0xfe;
            buf[position++] = (byte) length;
            buf[position++] = (byte) (length >>> 8);
            buf[position++] = (byte) (length >>> 16);
            buf[position++] = (byte) (length >>> 24);
            buf[position++] = (byte) (length >>> 32);
            buf[position++] = (byte) (length >>> 40);
            buf[position++] = (byte) (length >>> 48);
            buf[position++] = (byte) (length >>> 54);
        }
    }

    /**
     * Grow data array.
     */
    private void grow() {
        int newCapacity = buf.length + (buf.length >> 1);
        if (newCapacity - (Integer.MAX_VALUE - 8) > 0) newCapacity = Integer.MAX_VALUE - 8;
        buf = Arrays.copyOf(buf, newCapacity);
        this.limit = newCapacity;
    }

}