///|
pub fn sync_image_asset(
  handle : @render3d_types.ImageHandle,
  image : @render3d_types.ImageAsset,
) -> Unit {
  backend.images3d.set(handle, image)
  if image_handle_requires_mipmaps(handle) {
    ensure_texture_mipmaps(get_texture(image.path))
  }
}

///|
pub fn sync_cubemap_asset(
  handle : @render3d_types.CubemapHandle,
  cubemap : @render3d_types.CubemapAsset3D,
) -> Unit {
  backend.cubemaps3d.set(handle, cubemap)
}

///|
pub fn release_mesh_asset(handle : @render3d_types.MeshHandle) -> Unit {
  clear_triangle_mesh_cache_for_handle(handle)
  backend.meshes3d.remove(handle)
}

///|
pub fn release_material_asset(handle : @render3d_types.MaterialHandle) -> Unit {
  backend.materials3d.remove(handle)
}

///|
pub fn release_image_asset(handle : @render3d_types.ImageHandle) -> Unit {
  backend.images3d.remove(handle)
}

///|
pub fn release_cubemap_asset(handle : @render3d_types.CubemapHandle) -> Unit {
  backend.cubemaps3d.remove(handle)
}

///|
pub fn render3d_submit(frame : @render3d_types.RenderFrame3D) -> Unit {
  guard frame.camera is Some(active_camera) else { return }
  let shadow_state = render_directional_shadow_map(frame, active_camera)
  if !backend.frame_has_draw_commands {
    @raylib.clear_background(to_ray_color(frame.clear_color))
  }
  mark_frame_drawn()
  begin_3d(active_camera)
  draw_skybox3d(frame, active_camera)
  for item in frame.items {
    guard backend.meshes3d.get(item.mesh) is Some(mesh_asset) else { continue }
    let material = backend.materials3d
      .get(item.material)
      .unwrap_or(@render3d_types.default_standard_material3d())
    draw_mesh3d_instance(
      item.mesh,
      mesh_asset,
      material,
      item.transform,
      frame,
      active_camera.position,
      item.receive_shadows,
      shadow_state,
    )
  }
  draw_render3d_lines(frame)
  end_3d()
}

///|
fn draw_render3d_lines(frame : @render3d_types.RenderFrame3D) -> Unit {
  for line in frame.lines {
    @raylib.draw_line_3d(
      to_ray_vector3_smath(line.start),
      to_ray_vector3_smath(line.end),
      to_ray_color(line.color),
    )
  }
}

///|
fn draw_skybox3d(
  frame : @render3d_types.RenderFrame3D,
  camera : @render3d_types.FrameCamera3D,
) -> Unit {
  guard frame.skybox is Some(skybox) else { return }
  guard backend.cubemaps3d.get(skybox.cubemap) is Some(cubemap) else { return }
  match cubemap {
    SixFaces(faces) => {
      let half_size = skybox_half_size(camera)
      @rl.disable_depth_mask()
      @rl.disable_backface_culling()
      draw_skybox_face(
        faces.positive_x,
        camera.position,
        skybox.rotation,
        skybox.brightness,
        half_size,
        Vec3(1.0, -1.0, -1.0),
        Vec3(1.0, -1.0, 1.0),
        Vec3(1.0, 1.0, 1.0),
        Vec3(1.0, 1.0, -1.0),
      )
      draw_skybox_face(
        faces.negative_x,
        camera.position,
        skybox.rotation,
        skybox.brightness,
        half_size,
        Vec3(-1.0, -1.0, 1.0),
        Vec3(-1.0, -1.0, -1.0),
        Vec3(-1.0, 1.0, -1.0),
        Vec3(-1.0, 1.0, 1.0),
      )
      draw_skybox_face(
        faces.positive_y,
        camera.position,
        skybox.rotation,
        skybox.brightness,
        half_size,
        Vec3(-1.0, 1.0, -1.0),
        Vec3(1.0, 1.0, -1.0),
        Vec3(1.0, 1.0, 1.0),
        Vec3(-1.0, 1.0, 1.0),
      )
      draw_skybox_face(
        faces.negative_y,
        camera.position,
        skybox.rotation,
        skybox.brightness,
        half_size,
        Vec3(-1.0, -1.0, 1.0),
        Vec3(1.0, -1.0, 1.0),
        Vec3(1.0, -1.0, -1.0),
        Vec3(-1.0, -1.0, -1.0),
      )
      draw_skybox_face(
        faces.positive_z,
        camera.position,
        skybox.rotation,
        skybox.brightness,
        half_size,
        Vec3(1.0, -1.0, 1.0),
        Vec3(-1.0, -1.0, 1.0),
        Vec3(-1.0, 1.0, 1.0),
        Vec3(1.0, 1.0, 1.0),
      )
      draw_skybox_face(
        faces.negative_z,
        camera.position,
        skybox.rotation,
        skybox.brightness,
        half_size,
        Vec3(-1.0, -1.0, -1.0),
        Vec3(1.0, -1.0, -1.0),
        Vec3(1.0, 1.0, -1.0),
        Vec3(-1.0, 1.0, -1.0),
      )
      @rl.enable_backface_culling()
      @rl.enable_depth_mask()
    }
  }
}

///|
fn skybox_half_size(camera : @render3d_types.FrameCamera3D) -> Double {
  let safe_far = if camera.far > 0.001 { camera.far } else { 0.001 }
  let max_visible_half = safe_far * 0.45
  match camera.projection {
    Perspective => max_visible_half
    Orthographic =>
      camera.orthographic_size.map_or(max_visible_half, fn(size) {
        let cover_half = @cmp.maximum(size[X].abs(), size[Y].abs()) * 0.5
        if cover_half > max_visible_half {
          max_visible_half
        } else if cover_half <= 0.001 {
          max_visible_half
        } else {
          cover_half
        }
      })
  }
}

///|
fn draw_skybox_face(
  image : @render3d_types.ImageHandle,
  camera_position : @smath.Vec3,
  rotation : @smath.Quat,
  brightness : Double,
  half_size : Double,
  p0 : @smath.Vec3,
  p1 : @smath.Vec3,
  p2 : @smath.Vec3,
  p3 : @smath.Vec3,
) -> Unit {
  guard backend.images3d.get(image) is Some(image_asset) else { return }
  let texture = get_texture(image_asset.path)
  let color = clamp_u8((255.0 * @cmp.maximum(0.0, brightness)).to_int()).to_byte()
  @rl.set_texture(texture.id())
  @rl.color4ub(color, color, color, b'\xFF')
  @rl.begin(@rl.Triangles)
  emit_skybox_vertex(camera_position, rotation, half_size, p0, 0.0, 1.0)
  emit_skybox_vertex(camera_position, rotation, half_size, p1, 1.0, 1.0)
  emit_skybox_vertex(camera_position, rotation, half_size, p2, 1.0, 0.0)
  emit_skybox_vertex(camera_position, rotation, half_size, p0, 0.0, 1.0)
  emit_skybox_vertex(camera_position, rotation, half_size, p2, 1.0, 0.0)
  emit_skybox_vertex(camera_position, rotation, half_size, p3, 0.0, 0.0)
  @rl.end_()
  @rl.set_texture(0U)
}

///|
fn emit_skybox_vertex(
  camera_position : @smath.Vec3,
  rotation : @smath.Quat,
  half_size : Double,
  direction : @smath.Vec3,
  u : Double,
  v : Double,
) -> Unit {
  let position = camera_position +
    rotation.rotate_vec3(direction.scalar_mul(half_size))
  @rl.tex_coord2f(to_float(u), to_float(v))
  @rl.vertex3f(to_float(position.x), to_float(position.y), to_float(position.z))
}

///|
fn draw_mesh3d_instance(
  mesh_handle : @render3d_types.MeshHandle,
  mesh_asset : @render3d_types.MeshAsset,
  material : @render3d_types.StandardMaterial3D,
  transform : @render3d_types.FrameTransform3D,
  frame : @render3d_types.RenderFrame3D,
  camera_position : @smath.Vec3,
  receive_shadows : Bool,
  shadow_state : FrameShadowState?,
) -> Unit {
  @rl.push_matrix()
  @rl.translatef(
    to_float(transform.translation.x),
    to_float(transform.translation.y),
    to_float(transform.translation.z),
  )
  apply_quat_rotation(transform.rotation)
  @rl.scalef(
    to_float(transform.scale.x),
    to_float(transform.scale.y),
    to_float(transform.scale.z),
  )
  let texture_bundle = resolve_material_texture_bundle(material)
  match mesh_asset.primitive {
    Cube(size) =>
      draw_lit_cuboid(
        size, material, frame, camera_position, texture_bundle, receive_shadows,
        shadow_state,
      )
    Sphere(radius) =>
      draw_lit_sphere(
        radius, material, frame, camera_position, texture_bundle, receive_shadows,
        shadow_state,
      )
    Cylinder(radius_top, radius_bottom, height, slices) =>
      draw_lit_cylinder(
        radius_top, radius_bottom, height, slices, material, frame, camera_position,
        texture_bundle, receive_shadows, shadow_state,
      )
    Plane(size) => {
      let thickness = if size.y.abs() < 0.0001 { 0.02 } else { size.y }
      let cuboid = @smath.Vec3(size.x, thickness, size.z)
      draw_lit_cuboid(
        cuboid, material, frame, camera_position, texture_bundle, receive_shadows,
        shadow_state,
      )
    }
    Triangles(mesh) =>
      draw_triangle_mesh(
        mesh_handle, mesh, material, frame, camera_position, texture_bundle, receive_shadows,
        shadow_state,
      )
  }
  @rl.pop_matrix()
}

///|
const TEXTURED_SPHERE_STACKS : Int = 16

///|
const TEXTURED_SPHERE_SLICES : Int = 24

///|
fn draw_lit_cuboid(
  size : @smath.Vec3,
  material : @render3d_types.StandardMaterial3D,
  frame : @render3d_types.RenderFrame3D,
  camera_position : @smath.Vec3,
  texture_bundle : MaterialTextureBundle3D?,
  receive_shadows : Bool,
  shadow_state : FrameShadowState?,
) -> Unit {
  let key = "cube:\{size.x}:\{size.y}:\{size.z}"
  let mesh = get_or_create_primitive_mesh(key, fn() {
    @raylib.gen_mesh_cube(to_float(size.x), to_float(size.y), to_float(size.z))
  })
  draw_lit_mesh_with_material(
    mesh, frame, camera_position, material, texture_bundle, false, receive_shadows,
    shadow_state,
  )
}

///|
fn draw_lit_sphere(
  radius : Double,
  material : @render3d_types.StandardMaterial3D,
  frame : @render3d_types.RenderFrame3D,
  camera_position : @smath.Vec3,
  texture_bundle : MaterialTextureBundle3D?,
  receive_shadows : Bool,
  shadow_state : FrameShadowState?,
) -> Unit {
  let key = "sphere:\{radius}:\{TEXTURED_SPHERE_SLICES}:\{TEXTURED_SPHERE_STACKS}"
  let mesh = get_or_create_primitive_mesh(key, fn() {
    @raylib.gen_mesh_sphere(
      to_float(radius),
      TEXTURED_SPHERE_SLICES,
      TEXTURED_SPHERE_STACKS,
    )
  })
  draw_lit_mesh_with_material(
    mesh, frame, camera_position, material, texture_bundle, false, receive_shadows,
    shadow_state,
  )
}

///|
fn draw_lit_cylinder(
  radius_top : Double,
  radius_bottom : Double,
  height : Double,
  slices : Int,
  material : @render3d_types.StandardMaterial3D,
  frame : @render3d_types.RenderFrame3D,
  camera_position : @smath.Vec3,
  texture_bundle : MaterialTextureBundle3D?,
  receive_shadows : Bool,
  shadow_state : FrameShadowState?,
) -> Unit {
  let safe_slices = @cmp.maximum(3, slices)
  let key = "truncated_cylinder:\{radius_top}:\{radius_bottom}:\{height}:\{safe_slices}"
  let mesh = get_or_create_primitive_mesh(key, fn() {
    let (vertices, uvs, normals) = build_cylinder_mesh(
      radius_top, radius_bottom, height, safe_slices,
    )
    match
      create_uploaded_triangle_mesh(
        vertices,
        Some(uvs),
        None,
        normals,
        None,
        None,
      ) {
      Some(mesh) => mesh
      None => empty_uploaded_triangle_mesh()
    }
  })
  draw_lit_mesh_with_material(
    mesh, frame, camera_position, material, texture_bundle, false, receive_shadows,
    shadow_state,
  )
}

///|
fn resolve_material_texture_source(
  binding : @render3d_types.TextureBinding3D,
) -> MaterialTextureSource3D? {
  guard backend.images3d.get(binding.image) is Some(image_asset) else {
    warn_texture_issue(
      "missing_image_handle:\{image_handle_text(binding.image)}",
      "material texture handle \{image_handle_text(binding.image)} is not synced; fallback to default texture",
    )
    return None
  }
  let texture = get_texture(image_asset.path)
  apply_texture_sampler(texture, binding.sampler)
  let texture_id = @raylib.get_texture_id(texture)
  let width = @raylib.get_texture_width(texture)
  let height = @raylib.get_texture_height(texture)
  if texture_id <= 0 || width <= 0 || height <= 0 {
    warn_texture_issue(
      "invalid_texture:\{image_asset.path}",
      "failed to load texture '\{image_asset.path}' (id=\{texture_id}, size=\{width}x\{height}); fallback to base_color",
    )
    return None
  }
  Some({
    texture,
    texcoord_set: binding.texcoord_set,
    transform: binding.transform,
  })
}

///|
fn resolve_material_texture_bundle(
  material : @render3d_types.StandardMaterial3D,
) -> MaterialTextureBundle3D? {
  let base_source = material.base_color_texture.bind(
    resolve_material_texture_source,
  )
  let emissive_source = material.emissive_texture.bind(
    resolve_material_texture_source,
  )
  let metallic_roughness_source = material.metallic_roughness_texture.bind(
    resolve_material_texture_source,
  )
  let occlusion_source = material.occlusion_texture.bind(
    resolve_material_texture_source,
  )
  let normal_source = material.normal_texture.bind(
    resolve_material_texture_source,
  )
  if base_source is None &&
    emissive_source is None &&
    metallic_roughness_source is None &&
    occlusion_source is None &&
    normal_source is None {
    return None
  }
  let default_transform = @render3d_types.default_texture_transform3d()
  Some({
    base_texture: base_source.map_or(get_white_texture(), fn(source) {
      source.texture
    }),
    emissive_texture: emissive_source.map_or(get_white_texture(), fn(source) {
      source.texture
    }),
    metallic_roughness_texture: metallic_roughness_source.map_or(
      get_white_texture(),
      fn(source) { source.texture },
    ),
    occlusion_texture: occlusion_source.map_or(get_white_texture(), fn(source) {
      source.texture
    }),
    normal_texture: normal_source.map_or(null_texture(), fn(source) {
      source.texture
    }),
    has_base_texture: base_source is Some(_),
    has_emissive_texture: emissive_source is Some(_),
    has_metallic_roughness_texture: metallic_roughness_source is Some(_),
    has_occlusion_texture: occlusion_source is Some(_),
    has_normal_texture: normal_source is Some(_),
    base_transform: base_source.map_or(default_transform, fn(source) {
      source.transform
    }),
    emissive_transform: emissive_source.map_or(default_transform, fn(source) {
      source.transform
    }),
    metallic_roughness_transform: metallic_roughness_source.map_or(
      default_transform,
      fn(source) { source.transform },
    ),
    occlusion_transform: occlusion_source.map_or(default_transform, fn(source) {
      source.transform
    }),
    normal_transform: normal_source.map_or(default_transform, fn(source) {
      source.transform
    }),
    base_texcoord_set: base_source.map_or(0, fn(source) { source.texcoord_set }),
    emissive_texcoord_set: emissive_source.map_or(0, fn(source) {
      source.texcoord_set
    }),
    metallic_roughness_texcoord_set: metallic_roughness_source.map_or(0, fn(
      source,
    ) {
      source.texcoord_set
    }),
    occlusion_texcoord_set: occlusion_source.map_or(0, fn(source) {
      source.texcoord_set
    }),
    normal_texcoord_set: normal_source.map_or(0, fn(source) {
      source.texcoord_set
    }),
  })
}

///|
fn texture_wrap_to_raylib(wrap : @render3d_types.TextureWrap3D) -> Int {
  match wrap {
    ClampToEdge => @raylib.TextureWrapClamp
    MirroredRepeat => @raylib.TextureWrapMirrorRepeat
    Repeat => @raylib.TextureWrapRepeat
  }
}

///|
fn texture_filter_to_raylib(sampler : @render3d_types.TextureSampler3D) -> Int {
  let nearest = sampler.mag_filter == Nearest || sampler.min_filter == Nearest
  match sampler.mipmap_filter {
    Some(_) =>
      if nearest {
        @raylib.TextureFilterPoint
      } else {
        @raylib.TextureFilterTrilinear
      }
    None =>
      if nearest {
        @raylib.TextureFilterPoint
      } else {
        @raylib.TextureFilterBilinear
      }
  }
}

///|
fn sampler_requests_generated_mipmaps(
  sampler : @render3d_types.TextureSampler3D,
) -> Bool {
  sampler.mipmap_filter is Some(_) &&
  texture_filter_to_raylib(sampler) == @raylib.TextureFilterTrilinear
}

///|
fn maybe_ensure_binding_mipmaps(
  binding : @render3d_types.TextureBinding3D,
) -> Unit {
  if !sampler_requests_generated_mipmaps(binding.sampler) {
    return
  }
  if backend.images3d.get(binding.image) is Some(image_asset) {
    ensure_texture_mipmaps(get_texture(image_asset.path))
  }
}

///|
fn ensure_material_mipmaps(
  material : @render3d_types.StandardMaterial3D,
) -> Unit {
  if material.base_color_texture is Some(binding) {
    maybe_ensure_binding_mipmaps(binding)
  }
  if material.normal_texture is Some(binding) {
    maybe_ensure_binding_mipmaps(binding)
  }
  if material.emissive_texture is Some(binding) {
    maybe_ensure_binding_mipmaps(binding)
  }
  if material.metallic_roughness_texture is Some(binding) {
    maybe_ensure_binding_mipmaps(binding)
  }
  if material.occlusion_texture is Some(binding) {
    maybe_ensure_binding_mipmaps(binding)
  }
}

///|
fn material_uses_image_with_mipmaps(
  material : @render3d_types.StandardMaterial3D,
  image : @render3d_types.ImageHandle,
) -> Bool {
  let binding_matches = fn(binding : @render3d_types.TextureBinding3D) -> Bool {
    binding.image == image &&
    sampler_requests_generated_mipmaps(binding.sampler)
  }
  let uses_base = match material.base_color_texture {
    Some(binding) => binding_matches(binding)
    None => false
  }
  let uses_normal = match material.normal_texture {
    Some(binding) => binding_matches(binding)
    None => false
  }
  let uses_emissive = match material.emissive_texture {
    Some(binding) => binding_matches(binding)
    None => false
  }
  let uses_metallic_roughness = match material.metallic_roughness_texture {
    Some(binding) => binding_matches(binding)
    None => false
  }
  let uses_occlusion = match material.occlusion_texture {
    Some(binding) => binding_matches(binding)
    None => false
  }
  uses_base ||
  uses_normal ||
  uses_emissive ||
  uses_metallic_roughness ||
  uses_occlusion
}

///|
fn image_handle_requires_mipmaps(image : @render3d_types.ImageHandle) -> Bool {
  for material_handle, material in backend.materials3d {
    ignore(material_handle)
    if material_uses_image_with_mipmaps(material, image) {
      return true
    }
  }
  false
}

///|
fn apply_texture_sampler(
  texture : @raylib.Texture,
  sampler : @render3d_types.TextureSampler3D,
) -> Unit {
  @raylib.set_texture_wrap(texture, texture_wrap_to_raylib(sampler.wrap_u))
  @raylib.set_texture_filter(texture, texture_filter_to_raylib(sampler))
}

///|
fn build_cylinder_mesh(
  radius_top : Double,
  radius_bottom : Double,
  height : Double,
  slices : Int,
) -> (Array[@smath.Vec3], Array[@smath.Vec2], Array[@smath.Vec3]) {
  let vertices : Array[@smath.Vec3] = []
  let uvs : Array[@smath.Vec2] = []
  let normals : Array[@smath.Vec3] = []
  let segment_count = if slices < 3 { 3 } else { slices }
  let half_h = height / 2.0
  let slope_y = if height.abs() < 0.000001 {
    0.0
  } else {
    (radius_bottom - radius_top) / height
  }
  for segment in 0.. 0.000001 {
      let top_center = @smath.Vec3(0.0, half_h, 0.0)
      let top_normal = @smath.Vec3(0.0, 1.0, 0.0)
      push_textured_triangle(
        vertices,
        uvs,
        normals,
        top_center,
        top1,
        top0,
        Vec2(0.5, 0.5),
        Vec2(0.5 + 0.5 * @cmath.cos(phi1), 0.5 + 0.5 * @cmath.sin(phi1)),
        Vec2(0.5 + 0.5 * @cmath.cos(phi0), 0.5 + 0.5 * @cmath.sin(phi0)),
        top_normal,
        top_normal,
        top_normal,
      )
    }
    if radius_bottom.abs() > 0.000001 {
      let bottom_center = @smath.Vec3(0.0, -half_h, 0.0)
      let bottom_normal = @smath.Vec3(0.0, -1.0, 0.0)
      push_textured_triangle(
        vertices,
        uvs,
        normals,
        bottom_center,
        bot0,
        bot1,
        Vec2(0.5, 0.5),
        Vec2(0.5 + 0.5 * @cmath.cos(phi0), 0.5 + 0.5 * @cmath.sin(phi0)),
        Vec2(0.5 + 0.5 * @cmath.cos(phi1), 0.5 + 0.5 * @cmath.sin(phi1)),
        bottom_normal,
        bottom_normal,
        bottom_normal,
      )
    }
  }
  (vertices, uvs, normals)
}

///|
fn push_textured_triangle(
  vertices : Array[@smath.Vec3],
  uvs : Array[@smath.Vec2],
  normals : Array[@smath.Vec3],
  a : @smath.Vec3,
  b : @smath.Vec3,
  c : @smath.Vec3,
  uv_a : @smath.Vec2,
  uv_b : @smath.Vec2,
  uv_c : @smath.Vec2,
  n_a : @smath.Vec3,
  n_b : @smath.Vec3,
  n_c : @smath.Vec3,
) -> Unit {
  vertices.push(a)
  vertices.push(b)
  vertices.push(c)
  uvs.push(uv_a)
  uvs.push(uv_b)
  uvs.push(uv_c)
  normals.push(n_a)
  normals.push(n_b)
  normals.push(n_c)
}