/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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 org.apache.ignite.internal.util.io;

import static org.apache.ignite.internal.util.GridUnsafe.BYTE_ARR_OFF;
import static org.apache.ignite.internal.util.GridUnsafe.CHAR_ARR_OFF;
import static org.apache.ignite.internal.util.GridUnsafe.DOUBLE_ARR_OFF;
import static org.apache.ignite.internal.util.GridUnsafe.FLOAT_ARR_OFF;
import static org.apache.ignite.internal.util.GridUnsafe.INT_ARR_OFF;
import static org.apache.ignite.internal.util.GridUnsafe.IS_BIG_ENDIAN;
import static org.apache.ignite.internal.util.GridUnsafe.LONG_ARR_OFF;
import static org.apache.ignite.internal.util.GridUnsafe.SHORT_ARR_OFF;

import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.UTFDataFormatException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.Period;
import java.util.BitSet;
import java.util.Objects;
import java.util.UUID;
import org.apache.ignite.internal.tostring.IgniteToStringBuilder;
import org.apache.ignite.internal.tostring.IgniteToStringExclude;
import org.apache.ignite.internal.util.FastTimestamps;
import org.apache.ignite.internal.util.GridUnsafe;
import org.apache.ignite.internal.util.VarIntUtils;

/**
 * Data input based on {@code Unsafe} operations.
 */
public class IgniteUnsafeDataInput extends InputStream implements IgniteDataInput {
    /** Maximum data block length. */
    private static final int MAX_BLOCK_SIZE = 1024;

    /** Length of char buffer (for reading strings). */
    private static final int CHAR_BUF_SIZE = 256;

    private static final long NO_AUTO_SHRINK = -1;

    private final long shrinkCheckFrequencyMs;

    /** Buffer for reading general/block data. */
    @IgniteToStringExclude
    private final byte[] utfBuf = new byte[MAX_BLOCK_SIZE];

    /** Char buffer for fast string reads. */
    @IgniteToStringExclude
    private final char[] utfCharBuf = new char[CHAR_BUF_SIZE];

    /** Current offset into buf. */
    private int pos;

    /** End offset of valid data in buf, or -1 if no more block data. */
    private int end = -1;

    /** Bytes. */
    @IgniteToStringExclude
    private byte[] buf;

    /** Offset. */
    private int off;

    /** Max. */
    private int max;

    /** Underlying input stream. */
    @IgniteToStringExclude
    private InputStream in;

    /** Buffer for reading from stream. */
    @IgniteToStringExclude
    private byte[] inBuf = new byte[1024];

    /** Maximum message size. */
    private int maxOff;

    private int mark;

    /** Last length check timestamp. */
    private long lastAutoShrinkCheckTimestamp;

    /**
     * Creates a new input with auto-shrinking disabled and no internal buffer assigned.
     */
    public IgniteUnsafeDataInput() {
        this(NO_AUTO_SHRINK);
    }

    /**
     * Creates a new input with no internal buffer assigned.
     *
     * @param shrinkCheckFrequencyMs how often to check whether an underlying byte buffer needs to be shrunk
     *                               (disables the auto-shrinking if it's -1)
     */
    public IgniteUnsafeDataInput(long shrinkCheckFrequencyMs) {
        this.shrinkCheckFrequencyMs = shrinkCheckFrequencyMs;
    }

    /**
     * Creates a new input with auto-shrinking disabled.
     *
     * @param bytes array to initially (before automatic resize) use as an internal buffer
     */
    public IgniteUnsafeDataInput(byte[] bytes) {
        this(bytes, NO_AUTO_SHRINK);
    }

    /**
     * Creates a new input.
     *
     * @param bytes array to initially (before automatic resize) use as an internal buffer
     * @param shrinkCheckFrequencyMs how often to check whether an underlying byte buffer needs to be shrunk
     *                               (disables the auto-shrinking if it's -1)
     */
    public IgniteUnsafeDataInput(byte[] bytes, long shrinkCheckFrequencyMs) {
        this(shrinkCheckFrequencyMs);

        bytes(bytes, bytes.length);
    }

    /** {@inheritDoc} */
    @Override
    public void bytes(byte[] bytes, int len) {
        bytes(bytes, 0, len);
    }

    /**
     * Sets the internal buffer.
     *
     * @param bytes Bytes.
     * @param off Offset.
     * @param len Length.
     */
    public void bytes(byte[] bytes, int off, int len) {
        buf = bytes;

        max = len;
        this.off = off;
    }

    /** {@inheritDoc} */
    @Override
    public void inputStream(InputStream in) throws IOException {
        this.in = in;

        buf = inBuf;
    }

    /**
     * Reads from stream to buffer. If stream is {@code null}, this method is no-op.
     *
     * @param size Number of bytes to read.
     * @throws IOException In case of error.
     */
    private void fromStream(int size) throws IOException {
        if (in == null) {
            return;
        }

        maxOff = Math.max(maxOff, size);

        // Increase size of buffer if needed.
        if (size > inBuf.length) {
            buf = inBuf = new byte[Math.max(inBuf.length << 1, size)]; // Grow.
        } else if (isAutoShrinkEnabled()) {
            long now = FastTimestamps.coarseCurrentTimeMillis();

            if (now - lastAutoShrinkCheckTimestamp > shrinkCheckFrequencyMs) {
                int halfSize = inBuf.length >> 1;

                if (maxOff < halfSize) {
                    byte[] newInBuf = new byte[halfSize]; // Shrink.

                    System.arraycopy(inBuf, 0, newInBuf, 0, off);

                    buf = inBuf = newInBuf;
                }

                maxOff = 0;
                lastAutoShrinkCheckTimestamp = now;
            }
        }

        off = 0;
        max = 0;

        while (max != size) {
            int read = in.read(inBuf, max, size - max);

            if (read == -1) {
                throw new EOFException("End of stream reached: " + in);
            }

            max += read;
        }
    }

    private boolean isAutoShrinkEnabled() {
        return shrinkCheckFrequencyMs != NO_AUTO_SHRINK;
    }

    /**
     * Advances the offset doing a check for whether we fell off the buffer edge.
     *
     * @param more Bytes to move forward.
     * @return Old offset value.
     * @throws IOException In case of error.
     */
    private int advanceOffset(int more) throws IOException {
        int old = off;

        off += more;

        if (off > max) {
            throw new EOFException("Attempt to read beyond the end of the stream "
                    + "[pos=" + off + ", more=" + more + ", max=" + max + ']');
        }

        return old;
    }

    /** {@inheritDoc} */
    @SuppressWarnings("NonSynchronizedMethodOverridesSynchronizedMethod")
    @Override
    public void reset() throws IOException {
        if (in != null) {
            in.reset();
        } else {
            off = mark;
        }
    }

    /** {@inheritDoc} */
    @Override
    public void cleanup() throws IOException {
        in = null;

        off = 0;
        max = 0;
        mark = 0;
    }

    /** {@inheritDoc} */
    @Override
    public byte[] readByteArray(int arrSize) throws IOException {
        fromStream(arrSize);

        byte[] arr = new byte[arrSize];

        System.arraycopy(buf, advanceOffset(arrSize), arr, 0, arrSize);

        return arr;
    }

    /** {@inheritDoc} */
    @Override
    public short[] readShortArray(int arrSize) throws IOException {
        int bytesToCp = arrSize << 1;

        fromStream(bytesToCp);

        short[] arr = new short[arrSize];

        long off = BYTE_ARR_OFF + advanceOffset(bytesToCp);

        if (IS_BIG_ENDIAN) {
            for (int i = 0; i < arr.length; i++) {
                arr[i] = GridUnsafe.getShortLittleEndian(buf, off);

                off += 2;
            }
        } else {
            GridUnsafe.copyMemory(buf, off, arr, SHORT_ARR_OFF, bytesToCp);
        }

        return arr;
    }

    /** {@inheritDoc} */
    @Override
    public int[] readIntArray(int arrSize) throws IOException {
        int bytesToCp = arrSize << 2;

        fromStream(bytesToCp);

        int[] arr = new int[arrSize];

        long off = BYTE_ARR_OFF + advanceOffset(bytesToCp);

        if (IS_BIG_ENDIAN) {
            for (int i = 0; i < arr.length; i++) {
                arr[i] = GridUnsafe.getIntLittleEndian(buf, off);

                off += 4;
            }
        } else {
            GridUnsafe.copyMemory(buf, off, arr, INT_ARR_OFF, bytesToCp);
        }

        return arr;
    }

    /** {@inheritDoc} */
    @Override
    public double[] readDoubleArray(int arrSize) throws IOException {
        int bytesToCp = arrSize << 3;

        fromStream(bytesToCp);

        double[] arr = new double[arrSize];

        long off = BYTE_ARR_OFF + advanceOffset(bytesToCp);

        if (IS_BIG_ENDIAN) {
            for (int i = 0; i < arr.length; i++) {
                arr[i] = GridUnsafe.getDoubleLittleEndian(buf, off);

                off += 8;
            }
        } else {
            GridUnsafe.copyMemory(buf, off, arr, DOUBLE_ARR_OFF, bytesToCp);
        }

        return arr;
    }

    /** {@inheritDoc} */
    @Override
    public boolean[] readBooleanArray(int arrSize) throws IOException {
        boolean[] vals = new boolean[arrSize];

        for (int i = 0; i < arrSize; i++) {
            vals[i] = readBoolean();
        }

        return vals;
    }

    /** {@inheritDoc} */
    @Override
    public char[] readCharArray(int arrSize) throws IOException {
        int bytesToCp = arrSize << 1;

        fromStream(bytesToCp);

        char[] arr = new char[arrSize];

        long off = BYTE_ARR_OFF + advanceOffset(bytesToCp);

        if (IS_BIG_ENDIAN) {
            for (int i = 0; i < arr.length; i++) {
                arr[i] = GridUnsafe.getCharLittleEndian(buf, off);

                off += 2;
            }
        } else {
            GridUnsafe.copyMemory(buf, off, arr, CHAR_ARR_OFF, bytesToCp);
        }

        return arr;
    }

    /** {@inheritDoc} */
    @Override
    public BigInteger readBigInteger() throws IOException {
        int length = readInt();
        byte[] bytes = readByteArray(length);
        return new BigInteger(bytes);
    }

    /** {@inheritDoc} */
    @Override
    public BigDecimal readBigDecimal() throws IOException {
        short scale = readShort();
        BigInteger bigInteger = readBigInteger();

        return new BigDecimal(bigInteger, scale);
    }

    /** {@inheritDoc} */
    @Override
    public LocalTime readLocalTime() throws IOException {
        byte hour = readByte();
        byte minute = readByte();
        byte second = readByte();
        int nano = readInt();

        return LocalTime.of(hour, minute, second, nano);
    }

    /** {@inheritDoc} */
    @Override
    public LocalDate readLocalDate() throws IOException {
        int year = readInt();
        short month = readShort();
        short day = readShort();

        return LocalDate.of(year, month, day);
    }

    /** {@inheritDoc} */
    @Override
    public LocalDateTime readLocalDateTime() throws IOException {
        int year = readInt();
        short month = readShort();
        short day = readShort();
        byte hour = readByte();
        byte minute = readByte();
        byte second = readByte();
        int nano = readInt();

        return LocalDateTime.of(year, month, day, hour, minute, second, nano);
    }

    /** {@inheritDoc} */
    @Override
    public Instant readInstant() throws IOException {
        long epochSecond = readLong();
        int nano = readInt();
        return Instant.ofEpochSecond(epochSecond, nano);
    }


    /** {@inheritDoc} */
    @Override
    public Duration readDuration() throws IOException {
        long seconds = readLong();
        int nano = readInt();

        return Duration.ofSeconds(seconds, nano);
    }

    /** {@inheritDoc} */
    @Override
    public Period readPeriod() throws IOException {
        int years = readInt();
        int months = readInt();
        int days = readInt();

        return Period.of(years, months, days);
    }

    /** {@inheritDoc} */
    @Override
    public UUID readUuid() throws IOException {
        long high = readLong();
        long low = readLong();

        return new UUID(high, low);
    }

    /** {@inheritDoc} */
    @Override
    public BitSet readBitSet() throws IOException {
        int length = readInt();
        byte[] bytes = readByteArray(length);

        return BitSet.valueOf(bytes);
    }


    /** {@inheritDoc} */
    @Override
    public long[] readLongArray(int arrSize) throws IOException {
        int bytesToCp = arrSize << 3;

        fromStream(bytesToCp);

        long[] arr = new long[arrSize];

        long off = BYTE_ARR_OFF + advanceOffset(bytesToCp);

        if (IS_BIG_ENDIAN) {
            for (int i = 0; i < arr.length; i++) {
                arr[i] = GridUnsafe.getLongLittleEndian(buf, off);

                off += 8;
            }
        } else {
            GridUnsafe.copyMemory(buf, off, arr, LONG_ARR_OFF, bytesToCp);
        }

        return arr;
    }

    /** {@inheritDoc} */
    @Override
    public float[] readFloatArray(int arrSize) throws IOException {
        int bytesToCp = arrSize << 2;

        fromStream(bytesToCp);

        float[] arr = new float[arrSize];

        long off = BYTE_ARR_OFF + advanceOffset(bytesToCp);

        if (IS_BIG_ENDIAN) {
            for (int i = 0; i < arr.length; i++) {
                arr[i] = GridUnsafe.getFloatLittleEndian(buf, off);

                off += 4;
            }
        } else {
            GridUnsafe.copyMemory(buf, off, arr, FLOAT_ARR_OFF, bytesToCp);
        }

        return arr;
    }

    /** {@inheritDoc} */
    @Override
    public int readFewBytes(byte[] b, int off, int len) throws IOException {
        Objects.checkFromIndexSize(off, len, b.length);

        int n = 0;
        while (n < len) {
            int count = read(b, off + n, len - n);
            if (count < 0) {
                break;
            }
            n += count;
        }
        return n;
    }

    /** {@inheritDoc} */
    @Override
    public void readFully(byte[] b) throws IOException {
        int len = b.length;

        fromStream(len);

        System.arraycopy(buf, advanceOffset(len), b, 0, len);
    }

    /** {@inheritDoc} */
    @SuppressWarnings("NullableProblems")
    @Override
    public void readFully(byte[] b, int off, int len) throws IOException {
        fromStream(len);

        System.arraycopy(buf, advanceOffset(len), b, off, len);
    }

    /** {@inheritDoc} */
    @Override
    public int skipBytes(int n) {
        if (off + n > max) {
            n = max - off;
        }

        off += n;

        return n;
    }

    /** {@inheritDoc} */
    @Override
    public boolean readBoolean() throws IOException {
        fromStream(1);

        return GridUnsafe.getBoolean(buf, BYTE_ARR_OFF + advanceOffset(1));
    }

    /** {@inheritDoc} */
    @Override
    public byte readByte() throws IOException {
        fromStream(1);

        return GridUnsafe.getByte(buf, BYTE_ARR_OFF + advanceOffset(1));
    }

    /** {@inheritDoc} */
    @Override
    public int readUnsignedByte() throws IOException {
        return readByte() & 0xff;
    }

    /** {@inheritDoc} */
    @Override
    public short readShort() throws IOException {
        fromStream(2);

        long off = BYTE_ARR_OFF + advanceOffset(2);

        return IS_BIG_ENDIAN ? GridUnsafe.getShortLittleEndian(buf, off) : GridUnsafe.getShort(buf, off);
    }

    /** {@inheritDoc} */
    @Override
    public int readUnsignedShort() throws IOException {
        return readShort() & 0xffff;
    }

    /** {@inheritDoc} */
    @Override
    public char readChar() throws IOException {
        fromStream(2);

        long off = BYTE_ARR_OFF + this.off;

        char v = IS_BIG_ENDIAN ? GridUnsafe.getCharLittleEndian(buf, off) : GridUnsafe.getChar(buf, off);

        advanceOffset(2);

        return v;
    }

    /** {@inheritDoc} */
    @Override
    public int readInt() throws IOException {
        fromStream(4);

        long off = BYTE_ARR_OFF + advanceOffset(4);

        return IS_BIG_ENDIAN ? GridUnsafe.getIntLittleEndian(buf, off) : GridUnsafe.getInt(buf, off);
    }

    /** {@inheritDoc} */
    @Override
    public long readLong() throws IOException {
        fromStream(8);

        long off = BYTE_ARR_OFF + advanceOffset(8);

        return IS_BIG_ENDIAN ? GridUnsafe.getLongLittleEndian(buf, off) : GridUnsafe.getLong(buf, off);
    }

    /** {@inheritDoc} */
    @Override
    public float readFloat() throws IOException {
        int v = readInt();

        return Float.intBitsToFloat(v);
    }

    /** {@inheritDoc} */
    @Override
    public double readDouble() throws IOException {
        long v = readLong();

        return Double.longBitsToDouble(v);
    }

    /** {@inheritDoc} */
    @Override
    public int read() throws IOException {
        try {
            return readUnsignedByte();
        } catch (EOFException ignored) {
            return -1;
        }
    }

    /** {@inheritDoc} */
    @SuppressWarnings("NullableProblems")
    @Override
    public int read(byte[] b, int off, int len) throws IOException {
        if (b == null) {
            throw new NullPointerException();
        }

        if (off < 0 || len < 0 || len > b.length - off) {
            throw new IndexOutOfBoundsException();
        }

        if (len == 0) {
            return 0;
        }

        if (in != null) {
            return in.read(b, off, len);
        } else {
            int toRead = Math.min(len, max - this.off);
            if (toRead <= 0) {
                return -1;
            }

            System.arraycopy(buf, advanceOffset(toRead), b, off, toRead);

            return toRead;
        }
    }

    @Override
    public long readVarInt() throws IOException {
        return VarIntUtils.readVarInt(this);
    }

    /** {@inheritDoc} */
    @Override
    public String readLine() throws IOException {
        StringBuilder sb = new StringBuilder();

        int b;

        while ((b = read()) >= 0) {
            char c = (char) b;

            switch (c) {
                case '\n':
                    return sb.toString();

                case '\r':
                    b = read();

                    if (b < 0 || b == '\n') {
                        return sb.toString();
                    } else {
                        sb.append((char) b);
                    }

                    break;

                default:
                    sb.append(c);
            }
        }

        return sb.toString();
    }

    /** {@inheritDoc} */
    @Override
    public String readUTF() throws IOException {
        return readUtfBody(readVarIntAsInt());
    }

    /**
     * Reads in the "body" (i.e., the UTF representation minus the 2-byte
     * or 8-byte length header) of a UTF encoding, which occupies the next
     * utfLen bytes.
     *
     * @param utfLen UTF encoding length.
     * @return String.
     * @throws IOException In case of error.
     */
    private String readUtfBody(long utfLen) throws IOException {
        StringBuilder sbuf = new StringBuilder();

        end = pos = 0;

        while (utfLen > 0) {
            int avail = end - pos;

            if (avail >= 3 || avail == utfLen) {
                utfLen -= readUtfSpan(sbuf, utfLen);
            } else {
                // shift and refill buffer manually
                if (avail > 0) {
                    System.arraycopy(utfBuf, pos, utfBuf, 0, avail);
                }

                pos = 0;
                end = (int) Math.min(MAX_BLOCK_SIZE, utfLen);

                readFully(utfBuf, avail, end - avail);
            }
        }

        return sbuf.toString();
    }

    /**
     * Reads span of UTF-encoded characters out of internal buffer
     * (starting at offset pos and ending at or before offset end),
     * consuming no more than utfLen bytes. Appends read characters to
     * sbuf. Returns the number of bytes consumed.
     *
     * @param sbuf String builder.
     * @param utfLen UTF encoding length.
     * @return Number of bytes consumed.
     * @throws IOException In case of error.
     */
    @SuppressWarnings("ThrowFromFinallyBlock")
    private long readUtfSpan(StringBuilder sbuf, long utfLen) throws IOException {
        int cpos = 0;
        int start = pos;
        int avail = Math.min(end - pos, CHAR_BUF_SIZE);
        int stop = pos + ((utfLen > avail) ? avail - 2 : (int) utfLen);
        boolean outOfBounds = false;

        try {
            while (pos < stop) {
                int b1 = utfBuf[pos++] & 0xFF;

                int b2;
                int b3;

                switch (b1 >> 4) {
                    case 0:
                    case 1:
                    case 2:
                    case 3:
                    case 4:
                    case 5:
                    case 6:
                    case 7:
                        // 1 byte format: 0xxxxxxx
                        utfCharBuf[cpos++] = (char) b1;

                        break;

                    case 12:
                    case 13:
                        // 2 byte format: 110xxxxx 10xxxxxx
                        b2 = utfBuf[pos++];

                        if ((b2 & 0xC0) != 0x80) {
                            throw new UTFDataFormatException();
                        }

                        utfCharBuf[cpos++] = (char) (((b1 & 0x1F) << 6) | (b2 & 0x3F));

                        break;

                    case 14:
                        // 3 byte format: 1110xxxx 10xxxxxx 10xxxxxx
                        b3 = utfBuf[pos + 1];
                        b2 = utfBuf[pos];

                        pos += 2;

                        if ((b2 & 0xC0) != 0x80 || (b3 & 0xC0) != 0x80) {
                            throw new UTFDataFormatException();
                        }

                        utfCharBuf[cpos++] = (char) (((b1 & 0x0F) << 12) | ((b2 & 0x3F) << 6) | (b3 & 0x3F));

                        break;

                    default:
                        // 10xx xxxx, 1111 xxxx
                        throw new UTFDataFormatException();
                }
            }
        } catch (ArrayIndexOutOfBoundsException ignored) {
            outOfBounds = true;
        } finally {
            if (outOfBounds || (pos - start) > utfLen) {
                pos = start + (int) utfLen;

                throw new UTFDataFormatException();
            }
        }

        sbuf.append(utfCharBuf, 0, cpos);

        return pos - start;
    }

    /** {@inheritDoc} */
    @Override
    public void mark(int readLimit) {
        if (in != null) {
            in.mark(readLimit);
        } else {
            mark = off;
        }
    }

    /** {@inheritDoc} */
    @Override
    public boolean markSupported() {
        return in == null || in.markSupported();
    }

    /** {@inheritDoc} */
    @Override
    public int available() throws IOException {
        if (in != null) {
            return in.available();
        } else {
            return Math.max(buf.length - off, 0);
        }
    }

    /** {@inheritDoc} */
    @Override
    public <T> T materializeFromNextBytes(int bytesCount, Materializer<? extends T> materializer) throws IOException {
        fromStream(bytesCount);

        int prevOffset = advanceOffset(bytesCount);

        return materializer.materialize(buf, prevOffset, bytesCount);
    }

    /** {@inheritDoc} */
    @Override
    public String toString() {
        return IgniteToStringBuilder.toString(IgniteUnsafeDataInput.class, this);
    }
}
