/*
 * Copyright 2018 ConsenSys AG.
 *
 * Licensed 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 net.consensys.cava.rlp;

import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Objects.requireNonNull;

import net.consensys.cava.bytes.Bytes;
import net.consensys.cava.units.bigints.UInt256;

import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.Function;

/**
 * A reader for consuming values from an RLP encoded source.
 */
public interface RLPReader {

  /**
   * Determine if this reader is lenient by default.
   * <p>
   * A non-lenient reader will throw {@link InvalidRLPEncodingException} from any read method if the source RLP has not
   * used a minimal encoding format for the value.
   * 
   * @return {@code true} if the reader is lenient, and {@code false} otherwise (default).
   */
  boolean isLenient();

  /**
   * Read the next value from the RLP source.
   *
   * @return The bytes for the next value.
   * @throws InvalidRLPEncodingException If there is an error decoding the RLP source.
   * @throws EndOfRLPException If there are no more RLP values to read.
   */
  default Bytes readValue() {
    return readValue(isLenient());
  }

  /**
   * Read the next value from the RLP source.
   *
   * @param lenient If {@code false}, an exception will be thrown if the value is not minimally encoded.
   * @return The bytes for the next value.
   * @throws InvalidRLPEncodingException If there is an error decoding the RLP source.
   * @throws EndOfRLPException If there are no more RLP values to read.
   */
  Bytes readValue(boolean lenient);

  /**
   * Read a byte array from the RLP source.
   *
   * @return The byte array for the next value.
   * @throws InvalidRLPEncodingException If there is an error decoding the RLP source.
   * @throws EndOfRLPException If there are no more RLP values to read.
   */
  default byte[] readByteArray() {
    return readValue().toArrayUnsafe();
  }

  /**
   * Read a byte from the RLP source.
   *
   * @return The byte for the next value.
   * @throws InvalidRLPEncodingException If there is an error decoding the RLP source.
   * @throws EndOfRLPException If there are no more RLP values to read.
   */
  default byte readByte() {
    return readByte(isLenient());
  }

  /**
   * Read a byte from the RLP source.
   *
   * @param lenient If {@code false}, an exception will be thrown if the byte is not minimally encoded.
   * @return The byte for the next value.
   * @throws InvalidRLPEncodingException If there is an error decoding the RLP source.
   * @throws EndOfRLPException If there are no more RLP values to read.
   */
  default byte readByte(boolean lenient) {
    Bytes bytes = readValue(lenient);
    if (bytes.size() != 1) {
      throw new InvalidRLPTypeException("Value is not a single byte");
    }
    return bytes.get(0);
  }

  /**
   * Read an integer value from the RLP source.
   *
   * @return An integer.
   * @throws InvalidRLPEncodingException If there is an error decoding the RLP source.
   * @throws InvalidRLPTypeException If the next RLP value cannot be represented as an integer.
   * @throws EndOfRLPException If there are no more RLP values to read.
   */
  default int readInt() {
    return readInt(isLenient());
  }

  /**
   * Read an integer value from the RLP source.
   *
   * @param lenient If {@code false}, an exception will be thrown if the integer is not minimally encoded.
   * @return An integer.
   * @throws InvalidRLPEncodingException If there is an error decoding the RLP source, or the integer is not minimally
   *         encoded and `lenient` is {@code false}.
   * @throws InvalidRLPTypeException If the next RLP value cannot be represented as an integer.
   * @throws EndOfRLPException If there are no more RLP values to read.
   */
  default int readInt(boolean lenient) {
    Bytes bytes = readValue();
    if (!lenient && bytes.hasLeadingZeroByte()) {
      throw new InvalidRLPEncodingException("Integer value was not minimally encoded");
    }
    try {
      return bytes.toInt();
    } catch (IllegalArgumentException e) {
      throw new InvalidRLPTypeException("Value is too large to be represented as an int");
    }
  }

  /**
   * Read a long value from the RLP source.
   *
   * @return A long.
   * @throws InvalidRLPEncodingException If there is an error decoding the RLP source.
   * @throws InvalidRLPTypeException If the next RLP value cannot be represented as a long.
   * @throws EndOfRLPException If there are no more RLP values to read.
   */
  default long readLong() {
    return readLong(isLenient());
  }

  /**
   * Read a long value from the RLP source.
   *
   * @param lenient If {@code false}, an exception will be thrown if the integer is not minimally encoded.
   * @return A long.
   * @throws InvalidRLPEncodingException If there is an error decoding the RLP source, or the integer is not minimally
   *         encoded and `lenient` is {@code false}.
   * @throws InvalidRLPTypeException If the next RLP value cannot be represented as a long.
   * @throws EndOfRLPException If there are no more RLP values to read.
   */
  default long readLong(boolean lenient) {
    Bytes bytes = readValue();
    if (!lenient && bytes.hasLeadingZeroByte()) {
      throw new InvalidRLPEncodingException("Integer value was not minimally encoded");
    }
    try {
      return bytes.toLong();
    } catch (IllegalArgumentException e) {
      throw new InvalidRLPTypeException("Value is too large to be represented as a long");
    }
  }

  /**
   * Read a {@link UInt256} value from the RLP source.
   *
   * @return A {@link UInt256} value.
   * @throws InvalidRLPEncodingException If there is an error decoding the RLP source.
   * @throws InvalidRLPTypeException If the next RLP value cannot be represented as a long.
   * @throws EndOfRLPException If there are no more RLP values to read.
   */
  default UInt256 readUInt256() {
    return readUInt256(isLenient());
  }

  /**
   * Read a {@link UInt256} value from the RLP source.
   *
   * @param lenient If {@code false}, an exception will be thrown if the integer is not minimally encoded.
   * @return A {@link UInt256} value.
   * @throws InvalidRLPEncodingException If there is an error decoding the RLP source, or the integer is not minimally
   *         encoded and `lenient` is {@code false}.
   * @throws InvalidRLPTypeException If the next RLP value cannot be represented as a long.
   * @throws EndOfRLPException If there are no more RLP values to read.
   */
  default UInt256 readUInt256(boolean lenient) {
    Bytes bytes = readValue();
    if (!lenient && bytes.hasLeadingZeroByte()) {
      throw new InvalidRLPEncodingException("Integer value was not minimally encoded");
    }
    try {
      return UInt256.fromBytes(bytes);
    } catch (IllegalArgumentException e) {
      throw new InvalidRLPTypeException("Value is too large to be represented as a UInt256");
    }
  }

  /**
   * Read a big integer value from the RLP source.
   *
   * @return A big integer.
   * @throws InvalidRLPEncodingException If there is an error decoding the RLP source.
   * @throws InvalidRLPTypeException If the next RLP value cannot be represented as a big integer.
   * @throws EndOfRLPException If there are no more RLP values to read.
   */
  default BigInteger readBigInteger() {
    return readBigInteger(isLenient());
  }

  /**
   * Read a big integer value from the RLP source.
   *
   * @param lenient If {@code false}, an exception will be thrown if the integer is not minimally encoded.
   * @return A big integer.
   * @throws InvalidRLPEncodingException If there is an error decoding the RLP source, or the integer is not minimally
   *         encoded and `lenient` is {@code false}.
   * @throws InvalidRLPTypeException If the next RLP value cannot be represented as a big integer.
   * @throws EndOfRLPException If there are no more RLP values to read.
   */
  default BigInteger readBigInteger(boolean lenient) {
    Bytes bytes = readValue();
    if (!lenient && bytes.hasLeadingZeroByte()) {
      throw new InvalidRLPEncodingException("Integer value was not minimally encoded");
    }
    return bytes.toUnsignedBigInteger();
  }

  /**
   * Read a string value from the RLP source.
   *
   * @return A string.
   * @throws InvalidRLPEncodingException If there is an error decoding the RLP source.
   * @throws InvalidRLPTypeException If the next RLP value cannot be represented as a string.
   * @throws EndOfRLPException If there are no more RLP values to read.
   */
  default String readString() {
    return readString(isLenient());
  }

  /**
   * Read a string value from the RLP source.
   *
   * @param lenient If {@code false}, an exception will be thrown if the integer is not minimally encoded.
   * @return A string.
   * @throws InvalidRLPEncodingException If there is an error decoding the RLP source.
   * @throws InvalidRLPTypeException If the next RLP value cannot be represented as a string.
   * @throws EndOfRLPException If there are no more RLP values to read.
   */
  default String readString(boolean lenient) {
    return new String(readValue(lenient).toArrayUnsafe(), UTF_8);
  }

  /**
   * Check if the next item to be read is a list.
   *
   * @return {@code true} if the next item to be read is a list.
   * @throws EndOfRLPException If there are no more RLP values to read.
   */
  boolean nextIsList();

  /**
   * Check if the next item to be read is empty.
   *
   * @return {@code true} if the next item to be read is empty.
   * @throws EndOfRLPException If there are no more RLP values to read.
   */
  boolean nextIsEmpty();

  /**
   * Read a list of values from the RLP source.
   *
   * @param fn A function that will be provided a {@link RLPReader}.
   * @param <T> The result type of the reading function.
   * @return The result from the reading function.
   * @throws InvalidRLPEncodingException If there is an error decoding the RLP source.
   * @throws InvalidRLPTypeException If the next RLP value is not a list.
   * @throws EndOfRLPException If there are no more RLP values to read.
   */
  default <T> T readList(Function<RLPReader, T> fn) {
    return readList(isLenient(), fn);
  }

  /**
   * Read a list of values from the RLP source.
   *
   * @param lenient If {@code false}, an exception will be thrown if the integer is not minimally encoded.
   * @param fn A function that will be provided a {@link RLPReader}.
   * @param <T> The result type of the reading function.
   * @return The result from the reading function.
   * @throws InvalidRLPEncodingException If there is an error decoding the RLP source.
   * @throws InvalidRLPTypeException If the next RLP value is not a list.
   * @throws EndOfRLPException If there are no more RLP values to read.
   */
  <T> T readList(boolean lenient, Function<RLPReader, T> fn);

  /**
   * Read a list of values from the RLP source, populating a mutable output list.
   *
   * @param fn A function that will be provided with a {@link RLPReader} and a mutable output list.
   * @return The list supplied to {@code fn}.
   * @throws InvalidRLPEncodingException If there is an error decoding the RLP source.
   * @throws InvalidRLPTypeException If the next RLP value is not a list.
   * @throws EndOfRLPException If there are no more RLP values to read.
   */
  default List<Object> readList(BiConsumer<RLPReader, List<Object>> fn) {
    return readList(isLenient(), fn);
  }

  /**
   * Read a list of values from the RLP source, populating a mutable output list.
   *
   * @param lenient If {@code false}, an exception will be thrown if the integer is not minimally encoded.
   * @param fn A function that will be provided with a {@link RLPReader} and a mutable output list.
   * @return The list supplied to {@code fn}.
   * @throws InvalidRLPEncodingException If there is an error decoding the RLP source.
   * @throws InvalidRLPTypeException If the next RLP value is not a list.
   * @throws EndOfRLPException If there are no more RLP values to read.
   */
  default List<Object> readList(boolean lenient, BiConsumer<RLPReader, List<Object>> fn) {
    requireNonNull(fn);
    return readList(lenient, reader -> {
      List<Object> list = new ArrayList<>();
      fn.accept(reader, list);
      return list;
    });
  }

  /**
   * Skip the next value or list in the RLP source.
   *
   * @throws InvalidRLPEncodingException If there is an error decoding the RLP source.
   * @throws EndOfRLPException If there are no more RLP values to read.
   */
  default void skipNext() {
    skipNext(isLenient());
  }

  /**
   * Skip the next value or list in the RLP source.
   *
   * @param lenient If {@code false}, an exception will be thrown if the integer is not minimally encoded.
   * @throws InvalidRLPEncodingException If there is an error decoding the RLP source.
   * @throws EndOfRLPException If there are no more RLP values to read.
   */
  void skipNext(boolean lenient);

  /**
   * The number of remaining values to read.
   *
   * @return The number of remaining values to read.
   */
  int remaining();

  /**
   * Check if all values have been read.
   *
   * @return {@code true} if all values have been read.
   */
  boolean isComplete();
}
