/*
 * Copyright 2024 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.
 */

import XCTest

@testable import FlatBuffers

final class FlatBuffersUnionTests: XCTestCase {

  func testCreateMonstor() {

    var b = FlatBufferBuilder(initialSize: 20)
    let dmg: Int16 = 5
    let str = "Axe"
    let axe = b.create(string: str)
    let weapon = Weapon.createWeapon(builder: &b, offset: axe, dmg: dmg)
    let weapons = b.createVector(ofOffsets: [weapon])
    let root = LocalMonster.createMonster(
      builder: &b,
      offset: weapons,
      equipment: .Weapon,
      equippedOffset: weapon.o)
    b.finish(offset: root)
    let buffer = b.sizedByteArray
    // swiftformat:disable all
    XCTAssertEqual(
      buffer,
      [
        16, 0, 0, 0, 0, 0, 10, 0, 16, 0, 8, 0, 7, 0, 12, 0, 10, 0, 0, 0, 0, 0, 0, 1, 8, 0, 0, 0, 20,
        0, 0, 0, 1, 0, 0, 0, 12, 0, 0, 0, 8, 0, 12, 0, 8, 0, 6, 0, 8, 0, 0, 0, 0, 0, 5, 0, 4, 0, 0,
        0, 3, 0, 0, 0, 65, 120, 101, 0,
      ])
    // swiftformat:enable all
    let monster = LocalMonster.getRootAsMonster(bb: ByteBuffer(bytes: buffer))
    XCTAssertEqual(monster.weapon(at: 0)?.dmg, dmg)
    XCTAssertEqual(monster.weapon(at: 0)?.name, str)
    XCTAssertEqual(monster.weapon(at: 0)?.nameVector, [65, 120, 101])
    let p: Weapon? = monster.equiped()
    XCTAssertEqual(p?.dmg, dmg)
    XCTAssertEqual(p?.name, str)
    XCTAssertEqual(p?.nameVector, [65, 120, 101])
  }

  func testEndTableFinish() {
    var builder = FlatBufferBuilder(initialSize: 20)
    let sword = builder.create(string: "Sword")
    let axe = builder.create(string: "Axe")
    let weaponOne = Weapon.createWeapon(
      builder: &builder,
      offset: sword,
      dmg: 3)
    let weaponTwo = Weapon.createWeapon(builder: &builder, offset: axe, dmg: 5)
    let name = builder.create(string: "Orc")
    let inventory: [UInt8] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
    let inv = builder.createVector(inventory, size: 10)
    let weapons = builder.createVector(ofOffsets: [weaponOne, weaponTwo])
    let path = builder.createVector(ofStructs: [
      Vec(x: 4.0, y: 5.0, z: 6.0),
      Vec(x: 1.0, y: 2.0, z: 3.0),
    ])
    let orc = FinalMonster.createMonster(
      builder: &builder,
      position: Vec(x: 1, y: 2, z: 3),
      hp: 300,
      name: name,
      inventory: inv,
      color: .red,
      weapons: weapons,
      equipment: .Weapon,
      equippedOffset: weaponTwo,
      path: path)
    builder.finish(offset: orc)
    // swiftformat:disable all
    XCTAssertEqual(
      builder.sizedByteArray,
      [
        32, 0, 0, 0, 0, 0, 26, 0, 48, 0, 36, 0, 0, 0, 34, 0, 28, 0, 0, 0, 24, 0, 23, 0, 16, 0, 15,
        0, 8, 0, 4, 0, 26, 0, 0, 0, 44, 0, 0, 0, 104, 0, 0, 0, 0, 0, 0, 1, 60, 0, 0, 0, 0, 0, 0, 0,
        64, 0, 0, 0, 76, 0, 0, 0, 0, 0, 44, 1, 0, 0, 128, 63, 0, 0, 0, 64, 0, 0, 64, 64, 2, 0, 0, 0,
        0, 0, 128, 64, 0, 0, 160, 64, 0, 0, 192, 64, 0, 0, 128, 63, 0, 0, 0, 64, 0, 0, 64, 64, 2, 0,
        0, 0, 52, 0, 0, 0, 28, 0, 0, 0, 10, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 0, 3, 0, 0, 0,
        79, 114, 99, 0, 244, 255, 255, 255, 0, 0, 5, 0, 24, 0, 0, 0, 8, 0, 12, 0, 8, 0, 6, 0, 8, 0,
        0, 0, 0, 0, 3, 0, 12, 0, 0, 0, 3, 0, 0, 0, 65, 120, 101, 0, 5, 0, 0, 0, 83, 119, 111, 114,
        100, 0, 0, 0,
      ])
    // swiftformat:enable all
  }

  func testEnumVector() {
    let vectorOfEnums: [ColorsNameSpace.RGB] = [.blue, .green]

    var builder = FlatBufferBuilder(initialSize: 1)
    let off = builder.createVector(vectorOfEnums)
    let start = ColorsNameSpace.Monster.startMonster(&builder)
    ColorsNameSpace.Monster.add(colors: off, &builder)
    let end = ColorsNameSpace.Monster.endMonster(&builder, start: start)
    builder.finish(offset: end)
    // swiftformat:disable all
    XCTAssertEqual(
      builder.sizedByteArray,
      [
        12, 0, 0, 0, 0, 0, 6, 0, 8, 0, 4, 0, 6, 0, 0, 0, 4, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 1, 0,
        0, 0,
      ])
    // swiftformat:enable all
    let monster = ColorsNameSpace.Monster
      .getRootAsMonster(bb: builder.sizedBuffer)
    XCTAssertEqual(monster.colorsCount, 2)
    let colors = monster.colors
    XCTAssertEqual(colors[0], .blue)
    XCTAssertEqual(colors[1], .green)
  }

  func testUnionVector() {
    var fb = FlatBufferBuilder()

    let swordDmg: Int32 = 8
    let attackStart = Attacker.startAttacker(&fb)
    Attacker.add(swordAttackDamage: swordDmg, &fb)
    let attack = Attacker.endAttacker(&fb, start: attackStart)

    let characterType: [Character] = [.belle, .mulan, .bookfan]

    let characters = [
      fb.create(struct: BookReader(booksRead: 7)),
      attack,
      fb.create(struct: BookReader(booksRead: 2)),
    ]
    let types = fb.createVector(characterType)
    let characterVector = fb.createVector(ofOffsets: characters)
    let end = Movie.createMovie(
      &fb,
      charactersTypeVectorOffset: types,
      charactersVectorOffset: characterVector)
    Movie.finish(&fb, end: end)

    var buffer = fb.sizedBuffer
    let movie: Movie = getRoot(byteBuffer: &buffer)
    XCTAssertEqual(movie.charactersType.count, characterType.count)
    XCTAssertEqual(movie.characters.count, characters.count)

    let bufferCharactersType = movie.charactersType
    for (index, element) in bufferCharactersType.enumerated() {
      XCTAssertEqual(element, characterType[index])
    }

    XCTAssertEqual(
      movie.characters(at: 0, type: BookReader_Mutable.self)?.booksRead,
      7)
    XCTAssertEqual(
      movie.characters(at: 1, type: Attacker.self)?.swordAttackDamage,
      swordDmg)
    XCTAssertEqual(
      movie.characters(at: 2, type: BookReader_Mutable.self)?.booksRead,
      2)

    var objc: MovieT? = movie.unpack()
    XCTAssertEqual(
      movie.charactersType.count, objc?.characters.count ?? 0)
    XCTAssertEqual(
      movie.characters(at: 0, type: BookReader_Mutable.self)?.booksRead,
      (objc?.characters[0]?.value as? BookReader)?.booksRead)
    fb.clear()
    let newMovie = Movie.pack(&fb, obj: &objc)
    fb.finish(offset: newMovie)

    var _buffer = fb.sizedBuffer
    let packedMovie: Movie = getRoot(byteBuffer: &_buffer)

    XCTAssertEqual(
      packedMovie.characters(at: 0, type: BookReader_Mutable.self)?.booksRead,
      movie.characters(at: 0, type: BookReader_Mutable.self)?.booksRead)
    XCTAssertEqual(
      packedMovie.characters(at: 1, type: Attacker.self)?.swordAttackDamage,
      movie.characters(at: 1, type: Attacker.self)?.swordAttackDamage)
    XCTAssertEqual(
      packedMovie.characters(at: 2, type: BookReader_Mutable.self)?.booksRead,
      movie.characters(at: 2, type: BookReader_Mutable.self)?.booksRead)
  }

  func testStringUnion() {
    let string = "Awesome \\\\t\t\nstring!"
    var fb = FlatBufferBuilder()
    let stringOffset = fb.create(string: string)
    let characterType: [Character] = [.bookfan, .other]

    let characters = [
      fb.create(struct: BookReader(booksRead: 7)),
      stringOffset,
    ]
    let types = fb.createVector(characterType)
    let characterVector = fb.createVector(ofOffsets: characters)

    let end = Movie.createMovie(
      &fb,
      mainCharacterType: .other,
      mainCharacterOffset: Offset(offset: stringOffset.o),
      charactersTypeVectorOffset: types,
      charactersVectorOffset: characterVector)
    Movie.finish(&fb, end: end)

    var buffer = fb.sizedBuffer
    let movie: Movie = getRoot(byteBuffer: &buffer)
    XCTAssertEqual(movie.mainCharacter(type: String.self), string)
    XCTAssertEqual(
      movie.characters(at: 0, type: BookReader_Mutable.self)?.booksRead,
      7)
    XCTAssertEqual(movie.characters(at: 1, type: String.self), string)

    var objc: MovieT? = movie.unpack()
    XCTAssertEqual(objc?.mainCharacter?.value as? String, string)
    XCTAssertEqual((objc?.characters[0]?.value as? BookReader)?.booksRead, 7)
    XCTAssertEqual(objc?.characters[1]?.value as? String, string)
    fb.clear()
    let newMovie = Movie.pack(&fb, obj: &objc)
    fb.finish(offset: newMovie)

    var _buffer = fb.sizedBuffer
    let packedMovie: Movie = getRoot(byteBuffer: &_buffer)
    XCTAssertEqual(packedMovie.mainCharacter(type: String.self), string)
    XCTAssertEqual(
      packedMovie.characters(at: 0, type: BookReader_Mutable.self)?.booksRead,
      7)
    XCTAssertEqual(packedMovie.characters(at: 1, type: String.self), string)
  }

  func testEncoding() {
    let string = "Awesome \\\\t\t\nstring!"
    var fb = FlatBufferBuilder()

    let stringOffset = fb.create(string: string)

    let swordDmg: Int32 = 8
    let attackStart = Attacker.startAttacker(&fb)
    Attacker.add(swordAttackDamage: swordDmg, &fb)
    let attack = Attacker.endAttacker(&fb, start: attackStart)

    let characterType: [Character] = [.belle, .mulan, .bookfan, .other]

    let characters = [
      fb.create(struct: BookReader(booksRead: 7)),
      attack,
      fb.create(struct: BookReader(booksRead: 2)),
      stringOffset,
    ]
    let types = fb.createVector(characterType)
    let characterVector = fb.createVector(ofOffsets: characters)
    let end = Movie.createMovie(
      &fb,
      charactersTypeVectorOffset: types,
      charactersVectorOffset: characterVector)
    Movie.finish(&fb, end: end)

    var sizedBuffer = fb.sizedBuffer
    do {
      let reader: Movie = try getCheckedRoot(byteBuffer: &sizedBuffer)
      let encoder = JSONEncoder()
      encoder.keyEncodingStrategy = .convertToSnakeCase
      encoder.outputFormatting = [.sortedKeys]
      let data = try encoder.encode(reader)
      XCTAssertEqual(String(data: data, encoding: .utf8), jsonData)
    } catch {
      XCTFail(error.localizedDescription)
    }
  }

  var jsonData: String {
    """
    {"characters":[{"books_read":7},{"sword_attack_damage":8},{"books_read":2},"Awesome \\\\\\\\t\\t\\nstring!"],"characters_type":["Belle","MuLan","BookFan","Other"]}
    """
  }
}

public enum ColorsNameSpace {

  enum RGB: Int32, Enum, FlatbuffersVectorInitializable {
    static var min: ColorsNameSpace.RGB { .red }

    typealias T = Int32
    static var byteSize: Int { MemoryLayout<Int32>.size }
    var value: Int32 { rawValue }
    case red = 0, green = 1, blue = 2
  }

  struct Monster: FlatBufferTable {
    var __buffer: ByteBuffer! { _accessor.bb }

    private var _accessor: Table
    static func getRootAsMonster(bb: ByteBuffer) -> Monster {
      Monster(
        Table(
          bb: bb,
          position: Int32(bb.read(def: UOffset.self, position: bb.reader)) +
            Int32(bb.reader)))
    }

    init(_ t: Table) { _accessor = t }
    init(_ bb: ByteBuffer, o: Int32) { _accessor = Table(bb: bb, position: o) }

    public var colorsCount: Int32 {
      let o = _accessor.offset(4)
      return o == 0
        ? 0
        : _accessor
        .vector(count: o)
    }

    public var colors: FlatbufferVector<RGB> {
      _accessor.vector(at: 4, byteSize: 4)
    }

    static func startMonster(_ fbb: inout FlatBufferBuilder) -> UOffset {
      fbb
        .startTable(with: 1)
    }
    static func add(colors: Offset, _ fbb: inout FlatBufferBuilder) {
      fbb.add(
        offset: colors,
        at: 4)
    }
    static func endMonster(
      _ fbb: inout FlatBufferBuilder,
      start: UOffset)
      -> Offset
    {
      let end = Offset(offset: fbb.endTable(at: start))
      return end
    }
  }
}

enum Equipment: Byte { case none, Weapon }

enum Color3: Int8 {
  case red = 0
  case green, blue
}

struct FinalMonster {

  @inlinable
  static func createMonster(
    builder: inout FlatBufferBuilder,
    position: Vec,
    hp: Int16,
    name: Offset,
    inventory: Offset,
    color: Color3,
    weapons: Offset,
    equipment: Equipment = .none,
    equippedOffset: Offset,
    path: Offset) -> Offset
  {
    let start = builder.startTable(with: 11)
    builder.create(struct: position, position: 4)
    builder.add(element: hp, def: 100, at: 8)
    builder.add(offset: name, at: 10)
    builder.add(offset: inventory, at: 14)
    builder.add(element: color.rawValue, def: Color3.green.rawValue, at: 16)
    builder.add(offset: weapons, at: 18)
    builder.add(
      element: equipment.rawValue,
      def: Equipment.none.rawValue,
      at: 20)
    builder.add(offset: equippedOffset, at: 22)
    builder.add(offset: path, at: 24)
    return Offset(offset: builder.endTable(at: start))
  }
}

struct LocalMonster {

  private var __t: Table

  init(_ fb: ByteBuffer, o: Int32) { __t = Table(bb: fb, position: o) }
  init(_ t: Table) { __t = t }

  func weapon(at index: Int32) -> Weapon? {
    let o =
      __t
        .offset(4)
    return o == 0
      ? nil
      : Weapon.assign(
        __t.indirect(__t.vector(at: o) + (index * 4)),
        __t.bb)
  }

  func equiped<T: FlatBufferTable>() -> T? {
    let o = __t.offset(8)
    return o == 0 ? nil : __t.union(o)
  }

  static func getRootAsMonster(bb: ByteBuffer) -> LocalMonster {
    LocalMonster(
      Table(
        bb: bb,
        position: Int32(bb.read(def: UOffset.self, position: 0))))
  }

  @inlinable
  static func createMonster(
    builder: inout FlatBufferBuilder,
    offset: Offset,
    equipment: Equipment = .none,
    equippedOffset: UOffset) -> Offset
  {
    let start = builder.startTable(with: 3)
    builder.add(element: equippedOffset, def: 0, at: 8)
    builder.add(offset: offset, at: 4)
    builder.add(
      element: equipment.rawValue,
      def: Equipment.none.rawValue,
      at: 6)
    return Offset(offset: builder.endTable(at: start))
  }
}

struct Weapon: FlatBufferTable {

  var __buffer: ByteBuffer! { __t.bb }

  static let offsets: (name: VOffset, dmg: VOffset) = (4, 6)
  private var __t: Table

  init(_ t: Table) { __t = t }
  init(_ fb: ByteBuffer, o: Int32) { __t = Table(bb: fb, position: o) }

  var dmg: Int16 {
    let o = __t.offset(6)
    return o == 0
      ? 0
      : __t.readBuffer(
        of: Int16.self,
        at: o)
  }
  var nameVector: [UInt8]? { __t.getVector(at: 4) }
  var name: String? {
    let o = __t.offset(4)
    return o == 0 ? nil : __t.string(at: o)
  }

  static func assign(_ i: Int32, _ bb: ByteBuffer) -> Weapon {
    Weapon(
      Table(
        bb: bb,
        position: i))
  }

  @inlinable
  static func createWeapon(
    builder: inout FlatBufferBuilder,
    offset: Offset,
    dmg: Int16) -> Offset
  {
    let _start = builder.startTable(with: 2)
    Weapon.add(builder: &builder, name: offset)
    Weapon.add(builder: &builder, dmg: dmg)
    return Weapon.end(builder: &builder, startOffset: _start)
  }

  @inlinable
  static func end(
    builder: inout FlatBufferBuilder,
    startOffset: UOffset) -> Offset
  {
    Offset(offset: builder.endTable(at: startOffset))
  }

  @inlinable
  static func add(builder: inout FlatBufferBuilder, name: Offset) {
    builder.add(offset: name, at: Weapon.offsets.name)
  }

  @inlinable
  static func add(builder: inout FlatBufferBuilder, dmg: Int16) {
    builder.add(element: dmg, def: 0, at: Weapon.offsets.dmg)
  }
}
