///|
pub struct AppRuntime {
  priv state : RuntimeState
  priv program_runtime_snapshot_fn : () -> ProgramRuntimeSnapshot
  priv destroy_program_runtime_fn : Ref[() -> Unit]
}

///|
fn program_message_drain_limit() -> Int {
  1024
}

///|
fn[Msg] compact_pending_program_messages(
  pending_messages : Array[Msg],
  start : Int,
) -> Unit {
  let remaining : Array[Msg] = []
  for index in start.. Unit,
  start_task : (@core.EffectDescriptor, ((Msg) -> Unit) -> (() -> Unit)?) -> Unit,
) -> Int {
  let mut count = 0
  effect.runtime_visit(
    message => {
      dispatch(message)
      count = count + 1
    },
    run => {
      run(dispatch)
      count = count + 1
    },
    (_descriptor, run) => {
      run(dispatch)
      count = count + 1
    },
    (descriptor, start) => {
      start_task(descriptor, start)
      count = count + 1
    },
  )
  count
}

///|
pub fn[Msg] AppRuntime::new_view(
  root~ : @core.View[Msg],
  size~ : @core.Size,
  environment? : @core.Environment = @core.Environment::neutral(),
  on_message? : ((Msg) -> Unit)? = None,
) -> AppRuntime {
  {
    state: RuntimeState::new_view(root~, size~, environment~, on_message~),
    program_runtime_snapshot_fn: () => program_runtime_snapshot_empty(),
    destroy_program_runtime_fn: Ref(() => ()),
  }
}

///|
pub fn[Model, Msg] AppRuntime::new_program(
  program~ : @core.Program[Model, Msg],
  size~ : @core.Size,
  environment? : @core.Environment = @core.Environment::neutral(),
) -> AppRuntime {
  let (initial_model, initial_effect) = program.runtime_init()
  let model = Ref(initial_model)
  let program_runtime = ProgramRuntimeDiagnostics::new()
  let pending_messages : Array[Msg] = []
  let pending_index = Ref(0)
  let dispatching = Ref(false)
  let syncing_subscriptions = Ref(false)
  let message_batch_depth = Ref(0)
  let program_active = Ref(true)
  let runtime_ref : Ref[AppRuntime?] = Ref(None)
  let process_ref : Ref[(() -> Unit)?] = Ref(None)
  let pending_count = () => pending_messages.length() - pending_index.val
  let dispatch = message => {
    if program_active.val {
      pending_messages.push(message)
      program_runtime.record_message_enqueued(pending_count())
      if !syncing_subscriptions.val && message_batch_depth.val == 0 {
        match process_ref.val {
          Some(process) => process()
          None => ()
        }
      }
    } else {
      program_runtime.record_ignored_program_dispatch()
    }
  }
  let sync_subscriptions = () => {
    syncing_subscriptions.val = true
    program_runtime.sync_subscriptions(
      program.runtime_subscriptions(model.val),
      dispatch,
    )
    syncing_subscriptions.val = false
  }
  let sync_commands = () => {
    match runtime_ref.val {
      Some(runtime) =>
        runtime.state.set_action_commands(
          ActionCommandMap::new(
            bindings=program
              .runtime_commands(model.val)
              .map(program_command => {
                @core.CommandBinding::new(command=program_command.command(), handle=() => {
                  dispatch(program_command.message())
                })
              }),
          ),
        )
      None => ()
    }
  }
  let begin_message_batch = () => {
    message_batch_depth.val = message_batch_depth.val + 1
  }
  let end_message_batch = () => {
    message_batch_depth.val = message_batch_depth.val - 1
    if message_batch_depth.val == 0 && !syncing_subscriptions.val {
      match process_ref.val {
        Some(process) => process()
        None => ()
      }
    }
  }
  let process_messages = () => {
    if !dispatching.val && !syncing_subscriptions.val {
      program_runtime.record_message_drain_started(pending_count())
      dispatching.val = true
      let mut processed_this_drain = 0
      while pending_index.val < pending_messages.length() &&
            processed_this_drain < program_message_drain_limit() {
        let message = pending_messages[pending_index.val]
        pending_index.val = pending_index.val + 1
        processed_this_drain = processed_this_drain + 1
        program_runtime.record_dispatch()
        let (next_model, effect) = program.runtime_update(model.val, message)
        program_runtime.record_update()
        model.val = next_model
        match runtime_ref.val {
          Some(runtime) => runtime.state.request_root_rebuild()
          None => ()
        }
        sync_commands()
        let effect_summary = effect_plan_summary(effect)
        let effect_count = run_effect(effect, dispatch, (descriptor, start) => {
          program_runtime.start_effect_task(descriptor, start, dispatch)
        })
        program_runtime.record_effects(effect_summary, effect_count)
        sync_subscriptions()
        program_runtime.record_message_processed(pending_count())
      }
      if pending_index.val < pending_messages.length() {
        compact_pending_program_messages(pending_messages, pending_index.val)
        pending_index.val = 0
        program_runtime.record_message_processed(pending_count())
      } else {
        pending_messages.clear()
        pending_index.val = 0
        program_runtime.record_message_queue_cleared()
      }
      dispatching.val = false
    }
  }
  process_ref.val = Some(process_messages)
  let initial_effect_summary = effect_plan_summary(initial_effect)
  let initial_effect_count = run_effect(initial_effect, dispatch, (
    descriptor,
    start,
  ) => program_runtime.start_effect_task(descriptor, start, dispatch))
  program_runtime.record_effects(initial_effect_summary, initial_effect_count)
  sync_subscriptions()
  match process_ref.val {
    Some(process) => process()
    None => ()
  }
  let runtime = AppRuntime::{
    state: RuntimeState::new_view(
      root=program.runtime_view(
        model.val,
        @core.ViewEnvironment::new(viewport_size=size, environment~),
      ),
      size~,
      environment~,
      on_message=Some(dispatch),
      begin_message_batch~,
      end_message_batch~,
    ),
    program_runtime_snapshot_fn: () => program_runtime.snapshot(),
    destroy_program_runtime_fn: Ref(() => ()),
  }
  runtime_ref.val = Some(runtime)
  runtime.state.set_root_builder(() => {
    adapt_view_for_runtime(
      program.runtime_view(
        model.val,
        @core.ViewEnvironment::new(
          viewport_size=runtime.state.size,
          environment=runtime.state.environment,
        ),
      ),
      dispatch,
    )
  })
  sync_commands()
  runtime.destroy_program_runtime_fn.val = () => {
    if program_active.val {
      program_active.val = false
      program_runtime.cancel_all_effect_tasks(EffectTaskRuntimeDestroyed)
      program_runtime.cancel_all_subscriptions(SubscriptionRuntimeDestroyed)
    }
  }
  runtime
}

///|
pub fn[Msg] new_view(
  root~ : @core.View[Msg],
  size~ : @core.Size,
  environment? : @core.Environment = @core.Environment::neutral(),
  on_message? : ((Msg) -> Unit)? = None,
) -> AppRuntime {
  AppRuntime::new_view(root~, size~, environment~, on_message~)
}

///|
pub fn[Model, Msg] new_program(
  program~ : @core.Program[Model, Msg],
  size~ : @core.Size,
  environment? : @core.Environment = @core.Environment::neutral(),
) -> AppRuntime {
  AppRuntime::new_program(program~, size~, environment~)
}

///|
pub fn[Model, Msg] new_program_with_dimensions(
  program~ : @core.Program[Model, Msg],
  width~ : Double,
  height~ : Double,
  environment? : @core.Environment = @core.Environment::neutral(),
) -> AppRuntime {
  AppRuntime::new_program(
    program~,
    size=@core.Size::new(width~, height~),
    environment~,
  )
}

///|
pub fn AppRuntime::environment(self : AppRuntime) -> @core.Environment {
  self.state.environment()
}

///|
pub fn AppRuntime::text_system(self : AppRuntime) -> @core.TextSystem {
  self.state.text_system()
}

///|
pub fn AppRuntime::set_text_system(
  self : AppRuntime,
  text_system : @core.TextSystem,
) -> Unit {
  self.state.set_text_system(text_system)
}

///|
pub fn AppRuntime::register_font_data(
  self : AppRuntime,
  family : @core.FontFamily,
  data : Bytes,
) -> Unit {
  self.state.register_font_data(family, data)
}

///|
pub fn AppRuntime::set_environment(
  self : AppRuntime,
  environment : @core.Environment,
) -> Unit {
  self.state.set_environment(environment)
}

///|
pub fn AppRuntime::advance_time(self : AppRuntime, time_ms : Double) -> Unit {
  self.state.advance_time(time_ms)
}

///|
pub fn AppRuntime::destroy(self : AppRuntime) -> Unit {
  self.state.request_destroy(self.destroy_program_runtime_fn.val)
}

///|
pub fn AppRuntime::layout(self : AppRuntime) -> @core.LayoutResult {
  self.state.layout()
}

///|
pub fn AppRuntime::read_semantics(
  self : AppRuntime,
  since? : @core.SemanticsGeneration,
) -> Result[SemanticsRead, SemanticsReadError] {
  self.state.read_semantics(since)
}

///|
pub fn AppRuntime::perform_semantics_action(
  self : AppRuntime,
  request : PerformSemanticsActionRequest,
) -> Result[SemanticsActionReceipt, SemanticsActionDispatchError] {
  match self.state.perform_semantics_action(request) {
    Ok(receipt) => {
      let program = self.program_runtime_snapshot()
      Ok({
        ..receipt,
        pending_work: receipt.pending_work ||
        program.pending_message_count > 0 ||
        program.active_effect_task_count > 0,
      })
    }
    Err(error) => Err(error)
  }
}

///|
pub fn AppRuntime::focused_text_input(
  self : AppRuntime,
) -> @core.TextInputState {
  self.state.focused_text_input()
}

///|
pub fn AppRuntime::focus_next(self : AppRuntime) -> Bool {
  self.state.move_focus()
}

///|
pub fn AppRuntime::focus_previous(self : AppRuntime) -> Bool {
  self.state.move_focus(reverse=true)
}

///|
pub fn AppRuntime::focus_key(self : AppRuntime, key : String) -> Bool {
  self.state.focus_key(key)
}

///|
pub fn AppRuntime::draw_commands(self : AppRuntime) -> Array[@core.DrawCommand] {
  self.state.build_draw_commands()
}

///|
pub fn AppRuntime::draw_frame(self : AppRuntime) -> @core.DrawFrame {
  self.state.build_draw_frame()
}

///|
pub fn AppRuntime::size(self : AppRuntime) -> @core.Size {
  self.state.size
}

///|
pub fn AppRuntime::needs_redraw(self : AppRuntime) -> Bool {
  self.state.dirty.needs_redraw
}

///|
pub fn AppRuntime::needs_rebuild(self : AppRuntime) -> Bool {
  self.state.dirty.needs_rebuild
}

///|
pub fn AppRuntime::needs_layout(self : AppRuntime) -> Bool {
  self.state.dirty.needs_layout
}

///|
pub fn AppRuntime::needs_paint(self : AppRuntime) -> Bool {
  self.state.dirty.needs_paint
}

///|
pub fn AppRuntime::dirty_count(self : AppRuntime) -> Int {
  self.state.dirty_count()
}

///|
pub fn AppRuntime::dirty_element_ids(self : AppRuntime) -> Array[Int] {
  self.state.dirty_element_ids().map(id => id.value())
}

///|
pub fn AppRuntime::inspector_snapshot(self : AppRuntime) -> InspectorSnapshot {
  self.inspector_snapshot_from_state()
}

///|
pub fn AppRuntime::program_runtime_snapshot(
  self : AppRuntime,
) -> ProgramRuntimeSnapshot {
  (self.program_runtime_snapshot_fn)()
}

///|
pub fn AppRuntime::set_redraw_callback(
  self : AppRuntime,
  callback : () -> Unit,
) -> Unit {
  self.state.set_redraw_callback(callback)
}

///|
/// Installs an observer invoked with every `AppEvent` before it is processed.
/// Replaces any previously installed observer. Passing a no-op effectively
/// disables observation. This is the additive hook the agent layer consumes;
/// it is inert unless explicitly called.
pub fn AppRuntime::set_event_observer(
  self : AppRuntime,
  observer : (@core.AppEvent) -> Unit,
) -> Unit {
  self.state.event_observer = observer
}

///|
pub fn AppRuntime::action_commands(
  self : AppRuntime,
) -> Array[@core.ActionCommand] {
  self.state.action_commands()
}

///|
pub fn AppRuntime::dispatch_command(
  self : AppRuntime,
  intent : @core.CommandIntent,
) -> Bool {
  self.state.dispatch_command(intent)
}

///|
pub fn AppRuntime::dispatch_action_command(
  self : AppRuntime,
  command : @core.ActionCommand,
) -> Bool {
  self.state.dispatch_action_command(command)
}

///|
pub fn AppRuntime::dispatch_focused_text_command(
  self : AppRuntime,
  intent : @core.CommandIntent,
  paste_text? : String? = None,
) -> @core.TextCommandResult {
  self.state.dispatch_focused_text_command(intent, paste_text~)
}