///|
/// High-level handle to an embedded Lua 5.4 state.
///
/// Each `Lua` value wraps a `lua_State*` pointer and owns the
/// lifetime of that state. When the `Lua` value is collected, the
/// underlying state is automatically closed.
type Lua

///|
pub impl Show for Lua with output(self : Lua, logger : &Logger) -> Unit {
  @c.with_unsafe_borrowed(self, ptr => logger.write_string(
    "",
  ))
}

///|
pub impl ToJson for Lua with to_json(self : Lua) -> Json {
  @c.with_unsafe_borrowed(self, ptr => Json::string(
    "",
  ))
}

///|
fn[R] Lua::with_state(self : Lua, f : (@lua.State) -> R raise?) -> R raise? {
  let ptr = @c.unsafe_borrow(self)
  let state = @lua.State::of_pointer(ptr)
  let result = f(state)
  let _ : Lua = @c.unsafe_return(ptr)
  return result
}

///|
fn alloc(
  ptr : @c.Pointer[Unit],
  osize : UInt64,
  nsize : UInt64,
) -> @c.Pointer[Unit] {
  if ptr.is_null() && osize.to_int() is @lua.TypeThread {
    @moonbit.make_external_object(
      ptr => {
        let state : @lua.State = @lua.State::of_pointer(ptr)
        @lua.close(state)
      },
      nsize,
    )
  } else if nsize == 0 {
    @memory.free(ptr)
    return @c.Pointer::null()
  } else {
    return @memory.realloc(ptr, nsize)
  }
}

///|
/// Creates a new `Lua` state with all standard libraries loaded.
///
/// This is the preferred entry point for embedding Lua. It
/// configures the allocator so that the state is closed
/// automatically when the returned `Lua` value is collected.
#as_free_fn
pub fn Lua::new() -> Lua raise Err {
  guard @lua.new_state(alloc) is Some(lua) else {
    raise MemoryError(String("failed to create new Lua state"))
  }
  @lib.open_libs(lua)
  @c.unsafe_return(lua.to_pointer())
}

///|
/// High-level tagged representation of Lua values.
///
/// Values of this enum are used to move data between MoonBit code
/// and the underlying Lua state without exposing raw C pointers.
pub(all) enum Value {
  Integer(Int64)
  Number(Double)
  String(String)
  Boolean(Bool)
  Nil
  Thread(Lua)
  Table(Table)
  LightUserdata(LightUserdata)
  Userdata(Userdata)
  Function(Function)
}

///|
pub impl Show for Value with output(self : Value, logger : &Logger) -> Unit {
  match self {
    Integer(v) => logger.write_string(v.to_string())
    Number(v) => logger.write_string(v.to_string())
    String(v) => v.output(logger)
    Boolean(v) => logger.write_string(v.to_string())
    Nil => logger.write_string("nil")
    Thread(lua) => lua.output(logger)
    Table(table) => table.output(logger)
    LightUserdata(ptr) => ptr.output(logger)
    Userdata(userdata) => userdata.output(logger)
    Function(function) => function.output(logger)
  }
}

///|
pub impl ToJson for Value with to_json(self : Value) -> Json {
  match self {
    Integer(v) => Json::number(v.to_double(), repr=v.to_string())
    Number(v) => Json::number(v)
    String(v) => Json::string(v)
    Boolean(v) => Json::boolean(v)
    Nil => Json::null()
    Thread(lua) => lua.to_json()
    Table(table) => table.to_json()
    LightUserdata(ptr) => ptr.to_json()
    Userdata(userdata) => userdata.to_json()
    Function(function) => function.to_json()
  }
}

///|
/// Lightweight wrapper around a raw C pointer exposed to Lua.
///
/// This corresponds to Lua's `lightuserdata` type and does not
/// participate in garbage collection on the Lua side.
struct LightUserdata(@c.Pointer[Unit])

///|
pub impl Show for LightUserdata with output(
  self : LightUserdata,
  logger : &Logger,
) -> Unit {
  logger.write_string(
    "",
  )
}

///|
pub impl ToJson for LightUserdata with to_json(self : LightUserdata) -> Json {
  Json::string("")
}

///|
/// Opaque handle to full Lua userdata stored in the registry.
///
/// Instances of this type refer to heap-allocated blocks managed
/// by Lua and kept alive through a registry reference.
struct Userdata(Ref)

///|
pub impl Show for Userdata with output(self : Userdata, logger : &Logger) -> Unit {
  logger.write_string("")
}

///|
pub impl ToJson for Userdata with to_json(self : Userdata) -> Json {
  Json::string("")
}

///|
/// Opaque handle to a Lua function stored in the registry.
///
/// Values of this type can be obtained from the stack through
/// [`Value::Function`] and are represented by a registry reference.
struct Function(Ref)

///|
pub impl Show for Function with output(self : Function, logger : &Logger) -> Unit {
  logger.write_string("")
}

///|
pub impl ToJson for Function with to_json(self : Function) -> Json {
  Json::string("")
}

///|
fn state_get_value(lua : Lua, state : @lua.State, index : Int) -> Value? {
  guard @lua.type_(state, index) is Some(type_) else { None }
  match type_ {
    Userdata => {
      @lua.push_value(state, index)
      Some(Userdata(lua.new_ref()))
    }
    Function => {
      // Functions are treated as userdata in Lua, so we can return a reference
      @lua.push_value(state, index)
      Some(Function(lua.new_ref()))
    }
    Table => {
      @lua.push_value(state, index)
      Some(Table(lua.new_ref()))
    }
    String => {
      let bytes = @lua.to_string(state, index).unwrap()
      Some(String(@encoding/utf8.decode_lossy(bytes)))
    }
    Number =>
      if @lua.is_integer(state, index) {
        let v = @lua.to_integer(state, index)
        Some(Integer(v))
      } else {
        let v = @lua.to_number(state, index)
        Some(Number(v))
      }
    LightUserdata => {
      let ptr = @lua.to_userdata(state, index)
      Some(LightUserdata(ptr))
    }
    Boolean => {
      let v = @lua.to_boolean(state, index)
      Some(Boolean(v))
    }
    Nil => Some(Nil)
    Thread => {
      let thread = @lua.to_thread(state, index).unwrap()
      Some(Thread(@c.unsafe_return(thread.to_pointer())))
    }
  }
}

///|
fn state_pop_top_value(lua : Lua, state : @lua.State) -> Value? {
  guard @lua.type_(state, -1) is Some(type_) else { None }
  match type_ {
    Userdata => Some(Userdata(lua.new_ref()))
    Function => Some(Function(lua.new_ref()))
    Table => Some(Table(lua.new_ref()))
    String => {
      let bytes = @lua.to_string(state, -1).unwrap()
      @lua.pop(state, 1)
      Some(String(@encoding/utf8.decode_lossy(bytes)))
    }
    Number =>
      if @lua.is_integer(state, -1) {
        let v = @lua.to_integer(state, -1)
        @lua.pop(state, 1)
        Some(Integer(v))
      } else {
        let v = @lua.to_number(state, -1)
        @lua.pop(state, 1)
        Some(Number(v))
      }
    LightUserdata => {
      let ptr = @lua.to_userdata(state, -1)
      @lua.pop(state, 1)
      Some(LightUserdata(ptr))
    }
    Boolean => {
      let v = @lua.to_boolean(state, -1)
      @lua.pop(state, 1)
      Some(Boolean(v))
    }
    Nil => {
      @lua.pop(state, 1)
      Some(Nil)
    }
    Thread => {
      let thread = @lua.to_thread(state, -1).unwrap()
      @lua.pop(state, 1)
      Some(Thread(@c.unsafe_return(thread.to_pointer())))
    }
  }
}

///|
/// Reads a value from the Lua stack at the given index.
///
/// Returns `None` if the index is invalid or the slot does not
/// contain a value that can be represented as [`Value`].
pub fn Lua::get(self : Lua, index : Int) -> Value? {
  self.with_state(state => state_get_value(self, state, index))
}

///|
fn state_push_value(state : @lua.State, value : Value) -> Unit {
  match value {
    Integer(v) => @lua.push_integer(state, v)
    Number(v) => @lua.push_number(state, v)
    String(v) => {
      let bytes = @encoding/utf8.encode(v)
      @lua.push_string(state, bytes) |> ignore()
    }
    Boolean(v) => @lua.push_boolean(state, v)
    Nil => @lua.push_nil(state)
    Thread(v) => {
      let thread_ptr = @c.unsafe_borrow(v)
      let thread = @lua.State::of_pointer(thread_ptr)
      @lua.push_thread(state, thread) |> ignore()
    }
    Table(table) =>
      ignore(
        @lua.raw_get_i(state, @lua.registry_index, table.0.int().to_int64()),
      )
    Userdata(userdata) =>
      ignore(
        @lua.raw_get_i(state, @lua.registry_index, userdata.0.int().to_int64()),
      )
    Function(function) =>
      ignore(
        @lua.raw_get_i(state, @lua.registry_index, function.0.int().to_int64()),
      )
    LightUserdata(ptr) => @lua.push_light_userdata(state, ptr.0)
  }
}

///|
/// Pushes a high-level [`Value`] onto this state's Lua stack.
///
/// Tables, userdata, and functions are pushed by looking up their
/// internal registry reference; other variants are converted to the
/// corresponding Lua primitive type.
pub fn Lua::push(self : Lua, value : Value) -> Unit {
  self.with_state(state => state_push_value(state, value))
}

///|
/// Error type used by the high-level `Lua` API.
///
/// Each variant corresponds to a particular Lua status code and
/// carries the underlying Lua error value as a [`Value`].
pub suberror Err {
  /// a runtime error.
  RuntimeError(Value)
  /// memory allocation error. For such errors, Lua does not call the message handler.
  MemoryError(Value)
  /// error while running the message handler.
  MessageHandlerError(Value)
  /// syntax error during precompilation.
  SyntaxError(Value)
  /// a file-related error; e.g., it cannot open or read the file.
  FileError(Value)
  /// an unknown error.
  UnknownError(Int, Value)
} derive(ToJson)

///|
fn Err::new(status : Int, value : Value) -> Err {
  match status {
    @lua.ErrRun => RuntimeError(value)
    @lua.ErrMem => MemoryError(value)
    @lua.ErrErr => MessageHandlerError(value)
    @lua.ErrSyntax => SyntaxError(value)
    @aux.ErrFile => FileError(value)
    status => UnknownError(status, value)
  }
}

///|
fn Err::value(self : Err) -> Value {
  match self {
    SyntaxError(value) => value
    MessageHandlerError(value) => value
    MemoryError(value) => value
    RuntimeError(value) => value
    FileError(value) => value
    UnknownError(_, value) => value
  }
}

///|
pub impl Show for Err with output(self : Err, logger : &Logger) -> Unit {
  match self.value() {
    String(string) => string.output(logger)
    Number(number) => logger.write_string(number.to_string())
    Integer(integer) => logger.write_string(integer.to_string())
    Boolean(boolean) => logger.write_string(boolean.to_string())
    Nil => logger.write_string("nil")
    LightUserdata(light_userdata) => light_userdata.output(logger)
    Thread(thread) => thread.output(logger)
    Table(table) => table.output(logger)
    Userdata(userdata) => userdata.output(logger)
    Function(function) => function.output(logger)
  }
}

///|
fn state_eval(
  lua : Lua,
  state : @lua.State,
  code : String,
  args : Array[Value],
) -> Array[Value] raise Err {
  let base = @lua.get_top(state)
  let status = @aux.load_bytes(state, @encoding/utf8.encode(code), "=(eval)")
  guard status is @lua.Ok else {
    raise Err::new(status, state_pop_top_value(lua, state).unwrap())
  }
  // Push arguments onto the stack
  let n_args = args.length()
  for arg in args {
    state_push_value(state, arg)
  }
  let status = @lua.pcall(state, n_args, @lua.MultRet, 0)
  guard status is @lua.Ok else {
    raise Err::new(status, state_pop_top_value(lua, state).unwrap())
  }
  let results = []
  while @lua.get_top(state) > base {
    let value = state_pop_top_value(lua, state).unwrap()
    results.push(value)
  }
  results.rev_in_place()
  results
}

///|
/// Loads and runs a Lua chunk with the given arguments.
///
/// The `code` string is compiled as a Lua chunk and called with
/// the values from `args` as arguments. All return values are
/// collected from the stack and returned as an array of [`Value`](#Value).
///
/// Raises [`Err`](#Err) if compilation or execution fails.
pub fn Lua::eval(
  self : Lua,
  code : String,
  args : Array[Value],
) -> Array[Value] raise Err {
  self.with_state(state => state_eval(self, state, code, args))
}