///|
/// Scalar element types whose packed `FixedArray` representation can be copied
/// into an OpenGL buffer. This trait is sealed so user-defined reference types
/// cannot claim a native layout that MoonBit does not guarantee.
trait GlData {
  fn size_in_bytes() -> Int64
}

///|
pub impl GlData for Byte with fn size_in_bytes() {
  1L
}

///|
pub impl GlData for Int16 with fn size_in_bytes() {
  2L
}

///|
pub impl GlData for UInt16 with fn size_in_bytes() {
  2L
}

///|
pub impl GlData for Int with fn size_in_bytes() {
  4L
}

///|
pub impl GlData for UInt with fn size_in_bytes() {
  4L
}

///|
pub impl GlData for Int64 with fn size_in_bytes() {
  8L
}

///|
pub impl GlData for UInt64 with fn size_in_bytes() {
  8L
}

///|
pub impl GlData for Float with fn size_in_bytes() {
  4L
}

///|
pub impl GlData for Double with fn size_in_bytes() {
  8L
}

///|
fn[T : GlData] gl_buffer_data_size(data : FixedArray[T]) -> Int64 {
  data.length().to_int64() * T::size_in_bytes()
}

///|
let d_glBufferData : Dispatch = Dispatch::with_aliases("glBufferData", [
  "glBufferDataARB",
])

///|
let d_glBufferDataARB : Dispatch = Dispatch::with_aliases("glBufferDataARB", [
  "glBufferData",
])

///|
let d_glBufferSubData : Dispatch = Dispatch::with_aliases("glBufferSubData", [
  "glBufferSubDataARB",
])

///|
let d_glBufferSubDataARB : Dispatch = Dispatch::with_aliases(
  "glBufferSubDataARB",
  ["glBufferSubData"],
)

///|
/// Copy a packed scalar array into a new data store for the buffer currently
/// bound to `target`. The byte size is derived from `data`.
pub fn[T : GlData] gl_buffer_data(
  target : UInt,
  data : FixedArray[T],
  usage : UInt,
) -> Unit {
  let fp : FuncRef[(UInt, Int64, @pointer.Pointer[Unit], UInt) -> Unit] = d_glBufferData
    .get()
    .unsafe_into()
  let size = gl_buffer_data_size(data)
  @pointer.borrow_array(data, fn(data_pointer) {
    let raw : @pointer.Pointer[Unit] = data_pointer.cast()
    fp(target, size, raw, usage)
  })
}

///|
/// ARB spelling of `gl_buffer_data`.
pub fn[T : GlData] gl_buffer_data_arb(
  target : UInt,
  data : FixedArray[T],
  usage : UInt,
) -> Unit {
  let fp : FuncRef[(UInt, Int64, @pointer.Pointer[Unit], UInt) -> Unit] = d_glBufferDataARB
    .get()
    .unsafe_into()
  let size = gl_buffer_data_size(data)
  @pointer.borrow_array(data, fn(data_pointer) {
    let raw : @pointer.Pointer[Unit] = data_pointer.cast()
    fp(target, size, raw, usage)
  })
}

///|
/// Allocate an uninitialized data store for the buffer currently bound to
/// `target`. Use `gl_buffer_sub_data` or another OpenGL operation to fill it.
pub fn gl_buffer_allocate(target : UInt, size : Int64, usage : UInt) -> Unit {
  let fp : FuncRef[(UInt, Int64, @pointer.Pointer[Unit], UInt) -> Unit] = d_glBufferData
    .get()
    .unsafe_into()
  fp(target, size, @pointer.Pointer::null(), usage)
}

///|
/// ARB spelling of `gl_buffer_allocate`.
pub fn gl_buffer_allocate_arb(
  target : UInt,
  size : Int64,
  usage : UInt,
) -> Unit {
  let fp : FuncRef[(UInt, Int64, @pointer.Pointer[Unit], UInt) -> Unit] = d_glBufferDataARB
    .get()
    .unsafe_into()
  fp(target, size, @pointer.Pointer::null(), usage)
}

///|
/// Update part of the buffer currently bound to `target`. The byte size is
/// derived from `data`.
pub fn[T : GlData] gl_buffer_sub_data(
  target : UInt,
  offset : Int64,
  data : FixedArray[T],
) -> Unit {
  let fp : FuncRef[(UInt, Int64, Int64, @pointer.Pointer[Unit]) -> Unit] = d_glBufferSubData
    .get()
    .unsafe_into()
  let size = gl_buffer_data_size(data)
  @pointer.borrow_array(data, fn(data_pointer) {
    let raw : @pointer.Pointer[Unit] = data_pointer.cast()
    fp(target, offset, size, raw)
  })
}

///|
/// ARB spelling of `gl_buffer_sub_data`.
pub fn[T : GlData] gl_buffer_sub_data_arb(
  target : UInt,
  offset : Int64,
  data : FixedArray[T],
) -> Unit {
  let fp : FuncRef[(UInt, Int64, Int64, @pointer.Pointer[Unit]) -> Unit] = d_glBufferSubDataARB
    .get()
    .unsafe_into()
  let size = gl_buffer_data_size(data)
  @pointer.borrow_array(data, fn(data_pointer) {
    let raw : @pointer.Pointer[Unit] = data_pointer.cast()
    fp(target, offset, size, raw)
  })
}