///|
pub(all) struct LayoutResult {
  frame : Rect
  children : Array[LayoutResult]
} derive(Eq, Debug, ToJson)

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

///|
pub(all) struct PlacedNode {
  element_id : ElementId
  frame : Rect
  children : Array[PlacedNode]
} derive(Eq, Debug, ToJson)

///|
fn layout_spec(spec : ViewSpec, bounds : Rect) -> LayoutResult {
  mount_view_spec(spec).place(bounds).to_layout_result()
}

///|
fn layout_spec_with_text_system(
  spec : ViewSpec,
  bounds : Rect,
  text_system~ : TextSystem,
) -> LayoutResult {
  mount_view_spec(spec)
  .place_with_text_system(bounds, text_system~)
  .to_layout_result()
}

///|
pub fn hit_test(layout : LayoutResult, point : Point) -> Bool {
  if !layout.frame.contains(point) {
    false
  } else if layout.children.length() == 0 {
    true
  } else {
    layout.children.any(child => hit_test(child, point))
  }
}

///|
fn ElementNode::measure(
  self : ElementNode,
  constraints : Constraints,
) -> MeasuredNode {
  self.measure_with_text_system(constraints, text_system=TextSystem::fallback())
}

///|
fn ElementNode::measure_with_text_system(
  self : ElementNode,
  constraints : Constraints,
  text_system~ : TextSystem,
) -> MeasuredNode {
  if !self.dirty.needs_layout &&
    self.layout_cache.cached_measure_constraints == Some(constraints) {
    match self.layout_cache.cached_measured {
      Some(measured) => return measured
      None => ()
    }
  }
  let child_constraints = child_constraints_for_measure(self.spec, constraints)
  let children : Array[MeasuredNode] = []
  for child in self.children {
    children.push(
      child.measure_with_text_system(child_constraints, text_system~),
    )
  }
  let measured = {
    element_id: self.id,
    size: constraints.constrain(
      self.measured_size_from_children(children, text_system~),
    ),
    children,
  }
  self.layout_cache.cached_measure_constraints = Some(constraints)
  self.layout_cache.cached_measured = Some(measured)
  self.dirty.needs_layout = false
  measured
}

///|
fn ElementNode::place(self : ElementNode, bounds : Rect) -> PlacedNode {
  self.place_with_text_system(bounds, text_system=TextSystem::fallback())
}

///|
fn ElementNode::place_with_text_system(
  self : ElementNode,
  bounds : Rect,
  text_system~ : TextSystem,
  layout_direction? : LayoutDirection = LayoutDirection::LeftToRight,
) -> PlacedNode {
  if !self.dirty.needs_layout &&
    self.layout_cache.cached_place_frame == Some(bounds) {
    match self.layout_cache.cached_placed {
      Some(placed) => return placed
      None => ()
    }
  }
  let constraints = Constraints::tight(bounds.size)
  let measured = self.measure_with_text_system(constraints, text_system~)
  let placed = self.place_measured_with_text_system(
    measured,
    bounds,
    text_system~,
    layout_direction~,
  )
  self.layout_cache.cached_place_frame = Some(bounds)
  self.layout_cache.cached_placed = Some(placed)
  placed
}

///|
fn ElementNode::place_measured_with_text_system(
  self : ElementNode,
  measured : MeasuredNode,
  frame : Rect,
  text_system~ : TextSystem,
  layout_direction~ : LayoutDirection,
) -> PlacedNode {
  let child_frames = self.child_frames_for_place(
    frame,
    text_system~,
    layout_direction~,
  )
  let children : Array[PlacedNode] = []
  for index, child in self.children {
    if index < child_frames.length() && index < measured.children.length() {
      children.push(
        child.place_measured_with_text_system(
          measured.children[index],
          child_frames[index],
          text_system~,
          layout_direction~,
        ),
      )
    }
  }
  { element_id: self.id, frame, children }
}

///|
fn ElementNode::measured_size_from_children(
  self : ElementNode,
  children : Array[MeasuredNode],
  text_system~ : TextSystem,
) -> Size {
  match self.spec {
    EmptySpec => Size::new(width=0.0, height=0.0)
    TextSpec(data) => data.size
    ButtonSpec(data) => data.size
    TextFieldSpec(data) => data.size
    RichTextEditorSpec(data) => data.size
    ImageSpec(data) => data.size.unwrap_or(Size::new(width=0.0, height=0.0))
    CheckboxSpec(data) => checkbox_intrinsic_size(data)
    PickerSpec(_) => Size::new(width=200.0, height=40.0)
    DatePickerSpec(_) => Size::new(width=200.0, height=40.0)
    SearchBarSpec(_) => Size::new(width=240.0, height=40.0)
    SheetSpec(_) =>
      if children.length() > 0 {
        children[0].size.inflate(
          Insets::symmetric(horizontal=16.0, vertical=16.0),
        )
      } else {
        Size::new(width=240.0, height=200.0)
      }
    SpacerSpec(data) =>
      if data.weight > 0.0 {
        Size::new(width=0.0, height=0.0)
      } else {
        Size::new(width=data.min_length, height=data.min_length)
      }
    PaddingSpec(data) =>
      if children.length() > 0 {
        children[0].size.inflate(data.insets)
      } else {
        Size::new(width=0.0, height=0.0).inflate(data.insets)
      }
    ContainerSpec(data) =>
      match data.size {
        Some(size) => size
        None =>
          if children.length() > 0 {
            children[0].size
          } else {
            Size::new(width=0.0, height=0.0)
          }
      }
    FlexSpec(data) => measure_flex_measured(data.axis, data.spacing, children)
    FrameSpec(data) => {
      let child_size = if children.length() > 0 {
        children[0].size
      } else {
        Size::new(width=0.0, height=0.0)
      }
      resolve_frame_size(data, child_size)
    }
    StackSpec(_) => measure_stack_measured(children)
    ScrollSpec(data) =>
      match data.viewport {
        Some(size) => size
        None =>
          if children.length() > 0 {
            children[0].size
          } else {
            Size::new(width=0.0, height=0.0)
          }
      }
    GridSpec(data) =>
      measure_grid_measured(
        data.columns,
        data.row_spacing,
        data.column_spacing,
        children,
      )
    ListSpec(data) => measure_list_measured(data.spacing, children)
    ModifiedSpec(data) => modified_measured_size(data.modifier, children)
    CustomSpec(data) => (data.measure)(Constraints::unbounded())
    CustomLayoutSpec(data) =>
      (data.measure)({
        constraints: Constraints::unbounded(),
        child_sizes: children.map(child => child.size),
        child_baselines: self.children.map(child => {
          child.first_baseline(text_system~)
        }),
        child_priorities: self.children.map(child => child.layout_priority()),
        child_alignment_guides: custom_alignment_guides(
          children.map(child => child.size),
          self.children.map(child => child.first_baseline(text_system~)),
        ),
        cache: data.cache,
        cache_key: custom_layout_cache_key(
          self.spec,
          children.map(child => child.size),
        ),
        layout_direction: LayoutDirection::LeftToRight,
        safe_area: Insets::symmetric(),
        viewport: None,
      })
    ComponentSpec(_) =>
      if children.length() > 0 {
        children[0].size
      } else {
        Size::new(width=0.0, height=0.0)
      }
  }
}

///|
fn child_constraints_for_measure(
  spec : ViewSpec,
  constraints : Constraints,
) -> Constraints {
  match spec {
    PaddingSpec(data) => constraints.deflate(data.insets)
    SheetSpec(_) =>
      constraints.deflate(Insets::symmetric(horizontal=16.0, vertical=16.0))
    FrameSpec(data) => frame_constraints_for_measure(data, constraints)
    ModifiedSpec(data) => modified_child_constraints(data.modifier, constraints)
    ScrollSpec(_) => Constraints::unbounded()
    ComponentSpec(_) => constraints
    _ => constraints.loosen()
  }
}

///|
fn frame_constraints_for_measure(
  data : FrameViewSpec,
  constraints : Constraints,
) -> Constraints {
  match (data.width, data.height) {
    (Some(width), Some(height)) => constraints.tighten(width~, height~).loosen()
    (Some(width), None) => constraints.tighten(width~).loosen()
    (None, Some(height)) => constraints.tighten(height~).loosen()
    (None, None) => constraints.loosen()
  }
}

///|
fn frame_modifier_constraints_for_measure(
  data : FrameModifierSpec,
  constraints : Constraints,
) -> Constraints {
  match (data.width, data.height) {
    (Some(width), Some(height)) => constraints.tighten(width~, height~).loosen()
    (Some(width), None) => constraints.tighten(width~).loosen()
    (None, Some(height)) => constraints.tighten(height~).loosen()
    (None, None) => constraints.loosen()
  }
}

///|
fn ElementNode::child_frames_for_place(
  self : ElementNode,
  frame : Rect,
  text_system~ : TextSystem,
  layout_direction~ : LayoutDirection,
) -> Array[Rect] {
  match self.taffy_child_frames_for_place(frame) {
    Some(frames) => return frames
    None => ()
  }
  match self.spec {
    FlexSpec(data) =>
      flex_child_frames(
        data.axis,
        data.spacing,
        data.main_axis_alignment,
        data.cross_axis_alignment,
        self.children,
        frame,
      )
    PaddingSpec(data) => [frame.inset(data.insets)]
    ContainerSpec(data) => {
      let child_frame = match data.size {
        Some(size) =>
          Rect::new(
            x=frame.origin.x,
            y=frame.origin.y,
            width=size.width,
            height=size.height,
          )
        None => frame
      }
      if self.children.length() > 0 {
        [child_frame]
      } else {
        []
      }
    }
    FrameSpec(data) => [frame_rect_for_spec(data, frame)]
    StackSpec(_) => self.children.map(_child => frame)
    ScrollSpec(data) =>
      if self.children.length() > 0 {
        [scroll_child_frame(frame, data, self.children[0])]
      } else {
        []
      }
    SheetSpec(_) =>
      if self.children.length() > 0 {
        [frame.inset(Insets::symmetric(horizontal=16.0, vertical=16.0))]
      } else {
        []
      }
    GridSpec(data) =>
      grid_child_frames(
        data.columns,
        data.row_spacing,
        data.column_spacing,
        self.children,
        frame,
      )
    ListSpec(data) => list_child_frames(data.spacing, self.children, frame)
    CustomLayoutSpec(data) =>
      (data.place)({
        frame,
        measured_size: frame.size,
        child_sizes: self.children.map(child => child.intrinsic_size()),
        child_baselines: self.children.map(child => {
          child.first_baseline(text_system~)
        }),
        child_priorities: self.children.map(child => child.layout_priority()),
        child_alignment_guides: custom_alignment_guides(
          self.children.map(child => child.intrinsic_size()),
          self.children.map(child => child.first_baseline(text_system~)),
        ),
        cache: data.cache,
        cache_key: custom_layout_cache_key(
          self.spec,
          self.children.map(child => child.intrinsic_size()),
        ),
        layout_direction,
        safe_area: Insets::symmetric(),
        viewport: Some(frame.size),
      })
    ModifiedSpec(data) => modified_child_frames(data.modifier, frame)
    ComponentSpec(_) => if self.children.length() > 0 { [frame] } else { [] }
    _ => []
  }
}

///|
fn scroll_child_frame(
  frame : Rect,
  data : ScrollViewSpec,
  child : ElementNode,
) -> Rect {
  let viewport = data.viewport.unwrap_or(frame.size)
  let child_size = child.intrinsic_size()
  Rect::new(
    x=frame.origin.x - data.offset.x,
    y=frame.origin.y - data.offset.y,
    width=max_double(viewport.width, child_size.width),
    height=max_double(viewport.height, child_size.height),
  )
}

///|
fn modified_measured_size(
  modifier : ViewModifier,
  children : Array[MeasuredNode],
) -> Size {
  let child_size = if children.length() > 0 {
    children[0].size
  } else {
    Size::new(width=0.0, height=0.0)
  }
  match modifier {
    PaddingModifier(insets) => child_size.inflate(insets)
    FrameModifier(data) => resolve_frame_modifier_size(data, child_size)
    BackgroundModifier(_)
    | ForegroundModifier(_)
    | FontModifier(_)
    | CornerRadiusModifier(_)
    | OpacityModifier(_)
    | ShadowModifier(_)
    | BorderModifier(_)
    | OffsetModifier(_, _)
    | ClipModifier
    | ScaleModifier(_, _)
    | DisabledModifier(_)
    | AccessibilityLabelModifier(_)
    | SemanticsRoleModifier(_)
    | FocusableModifier(_)
    | OnTapModifier(_)
    | OnFileDropModifier(_)
    | KeyboardShortcutModifier(_)
    | FlexibleModifier(_)
    | LayoutPriorityModifier(_)
    | AlignModifier(_)
    | IntrinsicWidthModifier
    | IntrinsicHeightModifier => child_size
    AspectRatioModifier(ratio) => aspect_ratio_size(child_size, ratio)
  }
}

///|
fn modified_child_constraints(
  modifier : ViewModifier,
  constraints : Constraints,
) -> Constraints {
  match modifier {
    PaddingModifier(insets) => constraints.deflate(insets)
    FrameModifier(data) =>
      frame_modifier_constraints_for_measure(data, constraints)
    BackgroundModifier(_)
    | ForegroundModifier(_)
    | FontModifier(_)
    | CornerRadiusModifier(_)
    | OpacityModifier(_)
    | ShadowModifier(_)
    | BorderModifier(_)
    | OffsetModifier(_, _)
    | ClipModifier
    | ScaleModifier(_, _)
    | DisabledModifier(_)
    | AccessibilityLabelModifier(_)
    | SemanticsRoleModifier(_)
    | FocusableModifier(_)
    | OnTapModifier(_)
    | OnFileDropModifier(_)
    | KeyboardShortcutModifier(_)
    | FlexibleModifier(_)
    | LayoutPriorityModifier(_)
    | AlignModifier(_)
    | AspectRatioModifier(_)
    | IntrinsicWidthModifier
    | IntrinsicHeightModifier => constraints.loosen()
  }
}

///|
fn modified_child_frames(modifier : ViewModifier, frame : Rect) -> Array[Rect] {
  match modifier {
    PaddingModifier(insets) => [frame.inset(insets)]
    FrameModifier(data) => [frame_rect_for_modifier(data, frame)]
    BackgroundModifier(_)
    | ForegroundModifier(_)
    | FontModifier(_)
    | CornerRadiusModifier(_)
    | OpacityModifier(_)
    | ShadowModifier(_)
    | BorderModifier(_)
    | ClipModifier
    | ScaleModifier(_, _)
    | DisabledModifier(_)
    | AccessibilityLabelModifier(_)
    | SemanticsRoleModifier(_)
    | FocusableModifier(_)
    | OnTapModifier(_)
    | OnFileDropModifier(_)
    | KeyboardShortcutModifier(_)
    | FlexibleModifier(_)
    | LayoutPriorityModifier(_)
    | AlignModifier(_)
    | IntrinsicWidthModifier
    | IntrinsicHeightModifier => [frame]
    OffsetModifier(dx, dy) => [frame.offset(dx~, dy~)]
    AspectRatioModifier(ratio) => [aspect_ratio_rect(frame, ratio)]
  }
}

///|
fn aspect_ratio_size(size : Size, ratio : Double) -> Size {
  if size.width > 0.0 {
    Size::new(width=size.width, height=size.width / ratio)
  } else if size.height > 0.0 {
    Size::new(width=size.height * ratio, height=size.height)
  } else {
    size
  }
}

///|
fn aspect_ratio_rect(frame : Rect, ratio : Double) -> Rect {
  let target = aspect_ratio_size(frame.size, ratio).clamp(
    Constraints::loose(frame.size),
  )
  Rect::new(
    x=frame.origin.x,
    y=frame.origin.y,
    width=target.width,
    height=target.height,
  )
}

///|
fn measure_flex_measured(
  axis : Axis,
  spacing : Double,
  children : Array[MeasuredNode],
) -> Size {
  let mut main = 0.0
  let mut cross = 0.0
  for index, child in children {
    match axis {
      Axis::Horizontal => {
        main = main + child.size.width
        cross = max_double(cross, child.size.height)
      }
      Axis::Vertical => {
        main = main + child.size.height
        cross = max_double(cross, child.size.width)
      }
    }
    if index > 0 {
      main = main + spacing
    }
  }
  match axis {
    Axis::Horizontal => Size::new(width=main, height=cross)
    Axis::Vertical => Size::new(width=cross, height=main)
  }
}

///|
fn measure_stack_measured(children : Array[MeasuredNode]) -> Size {
  let mut width = 0.0
  let mut height = 0.0
  for child in children {
    width = max_double(width, child.size.width)
    height = max_double(height, child.size.height)
  }
  Size::new(width~, height~)
}

///|
fn measure_grid_measured(
  columns : Int,
  row_spacing : Double,
  column_spacing : Double,
  children : Array[MeasuredNode],
) -> Size {
  let count = children.length()
  if count == 0 {
    Size::new(width=0.0, height=0.0)
  } else {
    let columns = max_int(1, columns)
    let rows = grid_row_count(count, columns)
    let mut cell_width = 0.0
    let mut cell_height = 0.0
    for child in children {
      cell_width = max_double(cell_width, child.size.width)
      cell_height = max_double(cell_height, child.size.height)
    }
    Size::new(
      width=cell_width * columns.to_double() +
        column_spacing * max_int(0, columns - 1).to_double(),
      height=cell_height * rows.to_double() +
        row_spacing * max_int(0, rows - 1).to_double(),
    )
  }
}

///|
fn measure_list_measured(
  spacing : Double,
  children : Array[MeasuredNode],
) -> Size {
  let mut width = 0.0
  let mut height = 0.0
  for index, child in children {
    width = max_double(width, child.size.width)
    height = height + child.size.height
    if index > 0 {
      height = height + spacing
    }
  }
  Size::new(width~, height~)
}

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