package org.schabi.newpipe.streams;

import org.schabi.newpipe.streams.io.SharpStream;

import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;

/**
 * @author kapodamy
 */
public class DataReader {
    public static final int SHORT_SIZE = 2;
    public static final int LONG_SIZE = 8;
    public static final int INTEGER_SIZE = 4;
    public static final int FLOAT_SIZE = 4;

    private static final int BUFFER_SIZE = 128 * 1024; // 128 KiB

    private long position = 0;
    private final SharpStream stream;

    private InputStream view;
    private int viewSize;

    public DataReader(final SharpStream stream) {
        this.stream = stream;
        this.readOffset = this.readBuffer.length;
    }

    public long position() {
        return position;
    }

    public int read() throws IOException {
        if (fillBuffer()) {
            return -1;
        }

        position++;
        readCount--;

        return readBuffer[readOffset++] & 0xFF;
    }

    public long skipBytes(final long byteAmount) throws IOException {
        long amount = byteAmount;
        if (readCount < 0) {
            return 0;
        } else if (readCount == 0) {
            amount = stream.skip(amount);
        } else {
            if (readCount > amount) {
                readCount -= (int) amount;
                readOffset += (int) amount;
            } else {
                amount = readCount + stream.skip(amount - readCount);
                readCount = 0;
                readOffset = readBuffer.length;
            }
        }

        position += amount;
        return amount;
    }

    public int readInt() throws IOException {
        primitiveRead(INTEGER_SIZE);
        return primitive[0] << 24 | primitive[1] << 16 | primitive[2] << 8 | primitive[3];
    }

    public long readUnsignedInt()  throws IOException {
        final long value = readInt();
        return value & 0xffffffffL;
    }


    public short readShort() throws IOException {
        primitiveRead(SHORT_SIZE);
        return (short) (primitive[0] << 8 | primitive[1]);
    }

    public long readLong() throws IOException {
        primitiveRead(LONG_SIZE);
        final long high =
                primitive[0] << 24 | primitive[1] << 16 | primitive[2] << 8 | primitive[3];
        final long low = primitive[4] << 24 | primitive[5] << 16 | primitive[6] << 8 | primitive[7];
        return high << 32 | low;
    }

    public int read(final byte[] buffer) throws IOException {
        return read(buffer, 0, buffer.length);
    }

    public int read(final byte[] buffer, final int off, final int c) throws IOException {
        int offset = off;
        int count = c;

        if (readCount < 0) {
            return -1;
        }
        int total = 0;

        if (count >= readBuffer.length) {
            if (readCount > 0) {
                System.arraycopy(readBuffer, readOffset, buffer, offset, readCount);
                readOffset += readCount;

                offset += readCount;
                count -= readCount;

                total = readCount;
                readCount = 0;
            }
            total += Math.max(stream.read(buffer, offset, count), 0);
        } else {
            while (count > 0 && !fillBuffer()) {
                final int read = Math.min(readCount, count);
                System.arraycopy(readBuffer, readOffset, buffer, offset, read);

                readOffset += read;
                readCount -= read;

                offset += read;
                count -= read;

                total += read;
            }
        }

        position += total;
        return total;
    }

    public boolean available() {
        return readCount > 0 || stream.available() > 0;
    }

    public void rewind() throws IOException {
        stream.rewind();

        if ((position - viewSize) > 0) {
            viewSize = 0; // drop view
        } else {
            viewSize += position;
        }

        position = 0;
        readOffset = readBuffer.length;
        readCount = 0;
    }

    public boolean canRewind() {
        return stream.canRewind();
    }

    /**
     * Wraps this instance of {@code DataReader} into {@code InputStream}
     * object. Note: Any read in the {@code DataReader} will not modify
     * (decrease) the view size
     *
     * @param size the size of the view
     * @return the view
     */
    public InputStream getView(final int size) {
        if (view == null) {
            view = new InputStream() {
                @Override
                public int read() throws IOException {
                    if (viewSize < 1) {
                        return -1;
                    }
                    final int res = DataReader.this.read();
                    if (res > 0) {
                        viewSize--;
                    }
                    return res;
                }

                @Override
                public int read(final byte[] buffer) throws IOException {
                    return read(buffer, 0, buffer.length);
                }

                @Override
                public int read(final byte[] buffer, final int offset, final int count)
                        throws IOException {
                    if (viewSize < 1) {
                        return -1;
                    }

                    final int res = DataReader.this.read(buffer, offset, Math.min(viewSize, count));
                    viewSize -= res;

                    return res;
                }

                @Override
                public long skip(final long amount) throws IOException {
                    if (viewSize < 1) {
                        return 0;
                    }
                    final int res = (int) DataReader.this.skipBytes(Math.min(amount, viewSize));
                    viewSize -= res;

                    return res;
                }

                @Override
                public int available() {
                    return viewSize;
                }

                @Override
                public void close() {
                    viewSize = 0;
                }

                @Override
                public boolean markSupported() {
                    return false;
                }

            };
        }
        viewSize = size;

        return view;
    }

    private final short[] primitive = new short[LONG_SIZE];

    private void primitiveRead(final int amount) throws IOException {
        final byte[] buffer = new byte[amount];
        final int read = read(buffer, 0, amount);

        if (read != amount) {
            throw new EOFException("Truncated stream, missing "
                    + (amount - read) + " bytes");
        }

        for (int i = 0; i < amount; i++) {
            // the "byte" data type in java is signed and is very annoying
            primitive[i] = (short) (buffer[i] & 0xFF);
        }
    }

    private final byte[] readBuffer = new byte[BUFFER_SIZE];
    private int readOffset;
    private int readCount;

    private boolean fillBuffer() throws IOException {
        if (readCount < 0) {
            return true;
        }
        if (readOffset >= readBuffer.length) {
            readCount = stream.read(readBuffer);
            if (readCount < 1) {
                readCount = -1;
                return true;
            }
            readOffset = 0;
        }

        return readCount < 1;
    }
}
