///|
fn AppRuntime::inspector_snapshot_from_state(
  self : AppRuntime,
) -> InspectorSnapshot {
  let view_tree = self.state.view_tree_inspector_snapshot()
  {
    runtime: runtime_inspector_snapshot_from_state(
      self.state,
      self.program_runtime_snapshot(),
    ),
    view_tree,
    layout: layout_inspector_snapshot_from_state(self.state),
    render: render_inspector_snapshot_from_state(self.state),
    semantics: semantics_inspector_snapshot_from_state(self.state),
  }
}

///|
fn program_runtime_snapshot_empty() -> ProgramRuntimeSnapshot {
  {
    dispatched_count: 0,
    update_count: 0,
    ignored_program_dispatch_count: 0,
    message_enqueue_count: 0,
    message_drain_count: 0,
    pending_message_count: 0,
    max_pending_message_count: 0,
    effect_plan_count: 0,
    effect_count: 0,
    effect_none_count: 0,
    effect_batch_count: 0,
    effect_send_count: 0,
    effect_dispatch_count: 0,
    effect_run_count: 0,
    effect_task_count: 0,
    duplicate_effect_descriptor_key_count: 0,
    duplicate_effect_descriptor_keys: [],
    superseded_effect_task_key_count: 0,
    superseded_effect_task_keys: [],
    last_effect_summary: EffectPlanSummary::empty(),
    last_scheduled_effect_summary: EffectPlanSummary::empty(),
    active_effect_task_count: 0,
    started_effect_task_count: 0,
    completed_effect_task_count: 0,
    cancelled_effect_task_count: 0,
    ignored_effect_task_dispatch_count: 0,
    active_effect_tasks: [],
    effect_task_lifecycle: [],
    subscription_plan_count: 0,
    last_subscription_plan_summary: SubscriptionPlanSummary::empty(),
    active_subscription_count: 0,
    started_subscription_count: 0,
    reused_subscription_count: 0,
    cancelled_subscription_count: 0,
    duplicate_subscription_key_count: 0,
    duplicate_subscription_keys: [],
    ignored_subscription_dispatch_count: 0,
    active_subscriptions: [],
    active_subscription_kind_counts: [],
    subscription_lifecycle: [],
  }
}

///|
fn runtime_inspector_snapshot_from_state(
  state : RuntimeState,
  program : ProgramRuntimeSnapshot,
) -> RuntimeInspectorSnapshot {
  let dirty_count = state.dirty_count()
  let dirty_element_ids = state.dirty_element_ids().map(id => id.value())
  let dirty_build_element_ids = state.dirty.dirty_build_elements.items.map(id => {
    id.value()
  })
  let dirty_layout_element_ids = state.dirty.dirty_layout_elements.items.map(id => {
    id.value()
  })
  let dirty_paint_element_ids = state.dirty.dirty_paint_elements.items.map(id => {
    id.value()
  })
  let dirty_build_count = dirty_build_element_ids.length()
  let dirty_layout_count = dirty_layout_element_ids.length()
  let dirty_paint_count = dirty_paint_element_ids.length()
  let needs_rebuild = state.dirty.needs_rebuild
  let needs_layout = state.dirty.needs_layout
  let needs_paint = state.dirty.needs_paint
  let needs_redraw = state.dirty.needs_redraw
  let rebuild_reason = state.inspector_rebuild_reason()
  let dirty_reasons = state.inspector_dirty_reasons()
  let dirty_state = RuntimeDirtyStateSummary::{
    dirty_count,
    dirty_element_ids: dirty_element_ids.copy(),
    dirty_build_count,
    dirty_build_element_ids: dirty_build_element_ids.copy(),
    dirty_layout_count,
    dirty_layout_element_ids: dirty_layout_element_ids.copy(),
    dirty_paint_count,
    dirty_paint_element_ids: dirty_paint_element_ids.copy(),
    needs_rebuild,
    needs_layout,
    needs_paint,
    needs_redraw,
    has_pending_work: needs_rebuild ||
    needs_layout ||
    needs_paint ||
    needs_redraw ||
    dirty_count > 0,
    reason: rebuild_reason,
    reasons: dirty_reasons.copy(),
    damage_kind: state.stats.last_damage_region.kind(),
    damage_rect_count: state.stats.last_damage_region.rect_count(),
    damage_full_reason: state.stats.last_damage_region.full_reason(),
  }
  let draw_commands = state.cached_draw_commands()
  {
    size: state.size,
    dirty_count,
    dirty_element_ids,
    dirty_build_count,
    dirty_build_element_ids,
    dirty_layout_count,
    dirty_layout_element_ids,
    dirty_paint_count,
    dirty_paint_element_ids,
    dirty_state,
    needs_rebuild,
    needs_layout,
    needs_paint,
    needs_redraw,
    draw_command_count: draw_commands.length(),
    rebuild_pass_count: state.stats.rebuild_pass_count,
    layout_pass_count: state.stats.layout_pass_count,
    paint_pass_count: state.stats.paint_pass_count,
    draw_command_build_count: state.stats.draw_command_build_count,
    layout_cache_hit_count: state.element_tree.root.inspector_layout_cache_hits(),
    layout_cache_miss_count: state.element_tree.root.inspector_layout_cache_misses(),
    render_cache_hit_count: state.element_tree.root.inspector_render_cache_hits(),
    render_cache_miss_count: state.element_tree.root.inspector_render_cache_misses(),
    last_event_hit_path: state.last_event_hit_path.copy(),
    last_frame_timing: state.last_frame_timing,
    program_dispatched_count: program.dispatched_count,
    program_update_count: program.update_count,
    ignored_program_dispatch_count: program.ignored_program_dispatch_count,
    program_message_enqueue_count: program.message_enqueue_count,
    program_message_drain_count: program.message_drain_count,
    program_pending_message_count: program.pending_message_count,
    program_max_pending_message_count: program.max_pending_message_count,
    program_effect_plan_count: program.effect_plan_count,
    program_effect_count: program.effect_count,
    program_effect_none_count: program.effect_none_count,
    program_effect_batch_count: program.effect_batch_count,
    program_effect_send_count: program.effect_send_count,
    program_effect_dispatch_count: program.effect_dispatch_count,
    program_effect_run_count: program.effect_run_count,
    program_effect_task_count: program.effect_task_count,
    duplicate_effect_descriptor_key_count: program.duplicate_effect_descriptor_key_count,
    duplicate_effect_descriptor_keys: program.duplicate_effect_descriptor_keys,
    superseded_effect_task_key_count: program.superseded_effect_task_key_count,
    superseded_effect_task_keys: program.superseded_effect_task_keys,
    last_effect_summary: program.last_effect_summary,
    last_scheduled_effect_summary: program.last_scheduled_effect_summary,
    active_effect_task_count: program.active_effect_task_count,
    started_effect_task_count: program.started_effect_task_count,
    completed_effect_task_count: program.completed_effect_task_count,
    cancelled_effect_task_count: program.cancelled_effect_task_count,
    ignored_effect_task_dispatch_count: program.ignored_effect_task_dispatch_count,
    active_effect_tasks: program.active_effect_tasks,
    effect_task_lifecycle: program.effect_task_lifecycle,
    program_subscription_plan_count: program.subscription_plan_count,
    last_subscription_plan_summary: program.last_subscription_plan_summary,
    active_subscription_count: program.active_subscription_count,
    started_subscription_count: program.started_subscription_count,
    reused_subscription_count: program.reused_subscription_count,
    cancelled_subscription_count: program.cancelled_subscription_count,
    duplicate_subscription_key_count: program.duplicate_subscription_key_count,
    duplicate_subscription_keys: program.duplicate_subscription_keys,
    ignored_subscription_dispatch_count: program.ignored_subscription_dispatch_count,
    active_subscriptions: program.active_subscriptions,
    active_subscription_kind_counts: program.active_subscription_kind_counts,
    subscription_lifecycle: program.subscription_lifecycle,
    rebuild_reason,
    dirty_reasons,
  }
}

///|
fn layout_inspector_snapshot_from_state(
  state : RuntimeState,
) -> LayoutInspectorSnapshot {
  let layout = state.cached_layout()
  {
    root_frame: layout.frame,
    node_count: layout_node_count(layout),
    leaf_count: layout_leaf_count(layout),
    max_depth: layout_max_depth(layout),
    bounds: layout_bounds_entries(
      state.cached_layout_tree_root(),
      state.element_tree.root,
      depth=0,
    ),
  }
}

///|
fn render_inspector_snapshot_from_state(
  state : RuntimeState,
) -> RenderInspectorSnapshot {
  {
    ..render_inspector_from_commands(state.cached_draw_commands()),
    nodes: render_inspector_entries(state.render_tree.root, depth=0),
  }
}

///|
fn semantics_inspector_snapshot_from_state(
  state : RuntimeState,
) -> SemanticsInspectorSnapshot {
  let semantics = state.cached_semantics()
  let nodes = semantics.nodes()
  let mut root_role = SemanticsRole::None
  match semantics.root() {
    Some(root) =>
      for node in nodes {
        if node.node_id == root {
          root_role = node.role
          break
        }
      }
    None => ()
  }
  let mut focusable_count = 0
  let mut action_count = 0
  let mut selected_count = 0
  let mut checked_count = 0
  let mut expanded_count = 0
  let mut invalid_count = 0
  let mut required_count = 0
  let mut described_count = 0
  let nodes_by_id : Map[SemanticsNodeId, SemanticsNode] = Map([])
  for node in nodes {
    nodes_by_id[node.node_id] = node
    if node.focus_order is Some(_) {
      focusable_count = focusable_count + 1
    }
    action_count = action_count + node.actions.length()
    if node.state.selected {
      selected_count = selected_count + 1
    }
    if node.state.checked == Some(Checked) {
      checked_count = checked_count + 1
    }
    if node.state.expanded {
      expanded_count = expanded_count + 1
    }
    if node.state.invalid {
      invalid_count = invalid_count + 1
    }
    if node.state.required {
      required_count = required_count + 1
    }
    if node.description != "" {
      described_count = described_count + 1
    }
  }
  let max_depth = match semantics.root() {
    Some(root) => semantics_depth_from(root, nodes_by_id)
    None => 0
  }
  {
    root_role,
    node_count: nodes.length(),
    focusable_count,
    action_count,
    max_depth,
    selected_count,
    checked_count,
    expanded_count,
    invalid_count,
    required_count,
    described_count,
    nodes: match semantics.root() {
      Some(root) => semantics_inspector_entries(root, nodes_by_id, depth=0)
      None => []
    },
  }
}

///|
fn layout_node_count(node : LayoutResult) -> Int {
  let mut count = 1
  for child in node.children {
    count = count + layout_node_count(child)
  }
  count
}

///|
fn layout_leaf_count(node : LayoutResult) -> Int {
  if node.children.length() == 0 {
    1
  } else {
    let mut count = 0
    for child in node.children {
      count = count + layout_leaf_count(child)
    }
    count
  }
}

///|
fn layout_max_depth(node : LayoutResult) -> Int {
  let mut depth = 1
  for child in node.children {
    depth = runtime_devtools_max_int(depth, 1 + layout_max_depth(child))
  }
  depth
}

///|
fn layout_bounds_entries(
  node : PlacedNode,
  element : ElementNode,
  depth~ : Int,
) -> Array[LayoutBoundsInspectorEntry] {
  let entries : Array[LayoutBoundsInspectorEntry] = [
    {
      element_id: node.element_id.value(),
      frame: node.frame,
      constraints: element.layout_cache.cached_measure_constraints.unwrap_or(
        @core.Constraints::tight(node.frame.size),
      ),
      depth,
      child_count: node.children.length(),
    },
  ]
  for index, child in node.children {
    if index < element.children.length() {
      entries.append(
        layout_bounds_entries(child, element.children[index], depth=depth + 1),
      )
    }
  }
  entries
}

///|
fn render_inspector_entries(
  node : RenderNode,
  depth~ : Int,
) -> Array[RenderInspectorEntry] {
  let entries : Array[RenderInspectorEntry] = [
    {
      element_id: node.element_id.value(),
      frame: node.frame,
      paint_bounds: node.paint_bounds,
      depth,
      child_count: node.children.length(),
      command_count: node.paint_commands.length() +
      node.post_paint_commands.length(),
      overlay_command_count: node.overlay_paint_commands.length(),
      repaint_boundary: node.is_repaint_boundary,
      cache_key: node.layer_cache_key.unwrap_or(""),
    },
  ]
  for child in node.children {
    entries.append(render_inspector_entries(child, depth=depth + 1))
  }
  entries
}

///|
fn semantics_inspector_entries(
  id : SemanticsNodeId,
  nodes : Map[SemanticsNodeId, SemanticsNode],
  depth~ : Int,
) -> Array[SemanticsInspectorEntry] {
  guard nodes.get(id) is Some(node) else { return [] }
  let entries : Array[SemanticsInspectorEntry] = [
    {
      node_id: node.node_id.to_wire(),
      role: node.role,
      label: node.label,
      value: node.value,
      frame: node.frame,
      depth,
      child_count: node.children.length(),
      action_count: node.actions.length(),
      focusable: node.focus_order is Some(_),
      selected: node.state.selected,
      checked: node.state.checked == Some(Checked),
      expanded: node.state.expanded,
      disabled: node.state.disabled,
    },
  ]
  for child in node.children {
    entries.append(semantics_inspector_entries(child, nodes, depth=depth + 1))
  }
  entries
}

///|
fn render_inspector_from_commands(
  commands : Array[DrawCommand],
) -> RenderInspectorSnapshot {
  let mut text_count = 0
  let mut image_count = 0
  let mut clip_depth = 0
  let mut max_clip_depth = 0
  let mut layer_depth = 0
  let mut max_layer_depth = 0
  let mut filter_depth = 0
  let mut max_filter_depth = 0
  let mut path_count = 0
  let mut shader_count = 0
  let mut unbalanced_pop_count = 0
  for command in commands {
    match command {
      DrawText(_) => text_count = text_count + 1
      DrawImage(_) => image_count = image_count + 1
      PushClip(_) | PushRoundedClip(_) => {
        clip_depth = clip_depth + 1
        max_clip_depth = runtime_devtools_max_int(max_clip_depth, clip_depth)
      }
      PopClip | PopRoundedClip =>
        if clip_depth == 0 {
          unbalanced_pop_count = unbalanced_pop_count + 1
        } else {
          clip_depth = clip_depth - 1
        }
      PushLayer(_) | BeginRetainedLayer(_) => {
        layer_depth = layer_depth + 1
        max_layer_depth = runtime_devtools_max_int(max_layer_depth, layer_depth)
      }
      PopLayer | EndRetainedLayer(_) =>
        if layer_depth == 0 {
          unbalanced_pop_count = unbalanced_pop_count + 1
        } else {
          layer_depth = layer_depth - 1
        }
      PushFilter(_) => {
        filter_depth = filter_depth + 1
        max_filter_depth = runtime_devtools_max_int(
          max_filter_depth, filter_depth,
        )
      }
      PopFilter =>
        if filter_depth == 0 {
          unbalanced_pop_count = unbalanced_pop_count + 1
        } else {
          filter_depth = filter_depth - 1
        }
      DrawPath(_) => path_count = path_count + 1
      DrawShaderEffect(_) => shader_count = shader_count + 1
      _ => ()
    }
  }
  {
    command_count: commands.length(),
    text_count,
    image_count,
    clip_depth: max_clip_depth,
    open_clip_depth: clip_depth,
    layer_depth: max_layer_depth,
    open_layer_depth: layer_depth,
    filter_depth: max_filter_depth,
    open_filter_depth: filter_depth,
    path_count,
    shader_count,
    unbalanced_pop_count,
    nodes: [],
  }
}

///|
fn semantics_depth_from(
  id : SemanticsNodeId,
  nodes : Map[SemanticsNodeId, SemanticsNode],
) -> Int {
  match nodes.get(id) {
    Some(node) => {
      let mut depth = 1
      for child in node.children {
        depth = runtime_devtools_max_int(
          depth,
          1 + semantics_depth_from(child, nodes),
        )
      }
      depth
    }
    None => 0
  }
}

///|
fn runtime_devtools_max_int(a : Int, b : Int) -> Int {
  if a > b {
    a
  } else {
    b
  }
}