///|
priv struct MeasuredNode {
  element_id : ElementId
  size : Size
  flex_weight : Double
  children : Array[MeasuredNode]
} derive(Eq, Debug, ToJson)

///|
priv struct PlacedNode {
  element_id : ElementId
  frame : Rect
  children : Array[PlacedNode]
  /// Element child indices whose placed frames are overlay-positioned: their
  /// render subtrees are hoisted and painted after the whole tree so popup
  /// content escapes ancestor clip rects.
  overlay_child_indices : Array[Int]
  /// Hit region: the frame intersected with the accumulated ancestor clip.
  /// Hit-testing uses this instead of paint bounds so clipped content never
  /// wins hit-test over content painted below it.
  hit_rect : Rect
} derive(Eq, Debug, ToJson)

///|
fn PlacedNode::to_layout_result(self : PlacedNode) -> LayoutResult {
  {
    frame: self.frame,
    children: self.children.map(child => child.to_layout_result()),
  }
}

///|
fn ElementNode::measure_with_text_system(
  self : ElementNode,
  constraints : Constraints,
  environment~ : Environment,
  style? : ViewStyle = ViewStyle::empty(),
  text_system~ : TextSystem,
) -> MeasuredNode {
  if !self.has_dirty_layout() &&
    self.layout_cache.cached_measure_constraints == Some(constraints) &&
    self.layout_cache.cached_measure_environment == Some(environment) &&
    self.layout_cache.cached_measure_style == Some(style) {
    match self.layout_cache.cached_measured {
      Some(measured) => {
        self.layout_cache.hit_count = self.layout_cache.hit_count + 1
        return measured
      }
      None => ()
    }
  }
  self.layout_cache.miss_count = self.layout_cache.miss_count + 1
  let child_constraints = self.view_node.child_constraints(constraints)
  let child_environment = self.view_node.child_environment(environment)
  let child_style = self.view_node.child_style(style)
  let children : Array[MeasuredNode] = []
  for child in self.children {
    children.push(
      child.measure_with_text_system(
        child_constraints,
        environment=child_environment,
        style=child_style,
        text_system~,
      ),
    )
  }
  let view_node_layout = self.view_node.layout({
    constraints,
    child_sizes: children.map(child => child.size),
    child_flex_weights: children.map(child => child.flex_weight),
    state: self.control.view_state,
    text_system,
    environment,
    style,
  })
  let measured = {
    element_id: self.id,
    size: constraints.constrain(view_node_layout.size),
    flex_weight: self.view_node.flex_weight(),
    children,
  }
  self.layout_cache.cached_measure_constraints = Some(constraints)
  self.layout_cache.cached_measure_environment = Some(environment)
  self.layout_cache.cached_measure_style = Some(style)
  self.layout_cache.cached_measured = Some(measured)
  self.dirty.needs_layout = false
  self.refresh_descendant_dirty_flags()
  measured
}

///|
fn ElementNode::place_with_text_system(
  self : ElementNode,
  bounds : Rect,
  environment~ : Environment,
  style? : ViewStyle = ViewStyle::empty(),
  text_system~ : TextSystem,
  layout_direction? : LayoutDirection = LeftToRight,
  overlay_overrides? : Array[(Int, Array[(Int, Rect)])] = [],
) -> PlacedNode {
  if !self.has_dirty_layout() &&
    self.layout_cache.cached_place_frame == Some(bounds) &&
    self.layout_cache.cached_place_environment == Some(environment) &&
    self.layout_cache.cached_place_style == Some(style) &&
    overlay_overrides.is_empty() {
    match self.layout_cache.cached_placed {
      Some(placed) => {
        self.layout_cache.hit_count = self.layout_cache.hit_count + 1
        return placed
      }
      None => ()
    }
  }
  let constraints = Constraints::tight(bounds.size)
  let measured = self.measure_with_text_system(
    constraints,
    environment~,
    style~,
    text_system~,
  )
  let placed = self.place_measured_with_text_system(
    measured,
    bounds,
    environment~,
    style~,
    text_system~,
    layout_direction~,
    overlay_overrides~,
  )
  self.layout_cache.cached_place_frame = Some(bounds)
  self.layout_cache.cached_place_environment = Some(environment)
  self.layout_cache.cached_place_style = Some(style)
  self.layout_cache.cached_placed = Some(placed)
  self.refresh_descendant_dirty_flags()
  placed
}

///|
fn ElementNode::place_measured_with_text_system(
  self : ElementNode,
  measured : MeasuredNode,
  frame : Rect,
  environment~ : Environment,
  style~ : ViewStyle,
  text_system~ : TextSystem,
  layout_direction~ : LayoutDirection,
  overlay_overrides? : Array[(Int, Array[(Int, Rect)])] = [],
  accumulated_clip? : Rect? = None,
  fit_measured_sizes? : Bool = false,
) -> PlacedNode {
  // A tight-fit re-measure already settled this subtree under the final
  // frame's tight constraints, so its `measured` child sizes are the wrapped
  // sizes; feed them to `layout` instead of the unbounded intrinsic sizes so
  // internal frames reflect the extent the children actually occupy (ADR 0035).
  let child_sizes : Array[Size] = []
  if fit_measured_sizes && measured.children.length() == self.children.length() {
    for index, child in self.children {
      child_sizes.push(
        match measured.children.get(index) {
          Some(node) => node.size
          None => child.intrinsic_size(text_system~)
        },
      )
    }
  } else {
    for child in self.children {
      child_sizes.push(child.intrinsic_size(text_system~))
    }
  }
  let result = self.view_node.layout({
    constraints: Constraints::tight(frame.size),
    child_sizes,
    child_flex_weights: measured.children.map(child => child.flex_weight),
    state: self.control.view_state,
    text_system,
    environment,
    style,
  })
  let child_frames = result.child_frames.map(child_frame => {
    child_frame.offset(dx=frame.origin.x, dy=frame.origin.y)
  })
  let overrides = overlay_overrides_for(overlay_overrides, self.id.value())
  let child_environment = self.view_node.child_environment(environment)
  let child_style = self.view_node.child_style(style)
  // A clip container folds its own frame into the accumulated ancestor clip;
  // children inherit that, and their hit regions are the intersection.
  let child_clip : Rect? = if result.clips_children {
    match accumulated_clip {
      Some(clip) => frame.intersection(clip)
      None => Some(frame)
    }
  } else {
    accumulated_clip
  }
  let children : Array[PlacedNode] = []
  let overlay_child_indices : Array[Int] = []
  for index, child in self.children {
    if index < child_frames.length() && index < measured.children.length() {
      // Overlay-positioned children skip their parent-assigned provisional
      // frame and are placed directly at the absolute rect computed by the
      // post-layout placement pass.
      let is_overlay = overlay_child_override(overrides, index) is Some(_)
      let child_frame = match overlay_child_override(overrides, index) {
        Some(override_frame) => override_frame
        None => child_frames[index]
      }
      // Tight-fit parents (flex with weighted children) measure children
      // under looser constraints than the frame they finally place them in.
      // When the final frame is a different size, re-measure that subtree
      // under tight constraints so its internal layout re-wraps to the
      // extent it actually occupies; the measure memo is constraint-keyed, so
      // this is cache-safe and stays silent once the subtree settles (ADR 0035).
      let refits = result.child_frames_tight &&
        child_frame.size != measured.children[index].size
      let child_measured = if refits {
        child.measure_with_text_system(
          Constraints::tight(child_frame.size),
          environment=child_environment,
          style=child_style,
          text_system~,
        )
      } else {
        measured.children[index]
      }
      children.push(
        child.place_measured_with_text_system(
          child_measured,
          child_frame,
          environment=child_environment,
          style=child_style,
          text_system~,
          layout_direction~,
          overlay_overrides~,
          // Hoisted popups escape ancestor clips by design.
          accumulated_clip=if is_overlay { None } else { child_clip },
          fit_measured_sizes=refits || fit_measured_sizes,
        ),
      )
      if is_overlay {
        overlay_child_indices.push(index)
      }
    }
  }
  {
    element_id: self.id,
    frame,
    children,
    overlay_child_indices,
    hit_rect: match accumulated_clip {
      Some(clip) =>
        match frame.intersection(clip) {
          Some(hit) => hit
          None => Rect::new(x=0.0, y=0.0, width=0.0, height=0.0)
        }
      None => frame
    },
  }
}

///|
fn overlay_overrides_for(
  overrides : Array[(Int, Array[(Int, Rect)])],
  element_id : Int,
) -> Array[(Int, Rect)]? {
  for entry in overrides {
    if entry.0 == element_id {
      return Some(entry.1)
    }
  }
  None
}

///|
fn overlay_child_override(pairs : Array[(Int, Rect)]?, index : Int) -> Rect? {
  match pairs {
    Some(pairs) =>
      for pair in pairs {
        if pair.0 == index {
          return Some(pair.1)
        }
      }
    None => ()
  }
  None
}

///|
fn ElementNode::intrinsic_size(
  self : ElementNode,
  text_system~ : TextSystem,
) -> Size {
  if !self.has_dirty_layout() {
    match self.layout_cache.cached_intrinsic_size {
      Some(size) => return size
      None => ()
    }
    match self.layout_cache.cached_measured {
      Some(measured) => return measured.size
      None => ()
    }
  }
  let size = self.view_node.layout({
      constraints: Constraints::unbounded(),
      child_sizes: self.children.map(child => child.intrinsic_size(text_system~)),
      child_flex_weights: self.children.map(child => {
        child.view_node.flex_weight()
      }),
      state: self.control.view_state,
      // Thread the host's text system instead of the core fallback. Intrinsic
      // sizing feeds the child_sizes handed to a parent during placement, and
      // the fallback estimates advances from font size alone, which
      // underestimates real glyph runs. A view then receives a frame narrower
      // than the glyphs it draws, and renderers clip glyphs to that frame,
      // truncating the text.
      text_system,
      environment: Environment::neutral(),
      style: ViewStyle::empty(),
    }).size
  self.layout_cache.cached_intrinsic_size = Some(size)
  size
}