///|
/// Insets applied inside a layout canvas.
pub(all) struct Insets {
  top : Double
  right : Double
  bottom : Double
  left : Double
} derive(Eq, Debug, ToJson)

///|
pub fn Insets::new(
  top : Double,
  right : Double,
  bottom : Double,
  left : Double,
) -> Insets {
  {
    top: top.max(0.0),
    right: right.max(0.0),
    bottom: bottom.max(0.0),
    left: left.max(0.0),
  }
}

///|
pub fn Insets::uniform(value : Double) -> Insets {
  Insets::new(value, value, value, value)
}

///|
pub fn Insets::horizontal(self : Insets) -> Double {
  self.left + self.right
}

///|
pub fn Insets::vertical(self : Insets) -> Double {
  self.top + self.bottom
}

///|
pub fn Insets::apply(self : Insets, bounds : Rect) -> Rect {
  Rect::new(
    bounds.x + self.left,
    bounds.y + self.top,
    (bounds.width - self.horizontal()).max(0.0),
    (bounds.height - self.vertical()).max(0.0),
  )
}

///|
pub(all) enum LayoutDirection {
  Row
  Column
} derive(Eq, Debug, ToJson)

///|
pub(all) enum LayoutAlign {
  Start
  Center
  End
  Stretch
} derive(Eq, Debug, ToJson)

///|
pub(all) enum LayoutJustify {
  Start
  Center
  End
  SpaceBetween
  SpaceAround
  SpaceEvenly
} derive(Eq, Debug, ToJson)

///|
/// A placed content panel with a stable index for downstream rendering.
pub(all) struct LayoutBox {
  index : Int
  row : Int
  column : Int
  frame : Rect
  content : Rect
  center : Vec2
} derive(Eq, Debug, ToJson)

///|
/// Grid and flow layout configuration for poster and packaging compositions.
pub(all) struct LayoutSpec {
  bounds : Rect
  columns : Int
  rows : Int
  gap_x : Double
  gap_y : Double
  padding : Insets
  align : LayoutAlign
  justify : LayoutJustify
} derive(Eq, Debug, ToJson)

///|
pub fn LayoutSpec::new(bounds : Rect, columns : Int, rows : Int) -> LayoutSpec {
  {
    bounds,
    columns: columns.max(0),
    rows: rows.max(0),
    gap_x: 0.0,
    gap_y: 0.0,
    padding: Insets::uniform(0.0),
    align: Stretch,
    justify: Start,
  }
}

///|
pub fn LayoutSpec::with_gap(
  self : LayoutSpec,
  gap_x : Double,
  gap_y : Double,
) -> LayoutSpec {
  { ..self, gap_x: gap_x.max(0.0), gap_y: gap_y.max(0.0) }
}

///|
pub fn LayoutSpec::with_padding(
  self : LayoutSpec,
  padding : Insets,
) -> LayoutSpec {
  { ..self, padding, }
}

///|
pub fn LayoutSpec::aligned(
  self : LayoutSpec,
  align : LayoutAlign,
  justify : LayoutJustify,
) -> LayoutSpec {
  { ..self, align, justify }
}

///|
fn offset_for_justify(
  justify : LayoutJustify,
  available : Double,
  count : Int,
  item : Double,
  gap : Double,
) -> (Double, Double) {
  if count <= 0 {
    (0.0, 0.0)
  } else {
    let used = item * count.to_double() + gap * (count - 1).to_double()
    let free = (available - used).max(0.0)
    match justify {
      Start => (0.0, gap)
      Center => (free / 2.0, gap)
      End => (free, gap)
      SpaceBetween =>
        (
          0.0,
          if count <= 1 {
            0.0
          } else {
            gap + free / (count - 1).to_double()
          },
        )
      SpaceAround => {
        let unit = free / count.to_double()
        (unit / 2.0, gap + unit)
      }
      SpaceEvenly => {
        let unit = free / (count + 1).to_double()
        (unit, gap + unit)
      }
    }
  }
}

///|
fn align_offset(
  align : LayoutAlign,
  available : Double,
  item : Double,
) -> Double {
  let free = (available - item).max(0.0)
  match align {
    Start => 0.0
    Center => free / 2.0
    End => free
    Stretch => 0.0
  }
}

///|
fn cross_size(
  align : LayoutAlign,
  available : Double,
  preferred : Double,
) -> Double {
  match align {
    Stretch => available.max(0.0)
    _ => preferred.min(available.max(0.0)).max(0.0)
  }
}

///|
/// Generates a regular grid of content boxes.
pub fn layout_grid(spec : LayoutSpec) -> Array[LayoutBox] {
  let result = []
  let area = spec.padding.apply(spec.bounds)
  if spec.columns > 0 && spec.rows > 0 {
    let total_gap_x = spec.gap_x * (spec.columns - 1).to_double()
    let total_gap_y = spec.gap_y * (spec.rows - 1).to_double()
    let cell_width = ((area.width - total_gap_x) / spec.columns.to_double()).max(
      0.0,
    )
    let cell_height = ((area.height - total_gap_y) / spec.rows.to_double()).max(
      0.0,
    )
    for row in 0.. Array[LayoutBox] {
  let result = []
  if count > 0 {
    let main = match direction {
      Row => bounds.width
      Column => bounds.height
    }
    let cross = match direction {
      Row => bounds.height
      Column => bounds.width
    }
    let preferred_cross = item_size.min(cross).max(0.0)
    let (start, step) = offset_for_justify(
      justify,
      main,
      count,
      item_size.min(main).max(0.0),
      gap.max(0.0),
    )
    for index in 0..
          Rect::new(
            bounds.x + main_position,
            bounds.y + cross_position,
            item_size.min(main),
            cross_size(align, cross, preferred_cross),
          )
        Column =>
          Rect::new(
            bounds.x + cross_position,
            bounds.y + main_position,
            cross_size(align, cross, preferred_cross),
            item_size.min(main),
          )
      }
      result.push({
        index,
        row: if direction is Row {
          0
        } else {
          index
        },
        column: if direction is Row {
          index
        } else {
          0
        },
        frame,
        content: frame,
        center: frame.center(),
      })
    }
  }
  result
}

///|
/// Splits a frame into proportional horizontal panels.
pub fn split_horizontal(
  bounds : Rect,
  weights : Array[Double],
  gap : Double,
) -> Array[LayoutBox] {
  split_weighted(bounds, weights, gap, Row)
}

///|
/// Splits a frame into proportional vertical panels.
pub fn split_vertical(
  bounds : Rect,
  weights : Array[Double],
  gap : Double,
) -> Array[LayoutBox] {
  split_weighted(bounds, weights, gap, Column)
}

///|
fn split_weighted(
  bounds : Rect,
  weights : Array[Double],
  gap : Double,
  direction : LayoutDirection,
) -> Array[LayoutBox] {
  let result = []
  let total_weight = weights.fold(init=0.0, fn(total, value) {
    total + value.max(0.0)
  })
  if weights.length() > 0 && total_weight > 0.0 {
    let total_gap = gap.max(0.0) * (weights.length() - 1).to_double()
    let available = match direction {
      Row => (bounds.width - total_gap).max(0.0)
      Column => (bounds.height - total_gap).max(0.0)
    }
    let mut cursor = 0.0
    for index, weight in weights {
      let size = available * weight.max(0.0) / total_weight
      let frame = match direction {
        Row => Rect::new(bounds.x + cursor, bounds.y, size, bounds.height)
        Column => Rect::new(bounds.x, bounds.y + cursor, bounds.width, size)
      }
      result.push({
        index,
        row: if direction is Row {
          0
        } else {
          index
        },
        column: if direction is Row {
          index
        } else {
          0
        },
        frame,
        content: frame,
        center: frame.center(),
      })
      cursor += size + gap.max(0.0)
    }
  }
  result
}

///|
/// Fits a preferred aspect ratio inside every layout box.
pub fn fit_layout_boxes(
  boxes : Array[LayoutBox],
  aspect : Double,
) -> Array[LayoutBox] {
  let result = []
  if aspect <= 0.0 {
    boxes.copy()
  } else {
    for box_ in boxes {
      let current = box_.frame.width / box_.frame.height.max(0.000001)
      let fitted = if current > aspect {
        let width = box_.frame.height * aspect
        Rect::new(
          box_.frame.center().x - width / 2.0,
          box_.frame.y,
          width,
          box_.frame.height,
        )
      } else {
        let height = box_.frame.width / aspect
        Rect::new(
          box_.frame.x,
          box_.frame.center().y - height / 2.0,
          box_.frame.width,
          height,
        )
      }
      result.push({ ..box_, content: fitted, center: fitted.center() })
    }
    result
  }
}

///|
/// Places points evenly along a sampled path.
pub fn layout_on_path(
  path : Path,
  count : Int,
  subdivisions : Int,
  closed : Bool,
) -> Array[LayoutBox] {
  let result = []
  let points = path.flatten(subdivisions~)
  for index, sample in sample_polyline(points, count, closed~) {
    let frame = Rect::new(sample.point.x, sample.point.y, 0.0, 0.0)
    result.push({
      index,
      row: 0,
      column: index,
      frame,
      content: frame,
      center: sample.point,
    })
  }
  result
}

///|
/// Generates a centered golden-angle layout for decorative accents.
pub fn golden_angle_layout(
  center : Vec2,
  count : Int,
  radial_step : Double,
  phase : Double,
) -> Array[LayoutBox] {
  let result = []
  if count > 0 {
    let golden = @math.PI * (3.0 - 5.0.sqrt())
    for index in 0.. Vec2 {
  if spacing <= 0.0 {
    point
  } else {
    Vec2::new(
      (point.x / spacing).to_int().to_double() * spacing,
      (point.y / spacing).to_int().to_double() * spacing,
    )
  }
}

///|
/// Returns boxes that overlap another box by more than the requested tolerance.
pub fn overlapping_layout_boxes(
  boxes : Array[LayoutBox],
  tolerance : Double,
) -> Array[(Int, Int)] {
  let result = []
  let threshold = tolerance.max(0.0)
  if boxes.length() > 1 {
    for i in 0..<(boxes.length() - 1) {
      for j in (i + 1).. threshold && intersection.height > threshold {
          result.push((boxes[i].index, boxes[j].index))
        }
      }
    }
  }
  result
}

///|
/// Measures occupied area and bounds for a panel collection.
pub fn layout_metrics(boxes : Array[LayoutBox]) -> LayoutMetrics {
  let rectangles = []
  for box_ in boxes {
    rectangles.push(box_.content)
  }
  let bounds = if rectangles.length() == 0 {
    Rect::new(0.0, 0.0, 0.0, 0.0)
  } else {
    let mut result = rectangles[0]
    for rectangle in rectangles[1:] {
      result = result.union(rectangle)
    }
    result
  }
  let mut area = 0.0
  for rectangle in rectangles {
    area += rectangle.width * rectangle.height
  }
  {
    element_count: boxes.length(),
    occupied_area: area,
    path_length: 0.0,
    density: if bounds.width * bounds.height == 0.0 {
      0.0
    } else {
      area / (bounds.width * bounds.height)
    },
    bounds,
  }
}

///|
/// Creates an inset frame for a title, body and footer stack.
pub fn poster_stack(
  bounds : Rect,
  title_ratio : Double,
  footer_ratio : Double,
  gap : Double,
) -> Array[LayoutBox] {
  let body_ratio = (1.0 - title_ratio.max(0.0) - footer_ratio.max(0.0)).max(0.0)
  split_vertical(
    bounds,
    [title_ratio.max(0.0), body_ratio, footer_ratio.max(0.0)],
    gap,
  )
}

///|
/// Returns a safe area after applying a percentage bleed on every side.
pub fn bleed_safe_area(bounds : Rect, bleed : Double) -> Rect {
  let amount_x = bounds.width * bleed.max(0.0).min(0.5)
  let amount_y = bounds.height * bleed.max(0.0).min(0.5)
  Rect::new(
    bounds.x + amount_x,
    bounds.y + amount_y,
    (bounds.width - 2.0 * amount_x).max(0.0),
    (bounds.height - 2.0 * amount_y).max(0.0),
  )
}