/*
 * Copyright 2021 Google Inc. All rights reserved.
 *
 * 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.
 */
@file:Suppress("NOTHING_TO_INLINE")
@file:JvmName("FlexBuffers")

package com.google.flatbuffers.kotlin

import kotlin.jvm.JvmName

/**
 * Reads a FlexBuffer message in ReadBuf and returns [Reference] to the root element.
 *
 * @param buffer ReadBuf containing FlexBuffer message
 * @return [Reference] to the root object
 */
public fun getRoot(buffer: ReadBuffer): Reference {
  var end: Int = buffer.limit
  val byteWidth = buffer[--end].toInt()
  val packetType = buffer[--end].toInt()
  end -= byteWidth // The root data item.
  return Reference(buffer, end, ByteWidth(byteWidth), packetType)
}

/**
 * Represents an generic element in the buffer. It can be specialized into scalar types, using for
 * example, [Reference.toInt], or casted into Flexbuffer object types, like [Reference.toMap] or
 * [Reference.toBlob].
 */
@Suppress("NOTHING_TO_INLINE")
public class Reference
internal constructor(
  internal val buffer: ReadBuffer,
  internal val end: Int,
  internal val parentWidth: ByteWidth,
  internal val byteWidth: ByteWidth,
  public val type: FlexBufferType,
) {

  internal constructor(
    bb: ReadBuffer,
    end: Int,
    parentWidth: ByteWidth,
    packedType: Int,
  ) : this(
    bb,
    end,
    parentWidth,
    ByteWidth(1 shl (packedType and 3)),
    FlexBufferType((packedType shr 2)),
  )

  /**
   * Checks whether the element is null type
   *
   * @return true if null type
   */
  public val isNull: Boolean
    get() = type == T_NULL

  /**
   * Checks whether the element is boolean type
   *
   * @return true if boolean type
   */
  public val isBoolean: Boolean
    get() = type == T_BOOL

  /**
   * Checks whether the element type is numeric (signed/unsigned integers and floats)
   *
   * @return true if numeric type
   */
  public val isNumeric: Boolean
    get() = isIntOrUInt || isFloat

  /**
   * Checks whether the element type is signed or unsigned integers
   *
   * @return true if an integer type
   */
  public val isIntOrUInt: Boolean
    get() = isInt || isUInt

  /**
   * Checks whether the element type is float
   *
   * @return true if a float type
   */
  public val isFloat: Boolean
    get() = type == T_FLOAT || type == T_INDIRECT_FLOAT

  /**
   * Checks whether the element type is signed integer
   *
   * @return true if a signed integer type
   */
  public val isInt: Boolean
    get() = type == T_INT || type == T_INDIRECT_INT

  /**
   * Checks whether the element type is signed integer
   *
   * @return true if a signed integer type
   */
  public val isUInt: Boolean
    get() = type == T_UINT || type == T_INDIRECT_UINT

  /**
   * Checks whether the element type is string
   *
   * @return true if a string type
   */
  public val isString: Boolean
    get() = type == T_STRING

  /**
   * Checks whether the element type is key
   *
   * @return true if a key type
   */
  public val isKey: Boolean
    get() = type == T_KEY

  /**
   * Checks whether the element type is vector or a map. [TypedVector] are considered different
   * types and will return false.
   *
   * @return true if a vector type
   */
  public val isVector: Boolean
    get() = type == T_VECTOR || type == T_MAP

  /**
   * Checks whether the element type is typed vector
   *
   * @return true if a typed vector type
   */
  public val isTypedVector: Boolean
    get() = type.isTypedVector()

  /**
   * Checks whether the element type is a map
   *
   * @return true if a map type
   */
  public val isMap: Boolean
    get() = type == T_MAP

  /**
   * Checks whether the element type is a blob
   *
   * @return true if a blob type
   */
  public val isBlob: Boolean
    get() = type == T_BLOB

  /** Assumes [Reference] as a [Vector] and returns a [Reference] at index [index]. */
  public operator fun get(index: Int): Reference = toVector()[index]

  /** Assumes [Reference] as a [Map] and returns a [Reference] for the value at key [key]. */
  public operator fun get(key: String): Reference = toMap()[key]

  /**
   * Returns element as a [Boolean]. If element type is not boolean, it will be casted to integer
   * and compared against 0
   *
   * @return element as [Boolean]
   */
  public fun toBoolean(): Boolean = if (isBoolean) buffer.getBoolean(end) else toUInt() != 0u

  /**
   * Returns element as [Byte]. For vector types, it will return size of the vector. For String
   * type, it will be parsed as integer. Unsigned elements will become signed (with possible
   * overflow). Float elements will be casted to [Byte].
   *
   * @return [Byte] or 0 if fail to convert element to integer.
   */
  public fun toByte(): Byte = toULong().toByte()

  /**
   * Returns element as [Short]. For vector types, it will return size of the vector. For String
   * type, it will type to be parsed as integer. Unsigned elements will become signed (with possible
   * overflow). Float elements will be casted to [Short]
   *
   * @return [Short] or 0 if fail to convert element to integer.
   */
  public fun toShort(): Short = toULong().toShort()

  /**
   * Returns element as [Int]. For vector types, it will return size of the vector. For String type,
   * it will type to be parsed as integer. Unsigned elements will become signed (with possible
   * overflow). Float elements will be casted to [Int]
   *
   * @return [Int] or 0 if fail to convert element to integer.
   */
  public fun toInt(): Int = toULong().toInt()

  /**
   * Returns element as [Long]. For vector types, it will return size of the vector For String type,
   * it will type to be parsed as integer Unsigned elements will become negative Float elements will
   * be casted to integer
   *
   * @return [Long] integer or 0 if fail to convert element to long.
   */
  public fun toLong(): Long = toULong().toLong()

  /**
   * Returns element as [UByte]. For vector types, it will return size of the vector. For String
   * type, it will type to be parsed as integer. Negative elements will become unsigned counterpart.
   * Float elements will be casted to [UByte]
   *
   * @return [UByte] or 0 if fail to convert element to integer.
   */
  public fun toUByte(): UByte = toULong().toUByte()

  /**
   * Returns element as [UShort]. For vector types, it will return size of the vector. For String
   * type, it will type to be parsed as integer. Negative elements will become unsigned counterpart.
   * Float elements will be casted to [UShort]
   *
   * @return [UShort] or 0 if fail to convert element to integer.
   */
  public fun toUShort(): UShort = toULong().toUShort()

  /**
   * Returns element as [UInt]. For vector types, it will return size of the vector. For String
   * type, it will type to be parsed as integer. Negative elements will become unsigned counterpart.
   * Float elements will be casted to [UInt]
   *
   * @return [UInt] or 0 if fail to convert element to integer.
   */
  public fun toUInt(): UInt = toULong().toUInt()

  /**
   * Returns element as [ULong] integer. For vector types, it will return size of the vector For
   * String type, it will type to be parsed as integer Negative elements will become unsigned
   * counterpart. Float elements will be casted to integer
   *
   * @return [ULong] integer or 0 if fail to convert element to long.
   */
  public fun toULong(): ULong = resolve { pos: Int, width: ByteWidth ->
    when (type) {
      T_INDIRECT_INT,
      T_INDIRECT_UINT,
      T_INT,
      T_BOOL,
      T_UINT -> buffer.readULong(pos, width)
      T_FLOAT,
      T_INDIRECT_FLOAT -> buffer.readFloat(pos, width).toULong()
      T_STRING -> toString().toULong()
      T_VECTOR -> toVector().size.toULong()
      else -> 0UL
    }
  }

  /**
   * Returns element as [Float]. For vector types, it will return size of the vector For String
   * type, it will type to be parsed as [Float] Float elements will be casted to integer
   *
   * @return [Float] integer or 0 if fail to convert element to long.
   */
  public fun toFloat(): Float = resolve { pos: Int, width: ByteWidth ->
    when (type) {
      T_INDIRECT_FLOAT,
      T_FLOAT -> buffer.readFloat(pos, width).toFloat()
      T_INT -> buffer.readInt(end, parentWidth).toFloat()
      T_UINT,
      T_BOOL -> buffer.readUInt(end, parentWidth).toFloat()
      T_INDIRECT_INT -> buffer.readInt(pos, width).toFloat()
      T_INDIRECT_UINT -> buffer.readUInt(pos, width).toFloat()
      T_NULL -> 0.0f
      T_STRING -> toString().toFloat()
      T_VECTOR -> toVector().size.toFloat()
      else -> 0f
    }
  }

  /**
   * Returns element as [Double]. For vector types, it will return size of the vector For String
   * type, it will type to be parsed as [Double]
   *
   * @return [Float] integer or 0 if fail to convert element to long.
   */
  public fun toDouble(): Double = resolve { pos: Int, width: ByteWidth ->
    when (type) {
      T_INDIRECT_FLOAT,
      T_FLOAT -> buffer.readFloat(pos, width)
      T_INT -> buffer.readInt(pos, width).toDouble()
      T_UINT,
      T_BOOL -> buffer.readUInt(pos, width).toDouble()
      T_INDIRECT_INT -> buffer.readInt(pos, width).toDouble()
      T_INDIRECT_UINT -> buffer.readUInt(pos, width).toDouble()
      T_NULL -> 0.0
      T_STRING -> toString().toDouble()
      T_VECTOR -> toVector().size.toDouble()
      else -> 0.0
    }
  }

  /** Returns element as [Key] or invalid key. */
  public fun toKey(): Key =
    when (type) {
      T_KEY -> Key(buffer, buffer.indirect(end, parentWidth))
      else -> nullKey()
    }

  /**
   * Returns element as a [String]
   *
   * @return element as [String] or empty [String] if fail
   */
  override fun toString(): String =
    when (type) {
      T_STRING -> {
        val start = buffer.indirect(end, parentWidth)
        val size = buffer.readULong(start - byteWidth, byteWidth).toInt()
        buffer.getString(start, size)
      }
      T_KEY -> buffer.getKeyString(buffer.indirect(end, parentWidth))
      T_MAP -> "{ ${toMap().entries.joinToString(", ") { "${it.key}: ${it.value}"}} }"
      T_VECTOR,
      T_VECTOR_BOOL,
      T_VECTOR_FLOAT,
      T_VECTOR_INT,
      T_VECTOR_UINT,
      T_VECTOR_KEY,
      T_VECTOR_STRING_DEPRECATED -> "[ ${toVector().joinToString(", ") { it.toString() }} ]"
      T_INT -> toLong().toString()
      T_UINT -> toULong().toString()
      T_FLOAT -> toDouble().toString()
      else -> "${type.typeToString()}(end=$end)"
    }

  /**
   * Returns element as a [ByteArray], converting scalar types when possible.
   *
   * @return element as [ByteArray] or empty [ByteArray] if fail.
   */
  public fun toByteArray(): ByteArray {
    val vec =
      TypedVector(type.toElementTypedVector(), buffer, buffer.indirect(end, parentWidth), byteWidth)
    return when (type) {
      T_VECTOR_INT -> ByteArray(vec.size) { vec.getInt(it).toByte() }
      T_VECTOR_UINT -> ByteArray(vec.size) { vec.getUInt(it).toByte() }
      T_VECTOR -> ByteArray(vec.size) { vec[it].toByte() }
      T_VECTOR_FLOAT -> ByteArray(vec.size) { vec.getFloat(it).toInt().toByte() }
      else -> ByteArray(0)
    }
  }

  /**
   * Returns element as a [ByteArray], converting scalar types when possible.
   *
   * @return element as [ByteArray] or empty [ByteArray] if fail.
   */
  public fun toShortArray(): ShortArray {
    val vec =
      TypedVector(type.toElementTypedVector(), buffer, buffer.indirect(end, parentWidth), byteWidth)
    return when (type) {
      T_VECTOR_INT -> ShortArray(vec.size) { vec.getInt(it).toShort() }
      T_VECTOR_UINT -> ShortArray(vec.size) { vec.getUInt(it).toShort() }
      T_VECTOR -> ShortArray(vec.size) { vec[it].toShort() }
      T_VECTOR_FLOAT -> ShortArray(vec.size) { vec.getFloat(it).toInt().toShort() }
      else -> ShortArray(0)
    }
  }

  /**
   * Returns element as a [IntArray], converting scalar types when possible.
   *
   * @return element as [IntArray] or empty [IntArray] if fail.
   */
  public fun toIntArray(): IntArray {
    val vec =
      TypedVector(type.toElementTypedVector(), buffer, buffer.indirect(end, parentWidth), byteWidth)
    return when (type) {
      T_VECTOR_INT -> IntArray(vec.size) { vec.getInt(it).toInt() }
      T_VECTOR_UINT -> IntArray(vec.size) { vec.getUInt(it).toInt() }
      T_VECTOR -> IntArray(vec.size) { vec[it].toInt() }
      T_VECTOR_FLOAT -> IntArray(vec.size) { vec.getFloat(it).toInt() }
      else -> IntArray(0)
    }
  }

  /**
   * Returns element as a [LongArray], converting scalar types when possible.
   *
   * @return element as [LongArray] or empty [LongArray] if fail.
   */
  public fun toLongArray(): LongArray {
    val vec =
      TypedVector(type.toElementTypedVector(), buffer, buffer.indirect(end, parentWidth), byteWidth)
    return when (type) {
      T_VECTOR_INT -> LongArray(vec.size) { vec.getInt(it) }
      T_VECTOR_UINT -> LongArray(vec.size) { vec.getInt(it) }
      T_VECTOR -> LongArray(vec.size) { vec[it].toLong() }
      T_VECTOR_FLOAT -> LongArray(vec.size) { vec.getFloat(it).toLong() }
      else -> LongArray(0)
    }
  }

  /**
   * Returns element as a [UByteArray], converting scalar types when possible.
   *
   * @return element as [UByteArray] or empty [UByteArray] if fail.
   */
  public fun toUByteArray(): UByteArray {
    val vec =
      TypedVector(type.toElementTypedVector(), buffer, buffer.indirect(end, parentWidth), byteWidth)
    return when (type) {
      T_VECTOR_INT -> UByteArray(vec.size) { vec.getInt(it).toUByte() }
      T_VECTOR_UINT -> UByteArray(vec.size) { vec.getUInt(it).toUByte() }
      T_VECTOR -> UByteArray(vec.size) { vec[it].toUByte() }
      T_VECTOR_FLOAT -> UByteArray(vec.size) { vec.getFloat(it).toInt().toUByte() }
      else -> UByteArray(0)
    }
  }

  /**
   * Returns element as a [UIntArray], converting scalar types when possible.
   *
   * @return element as [UIntArray] or empty [UIntArray] if fail.
   */
  public fun toUShortArray(): UShortArray {
    val vec =
      TypedVector(type.toElementTypedVector(), buffer, buffer.indirect(end, parentWidth), byteWidth)
    return when (type) {
      T_VECTOR_INT -> UShortArray(vec.size) { vec.getInt(it).toUShort() }
      T_VECTOR_UINT -> UShortArray(vec.size) { vec.getUInt(it).toUShort() }
      T_VECTOR -> UShortArray(vec.size) { vec[it].toUShort() }
      T_VECTOR_FLOAT -> UShortArray(vec.size) { vec.getFloat(it).toUInt().toUShort() }
      else -> UShortArray(0)
    }
  }

  /**
   * Returns element as a [UIntArray], converting scalar types when possible.
   *
   * @return element as [UIntArray] or empty [UIntArray] if fail.
   */
  public fun toUIntArray(): UIntArray {
    val vec =
      TypedVector(type.toElementTypedVector(), buffer, buffer.indirect(end, parentWidth), byteWidth)
    return when (type) {
      T_VECTOR_INT -> UIntArray(vec.size) { vec.getInt(it).toUInt() }
      T_VECTOR_UINT -> UIntArray(vec.size) { vec.getUInt(it).toUInt() }
      T_VECTOR -> UIntArray(vec.size) { vec[it].toUInt() }
      T_VECTOR_FLOAT -> UIntArray(vec.size) { vec.getFloat(it).toUInt() }
      else -> UIntArray(0)
    }
  }

  /**
   * Returns element as a [ULongArray], converting scalar types when possible.
   *
   * @return element as [ULongArray] or empty [ULongArray] if fail.
   */
  public fun toULongArray(): ULongArray {
    val vec =
      TypedVector(type.toElementTypedVector(), buffer, buffer.indirect(end, parentWidth), byteWidth)
    return when (type) {
      T_VECTOR_INT -> ULongArray(vec.size) { vec.getUInt(it) }
      T_VECTOR_UINT -> ULongArray(vec.size) { vec.getUInt(it) }
      T_VECTOR -> ULongArray(vec.size) { vec[it].toULong() }
      T_VECTOR_FLOAT -> ULongArray(vec.size) { vec.getFloat(it).toULong() }
      else -> ULongArray(0)
    }
  }

  /**
   * Returns element as a [FloatArray], converting scalar types when possible.
   *
   * @return element as [FloatArray] or empty [FloatArray] if fail.
   */
  public fun toFloatArray(): FloatArray {
    val vec =
      TypedVector(type.toElementTypedVector(), buffer, buffer.indirect(end, parentWidth), byteWidth)
    return when (type) {
      T_VECTOR_FLOAT -> FloatArray(vec.size) { vec.getFloat(it).toFloat() }
      T_VECTOR_INT -> FloatArray(vec.size) { vec.getInt(it).toFloat() }
      T_VECTOR_UINT -> FloatArray(vec.size) { vec.getUInt(it).toFloat() }
      T_VECTOR -> FloatArray(vec.size) { vec[it].toFloat() }
      else -> FloatArray(0)
    }
  }

  /**
   * Returns element as a [DoubleArray], converting scalar types when possible.
   *
   * @return element as [DoubleArray] or empty [DoubleArray] if fail.
   */
  public fun toDoubleArray(): DoubleArray {
    val vec =
      TypedVector(type.toElementTypedVector(), buffer, buffer.indirect(end, parentWidth), byteWidth)
    return when (type) {
      T_VECTOR_FLOAT -> DoubleArray(vec.size) { vec[it].toDouble() }
      T_VECTOR_INT -> DoubleArray(vec.size) { vec[it].toDouble() }
      T_VECTOR_UINT -> DoubleArray(vec.size) { vec[it].toDouble() }
      T_VECTOR -> DoubleArray(vec.size) { vec[it].toDouble() }
      else -> DoubleArray(0)
    }
  }

  /**
   * Returns element as a [Vector]
   *
   * @return element as [Vector] or empty [Vector] if fail
   */
  public fun toVector(): Vector {
    return when {
      isVector -> Vector(buffer, buffer.indirect(end, parentWidth), byteWidth)
      isTypedVector ->
        TypedVector(
          type.toElementTypedVector(),
          buffer,
          buffer.indirect(end, parentWidth),
          byteWidth,
        )
      else -> emptyVector()
    }
  }

  /**
   * Returns element as a [Blob]
   *
   * @return element as [Blob] or empty [Blob] if fail
   */
  public fun toBlob(): Blob {
    return when (type) {
      T_BLOB,
      T_STRING -> Blob(buffer, buffer.indirect(end, parentWidth), byteWidth)
      else -> emptyBlob()
    }
  }

  /**
   * Returns element as a [Map].
   *
   * @return element as [Map] or empty [Map] if fail
   */
  public fun toMap(): Map =
    when (type) {
      T_MAP -> Map(buffer, buffer.indirect(end, parentWidth), byteWidth)
      else -> emptyMap()
    }

  private inline fun <T> resolve(crossinline block: (pos: Int, width: ByteWidth) -> T): T {
    return if (type.isIndirectScalar()) {
      block(buffer.indirect(end, byteWidth), byteWidth)
    } else {
      block(end, parentWidth)
    }
  }

  override fun equals(other: Any?): Boolean {
    if (this === other) return true
    if (other == null || this::class != other::class) return false
    other as Reference
    if (
      buffer != other.buffer ||
        end != other.end ||
        parentWidth != other.parentWidth ||
        byteWidth != other.byteWidth ||
        type != other.type
    )
      return false
    return true
  }

  override fun hashCode(): Int {
    var result = buffer.hashCode()
    result = 31 * result + end
    result = 31 * result + parentWidth.value
    result = 31 * result + byteWidth.value
    result = 31 * result + type.hashCode()
    return result
  }
}

/**
 * Represents any element that has a size property to it, like: [Map], [Vector] and [TypedVector].
 */
public open class Sized
internal constructor(
  public val buffer: ReadBuffer,
  public val end: Int,
  public val byteWidth: ByteWidth,
) {
  public open val size: Int = buffer.readSize(end, byteWidth)
}

/** Represent an array of bytes in the buffer. */
public open class Blob internal constructor(buffer: ReadBuffer, end: Int, byteWidth: ByteWidth) :
  Sized(buffer, end, byteWidth) {
  /**
   * Return [Blob] as [ReadBuffer]
   *
   * @return blob as [ReadBuffer]
   */
  public fun data(): ReadBuffer = buffer.slice(end, size)

  /**
   * Copy [Blob] into a [ByteArray]
   *
   * @return A [ByteArray] containing the blob data.
   */
  public fun toByteArray(): ByteArray {
    val result = ByteArray(size)
    for (i in 0 until size) {
      result[i] = buffer[end + i]
    }
    return result
  }

  /**
   * Return individual byte at a given position
   *
   * @param pos position of the byte to be read
   */
  public operator fun get(pos: Int): Byte {
    if (pos !in 0..size) error("$pos index out of bounds. Should be in range 0..$size")
    return buffer[end + pos]
  }

  override fun toString(): String = buffer.getString(end, size)
}

/** [Vector] represents an array of elements in the buffer. The element can be of any type. */
public open class Vector internal constructor(buffer: ReadBuffer, end: Int, byteWidth: ByteWidth) :
  Collection<Reference>, Sized(buffer, end, byteWidth) {

  /**
   * Returns a [Reference] from the [Vector] at position [index]. Returns a null reference
   *
   * @param index position in the vector.
   * @return [Reference] for a key or a null [Reference] if not found.
   */
  public open operator fun get(index: Int): Reference {
    if (index >= size) return nullReference()
    val packedType = buffer[(end + size * byteWidth.value + index)].toInt()
    val objEnd = end + index * byteWidth
    return Reference(buffer, objEnd, byteWidth, packedType)
  }

  // overrides from Collection<Reference>

  override fun contains(element: Reference): Boolean = find { it == element } != null

  override fun containsAll(elements: Collection<Reference>): Boolean {
    elements.forEach { if (!contains(it)) return false }
    return true
  }

  override fun isEmpty(): Boolean = size == 0

  override fun iterator(): Iterator<Reference> =
    object : Iterator<Reference> {
      var position = 0

      override fun hasNext(): Boolean = position != size

      override fun next(): Reference = get(position++)
    }
}

/** [TypedVector] represents an array of scalar elements of the same type in the buffer. */
public open class TypedVector(
  private val elementType: FlexBufferType,
  buffer: ReadBuffer,
  end: Int,
  byteWidth: ByteWidth,
) : Vector(buffer, end, byteWidth) {

  /**
   * Returns a [Reference] from the [TypedVector] at position [index]. Returns a null reference
   *
   * @param index position in the vector.
   * @return [Reference] for a key or a null [Reference] if not found.
   */
  override operator fun get(index: Int): Reference {
    if (index >= size) return nullReference()
    val childPos: Int = end + index * byteWidth
    return Reference(buffer, childPos, byteWidth, ByteWidth(1), elementType)
  }

  private inline fun <T> resolveAt(index: Int, crossinline block: (Int, ByteWidth) -> T): T {
    val childPos: Int = end + index * byteWidth
    return block(childPos, byteWidth)
  }

  internal fun getBoolean(index: Int): Boolean =
    resolveAt(index) { pos: Int, _: ByteWidth -> buffer.getBoolean(pos) }

  internal fun getInt(index: Int): Long =
    resolveAt(index) { pos: Int, width: ByteWidth -> buffer.readLong(pos, width) }

  internal fun getUInt(index: Int): ULong =
    resolveAt(index) { pos: Int, width: ByteWidth -> buffer.readULong(pos, width) }

  internal fun getFloat(index: Int): Double =
    resolveAt(index) { pos: Int, width: ByteWidth -> buffer.readFloat(pos, width) }
}

/** Represents a key element in the buffer. Keys are used to reference objects in a [Map] */
public data class Key(
  public val buffer: ReadBuffer,
  public val start: Int,
  public val end: Int = buffer.findFirst(ZeroByte, start),
) {

  val sizeInBytes: Int = end - start

  private val codePoint = CharArray(2)

  val sizeInChars: Int
    get() {
      var count = 0
      var i = start
      while (i < end) {
        val size = codePointSizeInBytes(i)
        i += size
        count += if (size == 4) 2 else 1
      }
      return count
    }

  public operator fun get(index: Int): Char {
    var count = 0
    var i = start
    var size = 0
    // we loop over the bytes to find the right position for the "char" at index i
    while (i < end && count < index) {
      size = codePointSizeInBytes(i)
      i += size
      // 4 bytes utf8 are 2 chars wide, the rest is on char.
      count += if (size == 4) 2 else 1
    }
    return when {
      count == index -> {
        Utf8.decodeUtf8CodePoint(buffer, i, codePoint)
        codePoint[0]
      }
      count == index + 1 && size == 4 -> {
        Utf8.decodeUtf8CodePoint(buffer, i - size, codePoint)
        codePoint[1]
      }
      else -> error("Invalid count=$count, index=$index")
    }
  }

  private inline fun codePointSizeInBytes(pos: Int): Int {
    val b = buffer[pos]
    return when {
      Utf8.isOneByte(b) -> 1
      Utf8.isTwoBytes(b) -> 2
      Utf8.isThreeBytes(b) -> 3
      else -> 4
    }
  }

  override fun toString(): String =
    if (sizeInBytes > 0) buffer.getString(start, sizeInBytes) else ""

  /** Checks whether Key is invalid or not. */
  public fun isInvalid(): Boolean = sizeInBytes <= 0
}

/** A Map class that provide support to access Key-Value data from Flexbuffers. */
public class Map internal constructor(buffer: ReadBuffer, end: Int, byteWidth: ByteWidth) :
  Sized(buffer, end, byteWidth), kotlin.collections.Map<Key, Reference> {

  // used for accessing the key vector elements
  private var keyVectorEnd: Int
  private var keyVectorByteWidth: ByteWidth

  init {
    val keysOffset = end - (3 * byteWidth) // 3 is number of prefixed fields
    keyVectorEnd = buffer.indirect(keysOffset, byteWidth)
    keyVectorByteWidth = ByteWidth(buffer.readInt(keysOffset + byteWidth, byteWidth))
  }

  /**
   * Returns a [Reference] from the [Map] at position [index]. Returns a null reference
   *
   * @param index position in the map
   * @return [Reference] for a key or a null [Reference] if not found.
   */
  public operator fun get(index: Int): Reference {
    if (index >= size) return nullReference()
    val packedPos = end + size * byteWidth + index
    val packedType = buffer[packedPos].toInt()
    val objEnd = end + index * byteWidth
    return Reference(buffer, objEnd, byteWidth, packedType)
  }

  /**
   * Returns a [Reference] from the [Map] for a given [String] [key].
   *
   * @param key access key to element on map
   * @return [Reference] for a key or a null [Reference] if not found.
   */
  public operator fun get(key: String): Reference {
    val index: Int = binarySearch(key)
    return if (index in 0 until size) {
      get(index)
    } else nullReference()
  }

  /**
   * Returns a [Reference] from the [Map] for a given [Key] [key].
   *
   * @param key access key to element on map
   * @return [Reference] for a key or a null [Reference] if not found.
   */
  override operator fun get(key: Key): Reference {
    val index = binarySearch(key)
    return if (index in 0 until size) {
      get(index)
    } else nullReference()
  }

  /**
   * Checks whether the map contains a [key].
   *
   * @param key [String]
   * @return true if key is found in the map, otherwise false.
   */
  public operator fun contains(key: String): Boolean = binarySearch(key) >= 0

  /**
   * Returns a [Key] for a given position [index] in the [Map].
   *
   * @param index of the key in the map
   * @return a Key for the given index. Out of bounds indexes returns invalid keys.
   */
  public fun keyAt(index: Int): Key {
    val childPos: Int = keyVectorEnd + index * keyVectorByteWidth
    return Key(buffer, buffer.indirect(childPos, keyVectorByteWidth))
  }

  /**
   * Returns a [Key] as [String] for a given position [index] in the [Map].
   *
   * @param index of the key in the map
   * @return a Key for the given index. Out of bounds indexes returns empty string.
   */
  public fun keyAsString(index: Int): String {
    val childPos: Int = keyVectorEnd + index * keyVectorByteWidth
    val start = buffer.indirect(childPos, keyVectorByteWidth)
    val end = buffer.findFirst(ZeroByte, start)
    return if (end > start) buffer.getString(start, end - start) else ""
  }

  // Overrides from kotlin.collections.Map<Key, Reference>

  public data class Entry(override val key: Key, override val value: Reference) :
    kotlin.collections.Map.Entry<Key, Reference>

  override val entries: Set<kotlin.collections.Map.Entry<Key, Reference>>
    get() = keys.map { Entry(it, get(it.toString())) }.toSet()

  override val keys: Set<Key>
    get() {
      val set = LinkedHashSet<Key>(size)
      for (i in 0 until size) {
        val key = keyAt(i)
        set.add(key)
      }
      return set
    }

  /**
   * Returns a [Vector] for accessing all values in the [Map].
   *
   * @return [Vector] of values.
   */
  override val values: Collection<Reference>
    get() = Vector(buffer, end, byteWidth)

  override fun containsKey(key: Key): Boolean {
    for (i in 0 until size) {
      if (key == keyAt(i)) return true
    }
    return false
  }

  override fun containsValue(value: Reference): Boolean = values.contains(value)

  override fun isEmpty(): Boolean = size == 0

  // Performs a binary search on a key vector and return index of the key in key vector
  private fun binarySearch(searchedKey: String) = binarySearch {
    compareCharSequence(it, searchedKey)
  }

  // Performs a binary search on a key vector and return index of the key in key vector
  private fun binarySearch(key: Key): Int = binarySearch { compareKeys(it, key.start) }

  private inline fun binarySearch(crossinline comparisonBlock: (Int) -> Int): Int {
    var low = 0
    var high = size - 1
    while (low <= high) {
      val mid = low + high ushr 1
      val keyPos: Int = buffer.indirect(keyVectorEnd + mid * keyVectorByteWidth, keyVectorByteWidth)
      val cmp: Int = comparisonBlock(keyPos)
      if (cmp < 0) low = mid + 1 else if (cmp > 0) high = mid - 1 else return mid // key found
    }
    return -(low + 1) // key not found
  }

  // compares a CharSequence against a T_KEY
  private fun compareKeys(start: Int, other: Int): Int {
    var bufferPos = start
    var otherPos = other
    val limit: Int = buffer.limit
    var c1: Byte = ZeroByte
    var c2: Byte = ZeroByte
    while (otherPos < limit) {
      c1 = buffer[bufferPos++]
      c2 = buffer[otherPos++]
      when {
        c1 == ZeroByte -> return c1 - c2
        c1 != c2 -> return c1 - c2
      }
    }
    return c1 - c2
  }

  // compares a CharSequence against a [CharSequence]
  private fun compareCharSequence(start: Int, other: CharSequence): Int {
    var bufferPos = start
    var otherPos = 0
    val limit: Int = buffer.limit
    val otherLimit = other.length
    // special loop for ASCII characters. Most of keys should be ASCII only, so this
    // loop should be optimized for that.
    // breaks if a multi-byte character is found
    while (otherPos < otherLimit) {
      val c2 = other[otherPos]
      // not a single byte codepoint
      if (c2.code >= 0x80) {
        break
      }
      val b: Byte = buffer[bufferPos]
      when {
        b == ZeroByte -> return -c2.code
        b < 0 -> break
        b != c2.code.toByte() -> return b - c2.code.toByte()
      }
      ++bufferPos
      ++otherPos
    }
    if (bufferPos < limit) return 0

    val comparisonBuffer = ByteArray(4)
    while (bufferPos < limit) {
      val sizeInBuff = Utf8.encodeUtf8CodePoint(other, otherPos, comparisonBuffer)
      if (sizeInBuff == 0) {
        return buffer[bufferPos].toInt()
      }
      for (i in 0 until sizeInBuff) {
        val bufferByte: Byte = buffer[bufferPos++]
        val otherByte: Byte = comparisonBuffer[i]
        when {
          bufferByte == ZeroByte -> return -otherByte
          bufferByte != otherByte -> return bufferByte - otherByte
        }
      }
      otherPos += if (sizeInBuff == 4) 2 else 1
    }
    return 0
  }
}
