/*
 * 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
 *
 *     https://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.trevni;

import java.io.EOFException;
import java.io.IOException;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;

/** Used to read values. */
class InputBuffer {
  private Input in;

  private long inLength;
  private long offset; // pos of next read from in

  private byte[] buf; // data from input
  private int pos; // position within buffer
  private int limit; // end of valid buffer data

  private CharsetDecoder utf8 = Charset.forName("UTF-8").newDecoder();

  private int bitCount; // position in booleans

  private int runLength; // length of run
  private int runValue; // value of run

  public InputBuffer(Input in) throws IOException {
    this(in, 0);
  }

  public InputBuffer(Input in, long position) throws IOException {
    this.in = in;
    this.inLength = in.length();
    this.offset = position;

    if (in instanceof InputBytes) { // use buffer directly
      this.buf = ((InputBytes) in).getBuffer();
      this.limit = (int) in.length();
      this.offset = limit;
      this.pos = (int) position;
    } else { // create new buffer
      this.buf = new byte[8192]; // big enough for primitives
    }
  }

  public void seek(long position) throws IOException {
    runLength = 0;
    if (position >= (offset - limit) && position <= offset) {
      pos = (int) (limit - (offset - position)); // seek in buffer;
      return;
    }
    pos = 0;
    limit = 0;
    offset = position;
  }

  public long tell() {
    return (offset - limit) + pos;
  }

  public long length() {
    return inLength;
  }

  /** The number of bytes remaining to be read from the underlying input. */
  public long remaining() {
    return inLength - tell();
  }

  /**
   * Validate a length or item count read from the input before it is used to size
   * an allocation. Rejects a negative value, and - when the number of bytes
   * remaining in the input is known - a value that could not possibly be backed
   * by the data that follows, assuming each counted element occupies at least
   * {@code minBytesPerElement} bytes on the wire. This guards against a
   * malformed, corrupted, or truncated file driving an oversized (or negative)
   * allocation.
   *
   * @param count              the length or item count read from the input
   * @param minBytesPerElement the minimum number of input bytes each counted
   *                           element occupies (use 1 for a raw byte length)
   * @return {@code count}, if it is valid
   * @throws IOException if {@code count} is negative or larger than the input can
   *                     support
   */
  public int checkLength(int count, long minBytesPerElement) throws IOException {
    if (count < 0)
      throw new IOException("Invalid negative length: " + count);
    long remaining = remaining();
    if (remaining >= 0 && minBytesPerElement > 0 && count > remaining / minBytesPerElement)
      throw new IOException("Length " + count + " exceeds the " + remaining
          + " bytes remaining in the input. The file is likely corrupted or truncated.");
    return count;
  }

  public <T extends Comparable> T readValue(ValueType type) throws IOException {
    switch (type) {
    case NULL:
      return null;
    case BOOLEAN:
      return (T) Boolean.valueOf(readBoolean());
    case INT:
      return (T) Integer.valueOf(readInt());
    case LONG:
      return (T) Long.valueOf(readLong());
    case FIXED32:
      return (T) Integer.valueOf(readFixed32());
    case FIXED64:
      return (T) Long.valueOf(readFixed64());
    case FLOAT:
      return (T) Float.valueOf(readFloat());
    case DOUBLE:
      return (T) Double.valueOf(readDouble());
    case STRING:
      return (T) readString();
    case BYTES:
      return (T) readBytes(null);
    default:
      throw new TrevniRuntimeException("Unknown value type: " + type);
    }
  }

  public void skipValue(ValueType type) throws IOException {
    switch (type) {
    case NULL:
      break;
    case BOOLEAN:
      readBoolean();
      break;
    case INT:
      readInt();
      break;
    case LONG:
      readLong();
      break;
    case FIXED32:
    case FLOAT:
      skip(4);
      break;
    case FIXED64:
    case DOUBLE:
      skip(8);
      break;
    case STRING:
    case BYTES:
      skipBytes();
      break;
    default:
      throw new TrevniRuntimeException("Unknown value type: " + type);
    }
  }

  public boolean readBoolean() throws IOException {
    if (bitCount == 0)
      read();
    int bits = buf[pos - 1] & 0xff;
    int bit = (bits >> bitCount) & 1;
    bitCount++;
    if (bitCount == 8)
      bitCount = 0;
    return bit != 0;
  }

  public int readLength() throws IOException {
    bitCount = 0;
    if (runLength > 0) {
      runLength--; // in run
      return runValue;
    }

    int length = readInt();
    if (length >= 0) // not a run
      return length;

    runLength = (1 - length) >>> 1; // start of run
    runValue = (length + 1) & 1;
    return runValue;
  }

  public int readInt() throws IOException {
    if ((limit - pos) < 5) { // maybe not in buffer
      int b = read();
      int n = b & 0x7f;
      for (int shift = 7; b > 0x7f; shift += 7) {
        b = read();
        n ^= (b & 0x7f) << shift;
      }
      return (n >>> 1) ^ -(n & 1); // back to two's-complement
    }
    int len = 1;
    int b = buf[pos] & 0xff;
    int n = b & 0x7f;
    if (b > 0x7f) {
      b = buf[pos + len++] & 0xff;
      n ^= (b & 0x7f) << 7;
      if (b > 0x7f) {
        b = buf[pos + len++] & 0xff;
        n ^= (b & 0x7f) << 14;
        if (b > 0x7f) {
          b = buf[pos + len++] & 0xff;
          n ^= (b & 0x7f) << 21;
          if (b > 0x7f) {
            b = buf[pos + len++] & 0xff;
            n ^= (b & 0x7f) << 28;
            if (b > 0x7f) {
              throw new IOException("Invalid int encoding");
            }
          }
        }
      }
    }
    pos += len;
    if (pos > limit)
      throw new EOFException();
    return (n >>> 1) ^ -(n & 1); // back to two's-complement
  }

  public long readLong() throws IOException {
    if ((limit - pos) < 10) { // maybe not in buffer
      int b = read();
      long n = b & 0x7f;
      for (int shift = 7; b > 0x7f; shift += 7) {
        b = read();
        n ^= (b & 0x7fL) << shift;
      }
      return (n >>> 1) ^ -(n & 1); // back to two's-complement
    }

    int b = buf[pos++] & 0xff;
    int n = b & 0x7f;
    long l;
    if (b > 0x7f) {
      b = buf[pos++] & 0xff;
      n ^= (b & 0x7f) << 7;
      if (b > 0x7f) {
        b = buf[pos++] & 0xff;
        n ^= (b & 0x7f) << 14;
        if (b > 0x7f) {
          b = buf[pos++] & 0xff;
          n ^= (b & 0x7f) << 21;
          if (b > 0x7f) {
            // only the low 28 bits can be set, so this won't carry
            // the sign bit to the long
            l = innerLongDecode((long) n);
          } else {
            l = n;
          }
        } else {
          l = n;
        }
      } else {
        l = n;
      }
    } else {
      l = n;
    }
    if (pos > limit) {
      throw new EOFException();
    }
    return (l >>> 1) ^ -(l & 1); // back to two's-complement
  }

  // splitting readLong up makes it faster because of the JVM does more
  // optimizations on small methods
  private long innerLongDecode(long l) throws IOException {
    int len = 1;
    int b = buf[pos] & 0xff;
    l ^= (b & 0x7fL) << 28;
    if (b > 0x7f) {
      b = buf[pos + len++] & 0xff;
      l ^= (b & 0x7fL) << 35;
      if (b > 0x7f) {
        b = buf[pos + len++] & 0xff;
        l ^= (b & 0x7fL) << 42;
        if (b > 0x7f) {
          b = buf[pos + len++] & 0xff;
          l ^= (b & 0x7fL) << 49;
          if (b > 0x7f) {
            b = buf[pos + len++] & 0xff;
            l ^= (b & 0x7fL) << 56;
            if (b > 0x7f) {
              b = buf[pos + len++] & 0xff;
              l ^= (b & 0x7fL) << 63;
              if (b > 0x7f) {
                throw new IOException("Invalid long encoding");
              }
            }
          }
        }
      }
    }
    pos += len;
    return l;
  }

  public float readFloat() throws IOException {
    return Float.intBitsToFloat(readFixed32());
  }

  public int readFixed32() throws IOException {
    if ((limit - pos) < 4) // maybe not in buffer
      return read() | (read() << 8) | (read() << 16) | (read() << 24);

    int len = 1;
    int n = (buf[pos] & 0xff) | ((buf[pos + len++] & 0xff) << 8) | ((buf[pos + len++] & 0xff) << 16)
        | ((buf[pos + len++] & 0xff) << 24);
    if ((pos + 4) > limit)
      throw new EOFException();
    pos += 4;
    return n;
  }

  public double readDouble() throws IOException {
    return Double.longBitsToDouble(readFixed64());
  }

  public long readFixed64() throws IOException {
    return (readFixed32() & 0xFFFFFFFFL) | (((long) readFixed32()) << 32);
  }

  public String readString() throws IOException {
    int length = checkLength(readInt(), 1);
    if (length <= (limit - pos)) { // in buffer
      String result = utf8.decode(ByteBuffer.wrap(buf, pos, length)).toString();
      pos += length;
      return result;
    }
    byte[] bytes = new byte[length];
    readFully(bytes, 0, length);
    return utf8.decode(ByteBuffer.wrap(bytes, 0, length)).toString();
  }

  public byte[] readBytes() throws IOException {
    byte[] result = new byte[checkLength(readInt(), 1)];
    readFully(result);
    return result;
  }

  public ByteBuffer readBytes(ByteBuffer old) throws IOException {
    int length = checkLength(readInt(), 1);
    ByteBuffer result;
    if (old != null && length <= old.capacity()) {
      result = old;
      ((Buffer) result).clear();
    } else {
      result = ByteBuffer.allocate(length);
    }
    readFully(result.array(), result.position(), length);
    ((Buffer) result).limit(length);
    return result;
  }

  public void skipBytes() throws IOException {
    skip(readInt());
  }

  private void skip(long length) throws IOException {
    seek(tell() + length);
  }

  public int read() throws IOException {
    if (pos >= limit) {
      limit = readInput(buf, 0, buf.length);
      pos = 0;
    }
    return buf[pos++] & 0xFF;
  }

  public void readFully(byte[] bytes) throws IOException {
    readFully(bytes, 0, bytes.length);
  }

  public void readFully(byte[] bytes, int start, int len) throws IOException {
    int buffered = limit - pos;
    if (len > buffered) { // buffer is insufficient

      System.arraycopy(buf, pos, bytes, start, buffered); // consume buffer
      start += buffered;
      len -= buffered;
      pos += buffered;
      if (len > buf.length) { // bigger than buffer
        do {
          int read = readInput(bytes, start, len); // read directly into result
          len -= read;
          start += read;
        } while (len > 0);
        return;
      }

      limit = readInput(buf, 0, buf.length); // refill buffer
      pos = 0;
    }

    System.arraycopy(buf, pos, bytes, start, len); // copy from buffer
    pos += len;
  }

  private int readInput(byte[] b, int start, int len) throws IOException {
    int read = in.read(offset, b, start, len);
    if (read < 0)
      throw new EOFException();
    offset += read;
    return read;
  }

}
