///|
/// Attach the lifecycle, frame, and image owners atomically. A rejected attach
/// disposes the unclaimed renderer session.
pub fn attach_window_surface(
  lifecycle : @window_lifecycle.WindowLifecycle,
  frames : @window_frame.WindowFrameStates,
  images : @window_image.WindowImageTaskStates,
  id : @backend_host.WindowId,
  renderer : @render.RendererSession,
  actions : @window_lifecycle.WindowSurfaceActions,
  image_task_starter? : @window_image.WindowImageTaskStarter = @window_image.window_image_task_starter_default(),
) -> Bool {
  if lifecycle.surface_actions(id) is Some(_) ||
    frames.lookup(id) is Some(_) ||
    images.lookup(id) is Some(_) {
    renderer.dispose()
    return false
  }
  if !lifecycle.attach_surface(id, actions) {
    renderer.dispose()
    return false
  }
  if !frames.attach(
      id,
      renderer,
      hooks=@window_frame.WindowFrameHooks::new(
        request_redraw=actions.request_redraw,
      ),
    ) {
    ignore(lifecycle.detach_surface(id))
    renderer.dispose()
    return false
  }
  if !images.attach(
      id,
      starter=image_task_starter,
      request_redraw=actions.request_redraw,
    ) {
    ignore(lifecycle.detach_surface(id))
    ignore(frames.dispose_window(id))
    return false
  }
  true
}

///|
pub fn dispatch_host_event(
  lifecycle : @window_lifecycle.WindowLifecycle,
  input : @window_input.WindowInputState,
  services : @window_services.WindowServiceState,
  id : @backend_host.WindowId,
  event : @backend_host.Event,
  adjust~ : (@backend_host.Event) -> @backend_host.Event?,
) -> Unit {
  match adjust(event) {
    Some(adjusted) => {
      ignore(lifecycle.apply_host_event(id, adjusted))
      match lifecycle.runtime_slot_lookup(id) {
        Some(slot) =>
          match adjusted {
            Keyboard(key) =>
              ignore(
                slot.driver.dispatch_keyboard_with_services(
                  key,
                  services.bridge(),
                ),
              )
            _ => ignore(slot.driver.dispatch(adjusted))
          }
        None => ()
      }
      ignore(input.publish(id, adjusted))
      sync_text_input_session(lifecycle, input)
    }
    None => ()
  }
}

///|
pub fn sync_text_input_session(
  lifecycle : @window_lifecycle.WindowLifecycle,
  input : @window_input.WindowInputState,
) -> Unit {
  match lifecycle.active_runtime_slot() {
    Some(slot) =>
      match lifecycle.surface_actions(slot.id) {
        Some(actions) =>
          match input.sync(slot.driver.focused_text_input()) {
            NoImeChange => ()
            action => (actions.request_ime_update)(action)
          }
        None => ()
      }
    None => ()
  }
}

///|
pub fn apply_window_request(
  lifecycle : @window_lifecycle.WindowLifecycle,
  input : @window_input.WindowInputState,
  services : @window_services.WindowServiceState,
  request : @backend_host.WindowRequest,
  open_window~ : (@backend_host.WindowOpenRequest) -> @backend_host.WindowRequestResult,
  dispose_window? : (@backend_host.WindowId) -> Unit = _ => (),
  exit~ : () -> Unit,
) -> @backend_host.WindowRequestResult {
  match request {
    OpenWindow(open_request) => open_window(open_request)
    FocusWindow(id) =>
      apply_platform_window_request(lifecycle, id, request, actions => {
        (actions.focus)()
      })
    CloseWindow(id) =>
      apply_platform_window_request(lifecycle, id, request, _ => {
        dispose_window(id)
        if lifecycle.runtime_slots_empty() {
          exit()
        }
      })
    ResizeWindow(id, metrics) =>
      apply_platform_window_request(lifecycle, id, request, actions => {
        let applied = (actions.request_resize)(metrics)
        dispatch_host_event(lifecycle, input, services, id, Resized(applied), adjust=event => {
          Some(event)
        })
        (actions.request_redraw)()
      })
    MinimizeWindow(id) =>
      apply_platform_window_request(lifecycle, id, request, actions => {
        (actions.set_minimized)(true)
      })
    ShowWindow(id) =>
      apply_platform_window_request(lifecycle, id, request, actions => {
        (actions.set_minimized)(false)
        (actions.set_visible)(true)
      })
    SetPrimaryWindow(id) =>
      apply_platform_window_request(lifecycle, id, request, _ => ())
  }
}

///|
pub fn apply_platform_window_request(
  lifecycle : @window_lifecycle.WindowLifecycle,
  id : @backend_host.WindowId,
  request : @backend_host.WindowRequest,
  apply : (@window_lifecycle.WindowSurfaceActions) -> Unit,
) -> @backend_host.WindowRequestResult {
  match lifecycle.surface_actions(id) {
    Some(actions) => {
      let result = lifecycle.apply_request_state(request)
      match result {
        WindowRejected(_) => ()
        _ => apply(actions)
      }
      result
    }
    None => WindowRejected("window request has no active platform window")
  }
}

///|
pub fn sync_observed_resize(
  lifecycle : @window_lifecycle.WindowLifecycle,
  input : @window_input.WindowInputState,
  services : @window_services.WindowServiceState,
  id : @backend_host.WindowId,
  metrics : @core.SurfaceMetrics,
) -> @core.SurfaceMetrics? {
  match lifecycle.surface_actions(id) {
    Some(actions) => {
      let applied = (actions.sync_observed_resize)(metrics)
      dispatch_host_event(lifecycle, input, services, id, Resized(applied), adjust=event => {
        Some(event)
      })
      (actions.request_redraw)()
      Some(applied)
    }
    None => None
  }
}

///|
pub fn run_window_redraw(
  lifecycle : @window_lifecycle.WindowLifecycle,
  frames : @window_frame.WindowFrameStates,
  images : @window_image.WindowImageTaskStates,
  input : @window_input.WindowInputState,
  id : @backend_host.WindowId,
  render~ : (
    @render.RendererSession,
    @window_lifecycle.WindowRuntimeSlot,
    @render.RenderFrameSubmission,
  ) -> @render.RenderFrameResult,
  finish_presented? : (@window_lifecycle.WindowRuntimeSlot) -> Bool = _ => false,
) -> Bool {
  match (lifecycle.runtime_slot_lookup(id), frames.lookup(id)) {
    (Some(slot), Some(frame_state)) => {
      let renderer = frame_state.renderer()
      if frame_state.pending() {
        let prior = drain_renderer_events(renderer, images.lookup(id))
        let prior_presented = frame_state.accept_present_completions(prior)
        if prior_presented {
          sync_text_input_session(lifecycle, input)
          if finish_presented(slot) {
            return true
          }
        }
        return prior_presented
      }
      slot.driver.begin_redraw()
      let draw_frame = slot.driver.draw_frame()
      guard frame_state.prepare_submission(draw_frame) is Some(submission) else {
        slot.driver.finish_redraw()
        return false
      }
      let result = render(renderer, slot, submission)
      let completions = drain_renderer_events(renderer, images.lookup(id))
      slot.driver.finish_redraw()
      let presented = frame_state.finish_submission(result, completions)
      sync_text_input_session(lifecycle, input)
      presented && finish_presented(slot)
    }
    _ => false
  }
}

///|
fn drain_renderer_events(
  renderer : @render.RendererSession,
  image_tasks : @window_image.WindowImageTasks?,
) -> Array[@render.RenderPresentCompletion] {
  let completions : Array[@render.RenderPresentCompletion] = []
  for event in renderer.drain_events() {
    match event {
      PresentCompleted(completion) => completions.push(completion)
      ImageLoadRequested(request) =>
        match image_tasks {
          Some(tasks) => ignore(tasks.schedule(renderer, request))
          None => ()
        }
    }
  }
  completions
}

///|
pub fn request_redraw_window_if_needed(
  lifecycle : @window_lifecycle.WindowLifecycle,
  _images : @window_image.WindowImageTaskStates,
  id : @backend_host.WindowId,
) -> Unit {
  match lifecycle.runtime_slot_lookup(id) {
    Some(slot) =>
      if runtime_slot_needs_redraw(slot) {
        request_platform_redraw(lifecycle, id)
      }
    None => ()
  }
}

///|
pub fn request_redraw(
  lifecycle : @window_lifecycle.WindowLifecycle,
  images : @window_image.WindowImageTaskStates,
) -> Unit {
  match lifecycle.active_runtime_slot() {
    Some(slot) => request_redraw_window_if_needed(lifecycle, images, slot.id)
    None => ()
  }
}

///|
pub fn request_platform_redraw(
  lifecycle : @window_lifecycle.WindowLifecycle,
  id : @backend_host.WindowId,
) -> Unit {
  match lifecycle.surface_actions(id) {
    Some(actions) => (actions.request_redraw)()
    None => ()
  }
}

///|
pub fn dispose_window(
  lifecycle : @window_lifecycle.WindowLifecycle,
  frames : @window_frame.WindowFrameStates,
  images : @window_image.WindowImageTaskStates,
  services : @window_services.WindowServiceState,
  id : @backend_host.WindowId,
) -> Unit {
  guard lifecycle.begin_close(id) else { return }
  let actions = lifecycle.surface_actions(id)
  let closes_host = lifecycle.runtime_slots().count() == 1
  // Stop image callbacks and tasks before disposing the renderer session.
  ignore(images.dispose_window(id))
  if closes_host {
    services.dispose()
  }
  ignore(frames.dispose_window(id))
  match actions {
    Some(surface) => {
      (surface.dispose_platform_views)()
      (surface.drop)()
    }
    None => ()
  }
  lifecycle.finish_close(id)
}

///|
pub fn runtime_slot_needs_redraw(
  slot : @window_lifecycle.WindowRuntimeSlot,
) -> Bool {
  let result = slot.driver.event_result()
  result.needs_redraw || result.redraw_requested
}

///|
pub fn to_host_id(render_id : @render.RenderWindowId) -> @backend_host.WindowId {
  @backend_host.WindowId::new(render_id.value)
}

///|
pub fn to_render_id(id : @backend_host.WindowId) -> @render.RenderWindowId {
  @render.RenderWindowId::new(id.value)
}

///|
pub fn to_render_metrics(
  metrics : @core.SurfaceMetrics,
) -> @core.SurfaceMetrics {
  @core.SurfaceMetrics::new(
    logical_size=metrics.logical_size,
    physical_size=metrics.physical_size,
    scale_factor=metrics.scale_factor,
  )
}