/*
 * 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.tuweni.bytes;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkElementIndex;
import static com.google.common.base.Preconditions.checkNotNull;
import static java.lang.String.format;
import static java.nio.ByteOrder.BIG_ENDIAN;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.math.BigInteger;
import java.nio.BufferOverflowException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.ReadOnlyBufferException;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Base64;
import java.util.List;
import java.util.Random;

import io.netty.buffer.ByteBuf;
import io.vertx.core.buffer.Buffer;

/**
 * A value made of bytes.
 *
 * <p>
 * This interface makes no thread-safety guarantee, and a {@link Bytes} value is generally not thread safe. However,
 * specific implementations may be thread-safe. For instance, the value returned by {@link #copy} is guaranteed to be
 * thread-safe as it is immutable.
 */
public interface Bytes extends Comparable<Bytes> {

  /**
   * The empty value (with 0 bytes).
   */
  Bytes EMPTY = wrap(new byte[0]);

  /**
   * Wrap the provided byte array as a {@link Bytes} value.
   *
   * <p>
   * Note that value is not copied and thus any future update to {@code value} will be reflected in the returned value.
   *
   * @param value The value to wrap.
   * @return A {@link Bytes} value wrapping {@code value}.
   */
  static Bytes wrap(byte[] value) {
    return wrap(value, 0, value.length);
  }

  /**
   * Wrap a slice of a byte array as a {@link Bytes} value.
   *
   * <p>
   * Note that value is not copied and thus any future update to {@code value} within the slice will be reflected in the
   * returned value.
   *
   * @param value The value to wrap.
   * @param offset The index (inclusive) in {@code value} of the first byte exposed by the returned value. In other
   *        words, you will have {@code wrap(value, o, l).get(0) == value[o]}.
   * @param length The length of the resulting value.
   * @return A {@link Bytes} value that expose the bytes of {@code value} from {@code offset} (inclusive) to
   *         {@code offset + length} (exclusive).
   * @throws IndexOutOfBoundsException if {@code offset < 0 || (value.length > 0 && offset >=
   *     value.length)}.
   * @throws IllegalArgumentException if {@code length < 0 || offset + length > value.length}.
   */
  static Bytes wrap(byte[] value, int offset, int length) {
    checkNotNull(value);
    if (length == 32) {
      return new ArrayWrappingBytes32(value, offset);
    }
    return new ArrayWrappingBytes(value, offset, length);
  }

  /**
   * Wrap the provided byte array as a {@link Bytes} value, encrypted in memory.
   *
   *
   * @param value The value to secure.
   * @return A {@link Bytes} value securing {@code value}.
   */
  static Bytes secure(byte[] value) {
    return secure(value, 0, value.length);
  }

  /**
   * Wrap a slice of a byte array as a {@link Bytes} value, encrypted in memory.
   *
   *
   * @param value The value to secure.
   * @param offset The index (inclusive) in {@code value} of the first byte exposed by the returned value. In other
   *        words, you will have {@code wrap(value, o, l).get(0) == value[o]}.
   * @param length The length of the resulting value.
   * @return A {@link Bytes} value that holds securely the bytes of {@code value} from {@code offset} (inclusive) to
   *         {@code offset + length} (exclusive).
   * @throws IndexOutOfBoundsException if {@code offset < 0 || (value.length > 0 && offset >=
   *     value.length)}.
   * @throws IllegalArgumentException if {@code length < 0 || offset + length > value.length}.
   */
  static Bytes secure(byte[] value, int offset, int length) {
    checkNotNull(value);
    return new GuardedByteArrayBytes(value, offset, length);
  }

  /**
   * Wrap a list of other values into a concatenated view.
   *
   * <p>
   * Note that the values are not copied and thus any future update to the values will be reflected in the returned
   * value. If copying the inputs is desired, use {@link #concatenate(Bytes...)}.
   *
   * @param values The values to wrap.
   * @return A value representing a view over the concatenation of all {@code values}.
   * @throws IllegalArgumentException if the result overflows an int.
   */
  static Bytes wrap(Bytes... values) {
    return ConcatenatedBytes.wrap(values);
  }

  /**
   * Create a value containing the concatenation of the values provided.
   *
   * @param values The values to copy and concatenate.
   * @return A value containing the result of concatenating the value from {@code values} in their provided order.
   * @throws IllegalArgumentException if the result overflows an int.
   */
  static Bytes concatenate(List<Bytes> values) {
    if (values.size() == 0) {
      return EMPTY;
    }

    int size;
    try {
      size = values.stream().mapToInt(Bytes::size).reduce(0, Math::addExact);
    } catch (ArithmeticException e) {
      throw new IllegalArgumentException("Combined length of values is too long (> Integer.MAX_VALUE)");
    }

    MutableBytes result = MutableBytes.create(size);
    int offset = 0;
    for (Bytes value : values) {
      value.copyTo(result, offset);
      offset += value.size();
    }
    return result;
  }

  /**
   * Create a value containing the concatenation of the values provided.
   *
   * @param values The values to copy and concatenate.
   * @return A value containing the result of concatenating the value from {@code values} in their provided order.
   * @throws IllegalArgumentException if the result overflows an int.
   */
  static Bytes concatenate(Bytes... values) {
    if (values.length == 0) {
      return EMPTY;
    }

    int size;
    try {
      size = Arrays.stream(values).mapToInt(Bytes::size).reduce(0, Math::addExact);
    } catch (ArithmeticException e) {
      throw new IllegalArgumentException("Combined length of values is too long (> Integer.MAX_VALUE)");
    }

    MutableBytes result = MutableBytes.create(size);
    int offset = 0;
    for (Bytes value : values) {
      value.copyTo(result, offset);
      offset += value.size();
    }
    return result;
  }

  /**
   * Wrap a full Vert.x {@link Buffer} as a {@link Bytes} value.
   *
   * <p>
   * Note that any change to the content of the buffer may be reflected in the returned value.
   *
   * @param buffer The buffer to wrap.
   * @return A {@link Bytes} value.
   */
  static Bytes wrapBuffer(Buffer buffer) {
    checkNotNull(buffer);
    if (buffer.length() == 0) {
      return EMPTY;
    }
    return new BufferWrappingBytes(buffer);
  }

  /**
   * Wrap a slice of a Vert.x {@link Buffer} as a {@link Bytes} value.
   *
   * <p>
   * Note that any change to the content of the buffer may be reflected in the returned value.
   *
   * @param buffer The buffer to wrap.
   * @param offset The offset in {@code buffer} from which to expose the bytes in the returned value. That is,
   *        {@code wrapBuffer(buffer, i, 1).get(0) == buffer.getByte(i)}.
   * @param size The size of the returned value.
   * @return A {@link Bytes} value.
   * @throws IndexOutOfBoundsException if {@code offset < 0 || (buffer.length() > 0 && offset >=
   *     buffer.length())}.
   * @throws IllegalArgumentException if {@code length < 0 || offset + length > buffer.length()}.
   */
  static Bytes wrapBuffer(Buffer buffer, int offset, int size) {
    checkNotNull(buffer);
    if (size == 0) {
      return EMPTY;
    }
    return new BufferWrappingBytes(buffer, offset, size);
  }

  /**
   * Wrap a full Netty {@link ByteBuf} as a {@link Bytes} value.
   *
   * <p>
   * Note that any change to the content of the byteBuf may be reflected in the returned value.
   *
   * @param byteBuf The {@link ByteBuf} to wrap.
   * @return A {@link Bytes} value.
   */
  static Bytes wrapByteBuf(ByteBuf byteBuf) {
    checkNotNull(byteBuf);
    if (byteBuf.capacity() == 0) {
      return EMPTY;
    }
    return new ByteBufWrappingBytes(byteBuf);
  }

  /**
   * Wrap a slice of a Netty {@link ByteBuf} as a {@link Bytes} value.
   *
   * <p>
   * Note that any change to the content of the buffer may be reflected in the returned value.
   *
   * @param byteBuf The {@link ByteBuf} to wrap.
   * @param offset The offset in {@code byteBuf} from which to expose the bytes in the returned value. That is,
   *        {@code wrapByteBuf(byteBuf, i, 1).get(0) == byteBuf.getByte(i)}.
   * @param size The size of the returned value.
   * @return A {@link Bytes} value.
   * @throws IndexOutOfBoundsException if {@code offset < 0 || (byteBuf.capacity() > 0 && offset >=
   *     byteBuf.capacity())}.
   * @throws IllegalArgumentException if {@code length < 0 || offset + length > byteBuf.capacity()}.
   */
  static Bytes wrapByteBuf(ByteBuf byteBuf, int offset, int size) {
    checkNotNull(byteBuf);
    if (size == 0) {
      return EMPTY;
    }
    return new ByteBufWrappingBytes(byteBuf, offset, size);
  }

  /**
   * Wrap a full Java NIO {@link ByteBuffer} as a {@link Bytes} value.
   *
   * <p>
   * Note that any change to the content of the byteBuf may be reflected in the returned value.
   *
   * @param byteBuffer The {@link ByteBuffer} to wrap.
   * @return A {@link Bytes} value.
   */
  static Bytes wrapByteBuffer(ByteBuffer byteBuffer) {
    checkNotNull(byteBuffer);
    if (byteBuffer.limit() == 0) {
      return EMPTY;
    }
    return new ByteBufferWrappingBytes(byteBuffer);
  }

  /**
   * Wrap a slice of a Java NIO {@link ByteBuf} as a {@link Bytes} value.
   *
   * <p>
   * Note that any change to the content of the buffer may be reflected in the returned value.
   *
   * @param byteBuffer The {@link ByteBuffer} to wrap.
   * @param offset The offset in {@code byteBuffer} from which to expose the bytes in the returned value. That is,
   *        {@code wrapByteBuffer(byteBuffer, i, 1).get(0) == byteBuffer.getByte(i)}.
   * @param size The size of the returned value.
   * @return A {@link Bytes} value.
   * @throws IndexOutOfBoundsException if {@code offset < 0 || (byteBuffer.limit() > 0 && offset >=
   *     byteBuf.limit())}.
   * @throws IllegalArgumentException if {@code length < 0 || offset + length > byteBuffer.limit()}.
   */
  static Bytes wrapByteBuffer(ByteBuffer byteBuffer, int offset, int size) {
    checkNotNull(byteBuffer);
    if (size == 0) {
      return EMPTY;
    }
    return new ByteBufferWrappingBytes(byteBuffer, offset, size);
  }

  /**
   * Create a value that contains the specified bytes in their specified order.
   *
   * @param bytes The bytes that must compose the returned value.
   * @return A value containing the specified bytes.
   */
  static Bytes of(byte... bytes) {
    return wrap(bytes);
  }

  /**
   * Create a value that contains the specified bytes in their specified order.
   *
   * @param bytes The bytes.
   * @return A value containing bytes are the one from {@code bytes}.
   * @throws IllegalArgumentException if any of the specified would be truncated when storing as a byte.
   */
  static Bytes of(int... bytes) {
    byte[] result = new byte[bytes.length];
    for (int i = 0; i < bytes.length; i++) {
      int b = bytes[i];
      checkArgument(b == (((byte) b) & 0xff), "%sth value %s does not fit a byte", i + 1, b);
      result[i] = (byte) b;
    }
    return Bytes.wrap(result);
  }

  /**
   * Return a 2-byte value corresponding to the provided value interpreted as an unsigned short.
   *
   * @param value The value, which must be no larger than an unsigned short.
   * @return A 2 bytes value corresponding to {@code value}.
   * @throws IllegalArgumentException if {@code value < 0} or {@code value} is too big to fit an unsigned 2-byte short
   *         (that is, if {@code value >= (1 << 16)}).
   */
  static Bytes ofUnsignedShort(int value) {
    return ofUnsignedShort(value, BIG_ENDIAN);
  }

  /**
   * Return a 2-byte value corresponding to the provided value interpreted as an unsigned short.
   *
   * @param value The value, which must be no larger than an unsigned short.
   * @param order The byte-order for the integer encoding.
   * @return A 2 bytes value corresponding to {@code value}.
   * @throws IllegalArgumentException if {@code value < 0} or {@code value} is too big to fit an unsigned 2-byte short
   *         (that is, if {@code value >= (1 << 16)}).
   */
  static Bytes ofUnsignedShort(int value, ByteOrder order) {
    checkArgument(
        value >= 0 && value <= BytesValues.MAX_UNSIGNED_SHORT,
        "Value %s cannot be represented as an unsigned short (it is negative or too big)",
        value);
    byte[] res = new byte[2];
    if (order == BIG_ENDIAN) {
      res[0] = (byte) ((value >> 8) & 0xFF);
      res[1] = (byte) (value & 0xFF);
    } else {
      res[0] = (byte) (value & 0xFF);
      res[1] = (byte) ((value >> 8) & 0xFF);
    }
    return Bytes.wrap(res);
  }

  /**
   * Return a 4-byte value corresponding to the provided value interpreted as an unsigned int.
   *
   * @param value The value, which must be no larger than an unsigned int.
   * @return A 4 bytes value corresponding to {@code value}.
   * @throws IllegalArgumentException if {@code value < 0} or {@code value} is too big to fit an unsigned 4-byte int
   *         (that is, if {@code value >= (1L << 32)}).
   */
  static Bytes ofUnsignedInt(long value) {
    return ofUnsignedInt(value, BIG_ENDIAN);
  }

  /**
   * Return a 4-byte value corresponding to the provided value interpreted as an unsigned int.
   *
   * @param value The value, which must be no larger than an unsigned int.
   * @param order The byte-order for the integer encoding.
   * @return A 4 bytes value corresponding to the encoded {@code value}.
   * @throws IllegalArgumentException if {@code value < 0} or {@code value} is too big to fit an unsigned 4-byte int
   *         (that is, if {@code value >= (1L << 32)}).
   */
  static Bytes ofUnsignedInt(long value, ByteOrder order) {
    checkArgument(
        value >= 0 && value <= BytesValues.MAX_UNSIGNED_INT,
        "Value %s cannot be represented as an unsigned int (it is negative or too big)",
        value);
    byte[] res = new byte[4];
    if (order == BIG_ENDIAN) {
      res[0] = (byte) ((value >> 24) & 0xFF);
      res[1] = (byte) ((value >> 16) & 0xFF);
      res[2] = (byte) ((value >> 8) & 0xFF);
      res[3] = (byte) ((value) & 0xFF);
    } else {
      res[0] = (byte) ((value) & 0xFF);
      res[1] = (byte) ((value >> 8) & 0xFF);
      res[2] = (byte) ((value >> 16) & 0xFF);
      res[3] = (byte) ((value >> 24) & 0xFF);
    }
    return Bytes.wrap(res);
  }

  /**
   * Return an 8-byte value corresponding to the provided value interpreted as an unsigned long.
   *
   * @param value The value, which will be interpreted as an unsigned long.
   * @return A 8 bytes value corresponding to {@code value}.
   * @throws IllegalArgumentException if {@code value < 0} or {@code value} is too big to fit an unsigned 8-byte int
   *         (that is, if {@code value >= (1L << 64)}).
   */
  static Bytes ofUnsignedLong(long value) {
    return ofUnsignedLong(value, BIG_ENDIAN);
  }

  /**
   * Return an 8-byte value corresponding to the provided value interpreted as an unsigned long.
   *
   * @param value The value, which will be interpreted as an unsigned long.
   * @param order The byte-order for the integer encoding.
   * @return A 8 bytes value corresponding to {@code value}.
   * @throws IllegalArgumentException if {@code value < 0} or {@code value} is too big to fit an unsigned 8-byte int
   *         (that is, if {@code value >= (1L << 64)}).
   */
  static Bytes ofUnsignedLong(long value, ByteOrder order) {
    byte[] res = new byte[8];
    if (order == BIG_ENDIAN) {
      res[0] = (byte) ((value >> 56) & 0xFF);
      res[1] = (byte) ((value >> 48) & 0xFF);
      res[2] = (byte) ((value >> 40) & 0xFF);
      res[3] = (byte) ((value >> 32) & 0xFF);
      res[4] = (byte) ((value >> 24) & 0xFF);
      res[5] = (byte) ((value >> 16) & 0xFF);
      res[6] = (byte) ((value >> 8) & 0xFF);
      res[7] = (byte) (value & 0xFF);
    } else {
      res[0] = (byte) ((value) & 0xFF);
      res[1] = (byte) ((value >> 8) & 0xFF);
      res[2] = (byte) ((value >> 16) & 0xFF);
      res[3] = (byte) ((value >> 24) & 0xFF);
      res[4] = (byte) ((value >> 32) & 0xFF);
      res[5] = (byte) ((value >> 40) & 0xFF);
      res[6] = (byte) ((value >> 48) & 0xFF);
      res[7] = (byte) ((value >> 56) & 0xFF);
    }
    return Bytes.wrap(res);
  }

  /**
   * Return the smallest bytes value whose bytes correspond to the provided long. That is, the returned value may be of
   * size less than 8 if the provided long has leading zero bytes.
   *
   * @param value The long from which to create the bytes value.
   * @return The minimal bytes representation corresponding to {@code l}.
   */
  static Bytes minimalBytes(long value) {
    if (value == 0) {
      return Bytes.EMPTY;
    }

    int zeros = Long.numberOfLeadingZeros(value);
    int resultBytes = 8 - (zeros / 8);

    byte[] result = new byte[resultBytes];
    int shift = 0;
    for (int i = 0; i < resultBytes; i++) {
      result[resultBytes - i - 1] = (byte) ((value >> shift) & 0xFF);
      shift += 8;
    }
    return Bytes.wrap(result);
  }

  /**
   * Parse a hexadecimal string into a {@link Bytes} value.
   *
   * <p>
   * This method is lenient in that {@code str} may of an odd length, in which case it will behave exactly as if it had
   * an additional 0 in front.
   *
   * @param str The hexadecimal string to parse, which may or may not start with "0x".
   * @return The value corresponding to {@code str}.
   * @throws IllegalArgumentException if {@code str} does not correspond to a valid hexadecimal representation.
   */
  static Bytes fromHexStringLenient(CharSequence str) {
    checkNotNull(str);
    return BytesValues.fromHexString(str, -1, true);
  }

  /**
   * Parse a hexadecimal string into a {@link Bytes} value of the provided size.
   *
   * <p>
   * This method allows for {@code str} to have an odd length, in which case it will behave exactly as if it had an
   * additional 0 in front.
   *
   * @param str The hexadecimal string to parse, which may or may not start with "0x".
   * @param destinationSize The size of the returned value, which must be big enough to hold the bytes represented by
   *        {@code str}. If it is strictly bigger those bytes from {@code str}, the returned value will be left padded
   *        with zeros.
   * @return A value of size {@code destinationSize} corresponding to {@code str} potentially left-padded.
   * @throws IllegalArgumentException if {@code str} does not correspond to a valid hexadecimal representation,
   *         represents more bytes than {@code destinationSize} or {@code destinationSize < 0}.
   */
  static Bytes fromHexStringLenient(CharSequence str, int destinationSize) {
    checkNotNull(str);
    checkArgument(destinationSize >= 0, "Invalid negative destination size %s", destinationSize);
    return BytesValues.fromHexString(str, destinationSize, true);
  }

  /**
   * Parse a hexadecimal string into a {@link Bytes} value.
   *
   * <p>
   * This method requires that {@code str} have an even length.
   *
   * @param str The hexadecimal string to parse, which may or may not start with "0x".
   * @return The value corresponding to {@code str}.
   * @throws IllegalArgumentException if {@code str} does not correspond to a valid hexadecimal representation, or is of
   *         an odd length.
   */
  static Bytes fromHexString(CharSequence str) {
    checkNotNull(str);
    return BytesValues.fromHexString(str, -1, false);
  }

  /**
   * Parse a hexadecimal string into a {@link Bytes} value.
   *
   * <p>
   * This method requires that {@code str} have an even length.
   *
   * @param str The hexadecimal string to parse, which may or may not start with "0x".
   * @param destinationSize The size of the returned value, which must be big enough to hold the bytes represented by
   *        {@code str}. If it is strictly bigger those bytes from {@code str}, the returned value will be left padded
   *        with zeros.
   * @return A value of size {@code destinationSize} corresponding to {@code str} potentially left-padded.
   * @throws IllegalArgumentException if {@code str} does correspond to a valid hexadecimal representation, or is of an
   *         odd length.
   * @throws IllegalArgumentException if {@code str} does not correspond to a valid hexadecimal representation, or is of
   *         an odd length, or represents more bytes than {@code destinationSize} or {@code destinationSize < 0}.
   */
  static Bytes fromHexString(CharSequence str, int destinationSize) {
    checkNotNull(str);
    checkArgument(destinationSize >= 0, "Invalid negative destination size %s", destinationSize);
    return BytesValues.fromHexString(str, destinationSize, false);
  }

  /**
   * Parse a base 64 string into a {@link Bytes} value.
   *
   * @param str The base 64 string to parse.
   * @return The value corresponding to {@code str}.
   */
  static Bytes fromBase64String(CharSequence str) {
    return Bytes.wrap(Base64.getDecoder().decode(str.toString()));
  }

  /**
   * Generate random bytes.
   *
   * @param size The number of bytes to generate.
   * @return A value containing the desired number of random bytes.
   */
  static Bytes random(int size) {
    return random(size, new SecureRandom());
  }

  /**
   * Generate random bytes.
   *
   * @param size The number of bytes to generate.
   * @param generator The generator for random bytes.
   * @return A value containing the desired number of random bytes.
   */
  static Bytes random(int size, Random generator) {
    byte[] array = new byte[size];
    generator.nextBytes(array);
    return Bytes.wrap(array);
  }

  /**
   *
   * Provides the number of bytes this value represents.
   * 
   * @return The number of bytes this value represents.
   */
  int size();

  /**
   * Retrieve a byte in this value.
   *
   * @param i The index of the byte to fetch within the value (0-indexed).
   * @return The byte at index {@code i} in this value.
   * @throws IndexOutOfBoundsException if {@code i < 0} or {i >= size()}.
   */
  byte get(int i);

  /**
   * Retrieve the 4 bytes starting at the provided index in this value as an integer.
   *
   * @param i The index from which to get the int, which must less than or equal to {@code size() - 4}.
   * @return An integer whose value is the 4 bytes from this value starting at index {@code i}.
   * @throws IndexOutOfBoundsException if {@code i < 0} or {@code i > size() - 4}.
   */
  default int getInt(int i) {
    return getInt(i, BIG_ENDIAN);
  }

  /**
   * Retrieve the 4 bytes starting at the provided index in this value as an integer.
   *
   * @param i The index from which to get the int, which must less than or equal to {@code size() - 4}.
   * @param order The byte-order for decoding the integer.
   * @return An integer whose value is the 4 bytes from this value starting at index {@code i}.
   * @throws IndexOutOfBoundsException if {@code i < 0} or {@code i > size() - 4}.
   */
  default int getInt(int i, ByteOrder order) {
    int size = size();
    checkElementIndex(i, size);
    if (i > (size - 4)) {
      throw new IndexOutOfBoundsException(
          format("Value of size %s has not enough bytes to read a 4 bytes int from index %s", size, i));
    }

    int value = 0;
    if (order == BIG_ENDIAN) {
      value |= ((int) get(i) & 0xFF) << 24;
      value |= ((int) get(i + 1) & 0xFF) << 16;
      value |= ((int) get(i + 2) & 0xFF) << 8;
      value |= ((int) get(i + 3) & 0xFF);
    } else {
      value |= ((int) get(i + 3) & 0xFF) << 24;
      value |= ((int) get(i + 2) & 0xFF) << 16;
      value |= ((int) get(i + 1) & 0xFF) << 8;
      value |= ((int) get(i) & 0xFF);
    }
    return value;
  }

  /**
   * The value corresponding to interpreting these bytes as an integer.
   *
   * @return An value corresponding to this value interpreted as an integer.
   * @throws IllegalArgumentException if {@code size() > 4}.
   */
  default int toInt() {
    return toInt(BIG_ENDIAN);
  }

  /**
   * The value corresponding to interpreting these bytes as an integer.
   *
   * @param order The byte-order for decoding the integer.
   * @return An value corresponding to this value interpreted as an integer.
   * @throws IllegalArgumentException if {@code size() > 4}.
   */
  default int toInt(ByteOrder order) {
    int size = size();
    checkArgument(size <= 4, "Value of size %s has more than 4 bytes", size());
    if (size == 0) {
      return 0;
    }
    if (order == BIG_ENDIAN) {
      int i = size;
      int value = ((int) get(--i) & 0xFF);
      if (i == 0) {
        return value;
      }
      value |= ((int) get(--i) & 0xFF) << 8;
      if (i == 0) {
        return value;
      }
      value |= ((int) get(--i) & 0xFF) << 16;
      if (i == 0) {
        return value;
      }
      return value | ((int) get(--i) & 0xFF) << 24;
    } else {
      int i = 0;
      int value = ((int) get(i) & 0xFF);
      if (++i == size) {
        return value;
      }
      value |= ((int) get(i++) & 0xFF) << 8;
      if (i == size) {
        return value;
      }
      value |= ((int) get(i++) & 0xFF) << 16;
      if (i == size) {
        return value;
      }
      return value | ((int) get(i) & 0xFF) << 24;
    }
  }

  /**
   * Whether this value contains no bytes.
   *
   * @return true if the value contains no bytes
   */
  default boolean isEmpty() {
    return size() == 0;
  }

  /**
   * Retrieves the 8 bytes starting at the provided index in this value as a long.
   *
   * @param i The index from which to get the long, which must less than or equal to {@code size() - 8}.
   * @return A long whose value is the 8 bytes from this value starting at index {@code i}.
   * @throws IndexOutOfBoundsException if {@code i < 0} or {@code i > size() - 8}.
   */
  default long getLong(int i) {
    return getLong(i, BIG_ENDIAN);
  }

  /**
   * Retrieves the 8 bytes starting at the provided index in this value as a long.
   *
   * @param i The index from which to get the long, which must less than or equal to {@code size() - 8}.
   * @param order The byte-order for decoding the integer.
   * @return A long whose value is the 8 bytes from this value starting at index {@code i}.
   * @throws IndexOutOfBoundsException if {@code i < 0} or {@code i > size() - 8}.
   */
  default long getLong(int i, ByteOrder order) {
    int size = size();
    checkElementIndex(i, size);
    if (i > (size - 8)) {
      throw new IndexOutOfBoundsException(
          format("Value of size %s has not enough bytes to read a 8 bytes long from index %s", size, i));
    }

    long value = 0;
    if (order == BIG_ENDIAN) {
      value |= ((long) get(i) & 0xFF) << 56;
      value |= ((long) get(i + 1) & 0xFF) << 48;
      value |= ((long) get(i + 2) & 0xFF) << 40;
      value |= ((long) get(i + 3) & 0xFF) << 32;
      value |= ((long) get(i + 4) & 0xFF) << 24;
      value |= ((long) get(i + 5) & 0xFF) << 16;
      value |= ((long) get(i + 6) & 0xFF) << 8;
      value |= ((long) get(i + 7) & 0xFF);
    } else {
      value |= ((long) get(i + 7) & 0xFF) << 56;
      value |= ((long) get(i + 6) & 0xFF) << 48;
      value |= ((long) get(i + 5) & 0xFF) << 40;
      value |= ((long) get(i + 4) & 0xFF) << 32;
      value |= ((long) get(i + 3) & 0xFF) << 24;
      value |= ((long) get(i + 2) & 0xFF) << 16;
      value |= ((long) get(i + 1) & 0xFF) << 8;
      value |= ((long) get(i) & 0xFF);
    }
    return value;
  }

  /**
   * The value corresponding to interpreting these bytes as a long.
   *
   * @return An value corresponding to this value interpreted as a long.
   * @throws IllegalArgumentException if {@code size() > 8}.
   */
  default long toLong() {
    return toLong(BIG_ENDIAN);
  }

  /**
   * The value corresponding to interpreting these bytes as a long.
   *
   * @param order The byte-order for decoding the integer.
   * @return An value corresponding to this value interpreted as a long.
   * @throws IllegalArgumentException if {@code size() > 8}.
   */
  default long toLong(ByteOrder order) {
    int size = size();
    checkArgument(size <= 8, "Value of size %s has more than 8 bytes", size());
    if (size == 0) {
      return 0;
    }
    if (order == BIG_ENDIAN) {
      int i = size;
      long value = ((long) get(--i) & 0xFF);
      if (i == 0) {
        return value;
      }
      value |= ((long) get(--i) & 0xFF) << 8;
      if (i == 0) {
        return value;
      }
      value |= ((long) get(--i) & 0xFF) << 16;
      if (i == 0) {
        return value;
      }
      value |= ((long) get(--i) & 0xFF) << 24;
      if (i == 0) {
        return value;
      }
      value |= ((long) get(--i) & 0xFF) << 32;
      if (i == 0) {
        return value;
      }
      value |= ((long) get(--i) & 0xFF) << 40;
      if (i == 0) {
        return value;
      }
      value |= ((long) get(--i) & 0xFF) << 48;
      if (i == 0) {
        return value;
      }
      return value | ((long) get(--i) & 0xFF) << 56;
    } else {
      int i = 0;
      long value = ((long) get(i) & 0xFF);
      if (++i == size) {
        return value;
      }
      value |= ((long) get(i) & 0xFF) << 8;
      if (++i == size) {
        return value;
      }
      value |= ((long) get(i) & 0xFF) << 16;
      if (++i == size) {
        return value;
      }
      value |= ((long) get(i) & 0xFF) << 24;
      if (++i == size) {
        return value;
      }
      value |= ((long) get(i) & 0xFF) << 32;
      if (++i == size) {
        return value;
      }
      value |= ((long) get(i) & 0xFF) << 40;
      if (++i == size) {
        return value;
      }
      value |= ((long) get(i) & 0xFF) << 48;
      if (++i == size) {
        return value;
      }
      return value | ((long) get(i) & 0xFF) << 56;
    }
  }

  /**
   * The BigInteger corresponding to interpreting these bytes as a two's-complement signed integer.
   *
   * @return A {@link BigInteger} corresponding to interpreting these bytes as a two's-complement signed integer.
   */
  default BigInteger toBigInteger() {
    return toBigInteger(BIG_ENDIAN);
  }

  /**
   * The BigInteger corresponding to interpreting these bytes as a two's-complement signed integer.
   *
   * @param order The byte-order for decoding the integer.
   * @return A {@link BigInteger} corresponding to interpreting these bytes as a two's-complement signed integer.
   */
  default BigInteger toBigInteger(ByteOrder order) {
    if (size() == 0) {
      return BigInteger.ZERO;
    }
    return new BigInteger((order == BIG_ENDIAN) ? toArrayUnsafe() : reverse().toArrayUnsafe());
  }

  /**
   * The BigInteger corresponding to interpreting these bytes as an unsigned integer.
   *
   * @return A positive (or zero) {@link BigInteger} corresponding to interpreting these bytes as an unsigned integer.
   */
  default BigInteger toUnsignedBigInteger() {
    return toUnsignedBigInteger(BIG_ENDIAN);
  }

  /**
   * The BigInteger corresponding to interpreting these bytes as an unsigned integer.
   *
   * @param order The byte-order for decoding the integer.
   * @return A positive (or zero) {@link BigInteger} corresponding to interpreting these bytes as an unsigned integer.
   */
  default BigInteger toUnsignedBigInteger(ByteOrder order) {
    return new BigInteger(1, (order == BIG_ENDIAN) ? toArrayUnsafe() : reverse().toArrayUnsafe());
  }

  /**
   * Whether this value has only zero bytes.
   *
   * @return {@code true} if all the bits of this value are zeros.
   */
  default boolean isZero() {
    for (int i = size() - 1; i >= 0; --i) {
      if (get(i) != 0)
        return false;
    }
    return true;
  }

  /**
   * Whether the bytes start with a zero bit value.
   *
   * @return true if the first bit equals zero
   */
  default boolean hasLeadingZero() {
    return size() > 0 && (get(0) & 0x80) == 0;
  }

  /**
   * Provides the number of zero bits preceding the highest-order ("leftmost") one-bit, or {@code size() * 8} if all
   * bits * are zero.
   * 
   * @return The number of zero bits preceding the highest-order ("leftmost") one-bit, or {@code size() * 8} if all bits
   *         are zero.
   */
  default int numberOfLeadingZeros() {
    int size = size();
    for (int i = 0; i < size; i++) {
      byte b = get(i);
      if (b == 0) {
        continue;
      }

      return (i * 8) + Integer.numberOfLeadingZeros(b & 0xFF) - 3 * 8;
    }
    return size * 8;
  }

  /**
   * Whether the bytes start with a zero byte value.
   *
   * @return true if the first byte equals zero
   */
  default boolean hasLeadingZeroByte() {
    return size() > 0 && get(0) == 0;
  }

  /**
   * Provides the number of leading zero bytes of the value
   * 
   * @return The number of leading zero bytes of the value.
   */
  default int numberOfLeadingZeroBytes() {
    int size = size();
    for (int i = 0; i < size; i++) {
      if (get(i) != 0) {
        return i;
      }
    }
    return size;
  }

  /**
   * Provides the number of trailing zero bytes of the value.
   * 
   * @return The number of trailing zero bytes of the value.
   */
  default int numberOfTrailingZeroBytes() {
    int size = size();
    for (int i = size; i >= 1; i--) {
      if (get(i - 1) != 0) {
        return size - i;
      }
    }
    return size;
  }

  /**
   * Provides the number of bits following and including the highest-order ("leftmost") one-bit, or zero if all bits are
   * zero.
   * 
   * @return The number of bits following and including the highest-order ("leftmost") one-bit, or zero if all bits are
   *         zero.
   */
  default int bitLength() {
    int size = size();
    for (int i = 0; i < size; i++) {
      byte b = get(i);
      if (b == 0)
        continue;

      return (size * 8) - (i * 8) - (Integer.numberOfLeadingZeros(b & 0xFF) - 3 * 8);
    }
    return 0;
  }

  /**
   * Return a bit-wise AND of these bytes and the supplied bytes.
   *
   * If this value and the supplied value are different lengths, then the shorter will be zero-padded to the left.
   *
   * @param other The bytes to perform the operation with.
   * @return The result of a bit-wise AND.
   */
  default Bytes and(Bytes other) {
    return and(other, MutableBytes.create(Math.max(size(), other.size())));
  }

  /**
   * Calculate a bit-wise AND of these bytes and the supplied bytes.
   *
   * <p>
   * If this value or the supplied value are shorter in length than the output vector, then they will be zero-padded to
   * the left. Likewise, if either this value or the supplied valid is longer in length than the output vector, then
   * they will be truncated to the left.
   *
   * @param other The bytes to perform the operation with.
   * @param result The mutable output vector for the result.
   * @param <T> The {@link MutableBytes} value type.
   * @return The {@code result} output vector.
   */
  default <T extends MutableBytes> T and(Bytes other, T result) {
    checkNotNull(other);
    checkNotNull(result);
    int rSize = result.size();
    int offsetSelf = rSize - size();
    int offsetOther = rSize - other.size();
    for (int i = 0; i < rSize; i++) {
      byte b1 = (i < offsetSelf) ? 0x00 : get(i - offsetSelf);
      byte b2 = (i < offsetOther) ? 0x00 : other.get(i - offsetOther);
      result.set(i, (byte) (b1 & b2));
    }
    return result;
  }

  /**
   * Return a bit-wise OR of these bytes and the supplied bytes.
   *
   * <p>
   * If this value and the supplied value are different lengths, then the shorter will be zero-padded to the left.
   *
   * @param other The bytes to perform the operation with.
   * @return The result of a bit-wise OR.
   */
  default Bytes or(Bytes other) {
    return or(other, MutableBytes.create(Math.max(size(), other.size())));
  }

  /**
   * Calculate a bit-wise OR of these bytes and the supplied bytes.
   *
   * <p>
   * If this value or the supplied value are shorter in length than the output vector, then they will be zero-padded to
   * the left. Likewise, if either this value or the supplied valid is longer in length than the output vector, then
   * they will be truncated to the left.
   *
   * @param other The bytes to perform the operation with.
   * @param result The mutable output vector for the result.
   * @param <T> The {@link MutableBytes} value type.
   * @return The {@code result} output vector.
   */
  default <T extends MutableBytes> T or(Bytes other, T result) {
    checkNotNull(other);
    checkNotNull(result);
    int rSize = result.size();
    int offsetSelf = rSize - size();
    int offsetOther = rSize - other.size();
    for (int i = 0; i < rSize; i++) {
      byte b1 = (i < offsetSelf) ? 0x00 : get(i - offsetSelf);
      byte b2 = (i < offsetOther) ? 0x00 : other.get(i - offsetOther);
      result.set(i, (byte) (b1 | b2));
    }
    return result;
  }

  /**
   * Return a bit-wise XOR of these bytes and the supplied bytes.
   *
   * <p>
   * If this value and the supplied value are different lengths, then the shorter will be zero-padded to the left.
   *
   * @param other The bytes to perform the operation with.
   * @return The result of a bit-wise XOR.
   */
  default Bytes xor(Bytes other) {
    return xor(other, MutableBytes.create(Math.max(size(), other.size())));
  }

  /**
   * Calculate a bit-wise XOR of these bytes and the supplied bytes.
   *
   * <p>
   * If this value or the supplied value are shorter in length than the output vector, then they will be zero-padded to
   * the left. Likewise, if either this value or the supplied valid is longer in length than the output vector, then
   * they will be truncated to the left.
   *
   * @param other The bytes to perform the operation with.
   * @param result The mutable output vector for the result.
   * @param <T> The {@link MutableBytes} value type.
   * @return The {@code result} output vector.
   */
  default <T extends MutableBytes> T xor(Bytes other, T result) {
    checkNotNull(other);
    checkNotNull(result);
    int rSize = result.size();
    int offsetSelf = rSize - size();
    int offsetOther = rSize - other.size();
    for (int i = 0; i < rSize; i++) {
      byte b1 = (i < offsetSelf) ? 0x00 : get(i - offsetSelf);
      byte b2 = (i < offsetOther) ? 0x00 : other.get(i - offsetOther);
      result.set(i, (byte) (b1 ^ b2));
    }
    return result;
  }

  /**
   * Return a bit-wise NOT of these bytes.
   *
   * @return The result of a bit-wise NOT.
   */
  default Bytes not() {
    return not(MutableBytes.create(size()));
  }

  /**
   * Calculate a bit-wise NOT of these bytes.
   *
   * <p>
   * If this value is shorter in length than the output vector, then it will be zero-padded to the left. Likewise, if
   * this value is longer in length than the output vector, then it will be truncated to the left.
   *
   * @param result The mutable output vector for the result.
   * @param <T> The {@link MutableBytes} value type.
   * @return The {@code result} output vector.
   */
  default <T extends MutableBytes> T not(T result) {
    checkNotNull(result);
    int rSize = result.size();
    int offsetSelf = rSize - size();
    for (int i = 0; i < rSize; i++) {
      byte b1 = (i < offsetSelf) ? 0x00 : get(i - offsetSelf);
      result.set(i, (byte) ~b1);
    }
    return result;
  }

  /**
   * Shift all bits in this value to the right.
   *
   * @param distance The number of bits to shift by.
   * @return A value containing the shifted bits.
   */
  default Bytes shiftRight(int distance) {
    return shiftRight(distance, MutableBytes.create(size()));
  }

  /**
   * Shift all bits in this value to the right.
   *
   * <p>
   * If this value is shorter in length than the output vector, then it will be zero-padded to the left. Likewise, if
   * this value is longer in length than the output vector, then it will be truncated to the left (after shifting).
   *
   * @param distance The number of bits to shift by.
   * @param result The mutable output vector for the result.
   * @param <T> The {@link MutableBytes} value type.
   * @return The {@code result} output vector.
   */
  default <T extends MutableBytes> T shiftRight(int distance, T result) {
    checkNotNull(result);
    int rSize = result.size();
    int offsetSelf = rSize - size();

    int d = distance / 8;
    int s = distance % 8;
    int resIdx = rSize - 1;
    for (int i = rSize - 1 - d; i >= 0; i--) {
      byte res;
      if (i < offsetSelf) {
        res = 0;
      } else {
        int selfIdx = i - offsetSelf;
        int leftSide = (get(selfIdx) & 0xFF) >>> s;
        int rightSide = (selfIdx == 0) ? 0 : get(selfIdx - 1) << (8 - s);
        res = (byte) (leftSide | rightSide);
      }
      result.set(resIdx--, res);
    }
    for (; resIdx >= 0; resIdx--) {
      result.set(resIdx, (byte) 0);
    }
    return result;
  }

  /**
   * Shift all bits in this value to the left.
   *
   * @param distance The number of bits to shift by.
   * @return A value containing the shifted bits.
   */
  default Bytes shiftLeft(int distance) {
    return shiftLeft(distance, MutableBytes.create(size()));
  }

  /**
   * Shift all bits in this value to the left.
   *
   * <p>
   * If this value is shorter in length than the output vector, then it will be zero-padded to the left. Likewise, if
   * this value is longer in length than the output vector, then it will be truncated to the left.
   *
   * @param distance The number of bits to shift by.
   * @param result The mutable output vector for the result.
   * @param <T> The {@link MutableBytes} value type.
   * @return The {@code result} output vector.
   */
  default <T extends MutableBytes> T shiftLeft(int distance, T result) {
    checkNotNull(result);
    int size = size();
    int rSize = result.size();
    int offsetSelf = rSize - size;

    int d = distance / 8;
    int s = distance % 8;
    int resIdx = 0;
    for (int i = d; i < rSize; i++) {
      byte res;
      if (i < offsetSelf) {
        res = 0;
      } else {
        int selfIdx = i - offsetSelf;
        int leftSide = get(selfIdx) << s;
        int rightSide = (selfIdx == size - 1) ? 0 : (get(selfIdx + 1) & 0xFF) >>> (8 - s);
        res = (byte) (leftSide | rightSide);
      }
      result.set(resIdx++, res);
    }
    for (; resIdx < rSize; resIdx++) {
      result.set(resIdx, (byte) 0);
    }
    return result;
  }

  /**
   * Create a new value representing (a view of) a slice of the bytes of this value.
   *
   * <p>
   * Please note that the resulting slice is only a view and as such maintains a link to the underlying full value. So
   * holding a reference to the returned slice may hold more memory than the slide represents. Use {@link #copy} on the
   * returned slice if that is not what you want.
   *
   * @param i The start index for the slice.
   * @return A new value providing a view over the bytes from index {@code i} (included) to the end.
   * @throws IndexOutOfBoundsException if {@code i < 0}.
   */
  default Bytes slice(int i) {
    if (i == 0) {
      return this;
    }
    int size = size();
    if (i >= size) {
      return EMPTY;
    }
    return slice(i, size - i);
  }

  /**
   * Create a new value representing (a view of) a slice of the bytes of this value.
   *
   * <p>
   * Please note that the resulting slice is only a view and as such maintains a link to the underlying full value. So
   * holding a reference to the returned slice may hold more memory than the slide represents. Use {@link #copy} on the
   * returned slice if that is not what you want.
   *
   * @param i The start index for the slice.
   * @param length The length of the resulting value.
   * @return A new value providing a view over the bytes from index {@code i} (included) to {@code i + length}
   *         (excluded).
   * @throws IllegalArgumentException if {@code length < 0}.
   * @throws IndexOutOfBoundsException if {@code i < 0} or {i >= size()} or {i + length > size()} .
   */
  Bytes slice(int i, int length);

  /**
   * Return a value equivalent to this one but guaranteed to 1) be deeply immutable (i.e. the underlying value will be
   * immutable) and 2) to not retain more bytes than exposed by the value.
   *
   * @return A value, equals to this one, but deeply immutable and that doesn't retain any "unreachable" bytes. For
   *         performance reasons, this is allowed to return this value however if it already fit those constraints.
   */
  Bytes copy();

  /**
   * Return a new mutable value initialized with the content of this value.
   *
   * @return A mutable copy of this value. This will copy bytes, modifying the returned value will <b>not</b> modify
   *         this value.
   */
  MutableBytes mutableCopy();

  /**
   * Copy the bytes of this value to the provided mutable one, which must have the same size.
   *
   * @param destination The mutable value to which to copy the bytes to, which must have the same size as this value. If
   *        you want to copy value where size differs, you should use {@link #slice} and/or
   *        {@link MutableBytes#mutableSlice} and apply the copy to the result.
   * @throws IllegalArgumentException if {@code this.size() != destination.size()}.
   */
  default void copyTo(MutableBytes destination) {
    checkNotNull(destination);
    checkArgument(
        destination.size() == size(),
        "Cannot copy %s bytes to destination of non-equal size %s",
        size(),
        destination.size());
    copyTo(destination, 0);
  }

  /**
   * Copy the bytes of this value to the provided mutable one from a particular offset.
   *
   * <p>
   * This is a (potentially slightly more efficient) shortcut for {@code
   * copyTo(destination.mutableSlice(destinationOffset, this.size()))}.
   *
   * @param destination The mutable value to which to copy the bytes to, which must have enough bytes from
   *        {@code destinationOffset} for the copied value.
   * @param destinationOffset The offset in {@code destination} at which the copy starts.
   * @throws IllegalArgumentException if the destination doesn't have enough room, that is if {@code
   *     this.size() > (destination.size() - destinationOffset)}.
   */
  default void copyTo(MutableBytes destination, int destinationOffset) {
    checkNotNull(destination);

    // Special casing an empty source or the following checks might throw (even though we have
    // nothing to copy anyway) and this gets inconvenient for generic methods using copyTo() as
    // they may have to special case empty values because of this. As an example,
    // concatenate(EMPTY, EMPTY) would need to be special cased without this.
    int size = size();
    if (size == 0) {
      return;
    }

    checkElementIndex(destinationOffset, destination.size());
    checkArgument(
        destination.size() - destinationOffset >= size,
        "Cannot copy %s bytes, destination has only %s bytes from index %s",
        size,
        destination.size() - destinationOffset,
        destinationOffset);

    for (int i = 0; i < size; i++) {
      destination.set(destinationOffset + i, get(i));
    }
  }

  /**
   * Append the bytes of this value to the {@link ByteBuffer}.
   *
   * @param byteBuffer The {@link ByteBuffer} to which to append this value.
   * @throws BufferOverflowException If the writer attempts to write more than the provided buffer can hold.
   * @throws ReadOnlyBufferException If the provided buffer is read-only.
   */
  default void appendTo(ByteBuffer byteBuffer) {
    checkNotNull(byteBuffer);
    for (int i = 0; i < size(); i++) {
      byteBuffer.put(get(i));
    }
  }

  /**
   * Append the bytes of this value to the provided Vert.x {@link Buffer}.
   *
   * <p>
   * Note that since a Vert.x {@link Buffer} will grow as necessary, this method never fails.
   *
   * @param buffer The {@link Buffer} to which to append this value.
   */
  default void appendTo(Buffer buffer) {
    checkNotNull(buffer);
    for (int i = 0; i < size(); i++) {
      buffer.appendByte(get(i));
    }
  }

  /**
   * Append this value as a sequence of hexadecimal characters.
   *
   * @param appendable The appendable
   * @param <T> The appendable type.
   * @return The appendable.
   */
  default <T extends Appendable> T appendHexTo(T appendable) {
    try {
      int size = size();
      for (int i = 0; i < size; i++) {
        byte b = get(i);
        appendable.append(AbstractBytes.HEX_CODE[b >> 4 & 15]);
        appendable.append(AbstractBytes.HEX_CODE[b & 15]);
      }
      return appendable;
    } catch (IOException e) {
      throw new UncheckedIOException(e);
    }
  }

  /**
   * Return the number of bytes in common between this set of bytes and another.
   *
   * @param other The bytes to compare to.
   * @return The number of common bytes.
   */
  default int commonPrefixLength(Bytes other) {
    checkNotNull(other);
    int ourSize = size();
    int otherSize = other.size();
    int i = 0;
    while (i < ourSize && i < otherSize && get(i) == other.get(i)) {
      i++;
    }
    return i;
  }

  /**
   * Return a slice over the common prefix between this set of bytes and another.
   *
   * @param other The bytes to compare to.
   * @return A slice covering the common prefix.
   */
  default Bytes commonPrefix(Bytes other) {
    return slice(0, commonPrefixLength(other));
  }

  /**
   * Return a slice of representing the same value but without any leading zero bytes.
   *
   * @return {@code value} if its left-most byte is non zero, or a slice that exclude any leading zero bytes.
   */
  default Bytes trimLeadingZeros() {
    int size = size();
    for (int i = 0; i < size; i++) {
      if (get(i) != 0) {
        return slice(i);
      }
    }
    return Bytes.EMPTY;
  }

  /**
   * Update the provided message digest with the bytes of this value.
   *
   * @param digest The digest to update.
   */
  default void update(MessageDigest digest) {
    checkNotNull(digest);
    digest.update(toArrayUnsafe());
  }

  /**
   * Computes the reverse array of bytes of the current bytes.
   *
   * @return a new Bytes value, containing the bytes in reverse order
   */
  default Bytes reverse() {
    byte[] reverse = new byte[size()];
    for (int i = 0; i < size(); i++) {
      reverse[size() - i - 1] = get(i);
    }
    return Bytes.wrap(reverse);
  }

  /**
   * Extract the bytes of this value into a byte array.
   *
   * @return A byte array with the same content than this value.
   */
  default byte[] toArray() {
    return toArray(BIG_ENDIAN);
  }

  /**
   * Extract the bytes of this value into a byte array.
   *
   * @param byteOrder the byte order to apply : big endian or little endian
   * @return A byte array with the same content than this value.
   */
  default byte[] toArray(ByteOrder byteOrder) {
    int size = size();
    byte[] array = new byte[size];
    if (byteOrder == BIG_ENDIAN) {
      for (int i = 0; i < size; i++) {
        array[i] = get(i);
      }
    } else {
      for (int i = 0; i < size(); i++) {
        array[size() - i - 1] = get(i);
      }
    }
    return array;
  }

  /**
   * Get the bytes represented by this value as byte array.
   *
   * <p>
   * Contrarily to {@link #toArray()}, this may avoid allocating a new array and directly return the backing array of
   * this value if said value is array backed and doing so is possible. As such, modifications to the returned array may
   * or may not impact this value. As such, this method should be used with care and hence the "unsafe" moniker.
   *
   * @return A byte array with the same content than this value, which may or may not be the direct backing of this
   *         value.
   */
  default byte[] toArrayUnsafe() {
    return toArray();
  }

  /**
   * Return the hexadecimal string representation of this value.
   *
   * @return The hexadecimal representation of this value, starting with "0x".
   */
  @Override
  String toString();

  /**
   * Provides this value represented as hexadecimal, starting with "0x".
   * 
   * @return This value represented as hexadecimal, starting with "0x".
   */
  default String toHexString() {
    return appendHexTo(new StringBuilder("0x")).toString();
  }

  /**
   * Provides this value represented as hexadecimal, with no prefix
   * 
   * @return This value represented as hexadecimal, with no prefix.
   */
  default String toUnprefixedHexString() {
    return appendHexTo(new StringBuilder()).toString();
  }

  default String toEllipsisHexString() {
    int size = size();
    if (size < 6) {
      return toHexString();
    }
    StringBuilder appendable = new StringBuilder("0x");
    for (int i = 0; i < 2; i++) {
      byte b = get(i);
      appendable.append(AbstractBytes.HEX_CODE[b >> 4 & 15]);
      appendable.append(AbstractBytes.HEX_CODE[b & 15]);
    }
    appendable.append("..");
    for (int i = 0; i < 2; i++) {
      byte b = get(i + size - 2);
      appendable.append(AbstractBytes.HEX_CODE[b >> 4 & 15]);
      appendable.append(AbstractBytes.HEX_CODE[b & 15]);
    }
    return appendable.toString();
  }

  /**
   * Provides this value represented as a minimal hexadecimal string (without any leading zero)
   * 
   * @return This value represented as a minimal hexadecimal string (without any leading zero).
   */
  default String toShortHexString() {
    StringBuilder hex = appendHexTo(new StringBuilder());

    int i = 0;
    while (i < hex.length() && hex.charAt(i) == '0') {
      i++;
    }
    return "0x" + hex.substring(i);
  }

  /**
   * Provides this value represented as a minimal hexadecimal string (without any leading zero, except if it's valued
   * zero or empty, in which case it returns 0x0).
   * 
   * @return This value represented as a minimal hexadecimal string (without any leading zero, except if it's valued
   *         zero or empty, in which case it returns 0x0).
   */
  default String toQuantityHexString() {
    if (Bytes.EMPTY.equals(this)) {
      return "0x0";
    }
    StringBuilder hex = appendHexTo(new StringBuilder());

    int i = 0;
    while (i < hex.length() - 1 && hex.charAt(i) == '0') {
      i++;
    }
    return "0x" + hex.substring(hex.charAt(hex.length() - 1) == '0' ? i : i++);
  }

  /**
   * Provides this value represented as base 64
   * 
   * @return This value represented as base 64.
   */
  default String toBase64String() {
    return Base64.getEncoder().encodeToString(toArrayUnsafe());
  }

  @Override
  default int compareTo(Bytes b) {
    checkNotNull(b);

    int sizeCmp = Integer.compare(bitLength(), b.bitLength());
    if (sizeCmp != 0) {
      return sizeCmp;
    }

    for (int i = 0; i < size(); i++) {
      int cmp = Integer.compare(get(i) & 0xff, b.get(i) & 0xff);
      if (cmp != 0) {
        return cmp;
      }
    }
    return 0;
  }
}
