///|
/// Opaque handle to a Lua table stored in the registry.
///
/// Instances of this type are created when converting Lua values
/// into [`Value::Table`] and keep the underlying table alive via a
/// registry reference.
struct Table(Ref)

///|
pub impl Show for Table with output(self : Table, logger : &Logger) -> Unit {
  let length = self.length()
  logger.write_string("{")
  let visit = for i in 1..=length {
    guard self.get(Integer(i.to_int64())) is Some(value) else { break i }
    logger.write_char(' ')
    logger.write_string(value.to_string())
    if i < length {
      logger.write_char(',')
    }
  } else {
    length + 1
  }
  for k, v in self {
    if k is Integer(i) && i >= 1 && i < visit.to_int64() {
      continue
    }
    logger.write_string(", [")
    logger.write_string(k.to_string())
    logger.write_string("] = ")
    logger.write_string(v.to_string())
  }
  logger.write_string(" }")
}

///|
pub impl ToJson for Table with to_json(self : Table) -> Json {
  let length = self.length()
  let mut array : FixedArray[Json?]? = Some(FixedArray::make(length, None))
  let mut count = 0
  let object : Map[String, Json] = Map::new()
  for k, v in self {
    if array is Some(a) && k is Integer(i) && i >= 1 && i <= length.to_int64() {
      let i = i.to_int() - 1
      // Fill the array with the value
      a[i] = Some(v.to_json())
      count += 1
    } else {
      if array is Some(a) {
        // We found that the table contains non-integer keys,
        // so we switch to an object
        for j, v in a {
          if v is Some(v) {
            object[(j + 1).to_string()] = v
          }
        }
        array = None
      }
      // Use the key-value pair in the object
      object[k.to_string()] = v.to_json()
    }
  }
  if array is Some(array) && count == length {
    let values = []
    for v in array {
      values.push(v.unwrap())
    }
    return Json::array(values)
  }
  Json::object(object)
}

///|
/// Returns an iterator over this table's key-value pairs.
///
/// The iterator traverses the table using `lua_next`, yielding
/// arbitrary keys and values as [`Value`] pairs until the table is
/// exhausted.
pub fn Table::iterator2(self : Table) -> Iterator2[Value, Value] {
  let lua = self.0.lua()
  let mut last_key = Nil
  Iterator2::new(() => lua.with_state(state => {
    // Push the table
    @lua.raw_get_i(state, @lua.registry_index, self.0.int().to_int64())
    |> ignore()
    // Push the last key
    state_push_value(state, last_key)
    // Get the next key-value pair
    if @lua.next(state, -2) {
      // Get the key (-2) and value (-1)
      let value = state_pop_top_value(lua, state).unwrap()
      let key = state_pop_top_value(lua, state).unwrap()
      // Pop the table
      let _ = @lua.pop(state, 1)
      // Update last_key for the next iteration
      last_key = key
      Some((key, value))
    } else {
      // No more elements
      @lua.pop(state, 1) // Pop the table
      None
    }
  }))
}

///|
/// Computes the length of this table using Lua's `#` operator.
///
/// This is equivalent to calling `@lua.len` on the underlying
/// table and converting the result to a MoonBit `Int`.
pub fn Table::length(self : Table) -> Int {
  @c.with_unsafe_borrowed(self, _ => {
    let lua = self.0.lua()
    lua.with_state(state => {
      // Push the table
      @lua.raw_get_i(state, @lua.registry_index, self.0.int().to_int64())
      |> ignore()
      @lua.raw_len(state, -1).to_int()
    })
  })
}

///|
pub fn Table::to_array(self : Table) -> Array[Value] {
  @c.with_unsafe_borrowed(self, _ => {
    let lua = self.0.lua()
    lua.with_state(state => {
      // Push the table
      @lua.raw_get_i(state, @lua.registry_index, self.0.int().to_int64())
      |> ignore()
      let len = @lua.raw_len(state, -1).to_int()
      let result = Array::new(capacity=len)
      for i in 1..=len {
        @lua.raw_get_i(state, -1, i.to_int64()) |> ignore()
        let value = state_pop_top_value(lua, state).unwrap()
        result.push(value)
      }
      @lua.pop(state, 1) // Pop the table
      result
    })
  })
}

///|
pub fn Table::get(self : Table, value : Value) -> Value? {
  @c.with_unsafe_borrowed(self, _ => {
    let lua = self.0.lua()
    lua.with_state(state => {
      // Push the table
      @lua.raw_get_i(state, @lua.registry_index, self.0.int().to_int64())
      |> ignore()
      // Push the key
      state_push_value(state, value)
      // Get the value
      @lua.get_table(state, -2) |> ignore()
      let result = state_pop_top_value(lua, state)
      @lua.pop(state, 1) // Pop the table
      result
    })
  })
}