/*
 *     Copyright (C) 2015  higherfrequencytrading.com
 *
 *     This program 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 3 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 Lesser General Public License for more details.
 *
 *     You should have received a copy of the GNU Lesser General Public License
 *     along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package net.openhft.lang.io;

import net.openhft.lang.io.serialization.BytesMarshallableSerializer;
import net.openhft.lang.io.serialization.JDKZObjectSerializer;
import net.openhft.lang.io.serialization.impl.VanillaBytesMarshallerFactory;
import net.openhft.lang.model.constraints.NotNull;
import sun.nio.ch.DirectBuffer;

import java.io.EOFException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author peter.lawrey
 */
public class ByteBufferBytes extends AbstractBytes implements IByteBufferBytes {
    private final ByteBuffer buffer;
    private final int start;
    private final int capacity;
    private int position;
    private int limit;
    private AtomicBoolean barrier;

    /**
     * Use the ByteBufferBytes.wrap(ByteBuffer) as DirectByteBuffer more efficient
     */
    @Deprecated
    public ByteBufferBytes(ByteBuffer buffer) {
        this(buffer, 0, buffer.capacity());
    }

    /**
     * Use the ByteBufferBytes.wrap(ByteBuffer) as DirectByteBuffer more efficient
     *
     * @param buffer the buffer to populate
     * @param start  start of buffer
     *               * @param capacity len of buffer
     */
    @Deprecated
    public ByteBufferBytes(ByteBuffer buffer, int start, int capacity) {
        super(BytesMarshallableSerializer.create(new VanillaBytesMarshallerFactory(), JDKZObjectSerializer.INSTANCE), new AtomicInteger(1));
        // We should set order to native, because compare-and-swap operations
        // end up with native ops. Bytes interfaces handles only native order.
        buffer.order(ByteOrder.nativeOrder());
        this.buffer = buffer;
        this.start = position = start;
        this.capacity = limit = (capacity + start);
    }

    public static IByteBufferBytes wrap(ByteBuffer buffer) {
        if (buffer instanceof DirectBuffer) {
            return new DirectByteBufferBytes(buffer);
        }

        return new ByteBufferBytes(buffer.slice());
    }

    public static IByteBufferBytes wrap(ByteBuffer buffer, int start, int capacity) {
        if (buffer instanceof DirectBuffer) {
            return new DirectByteBufferBytes(buffer, start, capacity);
        }

        return new ByteBufferBytes(buffer.slice(), start, capacity);
    }

    @Override
    public ByteBufferBytes slice() {
        return new ByteBufferBytes(buffer(), position, limit - position);
    }

    @Override
    public ByteBufferBytes slice(long offset, long length) {
        long sliceStart = position + offset;
        assert sliceStart >= start && sliceStart < capacity;
        long sliceEnd = sliceStart + length;
        assert sliceEnd > sliceStart && sliceEnd <= capacity;
        return new ByteBufferBytes(buffer(), (int) sliceStart, (int) length);
    }

    @Override
    public CharSequence subSequence(int start, int end) {
        int subStart = position + start;
        if (subStart < position || subStart > limit)
            throw new IndexOutOfBoundsException();
        int subEnd = position + end;
        if (subEnd < subStart || subEnd > limit)
            throw new IndexOutOfBoundsException();
        if (start == end)
            return "";
        return new ByteBufferBytes(buffer(), subStart, end - start);
    }

    @Override
    public ByteBufferBytes bytes() {
        return new ByteBufferBytes(buffer(), start, capacity - start);
    }

    @Override
    public ByteBufferBytes bytes(long offset, long length) {
        long sliceStart = start + offset;
        assert sliceStart >= start && sliceStart < capacity;
        long sliceEnd = sliceStart + length;
        assert sliceEnd > sliceStart && sliceEnd <= capacity;
        return new ByteBufferBytes(buffer(), (int) sliceStart, (int) length);
    }

    @Override
    public long address() {
        if (buffer instanceof DirectBuffer) {
            long address = ((DirectBuffer) buffer).address();
            if (address == 0)
                throw new IllegalStateException("This buffer has no address, is it empty?");
            return address;
        }
        throw new IllegalStateException("A heap ByteBuffer doesn't have a fixed address");
    }

    @Override
    public Bytes zeroOut() {
        clear();
        int i = start;
        for (; i < capacity - 7; i++)
            buffer.putLong(i, 0L);
        for (; i < capacity; i++)
            buffer.put(i, (byte) 0);
        return this;
    }

    @Override
    public Bytes zeroOut(long start, long end) {
        if (start < 0 || end > limit())
            throw new IllegalArgumentException("start: " + start + ", end: " + end);
        if (start >= end)
            return this;
        int i = (int) (this.start + start);
        int j = (int) (this.start + end);
        for (; i < j - 7; i++)
            buffer.putLong(i, 0L);
        for (; i < j; i++)
            buffer.put(i, (byte) 0);
        return this;
    }

    @Override
    public Bytes zeroOut(long start, long end, boolean ifNotZero) {
        // ByteBuffers are allocated in memory eagerly.
        return zeroOut(start, end);
    }

    public ByteBuffer buffer() {
        return buffer;
    }

    void readBarrier() {
        if (barrier == null) barrier = new AtomicBoolean();
        barrier.get();
    }

    void writeBarrier() {
        if (barrier == null) barrier = new AtomicBoolean();
        barrier.lazySet(false);
    }

    @Override
    public int read(@NotNull byte[] bytes, int off, int len) {
        checkArrayOffs(bytes.length, off, len);
        long left = remaining();
        if (left <= 0) return -1;
        int len2 = (int) Math.min(left, len);
        for (int i = 0; i < len2; i++)
            bytes[off + i] = readByte();
        return len2;
    }

    @Override
    public byte readByte() {
        if (position < capacity)
            return buffer.get(position++);
        throw new IndexOutOfBoundsException();
    }

    @Override
    public byte readByte(long offset) {
        int pos = (int) (start + offset);
        if (pos < capacity)
            return buffer.get(pos);
        throw new IndexOutOfBoundsException();
    }

    @Override
    public void readFully(@NotNull byte[] b, int off, int len) {
        checkArrayOffs(b.length, off, len);
        long left = remaining();
        if (left < len)
            throw new IllegalStateException(new EOFException());
        for (int i = 0; i < len; i++)
            b[off + i] = readByte();
    }

    @Override
    public void readFully(@NotNull char[] data, int off, int len) {
        checkArrayOffs(data.length, off, len);
        long left = remaining();
        if (left < len * 2L)
            throw new IllegalStateException(new EOFException());
        for (int i = 0; i < len; i++)
            data[off + i] = readChar();
    }

    @Override
    public void readFully(long offset, byte[] bytes, int off, int len) {
        checkArrayOffs(bytes.length, off, len);
        long left = remaining();
        if (left < len)
            throw new IllegalStateException(new EOFException());
        for (int i = 0; i < len; i++)
            bytes[off + i] = readByte(offset + i);
    }

    @Override
    public short readShort() {
        if (position + 2 <= capacity) {
            short s = buffer.getShort(position);
            position += 2;
            return s;
        }
        throw new IndexOutOfBoundsException();
    }

    @Override
    public short readShort(long offset) {
        int pos = (int) (start + offset);
        if (pos + 2 <= capacity)
            return buffer.getShort(pos);
        throw new IndexOutOfBoundsException();
    }

    @Override
    public char readChar() {
        if (position + 2 <= capacity) {
            char ch = buffer.getChar(position);
            position += 2;
            return ch;
        }
        throw new IndexOutOfBoundsException();
    }

    @Override
    public char readChar(long offset) {
        int pos = (int) (start + offset);
        if (pos + 2 <= capacity)
            return buffer.getChar(pos);
        throw new IndexOutOfBoundsException();
    }

    @Override
    public int readInt() {
        if (position + 4 <= capacity) {
            int i = buffer.getInt(position);
            position += 4;
            return i;
        }
        throw new IndexOutOfBoundsException();
    }

    @Override
    public int readInt(long offset) {
        int pos = (int) (start + offset);
        if (pos + 4 <= capacity)
            return buffer.getInt(pos);
        throw new IndexOutOfBoundsException();
    }

    @Override
    public int readVolatileInt() {
        readBarrier();
        return readInt();
    }

    @Override
    public int readVolatileInt(long offset) {
        readBarrier();
        return readInt(offset);
    }

    @Override
    public long readLong() {
        if (position + 8 <= capacity) {
            long l = buffer.getLong(position);
            position += 8;
            return l;
        }
        throw new IndexOutOfBoundsException();
    }

    @Override
    public long readLong(long offset) {
        int pos = (int) (start + offset);
        if (pos + 8 <= capacity)
            return buffer.getLong(pos);
        throw new IndexOutOfBoundsException();
    }

    @Override
    public long readVolatileLong() {
        readBarrier();
        return readLong();
    }

    @Override
    public long readVolatileLong(long offset) {
        readBarrier();
        return readLong(offset);
    }

    @Override
    public float readFloat() {
        if (position + 4 <= capacity) {
            float f = buffer.getFloat(position);
            position += 4;
            return f;
        }
        throw new IndexOutOfBoundsException();
    }

    @Override
    public float readFloat(long offset) {
        int pos = (int) (start + offset);
        if (pos + 4 <= capacity)
            return buffer.getFloat(pos);
        throw new IndexOutOfBoundsException();
    }

    @Override
    public double readDouble() {
        if (position + 8 <= capacity) {
            double d = buffer.getDouble(position);
            position += 8;
            return d;
        }
        throw new IndexOutOfBoundsException();
    }

    @Override
    public double readDouble(long offset) {
        int pos = (int) (start + offset);
        if (pos + 8 <= capacity)
            return buffer.getDouble(pos);
        throw new IndexOutOfBoundsException();
    }

    @Override
    public void write(int b) {
        if (position < capacity)
            buffer.put(position++, (byte) b);
        else
            throw new IndexOutOfBoundsException();
    }

    @Override
    public void writeByte(long offset, int b) {
        int pos = (int) (start + offset);
        if (pos < capacity)
            buffer.put(pos, (byte) b);
        else
            throw new IndexOutOfBoundsException();
    }

    @Override
    public void writeShort(int v) {
        if (position + 2 <= capacity) {
            buffer.putShort(position, (short) v);
            position += 2;

        } else {
            throw new IndexOutOfBoundsException();
        }
    }

    @Override
    public void writeShort(long offset, int v) {
        int pos = (int) (start + offset);
        if (pos + 2 <= capacity)
            buffer.putShort(pos, (short) v);
        else
            throw new IndexOutOfBoundsException();
    }

    @Override
    public void writeChar(int v) {
        if (position + 2 <= capacity) {
            buffer.putChar(position, (char) v);
            position += 2;

        } else {
            throw new IndexOutOfBoundsException();
        }
    }

    @Override
    public void writeChar(long offset, int v) {
        int pos = (int) (start + offset);
        if (pos + 2 <= capacity)
            buffer.putChar(pos, (char) v);
        else
            throw new IndexOutOfBoundsException();
    }

    @Override
    public void writeInt(int v) {
        if (position + 4 <= capacity) {
            buffer.putInt(position, v);
            position += 4;

        } else {
            throw new IndexOutOfBoundsException();
        }
    }

    @Override
    public void writeInt(long offset, int v) {
        int pos = (int) (start + offset);
        if (pos + 4 <= capacity)
            buffer.putInt(pos, v);
        else
            throw new IndexOutOfBoundsException();
    }

    @Override
    public void writeOrderedInt(int v) {
        writeInt(v);
        writeBarrier();
    }

    @Override
    public void writeOrderedInt(long offset, int v) {
        writeInt(offset, v);
        writeBarrier();
    }

    @Override
    public boolean compareAndSwapInt(long offset, int expected, int x) {
        if (buffer instanceof DirectBuffer)
            return NativeBytes.UNSAFE.compareAndSwapInt(null, ((DirectBuffer) buffer).address() + offset, expected, x);
        return NativeBytes.UNSAFE.compareAndSwapInt(buffer.array(), NativeBytes.BYTES_OFFSET + offset, expected, x);
    }

    @Override
    public void writeLong(long v) {
        if (position + 8 <= capacity) {
            buffer.putLong(position, v);
            position += 8;

        } else {
            throw new IndexOutOfBoundsException();
        }
    }

    @Override
    public void writeLong(long offset, long v) {
        int pos = (int) (start + offset);
        if (pos + 8 <= capacity)
            buffer.putLong(pos, v);
        else
            throw new IndexOutOfBoundsException();
    }

    @Override
    public void writeOrderedLong(long v) {
        writeLong(v);
        writeBarrier();
    }

    @Override
    public void writeOrderedLong(long offset, long v) {
        writeLong((int) offset, v);
        writeBarrier();
    }

    @Override
    public boolean compareAndSwapLong(long offset, long expected, long x) {
        if (buffer instanceof DirectBuffer)
            return NativeBytes.UNSAFE.compareAndSwapLong(null, ((DirectBuffer) buffer).address() + offset, expected, x);
        return NativeBytes.UNSAFE.compareAndSwapLong(buffer.array(), NativeBytes.BYTES_OFFSET + offset, expected, x);
    }

    @Override
    public void writeFloat(float v) {
        if (position + 4 <= capacity) {
            buffer.putFloat(position, v);
            position += 4;

        } else {
            throw new IndexOutOfBoundsException();
        }
    }

    @Override
    public void writeFloat(long offset, float v) {
        int pos = (int) (start + offset);
        if (pos + 4 <= capacity)
            buffer.putFloat(pos, v);
        else
            throw new IndexOutOfBoundsException();
    }

    @Override
    public void writeDouble(double v) {
        if (position + 8 <= capacity) {
            buffer.putDouble(position, v);
            position += 8;

        } else {
            throw new IndexOutOfBoundsException();
        }
    }

    @Override
    public void writeDouble(long offset, double v) {
        int pos = (int) (start + offset);
        if (pos + 8 <= capacity)
            buffer.putDouble(pos, v);
        else
            throw new IndexOutOfBoundsException();
    }

    @Override
    public void readObject(Object object, int start, int end) {
        throw new UnsupportedOperationException();
    }

    @Override
    public void writeObject(Object object, int start, int end) {
        throw new UnsupportedOperationException();
    }

    @Override
    public long position() {
        return position - start;
    }

    @Override
    public ByteBufferBytes position(long position) {
        if (start + position > Integer.MAX_VALUE)
            throw new IllegalArgumentException("Position to large");
        if (position < 0)
            throw new IllegalArgumentException("Position to small");

        this.position = (int) (start + position);
        return this;
    }

    @Override
    public long capacity() {
        return capacity - start;
    }

    @Override
    public long remaining() {
        return limit - position;
    }

    @Override
    public long limit() {
        return limit - start;
    }

    @Override
    public ByteBufferBytes limit(long limit) {
        this.limit = (int) (start + limit);
        return this;
    }

    @NotNull
    @Override
    public ByteOrder byteOrder() {
        return buffer.order();
    }

    @Override
    public void checkEndOfBuffer() throws IndexOutOfBoundsException {
        if (position < start || position > limit()) {
            throw new IndexOutOfBoundsException();
        }
    }

    @Override
    protected void cleanup() {
        IOTools.clean(buffer);
    }

    @Override
    public Bytes load() {
        int pageSize = NativeBytes.UNSAFE.pageSize();
        for (int offset = start; offset < capacity; offset += pageSize)
            buffer.get(offset);
        return this;
    }

    public void alignPositionAddr(int powerOf2) {
        position = (position + powerOf2 - 1) & ~(powerOf2 - 1);
    }
}
