/*
 * Copyright 2019 NAVER Corp.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.navercorp.pinpoint.common.buffer;

import com.navercorp.pinpoint.common.util.BytesUtils;

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

/**
 * @author emeroad
 */
public class FixedBuffer implements Buffer {
    protected static final int NULL = -1;
    protected byte[] buffer;
    protected int offset;

    public FixedBuffer() {
        this(32);
    }

    public FixedBuffer(final int bufferSize) {
        if (bufferSize < 0) {
            throw new IndexOutOfBoundsException("negative bufferSize:" + bufferSize);
        }
        this.buffer = new byte[bufferSize];
        this.offset = 0;
    }

    public FixedBuffer(final byte[] buffer) {
        if (buffer == null) {
            throw new NullPointerException("buffer");
        }
        this.buffer = buffer;
        this.offset = 0;
    }

    @Override
    public void putPadBytes(byte[] bytes, int totalLength) {
        if (bytes == null) {
            bytes = EMPTY;
        }
        if (bytes.length > totalLength) {
            throw new IndexOutOfBoundsException("bytes too big:" + bytes.length + " totalLength:" + totalLength);
        }
        putBytes(bytes);
        final int padSize = totalLength - bytes.length;
        if (padSize > 0) {
            putPad(padSize);
        }
    }

    private void putPad(int padSize) {
        for (int i = 0; i < padSize; i++) {
            putByte((byte)0);
        }
    }


    @Override
    public void putPrefixedBytes(final byte[] bytes) {
        if (bytes == null) {
            putSVInt(NULL);
        } else {
            putSVInt(bytes.length);
            putBytes(bytes);
        }
    }

    @Override
    public void put2PrefixedBytes(final byte[] bytes) {
        if (bytes == null) {
            putShort((short)NULL);
        } else {
            if (bytes.length > Short.MAX_VALUE) {
                throw new IndexOutOfBoundsException("too large bytes length:" + bytes.length);
            }
            putShort((short)bytes.length);
            putBytes(bytes);
        }
    }

    @Override
    public void put4PrefixedBytes(final byte[] bytes) {
        if (bytes == null) {
            putInt(NULL);
        } else {
            putInt(bytes.length);
            putBytes(bytes);
        }
    }

    @Override
    public void putPadString(String string, int totalLength) {
        final byte[] bytes = BytesUtils.toBytes(string);
        putPadBytes(bytes, totalLength);
    }

    @Override
    public void putPrefixedString(final String string) {
        final byte[] bytes = BytesUtils.toBytes(string);
        putPrefixedBytes(bytes);
    }

    @Override
    public void put2PrefixedString(final String string) {
        final byte[] bytes = BytesUtils.toBytes(string);
        if (bytes == null) {
            putShort((short)NULL);
            return;
        }
        if (bytes.length > Short.MAX_VALUE) {
            throw new IndexOutOfBoundsException("too large String size:" + bytes.length);
        }
        put2PrefixedBytes(bytes);
    }

    @Override
    public void put4PrefixedString(final String string) {
        final byte[] bytes = BytesUtils.toBytes(string);
        if (bytes == null) {
            putInt(NULL);
            return;
        }
        put4PrefixedBytes(bytes);
    }

    @Override
    public void putByte(final byte v) {
        this.buffer[offset++] = v;
    }


    @Override
    public void putBoolean(final boolean v) {
        if (v) {
            this.buffer[offset++] = BOOLEAN_TRUE;
        } else {
            this.buffer[offset++] = BOOLEAN_FALSE;
        }
    }


    @Override
    public void putInt(final int v) {
        this.offset = BytesUtils.writeInt(v, buffer, offset);
    }


    @Override
    public void putVInt(int v) {
        if (v >= 0) {
            putVar32(v);
        } else {
            putVar64((long) v);
        }
    }


    @Override
    public void putSVInt(int v) {
        this.offset = BytesUtils.writeSVar32(v, buffer, offset);
    }


    private void putVar32(int v) {
        this.offset = BytesUtils.writeVar32(v, buffer, offset);
    }

    @Override
    public void putShort(final short v) {
        this.offset = BytesUtils.writeShort(v, buffer, offset);
    }


    @Override
    public void putLong(final long v) {
        this.offset = BytesUtils.writeLong(v, buffer, offset);
    }


    @Override
    public void putVLong(long v) {
        putVar64(v);
    }


    @Override
    public void putSVLong(long v) {
        putVar64(BytesUtils.longToZigZag(v));
    }

    private void putVar64(long v) {
        this.offset = BytesUtils.writeVar64(v, buffer, offset);
    }

    @Override
    public void putDouble(double v) {
        putLong(Double.doubleToRawLongBits(v));
    }


    @Override
    public void putVDouble(double v) {
        putVLong(Double.doubleToRawLongBits(v));
    }

    @Override
    public void putSVDouble(double v) {
        putSVLong(Double.doubleToRawLongBits(v));
    }


    @Override
    public void putBytes(final byte[] v) {
        if (v == null) {
            throw new NullPointerException("v");
        }
        System.arraycopy(v, 0, buffer, offset, v.length);
        this.offset = offset + v.length;
    }


    @Override
    public byte getByte(int index) {
        return this.buffer[offset];
    }

    @Override
    public byte readByte() {
        return this.buffer[offset++];
    }

    @Override
    public int readUnsignedByte() {
        return readByte() & 0xff;
    }

    @Override
    public boolean readBoolean() {
        final byte b = readByte();
        return b == BOOLEAN_TRUE;
    }

    @Override
    public int readInt() {
        final int i = BytesUtils.bytesToInt(buffer, offset);
        this.offset = this.offset + 4;
        return i;
    }

    @Override
    public int readVInt() {
        // borrowing the protocol buffer's concept of variable-length encoding
        // copy https://github.com/google/protobuf 2.6.1
        // CodedInputStream.java -> int readRawVarint32()

        // See implementation notes for readRawVarint64
        fastpath: {
            int pos = this.offset;
            final int bufferSize = this.buffer.length;
            if (bufferSize == pos) {
                break fastpath;
            }

            final byte[] buffer = this.buffer;
            int x;
            if ((x = buffer[pos++]) >= 0) {
                this.offset = pos;
                return x;
            } else if (bufferSize - pos < 9) {
                break fastpath;
            } else if ((x ^= (buffer[pos++] << 7)) < 0) {
                x ^= (~0 << 7);
            } else if ((x ^= (buffer[pos++] << 14)) >= 0) {
                x ^= (~0 << 7) ^ (~0 << 14);
            } else if ((x ^= (buffer[pos++] << 21)) < 0) {
                x ^= (~0 << 7) ^ (~0 << 14) ^ (~0 << 21);
            } else {
                int y = buffer[pos++];
                x ^= y << 28;
                x ^= (~0 << 7) ^ (~0 << 14) ^ (~0 << 21) ^ (~0 << 28);
                if (y < 0 &&
                        buffer[pos++] < 0 &&
                        buffer[pos++] < 0 &&
                        buffer[pos++] < 0 &&
                        buffer[pos++] < 0 &&
                        buffer[pos++] < 0) {
                    break fastpath;  // Will throw malformedVarint()
                }
            }
            this.offset = pos;
            return x;
        }
        return (int) readVar64SlowPath();
    }


    /** Variant of readRawVarint64 for when uncomfortably close to the limit. */
    /* Visible for testing */
    long readVar64SlowPath() {
        int copyOffset = this.offset;
        long result = 0;
        for (int shift = 0; shift < 64; shift += 7) {
            final byte b = this.buffer[copyOffset++];
            result |= (long) (b & 0x7F) << shift;
            if ((b & 0x80) == 0) {
                this.offset = copyOffset;
                return result;
            }
        }
        throw new IllegalArgumentException("invalid varLong. start offset:" +  this.offset + " readOffset:" + offset);
    }

    @Override
    public int readSVInt() {
        return BytesUtils.zigzagToInt(readVInt());
    }

    @Override
    public short readShort() {
        final short i = BytesUtils.bytesToShort(buffer, offset);
        this.offset = this.offset + 2;
        return i;
    }

    public int readUnsignedShort() {
        return readShort() & 0xFFFF;
    }

    @Override
    public long readLong() {
        final long l = BytesUtils.bytesToLong(buffer, offset);
        this.offset = this.offset + 8;
        return l;
    }

    @Override
    public long readVLong() {
        // borrowing the protocol buffer's concept of variable-length encoding
        // copy https://github.com/google/protobuf 2.6.1
        // CodedInputStream.java -> long readRawVarint64() throws IOException

        // Implementation notes:
        //
        // Optimized for one-byte values, expected to be common.
        // The particular code below was selected from various candidates
        // empirically, by winning VarintBenchmark.
        //
        // Sign extension of (signed) Java bytes is usually a nuisance, but
        // we exploit it here to more easily obtain the sign of bytes read.
        // Instead of cleaning up the sign extension bits by masking eagerly,
        // we delay until we find the final (positive) byte, when we clear all
        // accumulated bits with one xor.  We depend on javac to constant fold.
        fastpath: {
            int pos = offset;
            int bufferSize = this.buffer.length;
            if (bufferSize == pos) {
                break fastpath;
            }

            final byte[] buffer = this.buffer;
            long x;
            int y;
            if ((y = buffer[pos++]) >= 0) {
                this.offset = pos;
                return y;
            } else if (bufferSize - pos < 9) {
                break fastpath;
            } else if ((x = y ^ (buffer[pos++] << 7)) < 0L) {
                x ^= (~0L << 7);
            } else if ((x ^= (buffer[pos++] << 14)) >= 0L) {
                x ^= (~0L << 7) ^ (~0L << 14);
            } else if ((x ^= (buffer[pos++] << 21)) < 0L) {
                x ^= (~0L << 7) ^ (~0L << 14) ^ (~0L << 21);
            } else if ((x ^= ((long) buffer[pos++] << 28)) >= 0L) {
                x ^= (~0L << 7) ^ (~0L << 14) ^ (~0L << 21) ^ (~0L << 28);
            } else if ((x ^= ((long) buffer[pos++] << 35)) < 0L) {
                x ^= (~0L << 7) ^ (~0L << 14) ^ (~0L << 21) ^ (~0L << 28) ^ (~0L << 35);
            } else if ((x ^= ((long) buffer[pos++] << 42)) >= 0L) {
                x ^= (~0L << 7) ^ (~0L << 14) ^ (~0L << 21) ^ (~0L << 28) ^ (~0L << 35) ^ (~0L << 42);
            } else if ((x ^= ((long) buffer[pos++] << 49)) < 0L) {
                x ^= (~0L << 7) ^ (~0L << 14) ^ (~0L << 21) ^ (~0L << 28) ^ (~0L << 35) ^ (~0L << 42)
                        ^ (~0L << 49);
            } else {
                x ^= ((long) buffer[pos++] << 56);
                x ^= (~0L << 7) ^ (~0L << 14) ^ (~0L << 21) ^ (~0L << 28) ^ (~0L << 35) ^ (~0L << 42)
                        ^ (~0L << 49) ^ (~0L << 56);
                if (x < 0L) {
                    if (buffer[pos++] < 0L) {
                        break fastpath;  // Will throw malformedVarint()
                    }
                }
            }
            this.offset = pos;
            return x;
        }
        return readVar64SlowPath();
    }


    @Override
    public long readSVLong() {
        return BytesUtils.zigzagToLong(readVLong());
    }

    @Override
    public double readDouble() {
        return Double.longBitsToDouble(this.readLong());
    }

    @Override
    public double readVDouble() {
        return Double.longBitsToDouble(this.readVLong());
    }

    @Override
    public double readSVDouble() {
        return Double.longBitsToDouble(this.readSVLong());
    }

    @Override
    public byte[] readPadBytes(int totalLength) {
        return readBytes(totalLength);
    }

    @Override
    public String readPadString(int totalLength) {
        return readString(totalLength);
    }

    @Override
    public String readPadStringAndRightTrim(int totalLength) {
        String string = BytesUtils.toStringAndRightTrim(buffer, offset, totalLength);
        this.offset = offset + totalLength;
        return string ;
    }


    @Override
    public byte[] readPrefixedBytes() {
        final int size = readSVInt();
        if (size == NULL) {
            return null;
        }
        if (size == 0) {
            return EMPTY;
        }
        return readBytes(size);
    }

    @Override
    public byte[] read2PrefixedBytes() {
        final int size = readShort();
        if (size == NULL) {
            return null;
        }
        if (size == 0) {
            return EMPTY;
        }
        return readBytes(size);
    }

    @Override
    public byte[] read4PrefixedBytes() {
        final int size = readInt();
        if (size == NULL) {
            return null;
        }
        if (size == 0) {
            return EMPTY;
        }
        return readBytes(size);
    }


    private byte[] readBytes(int size) {
        final byte[] b = new byte[size];
        System.arraycopy(buffer, offset, b, 0, size);
        this.offset = offset + size;
        return b;
    }

    @Override
    public String readPrefixedString() {
        final int size = readSVInt();
        if (size == NULL) {
            return null;
        }
        if (size == 0) {
            return "";
        }
        return readString(size);
    }

    @Override
    public String read2PrefixedString() {
        final int size = readShort();
        if (size == NULL) {
            return null;
        }
        if (size == 0) {
            return "";
        }
        return readString(size);
    }

    @Override
    public String read4PrefixedString() {
        final int size = readInt();
        if (size == NULL) {
            return null;
        }
        if (size == 0) {
            return "";
        }
        return readString(size);
    }


    protected String readString(final int size) {
        final String s = newString(size);
        this.offset = offset + size;
        return s;
    }

    protected String newString(final int size) {
        try {
            return new String(buffer, offset, size, UTF8);
        } catch (UnsupportedEncodingException ue) {
            return new String(buffer, offset, size, UTF8_CHARSET);
        }
    }

    /**
     * Be careful that if internal buffer's length is as same as offset,
     * then just return internal buffer without copying memory for improving performance.
     * @return
     */
    @Override
    public byte[] getBuffer() {
        if (offset == buffer.length) {
            return this.buffer;
        } else {
            return copyBuffer();
        }
    }

    @Override
    public byte[] copyBuffer() {
        final byte[] copy = new byte[offset];
        System.arraycopy(buffer, 0, copy, 0, offset);
        return copy;
    }

    @Override
    public ByteBuffer wrapByteBuffer() {
        return ByteBuffer.wrap(this.buffer, 0, offset);
    }

    /**
     * return internal buffer
     * @return
     */
    @Override
    public byte[] getInternalBuffer() {
        return this.buffer;
    }

    @Override
    public void setOffset(int offset) {
        this.offset = offset;
    }

    @Override
    public int getOffset() {
        return offset;
    }

    @Override
    public int remaining() {
        return buffer.length - offset;
    }

    @Override
    public boolean hasRemaining() {
        return offset < buffer.length;
    }
}
