/*
 * 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 java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.UTFDataFormatException;
import org.apache.ignite.internal.util.GridUnsafe;
import org.apache.ignite.internal.util.tostring.GridToStringExclude;
import org.apache.ignite.internal.util.typedef.internal.S;
import org.apache.ignite.internal.util.typedef.internal.SB;
import org.apache.ignite.internal.util.typedef.internal.U;

import static org.apache.ignite.IgniteSystemProperties.IGNITE_MARSHAL_BUFFERS_RECHECK;
import static org.apache.ignite.internal.util.GridUnsafe.BIG_ENDIAN;
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.LONG_ARR_OFF;
import static org.apache.ignite.internal.util.GridUnsafe.SHORT_ARR_OFF;

/**
 * Data input based on {@code Unsafe} operations.
 */
public class GridUnsafeDataInput extends InputStream implements GridDataInput {
    /** */
    private static final Long CHECK_FREQ = Long.getLong(IGNITE_MARSHAL_BUFFERS_RECHECK, 10000);

    /** 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;

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

    /** Char buffer for fast string reads. */
    @GridToStringExclude
    private final char[] urfCBuf = 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. */
    @GridToStringExclude
    private byte[] buf;

    /** Offset. */
    private int off;

    /** Max. */
    private int max;

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

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

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

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

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

    /**
     * @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);

        long now = U.currentTimeMillis();

        // Increase size of buffer if needed.
        if (size > inBuf.length)
            buf = inBuf = new byte[Math.max(inBuf.length << 1, size)]; // Grow.
        else if (now - lastCheck > CHECK_FREQ) {
            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;
            lastCheck = 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;
        }
    }

    /**
     * @param more Bytes to move forward.
     * @return Old offset value.
     * @throws IOException In case of error.
     */
    private int offset(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 {
        in = null;

        off = 0;
        max = 0;
    }

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

        fromStream(arrSize);

        byte[] arr = new byte[arrSize];

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

        return arr;
    }

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

        int bytesToCp = arrSize << 1;

        fromStream(bytesToCp);

        short[] arr = new short[arrSize];

        long off = BYTE_ARR_OFF + offset(bytesToCp);

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

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

        return arr;
    }

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

        int bytesToCp = arrSize << 2;

        fromStream(bytesToCp);

        int[] arr = new int[arrSize];

        long off = BYTE_ARR_OFF + offset(bytesToCp);

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

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

        return arr;
    }

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

        int bytesToCp = arrSize << 3;

        fromStream(bytesToCp);

        double[] arr = new double[arrSize];

        long off = BYTE_ARR_OFF + offset(bytesToCp);

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

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

        return arr;
    }

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

        boolean[] vals = new boolean[arrSize];

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

        return vals;
    }

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

        int bytesToCp = arrSize << 1;

        fromStream(bytesToCp);

        char[] arr = new char[arrSize];

        long off = BYTE_ARR_OFF + offset(bytesToCp);

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

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

        return arr;
    }

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

        int bytesToCp = arrSize << 3;

        fromStream(bytesToCp);

        long[] arr = new long[arrSize];

        long off = BYTE_ARR_OFF + offset(bytesToCp);

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

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

        return arr;
    }

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

        int bytesToCp = arrSize << 2;

        fromStream(bytesToCp);

        float[] arr = new float[arrSize];

        long off = BYTE_ARR_OFF + offset(bytesToCp);

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

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

        return arr;
    }

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

        fromStream(len);

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

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

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

    /** {@inheritDoc} */
    @Override public int skipBytes(int n) throws IOException {
        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 + offset(1));
    }

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

        return GridUnsafe.getByte(buf, BYTE_ARR_OFF + offset(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 + offset(2);

        return BIG_ENDIAN ? GridUnsafe.getShortLE(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 = BIG_ENDIAN ? GridUnsafe.getCharLE(buf, off) : GridUnsafe.getChar(buf, off);

        offset(2);

        return v;
    }

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

        long off = BYTE_ARR_OFF + offset(4);

        return BIG_ENDIAN ? GridUnsafe.getIntLE(buf, off) : GridUnsafe.getInt(buf, off);
    }

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

        long off = BYTE_ARR_OFF + offset(8);

        return BIG_ENDIAN ? GridUnsafe.getLongLE(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} */
    @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);

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

            return toRead;
        }
    }

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

        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.a((char)b);

                    break;

                default:
                    sb.a(c);
            }
        }

        return sb.toString();
    }

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

    /**
     * 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 || (long)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, b3;

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

                        break;

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

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

                        urfCBuf[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();

                        urfCBuf[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(urfCBuf, 0, cpos);

        return pos - start;
    }

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