// Copyright 2025 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

///|
pub struct Color {
  r : Float
  g : Float
  b : Float
  a : Float
}

///|
pub fn Color::new(r : Float, g : Float, b : Float, a : Float) -> Color {
  Color::{ r, g, b, a }
}

///|
pub fn Color::white() -> Color {
  Color::new(1.0F, 1.0F, 1.0F, 1.0F)
}

///|
pub fn Color::black() -> Color {
  Color::new(0.0F, 0.0F, 0.0F, 1.0F)
}

///|

///|
// ECS component marker for codegen (`mgstudio gen`).
#ecs.component
pub struct RenderLayers {
  mask : Int
}

///|
pub fn RenderLayers::default() -> RenderLayers {
  RenderLayers::{ mask: 1 }
}

///|
pub fn RenderLayers::layer(index : Int) -> RenderLayers {
  let clamped = if index < 0 { 0 } else { index }
  RenderLayers::{ mask: 1 << clamped }
}

///|
pub fn RenderLayers::matches(self : RenderLayers, other : RenderLayers) -> Bool {
  (self.mask & other.mask) != 0
}

///|
pub struct Viewport {
  physical_position : @math.IVec2
  physical_size : @math.IVec2
}

///|
pub fn Viewport::new(
  physical_position : @math.IVec2,
  physical_size : @math.IVec2,
) -> Viewport {
  Viewport::{ physical_position, physical_size }
}

///|

///|
// ECS component marker for codegen (`mgstudio gen`).
#ecs.component
pub struct Sprite {
  image : @asset.Handle[@asset.Image]
  texture_atlas : @asset.TextureAtlas?
  color : Color
  flip_x : Bool
  flip_y : Bool
  custom_size : @math.Vec2?
  rect : @math.Rect?
}

///|
pub fn Sprite::from_image(image : @asset.Handle[@asset.Image]) -> Sprite {
  Sprite::{
    image,
    texture_atlas: None,
    color: Color::white(),
    flip_x: false,
    flip_y: false,
    custom_size: None,
    rect: None,
  }
}

///|
pub fn Sprite::from_atlas_image(
  image : @asset.Handle[@asset.Image],
  atlas : @asset.TextureAtlas,
) -> Sprite {
  Sprite::{ ..Sprite::from_image(image), texture_atlas: Some(atlas) }
}

///|
pub fn Sprite::with_texture_atlas(
  self : Sprite,
  texture_atlas : @asset.TextureAtlas?,
) -> Sprite {
  Sprite::{ ..self, texture_atlas, }
}

///|
pub fn Sprite::with_color(self : Sprite, color : Color) -> Sprite {
  Sprite::{ ..self, color, }
}

///|
pub fn Sprite::with_flip_x(self : Sprite, flip_x : Bool) -> Sprite {
  Sprite::{ ..self, flip_x, }
}

///|
pub fn Sprite::with_flip_y(self : Sprite, flip_y : Bool) -> Sprite {
  Sprite::{ ..self, flip_y, }
}

///|
pub fn Sprite::with_custom_size(
  self : Sprite,
  custom_size : @math.Vec2?,
) -> Sprite {
  Sprite::{ ..self, custom_size, }
}

///|
pub fn Sprite::with_rect(self : Sprite, rect : @math.Rect?) -> Sprite {
  Sprite::{ ..self, rect, }
}

///|
// ECS component marker for codegen (`mgstudio gen`).
#ecs.component
pub struct Anchor {
  value : @math.Vec2
}

///|
pub fn Anchor::new(value : @math.Vec2) -> Anchor {
  Anchor::{ value, }
}

///|
pub fn Anchor::as_vec(self : Anchor) -> @math.Vec2 {
  self.value
}

///|
pub fn Anchor::bottom_left() -> Anchor {
  Anchor::new(@math.Vec2::new(-0.5F, -0.5F))
}

///|
pub fn Anchor::bottom_center() -> Anchor {
  Anchor::new(@math.Vec2::new(0.0F, -0.5F))
}

///|
pub fn Anchor::bottom_right() -> Anchor {
  Anchor::new(@math.Vec2::new(0.5F, -0.5F))
}

///|
pub fn Anchor::center_left() -> Anchor {
  Anchor::new(@math.Vec2::new(-0.5F, 0.0F))
}

///|
pub fn Anchor::center() -> Anchor {
  Anchor::new(@math.Vec2::new(0.0F, 0.0F))
}

///|
pub fn Anchor::center_right() -> Anchor {
  Anchor::new(@math.Vec2::new(0.5F, 0.0F))
}

///|
pub fn Anchor::top_left() -> Anchor {
  Anchor::new(@math.Vec2::new(-0.5F, 0.5F))
}

///|
pub fn Anchor::top_center() -> Anchor {
  Anchor::new(@math.Vec2::new(0.0F, 0.5F))
}

///|
pub fn Anchor::top_right() -> Anchor {
  Anchor::new(@math.Vec2::new(0.5F, 0.5F))
}

///|
pub fn Anchor::default() -> Anchor {
  Anchor::center()
}

///|

///|
// ECS component marker for codegen (`mgstudio gen`).
#ecs.component
pub struct Camera {
  viewport : Viewport?
  order : Int
  clear_color : Color
}

///|
pub fn Camera::new(clear_color : Color) -> Camera {
  Camera::{ viewport: None, order: 0, clear_color }
}

///|
pub fn Camera::default() -> Camera {
  Camera::new(Color::new(0.5F, 0.5F, 0.5F, 1.0F))
}

///|
pub fn Camera::with_order(self : Camera, order : Int) -> Camera {
  Camera::{ ..self, order, }
}

///|
pub fn Camera::with_viewport(self : Camera, viewport : Viewport) -> Camera {
  Camera::{ ..self, viewport: Some(viewport) }
}

///|
pub fn Camera::clear_viewport(self : Camera) -> Camera {
  Camera::{ ..self, viewport: None }
}

///|
/// The projection parameters for an orthographic camera.
/// Stored inside `Projection` (mirrors Bevy's `Projection::Orthographic`).
pub struct OrthographicProjection {
  scale : Float
}

///|
pub fn OrthographicProjection::new(scale : Float) -> OrthographicProjection {
  let safe = if scale == 0.0F { 1.0F } else { scale }
  OrthographicProjection::{ scale: safe }
}

///|
pub fn OrthographicProjection::default_2d() -> OrthographicProjection {
  OrthographicProjection::new(1.0F)
}

///|
pub fn OrthographicProjection::with_scale(
  self : OrthographicProjection,
  scale : Float,
) -> OrthographicProjection {
  self |> ignore
  OrthographicProjection::new(scale)
}

///|
/// Camera projection type (mirrors Bevy's `Projection` component).
// ECS component marker for codegen (`mgstudio gen`).
#ecs.component
pub(all) enum Projection {
  Orthographic(OrthographicProjection)
}

///|
pub fn Projection::orthographic_2d() -> Projection {
  Orthographic(OrthographicProjection::default_2d())
}

///|
/// Marker component that enables 2D camera semantics (mirrors Bevy's `Camera2d`).
// ECS component marker for codegen (`mgstudio gen`).
#ecs.component
pub struct Camera2d {}

///|
pub fn Camera2d::default() -> Camera2d {
  Camera2d::{  }
}

///|

///|
// ECS component marker for codegen (`mgstudio gen`).
#ecs.component
pub(all) enum RenderTarget {
  Screen
  Image(@asset.Handle[@asset.Image])
}

///|
pub struct ColorMaterialAsset {
  color : Color
}

///|
pub fn ColorMaterialAsset::new(color : Color) -> ColorMaterialAsset {
  ColorMaterialAsset::{ color, }
}

///|
let color_material_assets : Ref[@asset.Assets[ColorMaterialAsset]] = Ref::new(
  @asset.Assets::new(),
)

///|
fn color_material_get(
  handle : @asset.Handle[ColorMaterialAsset],
) -> ColorMaterialAsset? {
  color_material_assets.val.get(handle)
}

///|
// ECS component marker for codegen (`mgstudio gen`).
#ecs.component
pub struct ColorMaterial {
  handle : @asset.Handle[ColorMaterialAsset]
}

///|
pub fn ColorMaterial::new(color : Color) -> ColorMaterial {
  let handle = color_material_assets.val.add(ColorMaterialAsset::new(color))
  ColorMaterial::{ handle, }
}

///|
pub fn ColorMaterial::color(self : ColorMaterial) -> Color {
  match color_material_get(self.handle) {
    Some(mat) => mat.color
    None => Color::white()
  }
}

///|
pub struct Capsule2d {
  radius : Float
  half_length : Float
  segments : Int
}

///|
pub fn Capsule2d::new(
  radius : Float,
  half_length : Float,
  segments : Int,
) -> Capsule2d {
  Capsule2d::{ radius, half_length, segments }
}

///|
pub fn Capsule2d::default() -> Capsule2d {
  Capsule2d::new(0.5F, 0.5F, 16)
}

///|
pub struct Rectangle {
  half_size : @math.Vec2
}

///|
pub fn Rectangle::new(width : Float, height : Float) -> Rectangle {
  Rectangle::{ half_size: @math.Vec2::new(width / 2.0F, height / 2.0F) }
}

///|
pub fn Rectangle::from_size(size : @math.Vec2) -> Rectangle {
  Rectangle::{ half_size: @math.Vec2::new(size.x / 2.0F, size.y / 2.0F) }
}

///|
pub fn Rectangle::default() -> Rectangle {
  Rectangle::{ half_size: @math.Vec2::new(0.5F, 0.5F) }
}

///|
// ECS component marker for codegen (`mgstudio gen`).
#ecs.component
pub struct Mesh2d {
  mesh : @asset.Handle[@mesh.Mesh]
}

///|
let mesh_assets : Ref[@asset.Assets[@mesh.Mesh]] = Ref::new(
  @asset.Assets::new(),
)

///|
fn mesh_asset_add(mesh : @mesh.Mesh) -> @asset.Handle[@mesh.Mesh] {
  mesh_assets.val.add(mesh)
}

///|
fn mesh_asset_get(handle : @asset.Handle[@mesh.Mesh]) -> @mesh.Mesh? {
  mesh_assets.val.get(handle)
}

///|
pub fn Mesh2d::new(mesh : @asset.Handle[@mesh.Mesh]) -> Mesh2d {
  Mesh2d::{ mesh, }
}

///|
fn rectangle_to_mesh2d_geometry(rectangle : Rectangle) -> @mesh.Mesh2dGeometry {
  let hx = rectangle.half_size.x
  let hy = rectangle.half_size.y
  let positions = [
    @math.Vec2::new(-hx, -hy),
    @math.Vec2::new(hx, -hy),
    @math.Vec2::new(hx, hy),
    @math.Vec2::new(-hx, hy),
  ]
  let uvs = [
    @math.Vec2::new(0.0F, 0.0F),
    @math.Vec2::new(1.0F, 0.0F),
    @math.Vec2::new(1.0F, 1.0F),
    @math.Vec2::new(0.0F, 1.0F),
  ]
  let indices = [0, 1, 2, 0, 2, 3]
  @mesh.Mesh2dGeometry::new(positions, uvs, Some(indices))
}

///|
fn capsule_to_mesh2d_geometry(capsule : Capsule2d) -> @mesh.Mesh2dGeometry {
  let r = capsule.radius
  let half = capsule.half_length
  let mut segments = capsule.segments
  if segments < 4 {
    segments = 4
  }
  let pi = @math.pi_half() * 2.0F
  // Build a convex polygon perimeter (CCW), then triangulate as an indexed fan.
  let perimeter : Array[@math.Vec2] = []
  // Top arc: theta 0..pi (includes endpoints).
  for i in 0..<(segments + 1) {
    let t = Float::from_double(i.to_double()) /
      Float::from_double(segments.to_double())
    let theta = t * pi
    // theta in [0, pi]
    let x = @math.cos(theta) * r
    let y = @math.sin(theta) * r + half
    perimeter.push(@math.Vec2::new(x, y))
  }
  // Left side down to bottom arc start.
  perimeter.push(@math.Vec2::new(-r, -half))
  // Bottom arc: theta pi..2pi (skip first point to avoid dup).
  for i in 1..<(segments + 1) {
    let t = Float::from_double(i.to_double()) /
      Float::from_double(segments.to_double())
    let theta = pi + t * pi
    let x = @math.cos(theta) * r
    let y = @math.sin(theta) * r - half
    perimeter.push(@math.Vec2::new(x, y))
  }
  // Right side back to start is implicit.
  let positions : Array[@math.Vec2] = []
  positions.push(@math.Vec2::new(0.0F, 0.0F)) // center
  for p in perimeter {
    positions.push(p)
  }
  // Simple UVs based on bounds.
  let denom_x = if r == 0.0F { 1.0F } else { 2.0F * r }
  let denom_y = if half + r == 0.0F { 1.0F } else { 2.0F * (half + r) }
  let uvs : Array[@math.Vec2] = []
  for p in positions {
    let u = p.x / denom_x + 0.5F
    let v = p.y / denom_y + 0.5F
    uvs.push(@math.Vec2::new(u, v))
  }
  let indices : Array[Int] = []
  let n = perimeter.length()
  for i in 0.. Mesh2d {
  Mesh2d::from_geometry(capsule_to_mesh2d_geometry(capsule))
}

///|
pub fn Mesh2d::from_rectangle(rectangle : Rectangle) -> Mesh2d {
  Mesh2d::from_geometry(rectangle_to_mesh2d_geometry(rectangle))
}

///|
pub fn Mesh2d::from_geometry(geometry : @mesh.Mesh2dGeometry) -> Mesh2d {
  let mesh = @mesh.Mesh::from_2d_geometry(geometry)
  Mesh2d::new(mesh_asset_add(mesh))
}

///|
// ECS component marker for codegen (`mgstudio gen`).
#ecs.component
pub struct GpuMesh2d {
  mesh_id : Int
}

///|
pub fn GpuMesh2d::new(mesh_id : Int) -> GpuMesh2d {
  GpuMesh2d::{ mesh_id, }
}

///|
pub struct SpriteBundle {
  sprite : Sprite
  anchor : Anchor
  transform : @math.Transform
  layers : RenderLayers
  visibility : @visibility.Visibility
  inherited_visibility : @visibility.InheritedVisibility
  view_visibility : @visibility.ViewVisibility
}

///|
pub fn SpriteBundle::new(
  sprite : Sprite,
  transform : @math.Transform,
) -> SpriteBundle {
  SpriteBundle::{
    sprite,
    anchor: Anchor::default(),
    transform,
    layers: RenderLayers::default(),
    visibility: @visibility.Visibility::Inherited,
    inherited_visibility: @visibility.InheritedVisibility::new(true),
    view_visibility: @visibility.ViewVisibility::new(true),
  }
}

///|
pub fn SpriteBundle::new_with_layers(
  sprite : Sprite,
  transform : @math.Transform,
  layers : RenderLayers,
) -> SpriteBundle {
  SpriteBundle::{
    sprite,
    anchor: Anchor::default(),
    transform,
    layers,
    visibility: @visibility.Visibility::Inherited,
    inherited_visibility: @visibility.InheritedVisibility::new(true),
    view_visibility: @visibility.ViewVisibility::new(true),
  }
}

///|
pub struct Camera2dBundle {
  camera : Camera
  projection : Projection
  camera2d : Camera2d
  transform : @math.Transform
  target : RenderTarget
  layers : RenderLayers
}

///|
pub fn Camera2dBundle::new(
  camera : Camera,
  transform : @math.Transform,
) -> Camera2dBundle {
  Camera2dBundle::{
    camera,
    projection: Projection::orthographic_2d(),
    camera2d: Camera2d::default(),
    transform,
    target: RenderTarget::Screen,
    layers: RenderLayers::default(),
  }
}

///|
pub fn Camera2dBundle::new_with_projection(
  camera : Camera,
  projection : Projection,
  transform : @math.Transform,
) -> Camera2dBundle {
  Camera2dBundle::{
    camera,
    projection,
    camera2d: Camera2d::default(),
    transform,
    target: RenderTarget::Screen,
    layers: RenderLayers::default(),
  }
}

///|
pub fn Camera2dBundle::new_with_target(
  camera : Camera,
  transform : @math.Transform,
  target : RenderTarget,
) -> Camera2dBundle {
  Camera2dBundle::{
    camera,
    projection: Projection::orthographic_2d(),
    camera2d: Camera2d::default(),
    transform,
    target,
    layers: RenderLayers::default(),
  }
}

///|
pub fn Camera2dBundle::new_with_layers(
  camera : Camera,
  transform : @math.Transform,
  target : RenderTarget,
  layers : RenderLayers,
) -> Camera2dBundle {
  Camera2dBundle::{
    camera,
    projection: Projection::orthographic_2d(),
    camera2d: Camera2d::default(),
    transform,
    target,
    layers,
  }
}

///|
pub struct Mesh2dBundle {
  mesh : Mesh2d
  material : ColorMaterial
  transform : @math.Transform
  layers : RenderLayers
  visibility : @visibility.Visibility
  inherited_visibility : @visibility.InheritedVisibility
  view_visibility : @visibility.ViewVisibility
}

///|
pub fn Mesh2dBundle::new(
  mesh : Mesh2d,
  material : ColorMaterial,
  transform : @math.Transform,
) -> Mesh2dBundle {
  Mesh2dBundle::{
    mesh,
    material,
    transform,
    layers: RenderLayers::default(),
    visibility: @visibility.Visibility::Inherited,
    inherited_visibility: @visibility.InheritedVisibility::new(true),
    view_visibility: @visibility.ViewVisibility::new(true),
  }
}

///|
pub fn Mesh2dBundle::new_with_layers(
  mesh : Mesh2d,
  material : ColorMaterial,
  transform : @math.Transform,
  layers : RenderLayers,
) -> Mesh2dBundle {
  Mesh2dBundle::{
    mesh,
    material,
    transform,
    layers,
    visibility: @visibility.Visibility::Inherited,
    inherited_visibility: @visibility.InheritedVisibility::new(true),
    view_visibility: @visibility.ViewVisibility::new(true),
  }
}

///|
priv struct SpriteInstance {
  entity : @core.Entity
  transform : @math.Transform
  anchor_offset : @math.Vec2
  draw_scale : @math.Vec2
  color : Color
  texture : @asset.Handle[@asset.Image]
  layers : RenderLayers
  uv_min : @math.Vec2
  uv_max : @math.Vec2
}

///|
priv struct CameraInstance {
  entity : @core.Entity
  camera : Camera
  projection : OrthographicProjection
  transform : @math.Transform
  target : RenderTarget
  layers : RenderLayers
  computed : CameraComputed
}

///|
priv struct CameraComputed {
  target_size : @math.IVec2
  scale_factor : Float
}

///|
priv struct MeshInstance {
  entity : @core.Entity
  mesh_id : Int
  transform : @math.Transform
  color : Color
  layers : RenderLayers
}

///|
priv struct RenderTargetInfo {
  id : Int
  width : Int
  height : Int
}

///|

///|
// ECS resource marker for codegen (`mgstudio gen`).
#ecs.resource
struct RenderState {
  mut draw_enabled : Bool
  mut primary_camera : @core.Entity
  sprites : Array[SpriteInstance]
  extra_sprites : Array[SpriteInstance]
  cameras : Array[CameraInstance]
  meshes : Array[MeshInstance]
  render_targets : Array[RenderTargetInfo]
}

///|
fn RenderState::new() -> RenderState {
  RenderState::{
    draw_enabled: false,
    primary_camera: @core.Entity::placeholder(),
    sprites: [],
    extra_sprites: [],
    cameras: [],
    meshes: [],
    render_targets: [],
  }
}

///|
fn[W : HasRes_RenderState] render_state_ref(world : W) -> Ref[RenderState]? {
  HasRes_RenderState::get_render_state_resource(world).get_ref()
}

///|
fn[W : HasRes_RenderState + @core.ChangeTickWorld] render_state_ref_mut(
  world : W,
) -> Ref[RenderState]? {
  let tick = @core.ChangeTickWorld::read_change_tick(world)
  HasRes_RenderState::get_render_state_resource(world).get_ref_mut(tick)
}

///|
fn[W : HasRes_RenderState + @core.ChangeTickWorld] init_render_state_system(
  world : W,
) -> Unit {
  let tick = @core.ChangeTickWorld::read_change_tick(world)
  let res = HasRes_RenderState::get_render_state_resource(world)
  if !res.is_present() {
    res.insert(RenderState::new(), tick)
    gizmos_reset()
  }
}

///|
fn default_uv_min() -> @math.Vec2 {
  @math.Vec2::new(0.0F, 0.0F)
}

///|
fn default_uv_max() -> @math.Vec2 {
  @math.Vec2::new(1.0F, 1.0F)
}

///|
priv struct SpriteUvInfo {
  uv_min : @math.Vec2
  uv_max : @math.Vec2
}

///|
fn sprite_vec2_add(a : @math.Vec2, b : @math.Vec2) -> @math.Vec2 {
  @math.Vec2::new(a.x + b.x, a.y + b.y)
}

///|
fn sprite_vec2_mul(a : @math.Vec2, b : @math.Vec2) -> @math.Vec2 {
  @math.Vec2::new(a.x * b.x, a.y * b.y)
}

///|
fn sprite_draw_scale(
  sprite : Sprite,
  transform_scale : @math.Vec2,
) -> @math.Vec2 {
  let base = match sprite.custom_size {
    Some(v) => v
    None => @math.Vec2::new(1.0F, 1.0F)
  }
  sprite_vec2_mul(transform_scale, base)
}

///|
fn sprite_anchor_offset(
  anchor : Anchor,
  rotation : Float,
  draw_scale : @math.Vec2,
) -> @math.Vec2 {
  let a = anchor.as_vec()
  let offset_local = @math.Vec2::new(-a.x * draw_scale.x, -a.y * draw_scale.y)
  let cosv = @math.cos(rotation)
  let sinv = @math.sin(rotation)
  @math.Vec2::new(
    offset_local.x * cosv - offset_local.y * sinv,
    offset_local.x * sinv + offset_local.y * cosv,
  )
}

///|
fn swap_float_pair(a : Float, b : Float) -> (Float, Float) {
  (b, a)
}

///|
fn sprite_uv_from_sprite(sprite : Sprite) -> SpriteUvInfo? {
  let texture_id = sprite.image.id()
  let mut image_width = @asset.host_asset_texture_width(texture_id)
  let mut image_height = @asset.host_asset_texture_height(texture_id)
  if image_width <= 0 || image_height <= 0 {
    // Avoid division by zero for not-yet-loaded textures.
    image_width = 1
    image_height = 1
  }
  let image_rect = @math.Rect::new(
    @math.Vec2::new(0.0F, 0.0F),
    @math.Vec2::new(
      Float::from_double(image_width.to_double()),
      Float::from_double(image_height.to_double()),
    ),
  )
  let atlas_rect = match sprite.texture_atlas {
    Some(atlas) =>
      match @asset.asset_get_texture_atlas_layout(atlas.layout) {
        Some(layout_value) =>
          if atlas.index >= 0 && atlas.index < layout_value.textures.length() {
            Some(layout_value.textures[atlas.index].as_rect())
          } else {
            None
          }
        None => None
      }
    None => None
  }
  let texture_rect = match (atlas_rect, sprite.rect) {
    (None, None) => image_rect
    (None, Some(r)) => r
    (Some(ar), None) => ar
    (Some(ar), Some(r)) => {
      let min = sprite_vec2_add(r.min, ar.min)
      let max = sprite_vec2_add(r.max, ar.min)
      @math.Rect::new(min, max)
    }
  }
  let denom_x = Float::from_double(image_width.to_double())
  let denom_y = Float::from_double(image_height.to_double())
  if denom_x <= 0.0F || denom_y <= 0.0F {
    return None
  }
  let mut uv_min = @math.Vec2::new(
    texture_rect.min.x / denom_x,
    texture_rect.min.y / denom_y,
  )
  let mut uv_max = @math.Vec2::new(
    texture_rect.max.x / denom_x,
    texture_rect.max.y / denom_y,
  )
  if sprite.flip_x {
    let pair = swap_float_pair(uv_min.x, uv_max.x)
    uv_min = @math.Vec2::new(pair.0, uv_min.y)
    uv_max = @math.Vec2::new(pair.1, uv_max.y)
  }
  if sprite.flip_y {
    let pair = swap_float_pair(uv_min.y, uv_max.y)
    uv_min = @math.Vec2::new(uv_min.x, pair.0)
    uv_max = @math.Vec2::new(uv_max.x, pair.1)
  }
  Some(SpriteUvInfo::{ uv_min, uv_max })
}

///|
pub fn[W : HasRes_RenderState + @core.ChangeTickWorld] render2d_create_render_target(
  world : W,
  width : Int,
  height : Int,
  nearest : Bool,
) -> @asset.Handle[@asset.Image] {
  let id = @window.host_gpu_create_render_target(width, height, nearest)
  let rs = render_state_ref_mut(world).unwrap()
  rs.val.render_targets.push(RenderTargetInfo::{ id, width, height })
  @asset.Handle::new(id)
}

///|
pub fn[
  W : @core.SpawnWorld + @core.ChangeTickWorld + Has_Sprite + Has_Anchor + Has_RenderLayers + @math.Has_Transform + @visibility.Has_Visibility + @visibility.Has_InheritedVisibility + @visibility.Has_ViewVisibility,
] SpriteBundle::spawn(
  self : SpriteBundle,
  world : W,
) -> @core.Entity {
  let tick = @core.ChangeTickWorld::read_change_tick(world)
  let e = @core.SpawnWorld::spawn_empty(world)
  Has_Sprite::get_sprite_store(world).insert(e, self.sprite, tick)
  Has_Anchor::get_anchor_store(world).insert(e, self.anchor, tick)
  Has_RenderLayers::get_render_layers_store(world).insert(e, self.layers, tick)
  @visibility.Has_Visibility::get_visibility_store(world).insert(
    e,
    self.visibility,
    tick,
  )
  @visibility.Has_InheritedVisibility::get_inherited_visibility_store(world).insert(
    e,
    self.inherited_visibility,
    tick,
  )
  @visibility.Has_ViewVisibility::get_view_visibility_store(world).insert(
    e,
    self.view_visibility,
    tick,
  )
  @math.Has_Transform::get_transform_store(world).insert(
    e,
    self.transform,
    tick,
  )
  e
}

///|
pub fn[
  W : @core.SpawnWorld + @core.ChangeTickWorld + Has_Camera2d + Has_Camera + Has_Projection + Has_RenderTarget + Has_RenderLayers + @math.Has_Transform,
] Camera2dBundle::spawn(
  self : Camera2dBundle,
  world : W,
) -> @core.Entity {
  let tick = @core.ChangeTickWorld::read_change_tick(world)
  let e = @core.SpawnWorld::spawn_empty(world)
  Has_Camera2d::get_camera2d_store(world).insert(e, self.camera2d, tick)
  Has_Camera::get_camera_store(world).insert(e, self.camera, tick)
  Has_Projection::get_projection_store(world).insert(e, self.projection, tick)
  Has_RenderTarget::get_render_target_store(world).insert(e, self.target, tick)
  Has_RenderLayers::get_render_layers_store(world).insert(e, self.layers, tick)
  @math.Has_Transform::get_transform_store(world).insert(
    e,
    self.transform,
    tick,
  )
  e
}

///|
pub fn[
  W : @core.SpawnWorld + @core.ChangeTickWorld + Has_Mesh2d + Has_ColorMaterial + Has_GpuMesh2d + Has_RenderLayers + @math.Has_Transform + @visibility.Has_Visibility + @visibility.Has_InheritedVisibility + @visibility.Has_ViewVisibility,
] Mesh2dBundle::spawn(
  self : Mesh2dBundle,
  world : W,
) -> @core.Entity {
  let tick = @core.ChangeTickWorld::read_change_tick(world)
  let e = @core.SpawnWorld::spawn_empty(world)
  Has_Mesh2d::get_mesh2d_store(world).insert(e, self.mesh, tick)
  Has_ColorMaterial::get_color_material_store(world).insert(
    e,
    self.material,
    tick,
  )
  Has_RenderLayers::get_render_layers_store(world).insert(e, self.layers, tick)
  @visibility.Has_Visibility::get_visibility_store(world).insert(
    e,
    self.visibility,
    tick,
  )
  @visibility.Has_InheritedVisibility::get_inherited_visibility_store(world).insert(
    e,
    self.inherited_visibility,
    tick,
  )
  @visibility.Has_ViewVisibility::get_view_visibility_store(world).insert(
    e,
    self.view_visibility,
    tick,
  )
  @math.Has_Transform::get_transform_store(world).insert(
    e,
    self.transform,
    tick,
  )
  let mesh_id = match mesh_asset_get(self.mesh.mesh) {
    Some(mesh_asset) =>
      match mesh_asset.geometry2d.encode_triangle_positions_csv() {
        Some(vertices_csv) =>
          @window.host_gpu_create_mesh_triangles(vertices_csv)
        None => {
          let geometry = mesh_asset.geometry2d
          let message = match geometry.validate() {
            Some(err) => "Mesh2dGeometry validation failed: " + err.to_string()
            None => "Mesh2dGeometry flatten failed"
          }
          @window.host_debug_string(message)
          0
        }
      }
    None => {
      @window.host_debug_string("Mesh2d spawn failed: mesh handle not found")
      0
    }
  }
  Has_GpuMesh2d::get_gpu_mesh2d_store(world).insert(
    e,
    GpuMesh2d::new(mesh_id),
    tick,
  )
  e
}

///|
pub fn[W : HasRes_RenderState + @core.ChangeTickWorld] render2d_clear_overlay_sprites(
  world : W,
) -> Unit {
  render_state_ref_mut(world).map(fn(rs) { rs.val.extra_sprites.clear() })
  |> ignore
}

///|
pub fn[W : HasRes_RenderState + @core.ChangeTickWorld] render2d_push_sprite_uv(
  world : W,
  texture : @asset.Handle[@asset.Image],
  transform : @math.Transform,
  uv_min : @math.Vec2,
  uv_max : @math.Vec2,
  color : Color,
  layers : RenderLayers,
) -> Unit {
  render_state_ref_mut(world).map(fn(rs) {
    rs.val.extra_sprites.push(SpriteInstance::{
      entity: @core.Entity::placeholder(),
      transform,
      anchor_offset: @math.Vec2::new(0.0F, 0.0F),
      draw_scale: @math.Vec2::new(transform.scale.x, transform.scale.y),
      color,
      texture,
      layers,
      uv_min,
      uv_max,
    })
  })
  |> ignore
}

///|
fn render2d_find_render_target_size(
  state : RenderState,
  id : Int,
) -> RenderTargetInfo? {
  for target in state.render_targets {
    if target.id == id {
      return Some(target)
    }
  }
  None
}

///|
priv struct RenderTargetComputed {
  size : @math.IVec2
  scale_factor : Float
}

///|
fn render2d_target_info(
  state : RenderState,
  target : RenderTarget,
) -> RenderTargetComputed? {
  let mut width = @window.window_width()
  let mut height = @window.window_height()
  let mut scale_factor = @window.window_scale_factor()
  if target is Image(handle) {
    let target_id = handle.id()
    if render2d_find_render_target_size(state, target_id) is Some(info) {
      width = info.width
      height = info.height
      scale_factor = 1.0F
    } else {
      return None
    }
  }
  if width <= 0 || height <= 0 {
    return None
  }
  if scale_factor <= 0.0F {
    scale_factor = 1.0F
  }
  Some(RenderTargetComputed::{
    size: @math.IVec2::new(width, height),
    scale_factor,
  })
}

///|
fn render2d_clamp_viewport(
  viewport : Viewport,
  target_size : @math.IVec2,
) -> Viewport {
  let mut position = viewport.physical_position
  let mut size = viewport.physical_size
  if position.x < 0 || position.y < 0 {
    position = @math.IVec2::new(
      if position.x < 0 {
        0
      } else {
        position.x
      },
      if position.y < 0 {
        0
      } else {
        position.y
      },
    )
  }
  if size.x < 0 || size.y < 0 {
    size = @math.IVec2::new(
      if size.x < 0 {
        0
      } else {
        size.x
      },
      if size.y < 0 {
        0
      } else {
        size.y
      },
    )
  }
  let max_position = target_size.saturating_sub(size)
  position = position.min(max_position)
  let max_size = target_size.saturating_sub(position)
  size = size.min(max_size)
  Viewport::new(position, size)
}

///|
pub fn[W] render2d_camera_system(_world : W) -> Unit {
  // Legacy system retained for API compatibility; camera extraction computes
  // derived values each frame in `render_extract_system`.
}

///|
fn render2d_sorted_camera_indices(
  cameras : Array[CameraInstance],
) -> Array[Int] {
  let count = cameras.length()
  let indices : Array[Int] = []
  for i in 0.. Unit {
  guard render_state_ref_mut(world) is Some(rs) else { return }
  rs.val.draw_enabled = true
  rs.val.primary_camera = @core.Entity::placeholder()
  rs.val.sprites.clear()
  rs.val.cameras.clear()
  rs.val.meshes.clear()
  let camera2d_store = Has_Camera2d::get_camera2d_store(world)
  let camera_store = Has_Camera::get_camera_store(world)
  let projection_store = Has_Projection::get_projection_store(world)
  let target_store = Has_RenderTarget::get_render_target_store(world)
  let layers_store = Has_RenderLayers::get_render_layers_store(world)
  let view_visibility_store = @visibility.Has_ViewVisibility::get_view_visibility_store(
    world,
  )
  let transform_store = @math.Has_Transform::get_transform_store(world)
  let global_store = @transform.Has_GlobalTransform::get_global_transform_store(
    world,
  )
  camera2d_store.for_each(fn(entity, _marker) {
    let target = match target_store.get(entity) {
      Some(t) => t
      None => RenderTarget::Screen
    }
    let layers = match layers_store.get(entity) {
      Some(v) => v
      None => RenderLayers::default()
    }
    let camera = match camera_store.get(entity) {
      Some(v) => v
      None => Camera::default()
    }
    let projection = match projection_store.get(entity) {
      Some(Orthographic(p)) => p
      None => OrthographicProjection::default_2d()
    }
    let transform = match global_store.get(entity) {
      Some(gt) => gt.to_transform_approx()
      None =>
        match transform_store.get(entity) {
          Some(t) => t
          None => @math.Transform::identity()
        }
    }
    let mut computed = CameraComputed::{
      target_size: @math.IVec2::new(0, 0),
      scale_factor: 1.0F,
    }
    let mut camera_value = camera
    if render2d_target_info(rs.val, target) is Some(info) {
      computed = CameraComputed::{
        target_size: info.size,
        scale_factor: info.scale_factor,
      }
      if camera_value.viewport is Some(viewport) {
        let clamped = render2d_clamp_viewport(viewport, info.size)
        camera_value = Camera::{
          viewport: Some(clamped),
          order: camera_value.order,
          clear_color: camera_value.clear_color,
        }
      }
    }
    rs.val.cameras.push(CameraInstance::{
      entity,
      camera: camera_value,
      projection,
      transform,
      target,
      layers,
      computed,
    })
  })
  if rs.val.cameras.length() > 0 {
    let order = render2d_sorted_camera_indices(rs.val.cameras)
    if order.length() > 0 {
      rs.val.primary_camera = rs.val.cameras[order[0]].entity
    }
  }
  let sprite_store = Has_Sprite::get_sprite_store(world)
  let anchor_store = Has_Anchor::get_anchor_store(world)
  sprite_store.for_each(fn(entity, sprite) {
    if view_visibility_store.get(entity) is Some(vv) && !vv.is_visible() {
      return
    }
    let anchor = match anchor_store.get(entity) {
      Some(a) => a
      None => Anchor::default()
    }
    let layers = match layers_store.get(entity) {
      Some(v) => v
      None => RenderLayers::default()
    }
    let transform = match global_store.get(entity) {
      Some(gt) => gt.to_transform_approx()
      None =>
        match transform_store.get(entity) {
          Some(t) => t
          None => @math.Transform::identity()
        }
    }
    let mut uv_min = default_uv_min()
    let mut uv_max = default_uv_max()
    if sprite_uv_from_sprite(sprite) is Some(info) {
      uv_min = info.uv_min
      uv_max = info.uv_max
    }
    let draw_scale = sprite_draw_scale(
      sprite,
      @math.Vec2::new(transform.scale.x, transform.scale.y),
    )
    let anchor_offset = sprite_anchor_offset(
      anchor,
      transform.rotation_z(),
      draw_scale,
    )
    rs.val.sprites.push(SpriteInstance::{
      entity,
      transform,
      anchor_offset,
      draw_scale,
      color: sprite.color,
      texture: sprite.image,
      layers,
      uv_min,
      uv_max,
    })
  })
  let mesh_store = Has_Mesh2d::get_mesh2d_store(world)
  let gpu_mesh_store = Has_GpuMesh2d::get_gpu_mesh2d_store(world)
  let material_store = Has_ColorMaterial::get_color_material_store(world)
  mesh_store.for_each(fn(entity, _mesh) {
    if view_visibility_store.get(entity) is Some(vv) && !vv.is_visible() {
      return
    }
    guard gpu_mesh_store.get(entity) is Some(gpu) else { return }
    guard material_store.get(entity) is Some(mat) else { return }
    let layers = match layers_store.get(entity) {
      Some(v) => v
      None => RenderLayers::default()
    }
    let transform = match global_store.get(entity) {
      Some(gt) => gt.to_transform_approx()
      None =>
        match transform_store.get(entity) {
          Some(t) => t
          None => @math.Transform::identity()
        }
    }
    rs.val.meshes.push(MeshInstance::{
      entity,
      mesh_id: gpu.mesh_id,
      transform,
      color: mat.color(),
      layers,
    })
  })
}

///|
pub fn[W] render_prepare_system(_world : W) -> Unit {

}

///|
pub fn[W] render_queue_system(_world : W) -> Unit {

}

///|
fn cmp_int(a : Int, b : Int) -> Int {
  if a < b {
    -1
  } else if a > b {
    1
  } else {
    0
  }
}

///|
fn cmp_float(a : Float, b : Float) -> Int {
  if a < b {
    -1
  } else if a > b {
    1
  } else {
    0
  }
}

///|
priv enum DrawKind {
  SpriteMain(Int)
  SpriteExtra(Int)
  Mesh(Int)
}

///|
priv struct DrawItem {
  kind : DrawKind
  z : Float
  // 0 = opaque, 1 = transparent (opaque first for stable blending).
  phase : Int
  // Rough pipeline grouping: 0 = sprite, 1 = mesh.
  pipeline : Int
  // Batch key for opaque items (e.g. texture id / mesh id).
  batch : Int
  // Stable tie-break keys.
  entity_id : Int
  entity_generation : Int
  tie : Int
}

///|
fn draw_items_for_layers(
  sprites : Array[SpriteInstance],
  extra_sprites : Array[SpriteInstance],
  meshes : Array[MeshInstance],
  layers : RenderLayers,
) -> Array[DrawItem] {
  let items : Array[DrawItem] = []
  for i in 0..= 1.0F { 0 } else { 1 }
    items.push(DrawItem::{
      kind: SpriteMain(i),
      z: sprite.transform.translation.z,
      phase,
      pipeline: 0,
      batch: sprite.texture.id(),
      entity_id: sprite.entity.id,
      entity_generation: sprite.entity.generation,
      tie: i,
    })
  }
  for i in 0..= 1.0F { 0 } else { 1 }
    items.push(DrawItem::{
      kind: SpriteExtra(i),
      z: sprite.transform.translation.z,
      phase,
      pipeline: 0,
      batch: sprite.texture.id(),
      entity_id: sprite.entity.id,
      entity_generation: sprite.entity.generation,
      // Ensure extra-sprites tie-break after main sprites with the same entity key.
      tie: 1_000_000 + i,
    })
  }
  for i in 0..= 1.0F { 0 } else { 1 }
    items.push(DrawItem::{
      kind: Mesh(i),
      z: mesh.transform.translation.z,
      phase,
      pipeline: 1,
      batch: mesh.mesh_id,
      entity_id: mesh.entity.id,
      entity_generation: mesh.entity.generation,
      tie: i,
    })
  }
  items.sort_by(fn(a, b) {
    let cz = cmp_float(a.z, b.z)
    if cz != 0 {
      return cz
    }
    let cp = cmp_int(a.phase, b.phase)
    if cp != 0 {
      return cp
    }
    let cpipe = cmp_int(a.pipeline, b.pipeline)
    if cpipe != 0 {
      return cpipe
    }
    if a.phase == 0 {
      let cb = cmp_int(a.batch, b.batch)
      if cb != 0 {
        return cb
      }
    }
    let ce = cmp_int(a.entity_id, b.entity_id)
    if ce != 0 {
      return ce
    }
    let cg = cmp_int(a.entity_generation, b.entity_generation)
    if cg != 0 {
      return cg
    }
    cmp_int(a.tie, b.tie)
  })
  items
}

///|
test "render2d: unified draw items order sprites and meshes by z" {
  let layers = RenderLayers::default()
  let sprites : Array[SpriteInstance] = [
    SpriteInstance::{
      entity: @core.Entity::new(2, 0),
      transform: @math.Transform::from_xy_rotation_scale(
        @math.Vec2::new(0.0F, 0.0F),
        0.0F,
        @math.Vec2::new(1.0F, 1.0F),
        z=0.0F,
      ),
      anchor_offset: @math.Vec2::new(0.0F, 0.0F),
      draw_scale: @math.Vec2::new(1.0F, 1.0F),
      color: Color::new(1.0F, 1.0F, 1.0F, 1.0F),
      texture: @asset.Handle::new(2),
      layers,
      uv_min: @math.Vec2::new(0.0F, 0.0F),
      uv_max: @math.Vec2::new(1.0F, 1.0F),
    },
  ]
  let extra : Array[SpriteInstance] = []
  let meshes : Array[MeshInstance] = [
    MeshInstance::{
      entity: @core.Entity::new(1, 0),
      mesh_id: 7,
      transform: @math.Transform::from_xy_rotation_scale(
        @math.Vec2::new(0.0F, 0.0F),
        0.0F,
        @math.Vec2::new(1.0F, 1.0F),
        z=-1.0F,
      ),
      color: Color::white(),
      layers,
    },
  ]
  let items = draw_items_for_layers(sprites, extra, meshes, layers)
  let ok = items.length() == 2 &&
    items[0].kind is Mesh(0) &&
    items[1].kind is SpriteMain(0)
  inspect(ok, content="true")
}

///|
pub fn[W : HasRes_RenderState + @core.ChangeTickWorld] render_execute_system(
  world : W,
) -> Unit {
  guard render_state_ref_mut(world) is Some(rs) else { return }
  if !rs.val.draw_enabled {
    return
  }
  rs.val.draw_enabled = false
  gizmos_prepare()
  let cameras = rs.val.cameras
  if cameras.length() == 0 {
    gizmos_clear()
    return
  }
  let sprites = rs.val.sprites
  let extra_sprites = rs.val.extra_sprites
  let meshes = rs.val.meshes
  for index in render2d_sorted_camera_indices(cameras) {
    let camera = cameras[index]
    let mut target_id = -1
    let target_size = camera.computed.target_size
    let target_width = target_size.x
    let target_height = target_size.y
    if camera.target is Image(handle) {
      target_id = handle.id()
    }
    if target_width <= 0 || target_height <= 0 {
      continue
    }
    let mut scale_factor = camera.computed.scale_factor
    if scale_factor <= 0.0F {
      scale_factor = 1.0F
    }
    let mut viewport_x = 0
    let mut viewport_y = 0
    let mut viewport_width = target_width
    let mut viewport_height = target_height
    let mut view_width = target_width
    let mut view_height = target_height
    if camera.camera.viewport is Some(viewport) {
      viewport_x = if viewport.physical_position.x < 0 {
        0
      } else {
        viewport.physical_position.x
      }
      viewport_y = if viewport.physical_position.y < 0 {
        0
      } else {
        viewport.physical_position.y
      }
      viewport_width = if viewport.physical_size.x < 0 {
        0
      } else {
        viewport.physical_size.x
      }
      viewport_height = if viewport.physical_size.y < 0 {
        0
      } else {
        viewport.physical_size.y
      }
      let max_width = if target_width - viewport_x < 0 {
        0
      } else {
        target_width - viewport_x
      }
      let max_height = if target_height - viewport_y < 0 {
        0
      } else {
        target_height - viewport_y
      }
      if viewport_width > max_width {
        viewport_width = max_width
      }
      if viewport_height > max_height {
        viewport_height = max_height
      }
      if viewport_width <= 0 || viewport_height <= 0 {
        continue
      }
      view_width = viewport_width
      view_height = viewport_height
    }
    let view_width_f = Float::from_double(view_width.to_double()) / scale_factor
    let view_height_f = Float::from_double(view_height.to_double()) /
      scale_factor
    let view_width_logical = view_width_f.to_int()
    let view_height_logical = view_height_f.to_int()
    if view_width_logical <= 0 || view_height_logical <= 0 {
      continue
    }
    let clear = camera.camera.clear_color
    @window.host_gpu_begin_pass(
      target_id,
      view_width_logical,
      view_height_logical,
      clear.r,
      clear.g,
      clear.b,
      clear.a,
      camera.transform.translation.x,
      camera.transform.translation.y,
      camera.transform.rotation_z(),
      camera.projection.scale,
      viewport_x,
      viewport_y,
      viewport_width,
      viewport_height,
    )
    let draw_items = draw_items_for_layers(
      sprites,
      extra_sprites,
      meshes,
      camera.layers,
    )
    for item in draw_items {
      match item.kind {
        SpriteMain(i) => {
          let sprite = sprites[i]
          let transform = sprite.transform
          let pos = @math.Vec2::new(
            transform.translation.x + sprite.anchor_offset.x,
            transform.translation.y + sprite.anchor_offset.y,
          )
          @window.host_gpu_draw_sprite_uv(
            sprite.texture.id(),
            pos.x,
            pos.y,
            transform.rotation_z(),
            sprite.draw_scale.x,
            sprite.draw_scale.y,
            sprite.color.r,
            sprite.color.g,
            sprite.color.b,
            sprite.color.a,
            sprite.uv_min.x,
            sprite.uv_min.y,
            sprite.uv_max.x,
            sprite.uv_max.y,
          )
        }
        SpriteExtra(i) => {
          let sprite = extra_sprites[i]
          let transform = sprite.transform
          let pos = @math.Vec2::new(
            transform.translation.x + sprite.anchor_offset.x,
            transform.translation.y + sprite.anchor_offset.y,
          )
          @window.host_gpu_draw_sprite_uv(
            sprite.texture.id(),
            pos.x,
            pos.y,
            transform.rotation_z(),
            sprite.draw_scale.x,
            sprite.draw_scale.y,
            sprite.color.r,
            sprite.color.g,
            sprite.color.b,
            sprite.color.a,
            sprite.uv_min.x,
            sprite.uv_min.y,
            sprite.uv_max.x,
            sprite.uv_max.y,
          )
        }
        Mesh(i) => {
          let mesh = meshes[i]
          let transform = mesh.transform
          @window.host_gpu_draw_mesh(
            mesh.mesh_id,
            transform.translation.x,
            transform.translation.y,
            transform.rotation_z(),
            transform.scale.x,
            transform.scale.y,
            mesh.color.r,
            mesh.color.g,
            mesh.color.b,
            mesh.color.a,
          )
        }
      }
    }
    gizmos_draw_lines(camera.layers)
    @window.host_gpu_end_pass()
  }
  gizmos_clear()
  rs.val.extra_sprites.clear()
}

///|
pub fn[W : HasRes_RenderState] render2d_viewport_to_world_2d(
  world : W,
  screen : @math.Vec2,
) -> @math.Vec2? {
  guard render_state_ref(world) is Some(rs) else { return None }
  let id = rs.val.primary_camera
  if id.is_placeholder() {
    return None
  }
  let mut found : CameraInstance? = None
  for camera in rs.val.cameras {
    if camera.entity.id == id.id && camera.entity.generation == id.generation {
      found = Some(camera)
      break
    }
  }
  guard found is Some(cam) else { return None }
  render2d_camera_viewport_to_world_2d(
    screen,
    cam.camera,
    cam.projection,
    cam.transform,
    cam.computed.target_size,
    cam.computed.scale_factor,
  )
}

///|
pub fn[W : HasRes_RenderState] render2d_world_to_viewport(
  world_ : W,
  world : @math.Vec2,
) -> @math.Vec2? {
  guard render_state_ref(world_) is Some(rs) else { return None }
  let id = rs.val.primary_camera
  if id.is_placeholder() {
    return None
  }
  let mut found : CameraInstance? = None
  for camera in rs.val.cameras {
    if camera.entity.id == id.id && camera.entity.generation == id.generation {
      found = Some(camera)
      break
    }
  }
  guard found is Some(cam) else { return None }
  render2d_camera_world_to_viewport_2d(
    world,
    cam.camera,
    cam.projection,
    cam.transform,
    cam.computed.target_size,
    cam.computed.scale_factor,
  )
}

///|
/// Shared math for camera viewport conversions. `screen` is in logical pixels.
pub fn render2d_camera_viewport_to_world_2d(
  screen : @math.Vec2,
  camera : Camera,
  projection : OrthographicProjection,
  camera_transform : @math.Transform,
  target_size : @math.IVec2,
  scale_factor : Float,
) -> @math.Vec2? {
  let width = target_size.x
  let height = target_size.y
  if width <= 0 || height <= 0 {
    return None
  }
  let mut safe_scale_factor = scale_factor
  if safe_scale_factor <= 0.0F {
    safe_scale_factor = 1.0F
  }
  guard camera_viewport_rect_physical(width, height, camera.viewport)
    is Some((rect_x, rect_y, rect_width, rect_height)) else {
    return None
  }
  let rect_x_f = Float::from_double(rect_x.to_double()) / safe_scale_factor
  let rect_y_f = Float::from_double(rect_y.to_double()) / safe_scale_factor
  let rect_width_f = Float::from_double(rect_width.to_double()) /
    safe_scale_factor
  let rect_height_f = Float::from_double(rect_height.to_double()) /
    safe_scale_factor
  if rect_width_f <= 0.0F || rect_height_f <= 0.0F {
    return None
  }
  let half_width = rect_width_f / 2.0F
  let half_height = rect_height_f / 2.0F
  let scale = if projection.scale == 0.0F { 1.0F } else { projection.scale }
  let local_x = screen.x - rect_x_f
  let local_y = screen.y - rect_y_f
  let view_x = (local_x - half_width) * scale
  let view_y = (half_height - local_y) * scale
  let rot_z = camera_transform.rotation_z()
  let cosv = @math.cos(rot_z)
  let sinv = @math.sin(rot_z)
  let world_x = camera_transform.translation.x + view_x * cosv + view_y * sinv
  let world_y = camera_transform.translation.y - view_x * sinv + view_y * cosv
  Some(@math.Vec2::new(world_x, world_y))
}

///|
/// Shared math for camera viewport conversions. Return value is in logical pixels.
pub fn render2d_camera_world_to_viewport_2d(
  world : @math.Vec2,
  camera : Camera,
  projection : OrthographicProjection,
  camera_transform : @math.Transform,
  target_size : @math.IVec2,
  scale_factor : Float,
) -> @math.Vec2? {
  let width = target_size.x
  let height = target_size.y
  if width <= 0 || height <= 0 {
    return None
  }
  let mut safe_scale_factor = scale_factor
  if safe_scale_factor <= 0.0F {
    safe_scale_factor = 1.0F
  }
  guard camera_viewport_rect_physical(width, height, camera.viewport)
    is Some((rect_x, rect_y, rect_width, rect_height)) else {
    return None
  }
  let rect_x_f = Float::from_double(rect_x.to_double()) / safe_scale_factor
  let rect_y_f = Float::from_double(rect_y.to_double()) / safe_scale_factor
  let rect_width_f = Float::from_double(rect_width.to_double()) /
    safe_scale_factor
  let rect_height_f = Float::from_double(rect_height.to_double()) /
    safe_scale_factor
  if rect_width_f <= 0.0F || rect_height_f <= 0.0F {
    return None
  }
  let half_width = rect_width_f / 2.0F
  let half_height = rect_height_f / 2.0F
  let dx = world.x - camera_transform.translation.x
  let dy = world.y - camera_transform.translation.y
  let rot_z = camera_transform.rotation_z()
  let cosv = @math.cos(rot_z)
  let sinv = @math.sin(rot_z)
  let view_x = dx * cosv - dy * sinv
  let view_y = dx * sinv + dy * cosv
  let safe_scale = if projection.scale == 0.0F {
    1.0F
  } else {
    projection.scale
  }
  let local_x = view_x / safe_scale + half_width
  let local_y = half_height - view_y / safe_scale
  if local_x < 0.0F ||
    local_y < 0.0F ||
    local_x > rect_width_f ||
    local_y > rect_height_f {
    return None
  }
  Some(@math.Vec2::new(local_x + rect_x_f, local_y + rect_y_f))
}

///|
fn camera_viewport_rect_physical(
  target_width : Int,
  target_height : Int,
  viewport : Viewport?,
) -> (Int, Int, Int, Int)? {
  let width = target_width
  let height = target_height
  if width <= 0 || height <= 0 {
    return None
  }
  let mut rect_x = 0
  let mut rect_y = 0
  let mut rect_width = width
  let mut rect_height = height
  if viewport is Some(vp) {
    rect_x = if vp.physical_position.x < 0 { 0 } else { vp.physical_position.x }
    rect_y = if vp.physical_position.y < 0 { 0 } else { vp.physical_position.y }
    rect_width = if vp.physical_size.x < 0 { 0 } else { vp.physical_size.x }
    rect_height = if vp.physical_size.y < 0 { 0 } else { vp.physical_size.y }
    let max_width = if width - rect_x < 0 { 0 } else { width - rect_x }
    let max_height = if height - rect_y < 0 { 0 } else { height - rect_y }
    if rect_width > max_width {
      rect_width = max_width
    }
    if rect_height > max_height {
      rect_height = max_height
    }
    if rect_width <= 0 || rect_height <= 0 {
      return None
    }
  }
  Some((rect_x, rect_y, rect_width, rect_height))
}

///|
pub fn[W : HasRes_RenderState] render2d_debug_log_primary_camera(
  world : W,
) -> Unit {
  guard render_state_ref(world) is Some(rs) else { return }
  let id = rs.val.primary_camera
  if id.is_placeholder() {
    return
  }
  for camera in rs.val.cameras {
    if camera.entity.id == id.id && camera.entity.generation == id.generation {
      let target = camera.computed.target_size
      let scale = camera.computed.scale_factor
      let proj_scale = camera.projection.scale
      let mut viewport_text = "None"
      if camera.camera.viewport is Some(viewport) {
        let pos = viewport.physical_position
        let size = viewport.physical_size
        viewport_text = "pos=(" +
          pos.x.to_string() +
          "," +
          pos.y.to_string() +
          ") size=(" +
          size.x.to_string() +
          "," +
          size.y.to_string() +
          ")"
      }
      let message = "camera_debug target=(" +
        target.x.to_string() +
        "," +
        target.y.to_string() +
        ") scale=" +
        scale.to_string() +
        " proj_scale=" +
        proj_scale.to_string() +
        " viewport=" +
        viewport_text
      @window.host_debug_string(message)
      return
    }
  }
}

///|
pub fn[
  W : HasRes_RenderState + Has_Camera2d + Has_Camera + Has_Projection + Has_RenderTarget + Has_RenderLayers + Has_Sprite + Has_Anchor + Has_Mesh2d + Has_ColorMaterial + Has_GpuMesh2d + @math.Has_Transform + @transform.Has_GlobalTransform + @visibility.Has_ViewVisibility + @core.ChangeTickWorld,
] render2d_plugin(
  app : @app.App[W],
) -> @app.App[W] {
  app
  .add_startup_system(init_render_state_system)
  .add_system(render_extract_system)
  .add_post_update_system(render_prepare_system)
  .add_post_update_system(render_queue_system)
  .add_post_update_system(render_execute_system)
}