///|
pub(all) struct ViewportSnapshot {
  scene_revision : Int64
  scene : Scene
  camera : OrbitCamera
  bounds : Aabb
} derive(Debug)

///|
pub extend ViewportSnapshot with Debug::{to_repr}

///|
pub fn ViewportSnapshot::new(
  scene_revision~ : Int64,
  scene~ : Scene,
  camera~ : OrbitCamera,
) -> ViewportSnapshot {
  { scene_revision, scene, camera, bounds: scene.bounds(), }
}

///|
pub fn ViewportSnapshot::scene_revision(self : ViewportSnapshot) -> Int64 {
  self.scene_revision
}

///|
pub fn ViewportSnapshot::scene(self : ViewportSnapshot) -> Scene {
  self.scene
}

///|
pub fn ViewportSnapshot::camera(self : ViewportSnapshot) -> OrbitCamera {
  self.camera
}

///|
pub fn ViewportSnapshot::bounds(self : ViewportSnapshot) -> Aabb {
  self.bounds
}

///|
/// Latest-wins CPU bridge between a MoUI view and an external 3D surface.
/// Renderer residency and GPU cache state never cross this boundary.
pub struct ThreeDViewportBinding {
  latest : @ref.Ref[ViewportSnapshot?]
  status : @ref.Ref[ViewportStatus]
}

///|
pub(all) enum ViewportStatus {
  Loading
  Ready
  Unavailable(String)
  Failed(String)
} derive(Eq, Debug, ToJson)

///|
pub extend ViewportStatus with Debug::{to_repr}

///|
pub extend ViewportStatus with Eq::{not_equal, equal}

///|
pub extend ViewportStatus with ToJson::{to_json}

///|
pub fn ThreeDViewportBinding::new() -> ThreeDViewportBinding {
  { latest: Ref(None), status: Ref(Loading), }
}

///|
pub fn ThreeDViewportBinding::publish(
  self : ThreeDViewportBinding,
  snapshot : ViewportSnapshot,
) -> Bool {
  match self.latest.val {
    Some(previous) =>
      if snapshot.scene_revision() < previous.scene_revision() {
        false
      } else {
        self.latest.val = Some(snapshot)
        true
      }
    None => {
      self.latest.val = Some(snapshot)
      true
    }
  }
}

///|
pub fn ThreeDViewportBinding::snapshot(
  self : ThreeDViewportBinding,
) -> ViewportSnapshot? {
  self.latest.val
}

///|
pub fn ThreeDViewportBinding::set_status(
  self : ThreeDViewportBinding,
  status : ViewportStatus,
) -> Unit {
  self.status.val = status
}

///|
pub fn ThreeDViewportBinding::status(
  self : ThreeDViewportBinding,
) -> ViewportStatus {
  self.status.val
}

///|
/// Addon-owned bridge between a platform-view plugin and a concrete 3D
/// renderer. The bridge carries only CPU snapshots and surface geometry;
/// renderer resources never cross into the MoUI frame contract.
pub struct ThreeDViewportHost {
  binding : ThreeDViewportBinding
  surface_handler : @ref.Ref[(UInt64, Int, Int, Double) -> Unit]
  frame_handler : @ref.Ref[(ViewportSnapshot, Int, Int, Double) -> Unit]
  dispose_handler : @ref.Ref[(UInt64) -> Unit]
  invalidate_handler : @ref.Ref[() -> Unit]
}

///|
pub fn ThreeDViewportHost::new(
  binding~ : ThreeDViewportBinding,
) -> ThreeDViewportHost {
  {
    binding,
    surface_handler: Ref((_handle, _width, _height, _scale) => ()),
    frame_handler: Ref((_snapshot, _width, _height, _scale) => ()),
    dispose_handler: Ref(_handle => ()),
    invalidate_handler: Ref(() => ()),
  }
}

///|
pub fn ThreeDViewportHost::binding(
  self : ThreeDViewportHost,
) -> ThreeDViewportBinding {
  self.binding
}

///|
/// Called by a concrete renderer composition root once it is ready to
/// interpret the platform surface handle.
pub fn ThreeDViewportHost::set_surface_handler(
  self : ThreeDViewportHost,
  handler : (UInt64, Int, Int, Double) -> Unit,
) -> Unit {
  self.surface_handler.val = handler
}

///|
pub fn ThreeDViewportHost::set_frame_handler(
  self : ThreeDViewportHost,
  handler : (ViewportSnapshot, Int, Int, Double) -> Unit,
) -> Unit {
  self.frame_handler.val = handler
}

///|
pub fn ThreeDViewportHost::set_dispose_handler(
  self : ThreeDViewportHost,
  handler : (UInt64) -> Unit,
) -> Unit {
  self.dispose_handler.val = handler
}

///|
/// Install the platform redraw hook used when renderer availability changes
/// outside the ordinary MoUI model update path.
pub fn ThreeDViewportHost::set_invalidate_handler(
  self : ThreeDViewportHost,
  handler : () -> Unit,
) -> Unit {
  self.invalidate_handler.val = handler
}

///|
pub fn ThreeDViewportHost::invalidate(self : ThreeDViewportHost) -> Unit {
  (self.invalidate_handler.val)()
}

///|
/// Synchronize all placements owned by the 3D addon. The plugin passes the
/// host window handle; a native backend may replace it with a child surface
/// handle before invoking the frame handler.
pub fn ThreeDViewportHost::sync_platform_views(
  self : ThreeDViewportHost,
  window_handle : UInt64,
  frame : @core.DrawFrame,
  content_offset_y : Double,
) -> Unit {
  for placement in frame.platform_views {
    if placement.kind == "moui_3d_viewport" {
      self.sync_platform_view(
        window_handle, placement, window_handle, frame, content_offset_y,
      )
    }
  }
}

///|
/// Sync one placement after a platform plugin has created or resolved its
/// concrete child surface handle.
pub fn ThreeDViewportHost::sync_platform_view(
  self : ThreeDViewportHost,
  window_handle : UInt64,
  placement : @core.PlatformViewPlacement,
  surface_handle : UInt64,
  frame : @core.DrawFrame,
  content_offset_y : Double,
  scale_factor? : Double = 1.0,
) -> Unit {
  if placement.kind != "moui_3d_viewport" {
    return ()
  }
  let width = placement.frame.size.width.to_int()
  let height = placement.frame.size.height.to_int()
  let scale = if scale_factor > 0.0 { scale_factor } else { 1.0 }
  (self.surface_handler.val)(surface_handle, width, height, scale)
  match self.binding.snapshot() {
    Some(snapshot) => (self.frame_handler.val)(snapshot, width, height, scale)
    None => ()
  }
  ignore(window_handle)
  ignore(frame)
  ignore(content_offset_y)
}

///|
pub fn ThreeDViewportHost::dispose_platform_views(
  self : ThreeDViewportHost,
  window_handle : UInt64,
) -> Unit {
  (self.dispose_handler.val)(window_handle)
}

///|
pub fn[Msg] three_d_viewport(
  binding~ : ThreeDViewportBinding,
  snapshot~ : ViewportSnapshot,
  semantics_label? : String = "3D viewport",
  on_event? : ((ViewportEvent) -> Msg)? = None,
) -> @core.View[Msg] {
  ignore(binding.publish(snapshot))
  @core.View::from_node(ThreeDViewportNode::{ binding, semantics_label, }, event=(
    _node,
    context,
  ) => {
    match (context.event, on_event) {
      (PlatformView(event_id, event), Some(handler)) if event_id ==
        "moui-3d-viewport" =>
        match decode_viewport_event(event) {
          Some(value) => @core.ViewEventResult::message(handler(value))
          None => @core.ViewEventResult::ignored()
        }
      _ => @core.ViewEventResult::ignored()
    }
  })
}

///|
priv struct ThreeDViewportNode {
  binding : ThreeDViewportBinding
  semantics_label : String
}

///|
impl @core.ViewNode for ThreeDViewportNode with fn identity(_self) {
  @core.ViewIdentity::new(kind="ThreeDViewport")
}

///|
impl @core.ViewNode for ThreeDViewportNode with fn declaration(_self) {
  @core.ViewDeclaration::constant()
}

///|
impl @core.ViewNode for ThreeDViewportNode with fn layout(_self, context) {
  let width = if context.constraints.max.width > 1.0e8 {
    320.0
  } else {
    context.constraints.max.width
  }
  // A viewport is an intrinsically sized platform surface. Letting a leaf use
  // the column's full max height pushes the status row below a small window.
  // Keep a stable 16:9 presentation with a bounded desktop height instead.
  let aspect_height = width * 0.5625
  let height = if context.constraints.max.height > 1.0e8 {
    aspect_height.min(360.0)
  } else {
    aspect_height.min(360.0).min(context.constraints.max.height)
  }
  @core.ViewLayoutResult::new(
    size=context.constraints.constrain(@core.Size::new(width~, height~)),
  )
}

///|
impl @core.ViewNode for ThreeDViewportNode with fn platform_views(self, context) {
  let revision = match self.binding.snapshot() {
    Some(snapshot) => snapshot.scene_revision().to_string()
    None => "0"
  }
  [
    @core.PlatformViewPlacement::new(
      id="moui-3d-viewport",
      kind="moui_3d_viewport",
      frame=context.frame,
      clip=Some(context.frame),
      properties=[
        @core.PlatformViewProperty::new(key="scene_revision", value=revision),
      ],
    ),
  ]
}

///|
impl @core.ViewNode for ThreeDViewportNode with fn semantics(self) {
  @core.ViewSemanticsInfo::new(
    composition=Boundary,
    role=Group,
    label=self.semantics_label,
    description="Interactive 3D model viewport",
  )
}

///|
pub(all) enum ViewportEvent {
  PointerDown(@core.Point)
  PointerMove(@core.Point)
  PointerUp(@core.Point)
  Wheel(Double)
  Resize(@core.Size)
  Focus(Bool)
  Pick(NodeId)
  RendererStatus
} derive(Eq, Debug)

///|
pub extend ViewportEvent with Debug::{to_repr}

///|
pub extend ViewportEvent with Eq::{not_equal, equal}

///|
/// Decode the stable, human-readable platform event names used by the addon
/// plugin. Pointer payloads are `x,y`; wheel payload is a signed scalar.
pub fn decode_viewport_event(event : @core.PlatformViewEvent) -> ViewportEvent? {
  match event {
    Named(name~, value~, detail~, flag~) => {
      ignore(detail)
      if name == "pointer_down" {
        parse_point(value).map(point => PointerDown(point))
      } else if name == "pointer_move" {
        parse_point(value).map(point => PointerMove(point))
      } else if name == "pointer_up" {
        parse_point(value).map(point => PointerUp(point))
      } else if name == "wheel" {
        let amount = @string.parse_double(value) catch { _ => return None }
        Some(Wheel(amount))
      } else if name == "resize" {
        parse_point(value).map(point => {
          Resize(@core.Size::new(width=point.x, height=point.y))
        })
      } else if name == "focus" {
        Some(Focus(flag))
      } else if name == "pick" {
        let node_id = @string.parse_int(value) catch { _ => return None }
        Some(Pick(NodeId::new(node_id)))
      } else if name == "renderer_status" {
        Some(RendererStatus)
      } else {
        None
      }
    }
  }
}

///|
fn parse_point(value : String) -> @core.Point? {
  let parts = value.split(",").to_array()
  if parts.length() != 2 {
    return None
  }
  let x = @string.parse_double(parts[0].to_owned()) catch { _ => return None }
  let y = @string.parse_double(parts[1].to_owned()) catch { _ => return None }
  Some(@core.Point::new(x~, y~))
}