/*
 * 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.
 */
package com.google.flatbuffers.kotlin

import kotlin.jvm.JvmInline
import kotlin.math.min

// For now a typealias to guarantee type safety.
public typealias UnionOffset = Offset<Any>

public typealias UnionOffsetArray = OffsetArray<Any>

public typealias StringOffsetArray = OffsetArray<String>

public inline fun UnionOffsetArray(
  size: Int,
  crossinline call: (Int) -> Offset<Any>,
): UnionOffsetArray = UnionOffsetArray(IntArray(size) { call(it).value })

public inline fun StringOffsetArray(
  size: Int,
  crossinline call: (Int) -> Offset<String>,
): StringOffsetArray = StringOffsetArray(IntArray(size) { call(it).value })

/** Represents a "pointer" to a pointer types (table, string, struct) within the buffer */
@JvmInline
public value class Offset<T>(public val value: Int) {
  public fun toUnion(): UnionOffset = UnionOffset(value)
}

/** Represents an array of offsets. Used to avoid boxing offset types. */
@JvmInline
public value class OffsetArray<T>(public val value: IntArray) {
  public inline val size: Int
    get() = value.size

  public inline operator fun get(index: Int): Offset<T> = Offset(value[index])
}

public inline fun <T> OffsetArray(size: Int, crossinline call: (Int) -> Offset<T>): OffsetArray<T> {
  return OffsetArray(IntArray(size) { call(it).value })
}

/** Represents a "pointer" to a vector type with elements T */
@JvmInline public value class VectorOffset<T>(public val value: Int)

public fun <T> Int.toOffset(): Offset<T> = Offset(this)

public operator fun <T> Offset<T>.minus(other: Int): Offset<T> = Offset(this.value - other)

public operator fun <T> Int.minus(other: Offset<T>): Int {
  return this - other.value
}

/** All tables in the generated code derive from this class, and add their own accessors. */
public open class Table {

  /** Used to hold the position of the `bb` buffer. */
  public var bufferPos: Int = 0

  /** The underlying ReadWriteBuffer to hold the data of the Table. */
  public var bb: ReadWriteBuffer = emptyBuffer

  /** Used to hold the vtable position. */
  public var vtableStart: Int = 0

  /** Used to hold the vtable size. */
  public var vtableSize: Int = 0

  protected inline fun <reified T> Int.invalid(default: T, crossinline valid: (Int) -> T): T =
    if (this != 0) valid(this) else default

  protected inline fun <reified T> lookupField(
    i: Int,
    default: T,
    crossinline found: (Int) -> T,
  ): T = offset(i).invalid(default) { found(it) }

  /**
   * Look up a field in the vtable.
   *
   * @param vtableOffset An `int` offset to the vtable in the Table's ReadWriteBuffer.
   * @return Returns an offset into the object, or `0` if the field is not present.
   */
  public fun offset(vtableOffset: Int): Int =
    if (vtableOffset < vtableSize) bb.getShort(vtableStart + vtableOffset).toInt() else 0

  /**
   * Retrieve a relative offset.
   *
   * @param offset An `int` index into the Table's ReadWriteBuffer containing the relative offset.
   * @return Returns the relative offset stored at `offset`.
   */
  public fun indirect(offset: Int): Int = offset + bb.getInt(offset)

  /**
   * Create a Java `String` from UTF-8 data stored inside the FlatBuffer.
   *
   * This allocates a new string and converts to wide chars upon each access, which is not very
   * efficient. Instead, each FlatBuffer string also comes with an accessor based on
   * __vector_as_ReadWriteBuffer below, which is much more efficient, assuming your Java program can
   * handle UTF-8 data directly.
   *
   * @param offset An `int` index into the Table's ReadWriteBuffer.
   * @return Returns a `String` from the data stored inside the FlatBuffer at `offset`.
   */
  public fun string(offset: Int): String = string(offset, bb)

  /**
   * Get the length of a vector.
   *
   * @param offset An `int` index into the Table's ReadWriteBuffer.
   * @return Returns the length of the vector whose offset is stored at `offset`.
   */
  public fun vectorLength(offset: Int): Int {
    var newOffset = offset
    newOffset += bufferPos
    newOffset += bb.getInt(newOffset)
    return bb.getInt(newOffset)
  }

  /**
   * Get the start data of a vector.
   *
   * @param offset An `int` index into the Table's ReadWriteBuffer.
   * @return Returns the start of the vector data whose offset is stored at `offset`.
   */
  public fun vector(offset: Int): Int {
    var newOffset = offset
    newOffset += bufferPos
    return newOffset + bb.getInt(newOffset) + Int.SIZE_BYTES // data starts after the length
  }

  /**
   * Initialize vector as a ReadWriteBuffer.
   *
   * This is more efficient than using duplicate, since it doesn't copy the data nor allocates a new
   * [ReadBuffer], creating no garbage to be collected.
   *
   * @param buffer The [ReadBuffer] for the array
   * @param vectorOffset The position of the vector in the byte buffer
   * @param elemSize The size of each element in the array
   * @return The [ReadBuffer] for the array
   */
  public fun vectorAsBuffer(buffer: ReadWriteBuffer, vectorOffset: Int, elemSize: Int): ReadBuffer {
    val o = offset(vectorOffset)
    if (o == 0) return emptyBuffer
    val vectorStart = vector(o)
    return buffer.slice(vectorStart, vectorLength(o) * elemSize)
  }

  /**
   * Initialize any Table-derived type to point to the union at the given `offset`.
   *
   * @param t A `Table`-derived type that should point to the union at `offset`.
   * @param offset An `int` index into the Table's ReadWriteBuffer.
   * @return Returns the Table that points to the union at `offset`.
   */
  public fun union(t: Table, offset: Int): Table = union(t, offset, bb)

  /**
   * Sort tables by the key.
   *
   * @param offsets An 'int' indexes of the tables into the bb.
   * @param bb A `ReadWriteBuffer` to get the tables.
   */
  public fun <T> sortTables(offsets: Array<Offset<T>>, bb: ReadWriteBuffer) {
    val off = offsets.sortedWith { o1, o2 -> keysCompare(o1, o2, bb) }
    for (i in offsets.indices) offsets[i] = off[i]
  }

  /**
   * Compare two tables by the key.
   *
   * @param o1 An 'Integer' index of the first key into the bb.
   * @param o2 An 'Integer' index of the second key into the bb.
   * @param buffer A `ReadWriteBuffer` to get the keys.
   */
  public open fun keysCompare(o1: Offset<*>, o2: Offset<*>, buffer: ReadWriteBuffer): Int = 0

  /**
   * Re-init the internal state with an external buffer `ReadWriteBuffer` and an offset within.
   *
   * This method exists primarily to allow recycling Table instances without risking memory leaks
   * due to `ReadWriteBuffer` references.
   */
  public inline fun <reified T : Table> reset(i: Int, reuseBuffer: ReadWriteBuffer): T {
    bb = reuseBuffer
    if (bb != emptyBuffer) {
      bufferPos = i
      vtableStart = bufferPos - bb.getInt(bufferPos)
      vtableSize = bb.getShort(vtableStart).toInt()
    } else {
      bufferPos = 0
      vtableStart = 0
      vtableSize = 0
    }
    return this as T
  }

  /**
   * Resets the internal state with a null `ReadWriteBuffer` and a zero position.
   *
   * This method exists primarily to allow recycling Table instances without risking memory leaks
   * due to `ReadWriteBuffer` references. The instance will be unusable until it is assigned again
   * to a `ReadWriteBuffer`.
   */
  public inline fun <reified T : Table> reset(): T = reset(0, emptyBuffer)

  public companion object {

    public fun offset(vtableOffset: Int, offset: Offset<*>, bb: ReadWriteBuffer): Int {
      val vtable: Int = bb.capacity - offset.value
      return bb.getShort(vtable + vtableOffset - bb.getInt(vtable)) + vtable
    }

    /**
     * Retrieve a relative offset.
     *
     * @param offset An `int` index into a ReadWriteBuffer containing the relative offset.
     * @param bb from which the relative offset will be retrieved.
     * @return Returns the relative offset stored at `offset`.
     */
    public fun indirect(offset: Int, bb: ReadWriteBuffer): Int {
      return offset + bb.getInt(offset)
    }

    /**
     * Create a Java `String` from UTF-8 data stored inside the FlatBuffer.
     *
     * This allocates a new string and converts to wide chars upon each access, which is not very
     * efficient. Instead, each FlatBuffer string also comes with an accessor based on
     * __vector_as_ReadWriteBuffer below, which is much more efficient, assuming your Java program
     * can handle UTF-8 data directly.
     *
     * @param offset An `int` index into the Table's ReadWriteBuffer.
     * @param bb Table ReadWriteBuffer used to read a string at given offset.
     * @return Returns a `String` from the data stored inside the FlatBuffer at `offset`.
     */
    public fun string(offset: Int, bb: ReadWriteBuffer): String {
      var newOffset = offset
      newOffset += bb.getInt(newOffset)
      val length: Int = bb.getInt(newOffset)
      return bb.getString(newOffset + Int.SIZE_BYTES, length)
    }

    /**
     * Initialize any Table-derived type to point to the union at the given `offset`.
     *
     * @param t A `Table`-derived type that should point to the union at `offset`.
     * @param offset An `int` index into the Table's ReadWriteBuffer.
     * @param bb Table ReadWriteBuffer used to initialize the object Table-derived type.
     * @return Returns the Table that points to the union at `offset`.
     */
    public fun union(t: Table, offset: Int, bb: ReadWriteBuffer): Table =
      t.reset(indirect(offset, bb), bb)

    /**
     * Check if a [ReadWriteBuffer] contains a file identifier.
     *
     * @param bb A `ReadWriteBuffer` to check if it contains the identifier `ident`.
     * @param ident A `String` identifier of the FlatBuffer file.
     * @return True if the buffer contains the file identifier
     */
    public fun hasIdentifier(bb: ReadWriteBuffer?, ident: String): Boolean {
      val identifierLength = 4
      if (ident.length != identifierLength)
        throw AssertionError("FlatBuffers: file identifier must be length $identifierLength")
      for (i in 0 until identifierLength) {
        if (ident[i].code.toByte() != bb!![bb.limit + Int.SIZE_BYTES + i]) return false
      }
      return true
    }

    /**
     * Compare two strings in the buffer.
     *
     * @param offsetA An 'int' index of the first string into the bb.
     * @param offsetB An 'int' index of the second string into the bb.
     * @param bb A `ReadWriteBuffer` to get the strings.
     */
    public fun compareStrings(offsetA: Int, offsetB: Int, bb: ReadWriteBuffer): Int {
      var offset1 = offsetA
      var offset2 = offsetB
      offset1 += bb.getInt(offset1)
      offset2 += bb.getInt(offset2)
      val len1: Int = bb.getInt(offset1)
      val len2: Int = bb.getInt(offset2)
      val startPos1: Int = offset1 + Int.SIZE_BYTES
      val startPos2: Int = offset2 + Int.SIZE_BYTES
      val len: Int = min(len1, len2)
      for (i in 0 until len) {
        if (bb[i + startPos1] != bb[i + startPos2]) {
          return bb[i + startPos1] - bb[i + startPos2]
        }
      }
      return len1 - len2
    }

    /**
     * Compare string from the buffer with the 'String' object.
     *
     * @param offset An 'int' index of the first string into the bb.
     * @param key Second string as a byte array.
     * @param bb A `ReadWriteBuffer` to get the first string.
     */
    public fun compareStrings(offset: Int, key: ByteArray, bb: ReadWriteBuffer): Int {
      var offset1 = offset
      offset1 += bb.getInt(offset1)
      val len1: Int = bb.getInt(offset1)
      val len2 = key.size
      val startPos: Int = offset1 + Int.SIZE_BYTES
      val len: Int = min(len1, len2)
      for (i in 0 until len) {
        if (bb[i + startPos] != key[i]) return bb[i + startPos] - key[i]
      }
      return len1 - len2
    }
  }
}

/** All structs in the generated code derive from this class, and add their own accessors. */
public open class Struct {
  /** Used to hold the position of the `bb` buffer. */
  protected var bufferPos: Int = 0

  /** The underlying ByteBuffer to hold the data of the Struct. */
  protected var bb: ReadWriteBuffer = emptyBuffer

  /**
   * Re-init the internal state with an external buffer `ByteBuffer` and an offset within.
   *
   * This method exists primarily to allow recycling Table instances without risking memory leaks
   * due to `ByteBuffer` references.
   */
  protected inline fun <reified T : Struct> reset(i: Int, reuseBuffer: ReadWriteBuffer): T {
    bb = reuseBuffer
    bufferPos = if (bb != emptyBuffer) i else 0
    return this as T
  }

  /**
   * Resets internal state with a null `ByteBuffer` and a zero position.
   *
   * This method exists primarily to allow recycling Struct instances without risking memory leaks
   * due to `ByteBuffer` references. The instance will be unusable until it is assigned again to a
   * `ByteBuffer`.
   */
  private inline fun <reified T : Struct> reset(): T = reset(0, emptyBuffer)
}

public inline val <T> T.value: T
  get() = this

public const val VERSION_2_0_8: Int = 1
