///|
fn measure_flex_sizes(
  axis : Axis,
  spacing : Double,
  children : Array[ElementNode],
) -> Size {
  let mut main = 0.0
  let mut cross = 0.0
  for index, child in children {
    let size = child.intrinsic_size()
    match axis {
      Axis::Horizontal => {
        main = main + size.width
        cross = max_double(cross, size.height)
      }
      Axis::Vertical => {
        main = main + size.height
        cross = max_double(cross, 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_size(children : Array[ElementNode]) -> Size {
  let mut width = 0.0
  let mut height = 0.0
  for child in children {
    let size = child.intrinsic_size()
    width = max_double(width, size.width)
    height = max_double(height, size.height)
  }
  Size::new(width~, height~)
}

///|
fn measure_grid_size(
  columns : Int,
  row_spacing : Double,
  column_spacing : Double,
  children : Array[ElementNode],
) -> 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 {
      let size = child.intrinsic_size()
      cell_width = max_double(cell_width, size.width)
      cell_height = max_double(cell_height, 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_size(spacing : Double, children : Array[ElementNode]) -> Size {
  let mut width = 0.0
  let mut height = 0.0
  for index, child in children {
    let size = child.intrinsic_size()
    width = max_double(width, size.width)
    height = height + size.height
    if index > 0 {
      height = height + spacing
    }
  }
  Size::new(width~, height~)
}

///|
fn resolve_frame_size(data : FrameViewSpec, child : Size) -> Size {
  let width = data.width.unwrap_or(child.width)
  let height = data.height.unwrap_or(child.height)
  let min_width = data.min_width.unwrap_or(0.0)
  let min_height = data.min_height.unwrap_or(0.0)
  let max_width = data.max_width.unwrap_or(width)
  let max_height = data.max_height.unwrap_or(height)
  Size::new(width~, height~).clamp({
    min: Size::new(width=min_width, height=min_height),
    max: Size::new(
      width=max_double(min_width, max_width),
      height=max_double(min_height, max_height),
    ),
  })
}

///|
fn frame_rect_for_spec(data : FrameViewSpec, bounds : Rect) -> Rect {
  let size = resolve_frame_size(data, bounds.size)
  Rect::new(
    x=bounds.origin.x,
    y=bounds.origin.y,
    width=size.width,
    height=size.height,
  )
}

///|
fn resolve_frame_modifier_size(data : FrameModifierSpec, child : Size) -> Size {
  let width = data.width.unwrap_or(child.width)
  let height = data.height.unwrap_or(child.height)
  let min_width = data.min_width.unwrap_or(0.0)
  let min_height = data.min_height.unwrap_or(0.0)
  let max_width = data.max_width.unwrap_or(width)
  let max_height = data.max_height.unwrap_or(height)
  Size::new(width~, height~).clamp({
    min: Size::new(width=min_width, height=min_height),
    max: Size::new(
      width=max_double(min_width, max_width),
      height=max_double(min_height, max_height),
    ),
  })
}

///|
fn frame_rect_for_modifier(data : FrameModifierSpec, bounds : Rect) -> Rect {
  let size = resolve_frame_modifier_size(data, bounds.size)
  Rect::new(
    x=bounds.origin.x,
    y=bounds.origin.y,
    width=size.width,
    height=size.height,
  )
}

///|
fn modified_intrinsic_size(
  modifier : ViewModifier,
  children : Array[ElementNode],
) -> Size {
  let child_size = if children.length() > 0 {
    children[0].intrinsic_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 ElementNode::first_baseline(
  self : ElementNode,
  text_system? : TextSystem = TextSystem::fallback(),
) -> Double? {
  match self.spec {
    TextSpec(data) =>
      Some(
        text_system.measure_text(
          TextLayoutInput::new(text=data.text, font=data.font),
        ).baseline,
      )
    ButtonSpec(data) =>
      Some(
        text_system.measure_text(
          TextLayoutInput::new(text=data.text, font=data.font),
        ).baseline,
      )
    TextFieldSpec(data) =>
      Some(
        text_system.measure_text(
          TextLayoutInput::new(text=data.value.get(), font=data.font),
        ).baseline,
      )
    RichTextEditorSpec(data) =>
      Some(
        text_system.measure_text(
          TextLayoutInput::new(text=data.value.get(), font=data.font),
        ).baseline,
      )
    CheckboxSpec(data) =>
      Some(
        text_system.measure_text(
          TextLayoutInput::new(text=data.label, font=data.font),
        ).baseline,
      )
    ModifiedSpec(_) | ComponentSpec(_) =>
      if self.children.length() > 0 {
        self.children[0].first_baseline(text_system~)
      } else {
        None
      }
    _ => None
  }
}

///|
fn ElementNode::layout_priority(self : ElementNode) -> Double {
  match self.spec {
    ModifiedSpec(data) =>
      match data.modifier {
        LayoutPriorityModifier(priority) => priority
        _ =>
          if self.children.length() > 0 {
            self.children[0].layout_priority()
          } else {
            0.0
          }
      }
    ComponentSpec(_) =>
      if self.children.length() > 0 {
        self.children[0].layout_priority()
      } else {
        0.0
      }
    _ => 0.0
  }
}

///|
fn custom_layout_cache_key(
  _spec : ViewSpec,
  child_sizes : Array[Size],
) -> String {
  let prefix = "custom-layout"
  let parts : Array[String] = []
  for size in child_sizes {
    parts.push("\{size.width}x\{size.height}")
  }
  prefix + ":" + parts.join(",")
}

///|
fn custom_alignment_guides(
  child_sizes : Array[Size],
  child_baselines : Array[Double?],
) -> Array[AlignmentGuideSet] {
  let guides : Array[AlignmentGuideSet] = []
  for index, size in child_sizes {
    let baseline = if index < child_baselines.length() {
      child_baselines[index]
    } else {
      None
    }
    guides.push(AlignmentGuideSet::from_size(size, first_baseline=baseline))
  }
  guides
}

///|
fn spacer_weight(child : ElementNode) -> Double {
  match child.spec {
    SpacerSpec(data) => data.weight
    ModifiedSpec(data) =>
      match data.modifier {
        FlexibleModifier(weight) => weight
        _ =>
          if child.children.length() > 0 {
            spacer_weight(child.children[0])
          } else {
            0.0
          }
      }
    _ => 0.0
  }
}

///|
fn child_main_size(axis : Axis, size : Size) -> Double {
  match axis {
    Axis::Horizontal => size.width
    Axis::Vertical => size.height
  }
}

///|
fn flex_child_frames(
  axis : Axis,
  spacing : Double,
  main_axis_alignment : MainAxisAlignment,
  cross_axis_alignment : CrossAxisAlignment,
  children : Array[ElementNode],
  bounds : Rect,
) -> Array[Rect] {
  let count = children.length()
  let frames : Array[Rect] = []
  if count == 0 {
    frames
  } else {
    let total_spacing = spacing * (count.to_double() - 1.0)
    let mut child_main_total = 0.0
    let mut total_weight = 0.0
    let child_sizes = children.map(child => child.intrinsic_size())
    for index, size in child_sizes {
      let weight = spacer_weight(children[index])
      if weight > 0.0 {
        total_weight = total_weight + weight
        child_main_total = child_main_total + child_main_size(axis, size)
      } else {
        child_main_total = child_main_total + child_main_size(axis, size)
      }
    }
    let available_main = match axis {
      Axis::Horizontal => bounds.size.width
      Axis::Vertical => bounds.size.height
    }
    let remaining = max_double(
      0.0,
      available_main - child_main_total - total_spacing,
    )
    let alignment_remaining = if total_weight > 0.0 { 0.0 } else { remaining }
    let offsets = flex_offsets(
      main_axis_alignment, alignment_remaining, spacing, count,
    )
    let mut cursor = offsets.leading
    for index, size in child_sizes {
      let weight = spacer_weight(children[index])
      let main_size = if weight > 0.0 && total_weight > 0.0 {
        child_main_size(axis, size) + remaining * weight / total_weight
      } else {
        child_main_size(axis, size)
      }
      let cross_size = match axis {
        Axis::Horizontal => size.height
        Axis::Vertical => size.width
      }
      let cross_available = match axis {
        Axis::Horizontal => bounds.size.height
        Axis::Vertical => bounds.size.width
      }
      let cross = cross_axis_offset(
        cross_axis_alignment, cross_available, cross_size,
      )
      let frame = match axis {
        Axis::Horizontal =>
          Rect::new(
            x=bounds.origin.x + cursor,
            y=bounds.origin.y + cross.offset,
            width=main_size,
            height=cross.size,
          )
        Axis::Vertical =>
          Rect::new(
            x=bounds.origin.x + cross.offset,
            y=bounds.origin.y + cursor,
            width=cross.size,
            height=main_size,
          )
      }
      frames.push(frame)
      cursor = cursor + main_size + offsets.gap
    }
    frames
  }
}

///|
priv struct FlexOffsets {
  leading : Double
  gap : Double
}

///|
fn flex_offsets(
  alignment : MainAxisAlignment,
  remaining : Double,
  base_gap : Double,
  count : Int,
) -> FlexOffsets {
  match alignment {
    MainStart => { leading: 0.0, gap: base_gap }
    MainCenter => { leading: remaining / 2.0, gap: base_gap }
    MainEnd => { leading: remaining, gap: base_gap }
    SpaceBetween =>
      if count > 1 {
        { leading: 0.0, gap: base_gap + remaining / (count.to_double() - 1.0) }
      } else {
        { leading: 0.0, gap: base_gap }
      }
    SpaceAround => {
      let extra = remaining / count.to_double()
      { leading: extra / 2.0, gap: base_gap + extra }
    }
    SpaceEvenly => {
      let extra = remaining / (count.to_double() + 1.0)
      { leading: extra, gap: base_gap + extra }
    }
  }
}

///|
priv struct CrossAxisOffset {
  offset : Double
  size : Double
}

///|
fn cross_axis_offset(
  alignment : CrossAxisAlignment,
  available : Double,
  child : Double,
) -> CrossAxisOffset {
  match alignment {
    CrossStart => { offset: 0.0, size: child }
    CrossCenter =>
      { offset: max_double(0.0, available - child) / 2.0, size: child }
    CrossEnd => { offset: max_double(0.0, available - child), size: child }
    CrossStretch => { offset: 0.0, size: available }
  }
}

///|
fn grid_row_count(count : Int, columns : Int) -> Int {
  let columns = max_int(1, columns)
  (count + columns - 1) / columns
}

///|
fn grid_child_frames(
  columns : Int,
  row_spacing : Double,
  column_spacing : Double,
  children : Array[ElementNode],
  bounds : Rect,
) -> Array[Rect] {
  let frames : Array[Rect] = []
  if children.length() == 0 {
    frames
  } else {
    let columns = max_int(1, columns)
    let rows = grid_row_count(children.length(), columns)
    let total_column_spacing = column_spacing *
      max_int(0, columns - 1).to_double()
    let total_row_spacing = row_spacing * max_int(0, rows - 1).to_double()
    let cell_width = max_double(
      0.0,
      (bounds.size.width - total_column_spacing) / columns.to_double(),
    )
    let measured = measure_grid_size(
      columns, row_spacing, column_spacing, children,
    )
    let fallback_height = if rows > 0 {
      max_double(
        0.0,
        (bounds.size.height - total_row_spacing) / rows.to_double(),
      )
    } else {
      0.0
    }
    let cell_height = if measured.height > 0.0 {
      max_double(0.0, (measured.height - total_row_spacing) / rows.to_double())
    } else {
      fallback_height
    }
    for index, _child in children {
      let column = index - index / columns * columns
      let row = index / columns
      frames.push(
        Rect::new(
          x=bounds.origin.x + column.to_double() * (cell_width + column_spacing),
          y=bounds.origin.y + row.to_double() * (cell_height + row_spacing),
          width=cell_width,
          height=cell_height,
        ),
      )
    }
    frames
  }
}

///|
fn list_child_frames(
  spacing : Double,
  children : Array[ElementNode],
  bounds : Rect,
) -> Array[Rect] {
  let frames : Array[Rect] = []
  let mut cursor = bounds.origin.y
  for index, child in children {
    if index > 0 {
      cursor = cursor + spacing
    }
    let size = child.intrinsic_size()
    frames.push(
      Rect::new(
        x=bounds.origin.x,
        y=cursor,
        width=bounds.size.width,
        height=size.height,
      ),
    )
    cursor = cursor + size.height
  }
  frames
}

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