///|
fn upload_texture_transform_uniforms(
  shader : @raylib.Shader,
  offset_scale_loc : Int,
  rotation_loc : Int,
  transform : @render3d_types.TextureTransform3D,
) -> Unit {
  set_shader_uniform(
    shader,
    offset_scale_loc,
    uniform_texture_transform_offset_scale(transform),
  )
  set_shader_uniform(
    shader,
    rotation_loc,
    uniform_texture_transform_rotation(transform),
  )
}

///|
fn upload_lighting_uniforms(
  frame : @render3d_types.RenderFrame3D,
  camera_position : @smath.Vec3,
  material : @render3d_types.StandardMaterial3D,
  texture_bundle : MaterialTextureBundle3D?,
  mesh_has_tangents : Bool,
  receive_shadows : Bool,
  shadow_state : FrameShadowState?,
) -> LightingShaderState {
  let state = get_lighting_shader_state()
  let shader = state.shader
  let default_shadow_rect : ShadowAtlasRect = {
    offset: Vec2(0.0, 0.0),
    scale: Vec2(0.0, 0.0),
  }
  let ambient = frame.ambient_light
    .map(fn(light) { light_rgb(light.color, light.intensity) })
    .unwrap_or(@smath.Vec3::zero())
  set_shader_uniform(shader, state.ambient_light_loc, uniform_vec3(ambient))
  set_shader_uniform(
    shader,
    state.view_position_loc,
    uniform_vec3(camera_position),
  )
  let camera_forward = frame.camera.map_or(@smath.Vec3(0.0, 0.0, -1.0), fn(
    active_camera,
  ) {
    normalize_or(
      active_camera.target - active_camera.position,
      Vec3(0.0, 0.0, -1.0),
    )
  })
  set_shader_uniform(
    shader,
    state.view_forward_loc,
    uniform_vec3(camera_forward),
  )

  let directional_count = @cmp.minimum(
    frame.directional_lights.length(),
    MAX_DIRECTIONAL_LIGHTS,
  )
  let directional_directions : Array[@smath.Vec3] = []
  let directional_colors : Array[@smath.Vec3] = []
  for index in 0.. frame_shadow.directional_shadows.get(index)
      None => None
    }
    set_shader_uniform(
      shader,
      state.shadow_enabled_locs[index],
      uniform_int(if shadow is Some(_) { 1 } else { 0 }),
    )
    set_shader_uniform(
      shader,
      state.shadow_cascade_count_locs[index],
      uniform_int(shadow.map_or(0, fn(value) { value.cascades.length() })),
    )
    set_shader_uniform(
      shader,
      state.shadow_depth_bias_locs[index],
      uniform_float(shadow.map_or(0.0, fn(value) { value.depth_bias })),
    )
    set_shader_uniform(
      shader,
      state.shadow_normal_bias_locs[index],
      uniform_float(shadow.map_or(0.0, fn(value) { value.normal_bias })),
    )
    let base_index = index * MAX_DIRECTIONAL_CASCADES
    for cascade_index in 0.. frame_shadow.spot_shadows.get(index)
      None => None
    }
    set_shader_uniform(
      shader,
      state.spot_shadow_enabled_locs[index],
      uniform_int(if shadow is Some(_) { 1 } else { 0 }),
    )
    @raylib.set_shader_value_matrix(
      shader,
      state.spot_shadow_matrix_locs[index],
      shadow.map_or(@raylib.Matrix::identity(), fn(value) {
        value.light_view_projection
      }),
    )
    set_shader_uniform(
      shader,
      state.spot_shadow_rect_locs[index],
      uniform_shadow_atlas_rect(
        shadow.map_or(default_shadow_rect, fn(value) { value.atlas_rect }),
      ),
    )
    set_shader_uniform(
      shader,
      state.spot_shadow_depth_bias_locs[index],
      uniform_float(shadow.map_or(0.0, fn(value) { value.depth_bias })),
    )
    set_shader_uniform(
      shader,
      state.spot_shadow_normal_bias_locs[index],
      uniform_float(shadow.map_or(0.0, fn(value) { value.normal_bias })),
    )
  }
  set_shader_uniform(
    shader,
    state.spot_shadow_texel_size_loc,
    uniform_vec2(
      shadow_state.map_or(Vec2(0.0, 0.0), fn(value) { value.spot_texel_size }),
    ),
  )
  for index in 0.. frame_shadow.point_shadows.get(index)
      None => None
    }
    set_shader_uniform(
      shader,
      state.point_shadow_enabled_locs[index],
      uniform_int(if shadow is Some(_) { 1 } else { 0 }),
    )
    set_shader_uniform(
      shader,
      state.point_shadow_depth_bias_locs[index],
      uniform_float(shadow.map_or(0.0, fn(value) { value.depth_bias })),
    )
    set_shader_uniform(
      shader,
      state.point_shadow_normal_bias_locs[index],
      uniform_float(shadow.map_or(0.0, fn(value) { value.normal_bias })),
    )
    for face_index in 0.. Int {
  match mode {
    Opaque => 0
    Mask => 1
    Blend => 2
  }
}

///|
fn to_ray_camera3d(camera : @render3d_types.FrameCamera3D) -> @raylib.Camera3D {
  @raylib.Camera3D::new(
    to_ray_vector3_smath(camera.position),
    to_ray_vector3_smath(camera.target),
    to_ray_vector3_smath(camera.up),
    to_float(
      match camera.projection {
        Perspective => camera.fov_y
        Orthographic =>
          camera.orthographic_size.map_or(10.0, fn(size) { size[Y] })
      },
    ),
    match camera.projection {
      Perspective => @raylib.CameraPerspective
      Orthographic => @raylib.CameraOrthographic
    },
  )
}

///|
fn configured_filter() -> Int {
  if backend.image_smooth {
    @raylib.TextureFilterBilinear
  } else {
    @raylib.TextureFilterPoint
  }
}

///|
fn file_extension(path : String) -> String {
  if path.to_lower().has_prefix("data:") {
    return data_uri_extension(path)
  }
  let slash_idx = if path.rev_find("/") is Some(i) { i } else { -1 }
  let backslash_idx = if path.rev_find("\\") is Some(i) { i } else { -1 }
  let last_separator = if slash_idx > backslash_idx {
    slash_idx
  } else {
    backslash_idx
  }
  if path.rev_find(".") is Some(dot_idx) && dot_idx > last_separator {
    path.unsafe_substring(start=dot_idx, end=path.length())
  } else {
    ".bin"
  }
}

///|
fn data_uri_extension(uri : String) -> String {
  guard uri.find(",") is Some(comma_idx) else { return ".bin" }
  let meta = uri.unsafe_substring(start=0, end=comma_idx).to_lower()
  if meta.contains("image/png") {
    ".png"
  } else if meta.contains("image/jpeg") {
    ".jpg"
  } else if meta.contains("image/webp") {
    ".webp"
  } else if meta.contains("image/bmp") {
    ".bmp"
  } else if meta.contains("image/tga") {
    ".tga"
  } else {
    ".bin"
  }
}

///|
fn decode_data_uri_base64(uri : String) -> Bytes? {
  guard uri.find(",") is Some(comma_idx) else { return None }
  let meta = uri.unsafe_substring(start=0, end=comma_idx).to_lower()
  if !meta.has_suffix(";base64") {
    return None
  }
  let payload = uri.unsafe_substring(start=comma_idx + 1, end=uri.length())
  Some(@base64.decode_lossy(payload.view()))
}

///|
fn get_embedded_asset(path : String) -> Bytes? {
  if backend.embedded_asset_cache.get(path) is Some(cached) {
    return Some(cached)
  }
  if (backend.embedded_asset_lookup)(path) is Some(bytes) {
    backend.embedded_asset_cache.set(path, bytes)
    Some(bytes)
  } else {
    None
  }
}

///|
fn warn_texture_issue(key : String, message : String) -> Unit {
  if backend.texture_warning_keys.contains(key) {
    return
  }
  backend.texture_warning_keys.add(key)
  println("[selene-raylib][texture-warning] \{message}")
}

///|
fn get_or_create_primitive_mesh(
  key : String,
  create : () -> @raylib.Mesh,
) -> @raylib.Mesh {
  if backend.primitive_meshes.get(key) is Some(mesh) {
    return mesh
  }
  let mesh = create()
  backend.primitive_meshes.set(key, mesh)
  mesh
}

///|
fn mesh_handle_text(mesh_handle : @render3d_types.MeshHandle) -> String {
  mesh_handle.hash().to_string()
}

///|
fn image_handle_text(image_handle : @render3d_types.ImageHandle) -> String {
  image_handle.hash().to_string()
}

///|
fn triangle_mesh_cache_prefix(
  mesh_handle : @render3d_types.MeshHandle,
) -> String {
  "triangle_mesh:\{mesh_handle_text(mesh_handle)}:"
}

///|
fn triangle_mesh_cache_key(mesh_handle : @render3d_types.MeshHandle) -> String {
  "\{triangle_mesh_cache_prefix(mesh_handle)}mesh"
}

///|
fn clear_triangle_mesh_cache_for_handle(
  mesh_handle : @render3d_types.MeshHandle,
) -> Unit {
  let prefix = triangle_mesh_cache_prefix(mesh_handle)
  for pair in backend.triangle_mesh_cache.to_array() {
    if pair.0.has_prefix(prefix) {
      @raylib.unload_mesh(pair.1)
      backend.triangle_mesh_cache.remove(pair.0)
    }
  }
}

///|
fn to_fixed_vec3_array(values : Array[@smath.Vec3]) -> FixedArray[Float] {
  let out = FixedArray::make(values.length() * 3, to_float(0.0))
  for index in 0.. FixedArray[Float] {
  let out = FixedArray::make(values.length() * 2, to_float(0.0))
  for index in 0.. FixedArray[Byte] {
  let out = FixedArray::make(values.length() * 4, b'\x00')
  for index in 0.. FixedArray[Float] {
  let out = FixedArray::make(values.length() * 4, to_float(0.0))
  for index in 0.. Array[Int] {
  let indices : Array[Int] = []
  for index in 0.. Array[Int] {
  let triangle_indices : Array[Int] = []
  let push_index = fn(index : Int) { triangle_indices.push(index) }
  match topology {
    TriangleList => {
      let tri_count = indices.length() / 3
      for tri in 0..
      if indices.length() >= 3 {
        for i in 0..<(indices.length() - 2) {
          if i % 2 == 0 {
            push_index(indices[i])
            push_index(indices[i + 1])
            push_index(indices[i + 2])
          } else {
            push_index(indices[i + 1])
            push_index(indices[i])
            push_index(indices[i + 2])
          }
        }
      }
    TriangleFan =>
      if indices.length() >= 3 {
        let head = indices[0]
        for i in 1..<(indices.length() - 1) {
          push_index(head)
          push_index(indices[i])
          push_index(indices[i + 1])
        }
      }
  }
  triangle_indices
}

///|
fn resolve_triangle_indices(
  mesh_handle : @render3d_types.MeshHandle,
  mesh : @render3d_types.TriangleMesh3D,
) -> Array[Int]? {
  let base_indices = mesh.indices.unwrap_or(
    default_mesh_indices(mesh.positions.length()),
  )
  if base_indices.length() == 0 {
    return None
  }
  for index in base_indices {
    if index < 0 || index >= mesh.positions.length() {
      warn_texture_issue(
        "triangles_invalid_index:\{mesh_handle_text(mesh_handle)}:\{index}",
        "triangle mesh \{mesh_handle_text(mesh_handle)} contains out-of-range index \{index}; skipping draw",
      )
      return None
    }
  }
  let triangle_indices = triangulate_triangle_indices(
    base_indices,
    mesh.topology,
  )
  if triangle_indices.length() < 3 || triangle_indices.length() % 3 != 0 {
    return None
  }
  Some(triangle_indices)
}