/*
 * 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.JvmOverloads

/**
 * Class that helps you build a FlatBuffer. See the section "Use in Kotlin" in the main FlatBuffers
 * documentation.
 */
public class FlatBufferBuilder
@JvmOverloads
constructor(
  private val initialSize: Int = 1024,
  private var buffer: ReadWriteBuffer = ArrayReadWriteBuffer(initialSize),
) {
  // Remaining space in the ByteBuffer.
  private var space: Int = buffer.capacity

  // Minimum alignment encountered so far.
  private var minalign: Int = 1

  // The vtable for the current table.
  private var vtable: IntArray = IntArray(16)

  // The amount of fields we're actually using.
  private var vtableInUse: Int = 0

  // Whether we are currently serializing a table.
  private var nested: Boolean = false

  // Whether the buffer is finished.
  private var finished: Boolean = false

  // Starting offset of the current struct/table.
  private var objectStart: Int = 0

  // List of offsets of all vtables.
  private var vtables = IntArray(16)

  // Number of entries in `vtables` in use.
  private var numVtables = 0

  // For the current vector being built.
  private var vectorNumElems = 0

  // False omits default values from the serialized data.
  private var forceDefaults = false

  // map used to cache shared strings.
  private var stringPool: MutableMap<CharSequence, Offset<String>>? = null

  /** Reset the FlatBufferBuilder by purging all data that it holds. */
  public fun clear() {
    space = buffer.capacity
    buffer.clear()
    minalign = 1
    vtable.fill(0, 0, vtableInUse)
    vtableInUse = 0
    nested = false
    finished = false
    objectStart = 0
    numVtables = 0
    vectorNumElems = 0
    stringPool?.clear()
  }

  /**
   * Offset relative to the end of the buffer.
   *
   * @return Offset relative to the end of the buffer.
   */
  public fun offset(): Int = buffer.capacity - space

  /**
   * Add zero valued bytes to prepare a new entry to be added.
   *
   * @param byteSize Number of bytes to add.
   */
  public fun pad(byteSize: Int) {
    for (i in 0 until byteSize) buffer[--space] = 0.toByte()
  }

  /**
   * Prepare to write an element of `size` after `additional_bytes` have been written, e.g. if you
   * write a string, you need to align such the int length field is aligned to
   * [com.google.flatbuffers.Int.SIZE_BYTES], and the string data follows it directly. If all you
   * need to do is alignment, `additional_bytes` will be 0.
   *
   * @param size This is the of the new element to write.
   * @param additionalBytes The padding size.
   */
  public fun prep(size: Int, additionalBytes: Int) {
    // Track the biggest thing we've ever aligned to.
    if (size > minalign) minalign = size
    // Find the amount of alignment needed such that `size` is properly
    // aligned after `additional_bytes`

    val alignSize: Int = ((buffer.capacity - space + additionalBytes).inv() + 1).and(size - 1)
    // Reallocate the buffer if needed.
    while (space < alignSize + size + additionalBytes) {
      val oldBufSize: Int = buffer.capacity
      val newBufSize = buffer.moveWrittenDataToEnd(oldBufSize + alignSize + size + additionalBytes)
      space += newBufSize - oldBufSize
    }
    if (alignSize > 0) {
      pad(alignSize)
    }
  }

  /**
   * Add a `boolean` to the buffer, backwards from the current location. Doesn't align nor check for
   * space.
   *
   * @param x A `boolean` to put into the buffer.
   */
  public fun put(x: Boolean) {
    space -= Byte.SIZE_BYTES
    buffer[space] = (if (x) 1 else 0).toByte()
  }

  /**
   * Add a [UByte] to the buffer, backwards from the current location. Doesn't align nor check for
   * space.
   *
   * @param x A [UByte] to put into the buffer.
   */
  public fun put(x: UByte): Unit = put(x.toByte())

  /**
   * Add a [Byte] to the buffer, backwards from the current location. Doesn't align nor check for
   * space.
   *
   * @param x A [Byte] to put into the buffer.
   */
  public fun put(x: Byte) {
    space -= Byte.SIZE_BYTES
    buffer[space] = x
  }

  /**
   * Add a [UShort] to the buffer, backwards from the current location. Doesn't align nor check for
   * space.
   *
   * @param x A [UShort] to put into the buffer.
   */
  public fun put(x: UShort): Unit = put(x.toShort())

  /**
   * Add a [Short] to the buffer, backwards from the current location. Doesn't align nor check for
   * space.
   *
   * @param x A [Short] to put into the buffer.
   */
  public fun put(x: Short) {
    space -= Short.SIZE_BYTES
    buffer.set(space, x)
  }

  /**
   * Add an [UInt] to the buffer, backwards from the current location. Doesn't align nor check for
   * space.
   *
   * @param x An [UInt] to put into the buffer.
   */
  public fun put(x: UInt): Unit = put(x.toInt())

  /**
   * Add an [Int] to the buffer, backwards from the current location. Doesn't align nor check for
   * space.
   *
   * @param x An [Int] to put into the buffer.
   */
  public fun put(x: Int) {
    space -= Int.SIZE_BYTES
    buffer.set(space, x)
  }

  /**
   * Add a [ULong] to the buffer, backwards from the current location. Doesn't align nor check for
   * space.
   *
   * @param x A [ULong] to put into the buffer.
   */
  public fun put(x: ULong): Unit = put(x.toLong())

  /**
   * Add a [Long] to the buffer, backwards from the current location. Doesn't align nor check for
   * space.
   *
   * @param x A [Long] to put into the buffer.
   */
  public fun put(x: Long) {
    space -= Long.SIZE_BYTES
    buffer.set(space, x)
  }

  /**
   * Add a [Float] to the buffer, backwards from the current location. Doesn't align nor check for
   * space.
   *
   * @param x A [Float] to put into the buffer.
   */
  public fun put(x: Float) {
    space -= Float.SIZE_BYTES
    buffer.set(space, x)
  }

  /**
   * Add a [Double] to the buffer, backwards from the current location. Doesn't align nor check for
   * space.
   *
   * @param x A [Double] to put into the buffer.
   */
  public fun put(x: Double) {
    space -= Double.SIZE_BYTES
    buffer.set(space, x)
  }

  /**
   * Add a [Boolean] to the buffer, properly aligned, and grows the buffer (if necessary).
   *
   * @param x A [Boolean] to put into the buffer.
   */
  public fun add(x: Boolean) {
    prep(Byte.SIZE_BYTES, 0)
    put(x)
  }

  /**
   * Add a [UByte] to the buffer, properly aligned, and grows the buffer (if necessary).
   *
   * @param x A [UByte] to put into the buffer.
   */
  public fun add(x: UByte): Unit = add(x.toByte())

  /**
   * Add a [Byte] to the buffer, properly aligned, and grows the buffer (if necessary).
   *
   * @param x A [Byte] to put into the buffer.
   */
  public fun add(x: Byte) {
    prep(Byte.SIZE_BYTES, 0)
    put(x)
  }

  /**
   * Add a [UShort] to the buffer, properly aligned, and grows the buffer (if necessary).
   *
   * @param x A [UShort] to put into the buffer.
   */
  public fun add(x: UShort): Unit = add(x.toShort())

  /**
   * Add a [Short] to the buffer, properly aligned, and grows the buffer (if necessary).
   *
   * @param x A [Short] to put into the buffer.
   */
  public fun add(x: Short) {
    prep(Short.SIZE_BYTES, 0)
    put(x)
  }

  /**
   * Add an [Unit] to the buffer, properly aligned, and grows the buffer (if necessary).
   *
   * @param x An [Unit] to put into the buffer.
   */
  public fun add(x: UInt): Unit = add(x.toInt())

  /**
   * Add an [Int] to the buffer, properly aligned, and grows the buffer (if necessary).
   *
   * @param x An [Int] to put into the buffer.
   */
  public fun add(x: Int) {
    prep(Int.SIZE_BYTES, 0)
    put(x)
  }

  /**
   * Add a [ULong] to the buffer, properly aligned, and grows the buffer (if necessary).
   *
   * @param x A [ULong] to put into the buffer.
   */
  public fun add(x: ULong): Unit = add(x.toLong())

  /**
   * Add a `long` to the buffer, properly aligned, and grows the buffer (if necessary).
   *
   * @param x A `long` to put into the buffer.
   */
  public fun add(x: Long) {
    prep(Long.SIZE_BYTES, 0)
    put(x)
  }

  /**
   * Add a [Float] to the buffer, properly aligned, and grows the buffer (if necessary).
   *
   * @param x A [Float] to put into the buffer.
   */
  public fun add(x: Float) {
    prep(Float.SIZE_BYTES, 0)
    put(x)
  }

  /**
   * Add a [Double] to the buffer, properly aligned, and grows the buffer (if necessary).
   *
   * @param x A [Double] to put into the buffer.
   */
  public fun add(x: Double) {
    prep(Double.SIZE_BYTES, 0)
    put(x)
  }

  /**
   * Adds on offset, relative to where it will be written.
   *
   * @param off The offset to add.
   */
  public fun add(off: Offset<*>): Unit = addOffset(off.value)

  public fun add(off: VectorOffset<*>): Unit = addOffset(off.value)

  private fun addOffset(off: Int) {
    prep(Int.SIZE_BYTES, 0) // Ensure alignment is already done.
    put(buffer.capacity - space - off + Int.SIZE_BYTES)
  }

  /**
   * Start a new array/vector of objects. Users usually will not call this directly. The
   * `FlatBuffers` compiler will create a start/end method for vector types in generated code.
   *
   * The expected sequence of calls is:
   * 1. Start the array using this method.
   * 1. Call [.addOffset] `num_elems` number of times to set the offset of each element in the
   *    array.
   * 1. Call [.endVector] to retrieve the offset of the array.
   *
   * For example, to create an array of strings, do:
   * <pre>`// Need 10 strings
   * FlatBufferBuilder builder = new FlatBufferBuilder(existingBuffer);
   * int[] offsets = new int[10];
   *
   * for (int i = 0; i < 10; i++) {
   * offsets[i] = fbb.createString(" " + i);
   * }
   *
   * // Have the strings in the buffer, but don't have a vector.
   * // Add a vector that references the newly created strings:
   * builder.startVector(4, offsets.length, 4);
   *
   * // Add each string to the newly created vector
   * // The strings are added in reverse order since the buffer
   * // is filled in back to front
   * for (int i = offsets.length - 1; i >= 0; i--) {
   * builder.addOffset(offsets[i]);
   * }
   *
   * // Finish off the vector
   * int offsetOfTheVector = fbb.endVector();
   * `</pre> *
   *
   * @param elemSize The size of each element in the array.
   * @param numElems The number of elements in the array.
   * @param alignment The alignment of the array.
   */
  public fun startVector(elemSize: Int, numElems: Int, alignment: Int) {
    notNested()
    vectorNumElems = numElems
    prep(Int.SIZE_BYTES, elemSize * numElems)
    prep(alignment, elemSize * numElems) // Just in case alignment > int.
    nested = true
  }

  public fun startString(numElems: Int): Unit = startVector(1, numElems, 1)

  /**
   * Finish off the creation of an array and all its elements. The array must be created with
   * [.startVector].
   *
   * @return The offset at which the newly created array starts.
   * @see .startVector
   */
  public fun <T> endVector(): VectorOffset<T> {
    if (!nested) throw AssertionError("FlatBuffers: endVector called without startVector")
    nested = false
    put(vectorNumElems)
    return VectorOffset(offset())
  }

  public fun endString(): Offset<String> {
    if (!nested) throw AssertionError("FlatBuffers: endString called without startString")
    nested = false
    put(vectorNumElems)
    return Offset(offset())
  }

  private fun endVector(): Int {
    if (!nested) throw AssertionError("FlatBuffers: endVector called without startVector")
    nested = false
    put(vectorNumElems)
    return offset()
  }

  /**
   * Create a new array/vector and return a ByteBuffer to be filled later. Call [endVector] after
   * this method to get an offset to the beginning of vector.
   *
   * @param elemSize the size of each element in bytes.
   * @param numElems number of elements in the vector.
   * @param alignment byte alignment.
   * @return ByteBuffer with position and limit set to the space allocated for the array.
   */
  public fun createUnintializedVector(
    elemSize: Int,
    numElems: Int,
    alignment: Int,
  ): ReadWriteBuffer {
    val length = elemSize * numElems
    startVector(elemSize, numElems, alignment)
    space -= length
    buffer.writePosition = space
    return buffer.writeSlice(buffer.writePosition, length)
  }

  /**
   * Create a vector of tables.
   *
   * @param offsets Offsets of the tables.
   * @return Returns offset of the vector.
   */
  public fun <T> createVectorOfTables(offsets: Array<Offset<T>>): VectorOffset<T> {
    notNested()
    startVector(Int.SIZE_BYTES, offsets.size, Int.SIZE_BYTES)
    for (i in offsets.indices.reversed()) add(offsets[i])
    return VectorOffset(endVector())
  }

  /**
   * Create a vector of sorted by the key tables.
   *
   * @param obj Instance of the table subclass.
   * @param offsets Offsets of the tables.
   * @return Returns offset of the sorted vector.
   */
  public fun <T : Table> createSortedVectorOfTables(
    obj: T,
    offsets: Array<Offset<T>>,
  ): VectorOffset<T> {
    obj.sortTables(offsets, buffer)
    return createVectorOfTables(offsets)
  }

  /**
   * Encode the String `s` in the buffer using UTF-8. If a String with this exact contents has
   * already been serialized using this method, instead simply returns the offset of the existing
   * String.
   *
   * Usage of the method will incur into additional allocations, so it is advisable to use it only
   * when it is known upfront that your message will have several repeated strings.
   *
   * @param s The String to encode.
   * @return The offset in the buffer where the encoded String starts.
   */
  public fun createSharedString(s: CharSequence): Offset<String> {
    if (stringPool == null) {
      stringPool = HashMap()
      val offset = createString(s)
      stringPool!![s] = offset
      return offset
    }
    var offset = stringPool!![s]
    if (offset == null) {
      offset = createString(s)
      stringPool?.put(s, offset)
    }
    return offset
  }

  /**
   * Encode the [CharSequence] `s` in the buffer using UTF-8.
   *
   * @param s The [CharSequence] to encode.
   * @return The offset in the buffer where the encoded string starts.
   */
  public fun createString(s: CharSequence): Offset<String> {
    val length: Int = Utf8.encodedLength(s)
    add(0.toByte())
    startString(length)
    space -= length
    buffer.writePosition = space
    buffer.put(s, length)
    return endString()
  }

  /**
   * Create a string in the buffer from an already encoded UTF-8 string in a ByteBuffer.
   *
   * @param s An already encoded UTF-8 string as a `ByteBuffer`.
   * @return The offset in the buffer where the encoded string starts.
   */
  public fun createString(s: ReadBuffer): Offset<String> {
    val length: Int = s.limit
    add(0.toByte())
    startVector(1, length, 1)
    space -= length
    buffer.writePosition = space
    buffer.put(s)
    return endString()
  }

  /**
   * Create a byte array in the buffer.
   *
   * @param arr A source array with data
   * @return The offset in the buffer where the encoded array starts.
   */
  public fun createByteVector(arr: ByteArray): VectorOffset<Byte> {
    val length = arr.size
    startVector(1, length, 1)
    space -= length
    buffer.writePosition = space
    buffer.put(arr)
    return VectorOffset(endVector())
  }

  /**
   * Create a byte array in the buffer.
   *
   * @param arr a source array with data.
   * @param offset the offset in the source array to start copying from.
   * @param length the number of bytes to copy from the source array.
   * @return The offset in the buffer where the encoded array starts.
   */
  public fun createByteVector(arr: ByteArray, offset: Int, length: Int): VectorOffset<Byte> {
    startVector(1, length, 1)
    space -= length
    buffer.writePosition = space
    buffer.put(arr, offset, length)
    return VectorOffset(endVector())
  }

  /**
   * Create a byte array in the buffer.
   *
   * The source [ReadBuffer] position is advanced until [ReadBuffer.limit] after this call.
   *
   * @param data A source [ReadBuffer] with data.
   * @return The offset in the buffer where the encoded array starts.
   */
  public fun createByteVector(
    data: ReadBuffer,
    from: Int = 0,
    until: Int = data.limit,
  ): VectorOffset<Byte> {
    val length: Int = until - from
    startVector(1, length, 1)
    space -= length
    buffer.writePosition = space
    buffer.put(data, from, until)
    return VectorOffset(endVector())
  }

  /** Should not be accessing the final buffer before it is finished. */
  public fun finished() {
    if (!finished)
      throw AssertionError(
        "FlatBuffers: you can only access the serialized buffer after it has been" +
          " finished by FlatBufferBuilder.finish()."
      )
  }

  /**
   * Should not be creating any other object, string or vector while an object is being constructed.
   */
  public fun notNested() {
    if (nested) throw AssertionError("FlatBuffers: object serialization must not be nested.")
  }

  /**
   * Structures are always stored inline, they need to be created right where they're used. You'll
   * get this assertion failure if you created it elsewhere.
   *
   * @param obj The offset of the created object.
   */
  public fun nested(obj: Int) {
    if (obj != offset()) throw AssertionError("FlatBuffers: struct must be serialized inline.")
  }

  /**
   * Start encoding a new object in the buffer. Users will not usually need to call this directly.
   * The `FlatBuffers` compiler will generate helper methods that call this method internally.
   *
   * For example, using the "Monster" code found on the "landing page". An object of type `Monster`
   * can be created using the following code:
   * <pre>`int testArrayOfString = Monster.createTestarrayofstringVector(fbb, new int[] {
   * fbb.createString("test1"),
   * fbb.createString("test2")
   * });
   *
   * Monster.startMonster(fbb);
   * Monster.addPos(fbb, Vec3.createVec3(fbb, 1.0f, 2.0f, 3.0f, 3.0,
   * Color.Green, (short)5, (byte)6));
   * Monster.addHp(fbb, (short)80);
   * Monster.addName(fbb, str);
   * Monster.addInventory(fbb, inv);
   * Monster.addTestType(fbb, (byte)Any.Monster);
   * Monster.addTest(fbb, mon2);
   * Monster.addTest4(fbb, test4);
   * Monster.addTestarrayofstring(fbb, testArrayOfString);
   * int mon = Monster.endMonster(fbb);
   * `</pre> *
   *
   * Here:
   * * The call to `Monster#startMonster(FlatBufferBuilder)` will call this method with the right
   *   number of fields set.
   * * `Monster#endMonster(FlatBufferBuilder)` will ensure [.endObject] is called.
   *
   * It's not recommended to call this method directly. If it's called manually, you must ensure to
   * audit all calls to it whenever fields are added or removed from your schema. This is
   * automatically done by the code generated by the `FlatBuffers` compiler.
   *
   * @param numFields The number of fields found in this object.
   */
  public fun startTable(numFields: Int) {
    notNested()
    if (vtable.size < numFields) {
      vtable = IntArray(numFields)
    }
    vtableInUse = numFields
    for (i in 0 until vtableInUse) vtable[i] = 0
    nested = true
    objectStart = offset()
  }

  /**
   * Add a [Boolean] to a table at `o` into its vtable, with value `x` and default `d`. If
   * `force_defaults` is `false`, compare `x` against the default value `d`. If `x` contains the
   * default value, it can be skipped.
   */
  public fun add(o: Int, x: Boolean, d: Boolean?) {
    if (forceDefaults || x != d) {
      add(x)
      slot(o)
    }
  }

  // unboxed specialization
  public fun add(o: Int, x: Boolean, d: Boolean) {
    if (forceDefaults || x != d) {
      add(x)
      slot(o)
    }
  }

  /**
   * Add a [UByte] to a table at `o` into its vtable, with value `x` and default `d`. If
   * `force_defaults` is `false`, compare `x` against the default value `d`. If `x` contains the
   * default value, it can be skipped.
   */
  public fun add(o: Int, x: UByte, d: UByte?): Unit = add(o, x.toByte(), d?.toByte())

  // unboxed specialization
  public fun add(o: Int, x: UByte, d: UByte): Unit = add(o, x.toByte(), d.toByte())

  /**
   * Add a [Byte] to a table at `o` into its vtable, with value `x` and default `d`. If
   * `force_defaults` is `false`, compare `x` against the default value `d`. If `x` contains the
   * default value, it can be skipped.
   */
  public fun add(o: Int, x: Byte, d: Byte?) {
    if (forceDefaults || x != d) {
      add(x)
      slot(o)
    }
  }

  // unboxed specialization
  public fun add(o: Int, x: Byte, d: Byte) {
    if (forceDefaults || x != d) {
      add(x)
      slot(o)
    }
  }

  /**
   * Add a [UShort] to a table at `o` into its vtable, with value `x` and default `d`. If
   * `force_defaults` is `false`, compare `x` against the default value `d`. If `x` contains the
   * default value, it can be skipped.
   */
  public fun add(o: Int, x: UShort, d: UShort?): Unit = add(o, x.toShort(), d?.toShort())

  // unboxed specialization
  public fun add(o: Int, x: UShort, d: UShort): Unit = add(o, x.toShort(), d.toShort())

  /**
   * Add a [Short] to a table at `o` into its vtable, with value `x` and default `d`. If
   * `force_defaults` is `false`, compare `x` against the default value `d`. If `x` contains the
   * default value, it can be skipped.
   */
  public fun add(o: Int, x: Short, d: Short?) {
    if (forceDefaults || x != d) {
      add(x)
      slot(o)
    }
  }

  // unboxed specialization
  public fun add(o: Int, x: Short, d: Short) {
    if (forceDefaults || x != d) {
      add(x)
      slot(o)
    }
  }

  /**
   * Add a [UInt] to a table at `o` into its vtable, with value `x` and default `d`. If
   * `force_defaults` is `false`, compare `x` against the default value `d`. If `x` contains the
   * default value, it can be skipped.
   */
  public fun add(o: Int, x: UInt, d: UInt?): Unit = add(o, x.toInt(), d?.toInt())

  // unboxed specialization
  public fun add(o: Int, x: UInt, d: UInt): Unit = add(o, x.toInt(), d.toInt())

  /**
   * Add a [Int] to a table at `o` into its vtable, with value `x` and default `d`. If
   * `force_defaults` is `false`, compare `x` against the default value `d`. If `x` contains the
   * default value, it can be skipped.
   */
  public fun add(o: Int, x: Int, d: Int?) {
    if (forceDefaults || x != d) {
      add(x)
      slot(o)
    }
  }

  // unboxed specialization
  public fun add(o: Int, x: Int, d: Int) {
    if (forceDefaults || x != d) {
      add(x)
      slot(o)
    }
  }

  /**
   * Add a [ULong] to a table at `o` into its vtable, with value `x` and default `d`. If
   * `force_defaults` is `false`, compare `x` against the default value `d`. If `x` contains the
   * default value, it can be skipped.
   */
  public fun add(o: Int, x: ULong, d: ULong?): Unit = add(o, x.toLong(), d?.toLong())

  // unboxed specialization
  public fun add(o: Int, x: ULong, d: ULong): Unit = add(o, x.toLong(), d.toLong())

  /**
   * Add a [Long] to a table at `o` into its vtable, with value `x` and default `d`. If
   * `force_defaults` is `false`, compare `x` against the default value `d`. If `x` contains the
   * default value, it can be skipped.
   */
  public fun add(o: Int, x: Long, d: Long?) {
    if (forceDefaults || x != d) {
      add(x)
      slot(o)
    }
  }

  // unboxed specialization
  public fun add(o: Int, x: Long, d: Long) {
    if (forceDefaults || x != d) {
      add(x)
      slot(o)
    }
  }

  /**
   * Add a [Float] to a table at `o` into its vtable, with value `x` and default `d`. If
   * `force_defaults` is `false`, compare `x` against the default value `d`. If `x` contains the
   * default value, it can be skipped.
   */
  public fun add(o: Int, x: Float, d: Float?) {
    if (forceDefaults || x != d) {
      add(x)
      slot(o)
    }
  }

  // unboxed specialization
  public fun add(o: Int, x: Float, d: Float) {
    if (forceDefaults || x != d) {
      add(x)
      slot(o)
    }
  }

  /**
   * Add a [Double] to a table at `o` into its vtable, with value `x` and default `d`. If
   * `force_defaults` is `false`, compare `x` against the default value `d`. If `x` contains the
   * default value, it can be skipped.
   */
  public fun add(o: Int, x: Double, d: Double?) {
    if (forceDefaults || x != d) {
      add(x)
      slot(o)
    }
  }

  // unboxed specialization
  public fun add(o: Int, x: Double, d: Double) {
    if (forceDefaults || x != d) {
      add(x)
      slot(o)
    }
  }

  /**
   * Add an `offset` to a table at `o` into its vtable, with value `x` and default `d`.
   *
   * @param o The index into the vtable.
   * @param x An `offset` to put into the buffer, depending on how defaults are handled. If
   *   `force_defaults` is `false`, compare `x` against the default value `d`. If `x` contains the
   *   default value, it can be skipped.
   * @param d An `offset` default value to compare against when `force_defaults` is `false`.
   */
  public fun add(o: Int, x: Offset<*>, d: Int) {
    if (forceDefaults || x.value != d) {
      add(x)
      slot(o)
    }
  }

  public fun add(o: Int, x: VectorOffset<*>, d: Int) {
    if (forceDefaults || x.value != d) {
      add(x)
      slot(o)
    }
  }

  /**
   * Add a struct to the table. Structs are stored inline, so nothing additional is being added.
   *
   * @param vOffset The index into the vtable.
   * @param x The offset of the created struct.
   * @param d The default value is always `0`.
   */
  public fun addStruct(vOffset: Int, x: Offset<*>, d: Offset<*>?): Unit =
    addStruct(vOffset, x.value, d?.value)

  // unboxed specialization
  public fun addStruct(vOffset: Int, x: Offset<*>, d: Offset<*>): Unit =
    addStruct(vOffset, x.value, d.value)

  public fun addStruct(vOffset: Int, x: Int, d: Int?) {
    if (x != d) {
      nested(x)
      slot(vOffset)
    }
  }

  // unboxed specialization
  public fun addStruct(vOffset: Int, x: Int, d: Int) {
    if (x != d) {
      nested(x)
      slot(vOffset)
    }
  }

  /**
   * Set the current vtable at `voffset` to the current location in the buffer.
   *
   * @param vOffset The index into the vtable to store the offset relative to the end of the buffer.
   */
  public fun slot(vOffset: Int) {
    vtable[vOffset] = offset()
  }

  /**
   * Finish off writing the object that is under construction.
   *
   * @return The offset to the object inside [.dataBuffer].
   * @see .startTable
   */
  public fun <T> endTable(): Offset<T> {
    if (!nested) throw AssertionError("FlatBuffers: endTable called without startTable")

    val vtable = this.vtable

    add(0)
    val vtableloc = offset()
    // Write out the current vtable.
    var i: Int = vtableInUse - 1
    // Trim trailing zeroes.
    while (i >= 0 && vtable[i] == 0) {
      i--
    }
    val trimmedSize = i + 1
    while (i >= 0) {
      // Offset relative to the start of the table.
      add((if (vtable[i] != 0) vtableloc - vtable[i] else 0).toShort())
      i--
    }

    add((vtableloc - objectStart).toShort())
    add(((trimmedSize + 2) * Short.SIZE_BYTES).toShort())

    // Search for an existing vtable that matches the current one.
    var existingVtable = 0
    i = 0
    outer_loop@ while (i < numVtables) {
      val vt1: Int = buffer.capacity - vtables[i]
      val vt2 = space
      val len: Short = buffer.getShort(vt1)
      if (len == buffer.getShort(vt2)) {
        var j: Int = Short.SIZE_BYTES
        while (j < len) {
          if (buffer.getShort(vt1 + j) != buffer.getShort(vt2 + j)) {
            i++
            continue@outer_loop
          }
          j += Short.SIZE_BYTES
        }
        existingVtable = vtables[i]
        break@outer_loop
      }
      i++
    }
    if (existingVtable != 0) {
      // Found a match:
      // Remove the current vtable.
      space = buffer.capacity - vtableloc
      // Point table to existing vtable.
      buffer.set(space, existingVtable - vtableloc)
    } else {
      // No match:
      // Add the location of the current vtable to the list of vtables.
      if (numVtables == vtables.size) vtables = vtables.copyOf(numVtables * 2)
      vtables[numVtables++] = offset()
      // Point table to current vtable.
      buffer.set(buffer.capacity - vtableloc, offset() - vtableloc)
    }
    nested = false
    return Offset(vtableloc)
  }

  /**
   * Checks that a required field has been set in a given table that has just been constructed.
   *
   * @param table The offset to the start of the table from the `ByteBuffer` capacity.
   * @param field The offset to the field in the vtable.
   */
  public fun required(table: Offset<*>, field: Int, fileName: String? = null) {
    val tableStart: Int = buffer.capacity - table
    val vtableStart: Int = tableStart - buffer.getInt(tableStart)
    val ok = buffer.getShort(vtableStart + field).toInt() != 0
    // If this fails, the caller will show what field needs to be set.
    if (!ok) throw AssertionError("FlatBuffers: field ${fileName ?: field} must be set")
  }

  /**
   * Finalize a buffer, pointing to the given `root_table`.
   *
   * @param rootTable An offset to be added to the buffer.
   * @param sizePrefix Whether to prefix the size to the buffer.
   */
  protected fun finish(rootTable: Offset<*>, sizePrefix: Boolean) {
    prep(minalign, Int.SIZE_BYTES + if (sizePrefix) Int.SIZE_BYTES else 0)
    add(rootTable)
    if (sizePrefix) {
      add(buffer.capacity - space)
    }
    buffer.writePosition = space
    finished = true
  }

  /**
   * Finalize a buffer, pointing to the given `root_table`.
   *
   * @param rootTable An offset to be added to the buffer.
   */
  public fun finish(rootTable: Offset<*>) {
    finish(rootTable, false)
  }

  /**
   * Finalize a buffer, pointing to the given `root_table`, with the size prefixed.
   *
   * @param rootTable An offset to be added to the buffer.
   */
  public fun finishSizePrefixed(rootTable: Offset<*>) {
    finish(rootTable, true)
  }

  /**
   * Finalize a buffer, pointing to the given `root_table`.
   *
   * @param rootTable An offset to be added to the buffer.
   * @param fileIdentifier A FlatBuffer file identifier to be added to the buffer before
   *   `root_table`.
   * @param sizePrefix Whether to prefix the size to the buffer.
   */
  protected fun finish(rootTable: Offset<*>, fileIdentifier: String, sizePrefix: Boolean) {
    val identifierSize = 4
    prep(minalign, Int.SIZE_BYTES + identifierSize + if (sizePrefix) Int.SIZE_BYTES else 0)
    if (fileIdentifier.length != identifierSize)
      throw AssertionError("FlatBuffers: file identifier must be length " + identifierSize)
    for (i in identifierSize - 1 downTo 0) {
      add(fileIdentifier[i].code.toByte())
    }
    finish(rootTable, sizePrefix)
  }

  /**
   * Finalize a buffer, pointing to the given `root_table`.
   *
   * @param rootTable An offset to be added to the buffer.
   * @param fileIdentifier A FlatBuffer file identifier to be added to the buffer before
   *   `root_table`.
   */
  public fun finish(rootTable: Offset<*>, fileIdentifier: String) {
    finish(rootTable, fileIdentifier, false)
  }

  /**
   * Finalize a buffer, pointing to the given `root_table`, with the size prefixed.
   *
   * @param rootTable An offset to be added to the buffer.
   * @param fileIdentifier A FlatBuffer file identifier to be added to the buffer before
   *   `root_table`.
   */
  public fun finishSizePrefixed(rootTable: Offset<*>, fileIdentifier: String) {
    finish(rootTable, fileIdentifier, true)
  }

  /**
   * In order to save space, fields that are set to their default value don't get serialized into
   * the buffer. Forcing defaults provides a way to manually disable this optimization.
   *
   * @param forceDefaults When set to `true`, always serializes default values.
   * @return Returns `this`.
   */
  public fun forceDefaults(forceDefaults: Boolean): FlatBufferBuilder {
    this.forceDefaults = forceDefaults
    return this
  }

  /**
   * Get the ByteBuffer representing the FlatBuffer. Only call this after you've called `finish()`.
   * The actual data starts at the ByteBuffer's current position, not necessarily at `0`.
   *
   * @return The [ReadBuffer] representing the FlatBuffer
   */
  public fun dataBuffer(): ReadWriteBuffer {
    finished()
    return buffer
  }

  /**
   * A utility function to copy and return the ByteBuffer data as a `byte[]`.
   *
   * @return A full copy of the [data buffer][.dataBuffer].
   */
  public fun sizedByteArray(start: Int = space, length: Int = buffer.capacity - space): ByteArray {
    finished()
    val array = ByteArray(length)
    buffer.getBytes(array, start)
    return array
  }

  /**
   * Helper function to test if a field is present in the table
   *
   * @param offset virtual table offset
   * @return true if the filed is present
   */
  public fun Table.isFieldPresent(offset: Int): Boolean = this.offset(offset) != 0
}

public fun Double.sign(): Double =
  when {
    this.isNaN() -> Double.NaN
    this > 0 -> 1.0
    this < 0 -> -1.0
    else -> this
  }

public fun Float.sign(): Float =
  when {
    this.isNaN() -> Float.NaN
    this > 0 -> 1.0f
    this < 0 -> -1.0f
    else -> this
  }

public fun Int.sign(): Int =
  when {
    this > 0 -> 1
    this < 0 -> -1
    else -> this
  }
