///|
/// A breakpoint maps a canvas width to a named production layout profile.
pub(all) struct BreakpointSpec {
  name : String
  min_width : Double
  max_width : Double?
  columns : Int
  gutter : Double
  padding : Insets
} derive(Eq, Debug, ToJson)

///|
pub(all) struct ResponsiveCanvas {
  name : String
  bounds : Rect
  breakpoints : Array[BreakpointSpec]
  active : BreakpointSpec?
  boxes : Array[LayoutBox]
} derive(Debug)

///|
pub(all) enum AnchorEdge {
  AnchorLeft
  AnchorTop
  AnchorRight
  AnchorBottom
  AnchorCenterX
  AnchorCenterY
  AnchorWidth
  AnchorHeight
} derive(Eq, Debug, ToJson)

///|
pub(all) struct LayoutAnchor {
  source : Int
  source_edge : AnchorEdge
  target : Int?
  target_edge : AnchorEdge
  constant : Double
  priority : Int
} derive(Eq, Debug, ToJson)

///|
pub(all) struct ResponsiveItem {
  id : Int
  preferred_width : Double
  preferred_height : Double
  min_width : Double
  min_height : Double
  max_width : Double
  max_height : Double
  aspect : Double?
  grow : Double
  shrink : Double
  frame : Rect
} derive(Eq, Debug, ToJson)

///|
pub(all) struct ResponsiveLayoutReport {
  item_count : Int
  occupied_area : Double
  overflow_area : Double
  gap_error : Double
  aspect_error : Double
  overlap_count : Int
  score : Double
} derive(Eq, Debug, ToJson)

///|
pub fn BreakpointSpec::new(
  name : String,
  min_width : Double,
  columns : Int,
) -> BreakpointSpec {
  {
    name,
    min_width: min_width.max(0.0),
    max_width: None,
    columns: columns.max(1),
    gutter: 0.0,
    padding: Insets::uniform(0.0),
  }
}

///|
pub fn BreakpointSpec::until(
  self : BreakpointSpec,
  max_width : Double,
) -> BreakpointSpec {
  { ..self, max_width: Some(max_width.max(self.min_width)) }
}

///|
pub fn BreakpointSpec::with_gutter(
  self : BreakpointSpec,
  gutter : Double,
) -> BreakpointSpec {
  { ..self, gutter: gutter.max(0.0) }
}

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

///|
pub fn breakpoint_matches(spec : BreakpointSpec, width : Double) -> Bool {
  let range_ok = match spec.max_width {
    Some(maximum) => width <= maximum
    None => true
  }
  width >= spec.min_width && range_ok
}

///|
pub fn choose_breakpoint(
  specs : Array[BreakpointSpec],
  width : Double,
) -> BreakpointSpec? {
  let mut selected : BreakpointSpec? = None
  for spec in specs {
    if breakpoint_matches(spec, width) {
      match selected {
        None => selected = Some(spec)
        Some(previous) =>
          if spec.min_width >= previous.min_width {
            selected = Some(spec)
          }
      }
    }
  }
  selected
}

///|
pub fn normalize_breakpoints(
  specs : Array[BreakpointSpec],
) -> Array[BreakpointSpec] {
  let result = specs.copy()
  for index in 1.. 0 && result[position - 1].min_width > value.min_width {
      result[position] = result[position - 1]
      position -= 1
    }
    result[position] = value
  }
  result
}

///|
pub fn breakpoint_gaps(specs : Array[BreakpointSpec]) -> Array[String] {
  let errors = []
  let ordered = normalize_breakpoints(specs)
  if ordered.length() > 1 {
    for index in 0..<(ordered.length() - 1) {
      match ordered[index].max_width {
        Some(maximum) =>
          if maximum < ordered[index + 1].min_width {
            errors.push(
              "gap between " +
              ordered[index].name +
              " and " +
              ordered[index + 1].name,
            )
          }
        None =>
          if index + 1 < ordered.length() {
            errors.push(
              "unbounded breakpoint before " + ordered[index + 1].name,
            )
          }
      }
    }
  }
  errors
}

///|
pub fn ResponsiveCanvas::new(name : String, bounds : Rect) -> ResponsiveCanvas {
  { name, bounds, breakpoints: [], active: None, boxes: [] }
}

///|
pub fn ResponsiveCanvas::add_breakpoint(
  self : ResponsiveCanvas,
  spec : BreakpointSpec,
) -> Unit {
  self.breakpoints.push(spec)
}

///|
pub fn ResponsiveCanvas::select(
  self : ResponsiveCanvas,
  width : Double,
) -> ResponsiveCanvas {
  { ..self, active: choose_breakpoint(self.breakpoints, width) }
}

///|
pub fn ResponsiveCanvas::current_columns(self : ResponsiveCanvas) -> Int {
  match self.active {
    Some(spec) => spec.columns
    None => 1
  }
}

///|
pub fn ResponsiveCanvas::current_area(self : ResponsiveCanvas) -> Rect {
  match self.active {
    Some(spec) => spec.padding.apply(self.bounds)
    None => self.bounds
  }
}

///|
pub fn ResponsiveCanvas::with_boxes(
  self : ResponsiveCanvas,
  boxes : Array[LayoutBox],
) -> ResponsiveCanvas {
  { ..self, boxes, }
}

///|
pub fn LayoutAnchor::new(
  source : Int,
  source_edge : AnchorEdge,
  target : Int?,
  target_edge : AnchorEdge,
  constant : Double,
  priority : Int,
) -> LayoutAnchor {
  {
    source,
    source_edge,
    target,
    target_edge,
    constant,
    priority: priority.max(0),
  }
}

///|
pub fn anchor_value(edge : AnchorEdge, frame : Rect) -> Double {
  match edge {
    AnchorLeft => frame.x
    AnchorTop => frame.y
    AnchorRight => frame.right()
    AnchorBottom => frame.bottom()
    AnchorCenterX => frame.center().x
    AnchorCenterY => frame.center().y
    AnchorWidth => frame.width
    AnchorHeight => frame.height
  }
}

///|
pub fn set_anchor_value(
  edge : AnchorEdge,
  frame : Rect,
  value : Double,
) -> Rect {
  match edge {
    AnchorLeft => Rect::new(value, frame.y, frame.width, frame.height)
    AnchorTop => Rect::new(frame.x, value, frame.width, frame.height)
    AnchorRight =>
      Rect::new(frame.x, frame.y, (value - frame.x).max(0.0), frame.height)
    AnchorBottom =>
      Rect::new(frame.x, frame.y, frame.width, (value - frame.y).max(0.0))
    AnchorCenterX =>
      Rect::new(value - frame.width / 2.0, frame.y, frame.width, frame.height)
    AnchorCenterY =>
      Rect::new(frame.x, value - frame.height / 2.0, frame.width, frame.height)
    AnchorWidth => Rect::new(frame.x, frame.y, value.max(0.0), frame.height)
    AnchorHeight => Rect::new(frame.x, frame.y, frame.width, value.max(0.0))
  }
}

///|
pub fn apply_layout_anchor(frames : Array[Rect], anchor : LayoutAnchor) -> Unit {
  if anchor.source >= 0 && anchor.source < frames.length() {
    let base = match anchor.target {
      Some(target) =>
        if target >= 0 && target < frames.length() {
          anchor_value(anchor.target_edge, frames[target])
        } else {
          0.0
        }
      None => 0.0
    }
    frames[anchor.source] = set_anchor_value(
      anchor.source_edge,
      frames[anchor.source],
      base + anchor.constant,
    )
  }
}

///|
pub fn apply_layout_anchors(
  frames : Array[Rect],
  anchors : Array[LayoutAnchor],
  passes : Int,
) -> Array[Rect] {
  let result = frames.copy()
  let ordered = anchors.copy()
  for index in 1.. 0 && ordered[position - 1].priority < value.priority {
      ordered[position] = ordered[position - 1]
      position -= 1
    }
    ordered[position] = value
  }
  for _ in 0.. ResponsiveItem {
  {
    id,
    preferred_width: width.max(0.0),
    preferred_height: height.max(0.0),
    min_width: 0.0,
    min_height: 0.0,
    max_width: 1000000.0,
    max_height: 1000000.0,
    aspect: None,
    grow: 0.0,
    shrink: 1.0,
    frame: Rect::new(0.0, 0.0, width, height),
  }
}

///|
pub fn ResponsiveItem::min_size(
  self : ResponsiveItem,
  width : Double,
  height : Double,
) -> ResponsiveItem {
  { ..self, min_width: width.max(0.0), min_height: height.max(0.0) }
}

///|
pub fn ResponsiveItem::max_size(
  self : ResponsiveItem,
  width : Double,
  height : Double,
) -> ResponsiveItem {
  {
    ..self,
    max_width: width.max(self.min_width),
    max_height: height.max(self.min_height),
  }
}

///|
pub fn ResponsiveItem::ratio(
  self : ResponsiveItem,
  aspect : Double,
) -> ResponsiveItem {
  { ..self, aspect: if aspect > 0.0 { Some(aspect) } else { None } }
}

///|
pub fn ResponsiveItem::flex(
  self : ResponsiveItem,
  grow : Double,
  shrink : Double,
) -> ResponsiveItem {
  { ..self, grow: grow.max(0.0), shrink: shrink.max(0.0) }
}

///|
pub fn responsive_item_size(
  item : ResponsiveItem,
  available : Double,
) -> Double {
  item.preferred_width
  .min(available.max(0.0))
  .max(item.min_width)
  .min(item.max_width)
}

///|
pub fn responsive_row(
  bounds : Rect,
  items : Array[ResponsiveItem],
  gap : Double,
  align : LayoutAlign,
) -> Array[ResponsiveItem] {
  let result = items.copy()
  let spacing = gap.max(0.0)
  let mut preferred = 0.0
  let mut flex_total = 0.0
  for item in result {
    preferred += item.preferred_width
    flex_total += item.grow
  }
  let available = (bounds.width -
  spacing * (result.length() - 1).max(0).to_double()).max(0.0)
  let extra = (available - preferred).max(0.0)
  let mut cursor = bounds.x
  for index, item in result {
    let flex_share = if flex_total > 0.0 {
      extra * item.grow / flex_total
    } else {
      0.0
    }
    let width = (item.preferred_width + flex_share)
      .max(item.min_width)
      .min(item.max_width)
      .min(available)
    let height = item.preferred_height
      .min(bounds.height)
      .max(item.min_height)
      .min(item.max_height)
    let cross_offset = match align {
      Start | Stretch => 0.0
      Center => (bounds.height - height).max(0.0) / 2.0
      End => (bounds.height - height).max(0.0)
    }
    let y = bounds.y + cross_offset
    let actual_height = if align is Stretch { bounds.height } else { height }
    result[index] = {
      ..item,
      frame: Rect::new(cursor, y, width, actual_height),
    }
    cursor += width + spacing
  }
  result
}

///|
pub fn responsive_column(
  bounds : Rect,
  items : Array[ResponsiveItem],
  gap : Double,
  align : LayoutAlign,
) -> Array[ResponsiveItem] {
  let result = items.copy()
  let spacing = gap.max(0.0)
  let mut preferred = 0.0
  let mut flex_total = 0.0
  for item in result {
    preferred += item.preferred_height
    flex_total += item.grow
  }
  let available = (bounds.height -
  spacing * (result.length() - 1).max(0).to_double()).max(0.0)
  let extra = (available - preferred).max(0.0)
  let mut cursor = bounds.y
  for index, item in result {
    let flex_share = if flex_total > 0.0 {
      extra * item.grow / flex_total
    } else {
      0.0
    }
    let height = (item.preferred_height + flex_share)
      .max(item.min_height)
      .min(item.max_height)
      .min(available)
    let width = item.preferred_width
      .min(bounds.width)
      .max(item.min_width)
      .min(item.max_width)
    let cross_offset = match align {
      Start | Stretch => 0.0
      Center => (bounds.width - width).max(0.0) / 2.0
      End => (bounds.width - width).max(0.0)
    }
    let x = bounds.x + cross_offset
    let actual_width = if align is Stretch { bounds.width } else { width }
    result[index] = {
      ..item,
      frame: Rect::new(x, cursor, actual_width, height),
    }
    cursor += height + spacing
  }
  result
}

///|
pub fn responsive_grid(
  bounds : Rect,
  items : Array[ResponsiveItem],
  columns : Int,
  gap_x : Double,
  gap_y : Double,
) -> Array[ResponsiveItem] {
  let result = items.copy()
  let column_count = columns.max(1)
  let width = (bounds.width - gap_x.max(0.0) * (column_count - 1).to_double()).max(
      0.0,
    ) /
    column_count.to_double()
  let row_count = if result.length() == 0 {
    0
  } else {
    (result.length() + column_count - 1) / column_count
  }
  let row_height = if row_count == 0 {
    0.0
  } else {
    (bounds.height - gap_y.max(0.0) * (row_count - 1).to_double()).max(0.0) /
    row_count.to_double()
  }
  for index, item in result {
    let row = index / column_count
    let column = index % column_count
    let frame_width = responsive_item_size(item, width)
    let frame_height = match item.aspect {
      Some(aspect) => (frame_width / aspect).min(row_height)
      None => item.preferred_height.min(row_height)
    }
    let frame = Rect::new(
      bounds.x +
      column.to_double() * (width + gap_x.max(0.0)) +
      (width - frame_width) / 2.0,
      bounds.y +
      row.to_double() * (row_height + gap_y.max(0.0)) +
      (row_height - frame_height).max(0.0) / 2.0,
      frame_width,
      frame_height,
    )
    result[index] = { ..item, frame, }
  }
  result
}

///|
pub fn responsive_masonry(
  bounds : Rect,
  items : Array[ResponsiveItem],
  columns : Int,
  gap : Double,
) -> Array[ResponsiveItem] {
  let result = items.copy()
  let count = columns.max(1)
  let column_width = (bounds.width - gap.max(0.0) * (count - 1).to_double()).max(
      0.0,
    ) /
    count.to_double()
  let heights = []
  for _ in 0.. width / aspect
      None => item.preferred_height
    }
    let x = bounds.x +
      selected.to_double() * (column_width + gap.max(0.0)) +
      (column_width - width).max(0.0) / 2.0
    let frame = Rect::new(
      x,
      heights[selected],
      width,
      height.max(item.min_height).min(item.max_height),
    )
    result[index] = { ..item, frame, }
    heights[selected] = frame.bottom() + gap.max(0.0)
  }
  result
}

///|
pub fn responsive_pack(
  bounds : Rect,
  items : Array[ResponsiveItem],
  gap : Double,
) -> Array[ResponsiveItem] {
  let result = items.copy()
  let mut x = bounds.x
  let mut y = bounds.y
  let mut line_height = 0.0
  for index, item in result {
    let width = responsive_item_size(item, bounds.width)
    let height = item.preferred_height.min(bounds.height)
    if x > bounds.x && x + width > bounds.right() {
      x = bounds.x
      y += line_height + gap.max(0.0)
      line_height = 0.0
    }
    if y + height > bounds.bottom() {
      result[index] = {
        ..item,
        frame: Rect::new(x, bounds.bottom(), width, 0.0),
      }
    } else {
      result[index] = { ..item, frame: Rect::new(x, y, width, height) }
    }
    x += width + gap.max(0.0)
    line_height = line_height.max(height)
  }
  result
}

///|
pub fn responsive_apply_aspect(
  items : Array[ResponsiveItem],
) -> Array[ResponsiveItem] {
  let result = items.copy()
  for index, item in result {
    match item.aspect {
      Some(aspect) => {
        let height = (item.frame.width / aspect)
          .min(item.max_height)
          .max(item.min_height)
        result[index] = {
          ..item,
          frame: Rect::new(item.frame.x, item.frame.y, item.frame.width, height),
        }
      }
      None => ()
    }
  }
  result
}

///|
pub fn responsive_resolve_anchors(
  items : Array[ResponsiveItem],
  anchors : Array[LayoutAnchor],
  passes : Int,
) -> Array[ResponsiveItem] {
  let frames = []
  for item in items {
    frames.push(item.frame)
  }
  let resolved = apply_layout_anchors(frames, anchors, passes)
  let result = items.copy()
  for index, item in result {
    result[index] = { ..item, frame: resolved[index] }
  }
  result
}

///|
pub fn responsive_overflow(
  items : Array[ResponsiveItem],
  bounds : Rect,
) -> Double {
  let mut total = 0.0
  for item in items {
    let clipped = item.frame.intersect(bounds)
    total += item.frame.width * item.frame.height -
      clipped.width * clipped.height
  }
  total.max(0.0)
}

///|
pub fn responsive_overlap_count(
  items : Array[ResponsiveItem],
  tolerance : Double,
) -> Int {
  let mut count = 0
  if items.length() > 1 {
    for i in 0..<(items.length() - 1) {
      for j in (i + 1).. tolerance.max(0.0) &&
          intersection.height > tolerance.max(0.0) {
          count += 1
        }
      }
    }
  }
  count
}

///|
pub fn responsive_report(
  items : Array[ResponsiveItem],
  bounds : Rect,
  expected_gap : Double,
) -> ResponsiveLayoutReport {
  let mut occupied = 0.0
  let mut aspect_error = 0.0
  for item in items {
    occupied += item.frame.width * item.frame.height
    match item.aspect {
      Some(aspect) =>
        aspect_error += (item.frame.width / item.frame.height.max(0.0001) -
        aspect).abs()
      None => ()
    }
  }
  let mut gap_error = 0.0
  if items.length() > 1 {
    for i in 0..<(items.length() - 1) {
      for j in (i + 1).. String {
  "item_count,occupied_area,overflow_area,gap_error,aspect_error,overlap_count,score\n" +
  report.item_count.to_string() +
  "," +
  report.occupied_area.to_string() +
  "," +
  report.overflow_area.to_string() +
  "," +
  report.gap_error.to_string() +
  "," +
  report.aspect_error.to_string() +
  "," +
  report.overlap_count.to_string() +
  "," +
  report.score.to_string() +
  "\n"
}

///|
pub fn responsive_layout_variants(
  bounds : Rect,
  items : Array[ResponsiveItem],
  specs : Array[BreakpointSpec],
) -> Array[(String, Array[ResponsiveItem], ResponsiveLayoutReport)] {
  let result = []
  for spec in specs {
    let area = spec.padding.apply(bounds)
    let laid_out = responsive_grid(
      area,
      items,
      spec.columns,
      spec.gutter,
      spec.gutter,
    )
    result.push(
      (spec.name, laid_out, responsive_report(laid_out, area, spec.gutter)),
    )
  }
  result
}

///|
pub fn responsive_layout_signature(items : Array[ResponsiveItem]) -> String {
  let builder = StringBuilder()
  for item in items {
    builder.write_string(
      item.id.to_string() +
      ":" +
      item.frame.x.to_string() +
      "," +
      item.frame.y.to_string() +
      "," +
      item.frame.width.to_string() +
      "," +
      item.frame.height.to_string() +
      ";",
    )
  }
  builder.to_string()
}

///|
pub fn responsive_safe_area(
  bounds : Rect,
  margin : Double,
  aspect : Double?,
) -> Rect {
  let inset = bounds.expand(-margin.max(0.0))
  match aspect {
    None => inset
    Some(ratio) => responsive_fit_rect(inset, ratio)
  }
}

///|
fn responsive_fit_rect(bounds : Rect, aspect : Double) -> Rect {
  if aspect <= 0.0 {
    bounds
  } else {
    let current = bounds.width / bounds.height.max(0.0001)
    if current > aspect {
      let width = bounds.height * aspect
      Rect::new(bounds.center().x - width / 2.0, bounds.y, width, bounds.height)
    } else {
      let height = bounds.width / aspect
      Rect::new(
        bounds.x,
        bounds.center().y - height / 2.0,
        bounds.width,
        height,
      )
    }
  }
}

///|
pub fn responsive_center_item(
  item : ResponsiveItem,
  bounds : Rect,
) -> ResponsiveItem {
  let frame = Rect::new(
    bounds.center().x - item.frame.width / 2.0,
    bounds.center().y - item.frame.height / 2.0,
    item.frame.width,
    item.frame.height,
  )
  { ..item, frame, }
}

///|
pub fn responsive_translate(
  items : Array[ResponsiveItem],
  offset : Vec2,
) -> Array[ResponsiveItem] {
  let result = items.copy()
  for index, item in result {
    result[index] = {
      ..item,
      frame: Rect::new(
        item.frame.x + offset.x,
        item.frame.y + offset.y,
        item.frame.width,
        item.frame.height,
      ),
    }
  }
  result
}

///|
pub fn responsive_scale(
  items : Array[ResponsiveItem],
  center : Vec2,
  scale : Double,
) -> Array[ResponsiveItem] {
  let result = items.copy()
  let factor = scale.max(0.0)
  for index, item in result {
    let position = Vec2::new(item.frame.x, item.frame.y)
      .sub(center)
      .scale(factor)
      .add(center)
    result[index] = {
      ..item,
      frame: Rect::new(
        position.x,
        position.y,
        item.frame.width * factor,
        item.frame.height * factor,
      ),
    }
  }
  result
}