// picogkffi viewer: OpenGL viewer for visualization.

///|
pub struct Viewer {
  h : Int64
}

///|
/// Create a new viewer with title and window size.
/// Requires a display + OpenGL context.
pub fn new_viewer(title : String, width : Int, height : Int) -> Viewer {
  let title_buf = cstr(title)
  let size_buf = FixedArray::make(8, 0L.to_byte())
  write_float_le(size_buf, 0, Float::from_int(width))
  write_float_le(size_buf, 4, Float::from_int(height))
  Viewer::{
    h: viewer_h_create(title_buf, size_buf, 0L, 0L, 0L, 0L, 0L, 0L, 0L),
  }
}

///|
/// Returns true if the viewer is still valid.
pub fn Viewer::is_valid(self : Viewer) -> Bool {
  viewer_b_is_valid(self.h)
}

///|
/// Destroy the viewer.
pub fn Viewer::destroy(self : Viewer) -> Unit {
  viewer_destroy(self.h)
}

///|
/// Request a redraw.
pub fn Viewer::request_update(self : Viewer) -> Unit {
  viewer_request_update(self.h)
}

///|
/// Process one frame. Returns false if the viewer was closed.
pub fn Viewer::poll(self : Viewer) -> Bool {
  viewer_b_poll(self.h)
}

///|
/// Request a screenshot to the given file path.
pub fn Viewer::request_screen_shot(self : Viewer, path : String) -> Unit {
  viewer_request_screen_shot(self.h, cstr(path))
}

///|
/// Enable or disable experimental features.
pub fn Viewer::enable_experimental(self : Viewer, enable : Bool) -> Unit {
  viewer_enable_experimental(self.h, enable)
}

///|
/// Request the viewer to close.
pub fn Viewer::request_close(self : Viewer) -> Unit {
  viewer_request_close(self.h)
}

///|
/// Load IBL lighting from DDS data.
pub fn Viewer::load_light_setup(
  self : Viewer,
  diffuse : FixedArray[Byte],
  specular : FixedArray[Byte],
) -> Bool {
  viewer_b_load_light_setup(
    self.h,
    diffuse,
    diffuse.length(),
    specular,
    specular.length(),
  )
}

///|
/// Add a mesh to the viewer at the given group.
pub fn Viewer::add_mesh(self : Viewer, group : Int, mesh : Mesh) -> Unit {
  viewer_add_mesh(get_instance(), self.h, group, mesh.h)
}

///|
/// Remove a mesh from the viewer.
pub fn Viewer::remove_mesh(self : Viewer, mesh : Mesh) -> Unit {
  viewer_remove_mesh(get_instance(), self.h, mesh.h)
}

///|
/// Set the transform matrix for a mesh.
pub fn Viewer::set_mesh_matrix(
  self : Viewer,
  mesh : Mesh,
  matrix : FixedArray[Byte],
) -> Unit {
  viewer_set_mesh_matrix(get_instance(), self.h, mesh.h, matrix)
}

///|
/// Add voxels to the viewer at the given group.
pub fn Viewer::add_voxels(self : Viewer, group : Int, vox : Voxels) -> Unit {
  viewer_add_voxels(get_instance(), self.h, group, vox.h)
}

///|
/// Remove voxels from the viewer.
pub fn Viewer::remove_voxels(self : Viewer, vox : Voxels) -> Unit {
  viewer_remove_voxels(get_instance(), self.h, vox.h)
}

///|
/// Set the transform matrix for voxels.
pub fn Viewer::set_voxels_matrix(
  self : Viewer,
  vox : Voxels,
  matrix : FixedArray[Byte],
) -> Unit {
  viewer_set_voxels_matrix(get_instance(), self.h, vox.h, matrix)
}

///|
/// Add a polyline to the viewer at the given group.
pub fn Viewer::add_poly_line(self : Viewer, group : Int, pl : PolyLine) -> Unit {
  viewer_add_poly_line(get_instance(), self.h, group, pl.h)
}

///|
/// Remove a polyline from the viewer.
pub fn Viewer::remove_poly_line(self : Viewer, pl : PolyLine) -> Unit {
  viewer_remove_poly_line(get_instance(), self.h, pl.h)
}

///|
/// Set the transform matrix for a polyline.
pub fn Viewer::set_poly_line_matrix(
  self : Viewer,
  pl : PolyLine,
  matrix : FixedArray[Byte],
) -> Unit {
  viewer_set_poly_line_matrix(get_instance(), self.h, pl.h, matrix)
}

///|
/// Remove all objects from the viewer.
pub fn Viewer::remove_all_objects(self : Viewer) -> Unit {
  viewer_remove_all_objects(self.h)
}

///|
/// Set visibility of a group.
pub fn Viewer::set_group_visible(
  self : Viewer,
  group : Int,
  visible : Bool,
) -> Unit {
  viewer_set_group_visible(self.h, group, visible)
}

///|
/// Set PBR material for a group.
pub fn Viewer::set_group_material(
  self : Viewer,
  group : Int,
  color : ColorFloat,
  metallic : Double,
  roughness : Double,
) -> Unit {
  viewer_set_group_material(
    self.h,
    group,
    pack_color(color),
    Float::from_double(metallic),
    Float::from_double(roughness),
  )
}

///|
/// Set transform matrix for a group.
pub fn Viewer::set_group_matrix(
  self : Viewer,
  group : Int,
  matrix : FixedArray[Byte],
) -> Unit {
  viewer_set_group_matrix(self.h, group, matrix)
}

///|
/// Enable overhang warnings for a group.
pub fn Viewer::enable_group_warn_overhang(
  self : Viewer,
  group : Int,
  max_angle : Double,
  min_length : Double,
) -> Unit {
  viewer_enable_group_warn_overhang(
    self.h,
    group,
    Float::from_double(max_angle),
    Float::from_double(min_length),
  )
}

///|
/// Disable overhang warnings for a group.
pub fn Viewer::disable_group_warn_overhang(self : Viewer, group : Int) -> Unit {
  viewer_disable_group_warn_overhang(self.h, group)
}

///|
/// Get the scene bounding box.
pub fn Viewer::bounding_box(self : Viewer) -> BBox3 {
  let out = FixedArray::make(24, 0L.to_byte())
  viewer_get_bounding_box(self.h, out)
  unpack_bbox3(out)
}

///|
/// Run the viewer event loop until closed.
pub fn Viewer::run(self : Viewer) -> Unit {
  while self.poll() {

  }
  self.destroy()
}

///|
/// Take a screenshot by polling frames.
pub fn Viewer::screenshot(self : Viewer, path : String, frames : Int) -> Unit {
  self.request_screen_shot(path)
  for _ in 0.. GpuTex {
  GpuTex::{ h: viewer_gpu_tex_h_create(viewer.h, width, height, cstr(name)) }
}

///|
pub fn GpuTex::refresh(self : GpuTex, viewer : Viewer, name : String) -> Unit {
  viewer_gpu_tex_refresh(viewer.h, self.h, cstr(name))
}

///|
pub fn GpuTex::mark_for_cleanup(self : GpuTex, viewer : Viewer) -> Unit {
  viewer_gpu_tex_mark_for_cleanup(viewer.h, self.h)
}

// --- Quad ---

///|
pub struct Quad {
  h : Int64
}

///|
pub fn new_quad(
  viewer : Viewer,
  tex : GpuTex,
  color : ColorFloat,
  size : Double,
  matrix : FixedArray[Byte],
  screen_space : Bool,
  centered : Bool,
  flip_y : Bool,
) -> Quad {
  Quad::{
    h: viewer_quad_h_create(
      viewer.h,
      tex.h,
      pack_color(color),
      Float::from_double(size),
      matrix,
      screen_space,
      centered,
      flip_y,
    ),
  }
}

///|
pub fn Quad::destroy(self : Quad, viewer : Viewer) -> Unit {
  viewer_quad_destroy(viewer.h, self.h)
}

///|
pub fn Quad::set_matrix(
  self : Quad,
  viewer : Viewer,
  matrix : FixedArray[Byte],
) -> Unit {
  viewer_quad_set_matrix(viewer.h, self.h, matrix)
}

// --- SideBar ---

///|
pub struct SideBar {
  h : Int64
}

///|
pub fn new_sidebar(
  viewer : Viewer,
  right : Bool,
  width : Int,
  height : Int,
  font_size : Int,
  bg_color : ColorFloat,
  fg_color : ColorFloat,
) -> SideBar {
  SideBar::{
    h: viewer_sidebar_h_create(
      viewer.h,
      right,
      width,
      height,
      font_size,
      pack_color(bg_color),
      pack_color(fg_color),
    ),
  }
}

///|
pub fn SideBar::destroy(self : SideBar, viewer : Viewer) -> Unit {
  viewer_sidebar_destroy(viewer.h, self.h)
}

// --- ViewerEx: Viewer with camera callbacks ---

///|
/// ViewerEx is a viewer with full camera control callbacks (C-side).
/// This provides the same rendering quality as the Go ViewerEx / PicoPie viewer.
pub struct ViewerEx {
  h : Int64
}

///|
/// Default camera azimuth matching PicoPie / Go DefaultCameraState.
pub let default_azimuth : Double = 45.0 * 3.14159265358979 / 180.0

///|
/// Default camera elevation matching PicoPie / Go DefaultCameraState.
pub let default_elevation : Double = 25.0 * 3.14159265358979 / 180.0

///|
/// Create a new ViewerEx with default camera state.
/// Requires a display + OpenGL context.
pub fn new_viewer_ex(
  title : String,
  width : Int,
  height : Int,
  bg_r : Double,
  bg_g : Double,
  bg_b : Double,
  bg_a : Double,
) -> ViewerEx {
  let title_buf = cstr(title)
  // Pack camera params into a byte buffer to avoid ARM64 float register overflow
  let cam = FixedArray::make(44, 0L.to_byte())
  // target_x, target_y, target_z
  write_float_le(cam, 0, 0.0)
  write_float_le(cam, 4, 0.0)
  write_float_le(cam, 8, 0.0)
  // radius
  write_float_le(cam, 12, 10.0)
  // azimuth, elevation
  write_float_le(cam, 16, Float::from_double(default_azimuth))
  write_float_le(cam, 20, Float::from_double(default_elevation))
  // zoom
  write_float_le(cam, 24, 1.0)
  // bg_r, bg_g, bg_b, bg_a
  write_float_le(cam, 28, Float::from_double(bg_r))
  write_float_le(cam, 32, Float::from_double(bg_g))
  write_float_le(cam, 36, Float::from_double(bg_b))
  write_float_le(cam, 40, Float::from_double(bg_a))
  let h = mbt_viewer_create(
    title_buf,
    Float::from_int(width),
    Float::from_int(height),
    cam,
  )
  ViewerEx::{ h, }
}

///|
/// Returns true if the viewer is still valid.
pub fn ViewerEx::is_valid(self : ViewerEx) -> Bool {
  self.h != 0L
}

///|
/// Destroy the viewer.
pub fn ViewerEx::destroy(self : ViewerEx) -> Unit {
  mbt_viewer_close(self.h)
}

///|
/// Request a redraw.
pub fn ViewerEx::request_update(self : ViewerEx) -> Unit {
  viewer_request_update(self.h)
}

///|
/// Process one frame. Returns false if the viewer was closed.
pub fn ViewerEx::poll(self : ViewerEx) -> Bool {
  viewer_b_poll(self.h)
}

///|
/// Request the viewer to close.
pub fn ViewerEx::request_close(self : ViewerEx) -> Unit {
  viewer_request_close(self.h)
}

///|
/// Add voxels to the viewer at the given group.
pub fn ViewerEx::add_voxels(self : ViewerEx, group : Int, vox : Voxels) -> Unit {
  mbt_viewer_add_voxels(self.h, group, get_instance(), vox.h)
}

///|
/// Add a mesh to the viewer at the given group.
pub fn ViewerEx::add_mesh(self : ViewerEx, group : Int, mesh : Mesh) -> Unit {
  viewer_add_mesh(get_instance(), self.h, group, mesh.h)
}

///|
/// Set PBR material for a group.
pub fn ViewerEx::set_group_material(
  self : ViewerEx,
  group : Int,
  color : ColorFloat,
  metallic : Double,
  roughness : Double,
) -> Unit {
  mbt_viewer_set_group_material(
    self.h,
    group,
    Float::from_double(color.r),
    Float::from_double(color.g),
    Float::from_double(color.b),
    Float::from_double(color.a),
    Float::from_double(metallic),
    Float::from_double(roughness),
  )
}

///|
/// Set visibility of a group.
pub fn ViewerEx::set_group_visible(
  self : ViewerEx,
  group : Int,
  visible : Bool,
) -> Unit {
  viewer_set_group_visible(self.h, group, visible)
}

///|
/// Remove all objects from the viewer.
pub fn ViewerEx::remove_all_objects(self : ViewerEx) -> Unit {
  viewer_remove_all_objects(self.h)
}

///|
/// Get the scene bounding box.
pub fn ViewerEx::bounding_box(self : ViewerEx) -> BBox3 {
  let out = FixedArray::make(24, 0L.to_byte())
  viewer_get_bounding_box(self.h, out)
  unpack_bbox3(out)
}

///|
/// Take a screenshot to PNG. Does autofit + warm-up frames + screenshot.
/// Returns true on success.
pub fn ViewerEx::screenshot_png(
  self : ViewerEx,
  path : String,
  frames : Int,
) -> Bool {
  let result = mbt_viewer_screenshot_png(self.h, cstr(path), frames)
  result == 0
}

///|
/// Take a screenshot to PNG, optionally keeping the raw TGA file.
/// Returns true on success.
pub fn ViewerEx::screenshot_png_keep_tga(
  self : ViewerEx,
  path : String,
  frames : Int,
  keep_tga : Bool,
) -> Bool {
  let kt : Int = if keep_tga { 1 } else { 0 }
  let result = mbt_viewer_screenshot_png_keep_tga(self.h, cstr(path), frames, kt)
  result == 0
}

///|
/// Run the viewer event loop until closed.
pub fn ViewerEx::run(self : ViewerEx) -> Unit {
  while self.poll() {

  }
  self.destroy()
}

// --- C extern declarations for ViewerEx ---

///|
#borrow(title, cam_params)
extern "C" fn mbt_viewer_create(
  title : FixedArray[Byte],
  width : Float,
  height : Float,
  cam_params : FixedArray[Byte],
) -> Int64 = "mbt_viewer_create"

///|
extern "C" fn mbt_viewer_close(viewer : Int64) -> Unit = "mbt_viewer_close"

///|
extern "C" fn mbt_viewer_add_voxels(
  viewer : Int64,
  group : Int,
  instance : Int64,
  vox : Int64,
) -> Unit = "mbt_viewer_add_voxels"

///|
extern "C" fn mbt_viewer_set_group_material(
  viewer : Int64,
  group : Int,
  r : Float,
  g : Float,
  b : Float,
  a : Float,
  metallic : Float,
  roughness : Float,
) -> Unit = "mbt_viewer_set_group_material"

///|
#borrow(png_path)
extern "C" fn mbt_viewer_screenshot_png(
  viewer : Int64,
  png_path : FixedArray[Byte],
  frames : Int,
) -> Int = "mbt_viewer_screenshot_png"

///|
#borrow(png_path)
extern "C" fn mbt_viewer_screenshot_png_keep_tga(
  viewer : Int64,
  png_path : FixedArray[Byte],
  frames : Int,
  keep_tga : Int,
) -> Int = "mbt_viewer_screenshot_png_keep_tga"