///|
/// Errors raised by grid layout construction and indexing.
pub(all) suberror LayoutError {
  InvalidDimension
  InvalidSpacing
  InvalidRatios
  InvalidBounds
  CellOutOfBounds
}

///|
/// A rectangular area. Coordinate units and origin are defined by the
/// producing API: layouts return display pixels; Axes::get_position returns
/// normalized Figure coordinates with a lower-left origin.
pub struct Rect {
  x : Double
  y : Double
  width : Double
  height : Double
}

///|
/// Construct a rectangle without imposing coordinate units.
///
/// ```mbt check
/// test {
///   let bounds = @gridspec.rect(10.0, 20.0, 30.0, 40.0)
///   assert_eq((bounds.x(), bounds.y()), (10.0, 20.0))
///   assert_eq((bounds.width(), bounds.height()), (30.0, 40.0))
/// }
/// ```
pub fn rect(x : Double, y : Double, width : Double, height : Double) -> Rect {
  { x, y, width, height, }
}

///|
/// The mutable layout state shared by a GridSpec and its nested children.
priv struct GridLayout {
  rows : Int32
  columns : Int32
  mut hspace : Double?
  mut vspace : Double?
  mut width_ratios : Array[Double]
  mut height_ratios : Array[Double]
}

///|
/// The span that owns a nested GridSpec.
///
/// The reference keeps changes to a parent GridSpec's ratios visible to
/// children, matching Matplotlib's GridSpecFromSubplotSpec behavior.
priv struct GridParent {
  layout : @ref.Ref[GridLayout]
  row_start : Int32
  row_end : Int32
  column_start : Int32
  column_end : Int32
}

///|
pub fn Rect::x(self : Rect) -> Double {
  self.x
}

///|
pub fn Rect::y(self : Rect) -> Double {
  self.y
}

///|
pub fn Rect::width(self : Rect) -> Double {
  self.width
}

///|
pub fn Rect::height(self : Rect) -> Double {
  self.height
}

///|
/// A regular rows-by-columns grid with deterministic spacing.
pub struct GridSpec {
  rows : Int32
  columns : Int32
  mut hspace : Double
  mut vspace : Double
  mut width_ratios : Array[Double]
  mut height_ratios : Array[Double]
  priv mut parent_chain : Array[GridParent]
  priv layout_ref : @ref.Ref[GridLayout]
  share_x : ShareMode
  share_y : ShareMode
}

///|
/// A rectangular span of rows and columns in a GridSpec.
pub struct SubplotSpec {
  grid : GridSpec
  row_start : Int32
  row_end : Int32
  column_start : Int32
  column_end : Int32
}

///|
/// Construct a regular grid. Spacing is measured as a fraction of the cell size.
/// `hspace` is horizontal and `vspace` vertical. Omitted values inherit the
/// figure's spacing during layout, falling back to 0.1 for standalone grids.
pub fn gridspec(
  rows : Int32,
  columns : Int32,
  hspace? : Double,
  vspace? : Double,
  width_ratios~ : Array[Double] = [],
  height_ratios~ : Array[Double] = [],
) -> GridSpec raise LayoutError {
  if rows <= 0 || columns <= 0 {
    raise LayoutError::InvalidDimension
  }
  validate_spacing(hspace.unwrap_or(0.1))
  validate_spacing(vspace.unwrap_or(0.1))
  let width_ratios = normalize_ratios(columns, width_ratios)
  let height_ratios = normalize_ratios(rows, height_ratios)
  let layout_ref = @ref.new({
    rows,
    columns,
    hspace,
    vspace,
    width_ratios: width_ratios.copy(),
    height_ratios: height_ratios.copy(),
  })
  {
    rows,
    columns,
    hspace: hspace.unwrap_or(0.1),
    vspace: vspace.unwrap_or(0.1),
    width_ratios,
    height_ratios,
    parent_chain: [],
    layout_ref,
    share_x: ShareMode::None,
    share_y: ShareMode::None,
  }
}

///|
fn normalize_ratios(
  expected : Int32,
  values : Array[Double],
) -> Array[Double] raise LayoutError {
  let source = if values.length() == 0 {
    let result = Array(capacity=expected)
    for _ in 0.. Unit raise LayoutError {
  if value < 0.0 || value.is_nan() || value.is_inf() {
    raise LayoutError::InvalidSpacing
  }
}

///|
pub fn GridSpec::rows(self : GridSpec) -> Int32 {
  self.layout_ref.val.rows
}

///|
pub fn GridSpec::columns(self : GridSpec) -> Int32 {
  self.layout_ref.val.columns
}

///|
pub fn GridSpec::hspace(self : GridSpec) -> Double {
  self.layout_ref.val.hspace.unwrap_or(0.1)
}

///|
pub fn GridSpec::vspace(self : GridSpec) -> Double {
  self.layout_ref.val.vspace.unwrap_or(0.1)
}

///|
/// Update the horizontal gap between grid columns.
///
/// The value is measured as a fraction of the average cell width. Because
/// nested grids retain the parent layout reference, existing child subplot
/// rectangles observe this change immediately.
pub fn GridSpec::set_hspace(
  self : GridSpec,
  value : Double,
) -> Unit raise LayoutError {
  validate_spacing(value)
  self.hspace = value
  self.layout_ref.val.hspace = Some(value)
}

///|
/// Update the vertical gap between grid rows.
///
/// The value is measured as a fraction of the average cell height and is
/// visible through all nested subplot specifications belonging to this grid.
pub fn GridSpec::set_vspace(
  self : GridSpec,
  value : Double,
) -> Unit raise LayoutError {
  validate_spacing(value)
  self.vspace = value
  self.layout_ref.val.vspace = Some(value)
}

///|
/// Return relative widths for the grid columns.
pub fn GridSpec::width_ratios(self : GridSpec) -> Array[Double] {
  self.layout_ref.val.width_ratios.copy()
}

///|
/// Return relative heights for the grid rows.
pub fn GridSpec::height_ratios(self : GridSpec) -> Array[Double] {
  self.layout_ref.val.height_ratios.copy()
}

///|
/// Set relative widths for the grid columns.
pub fn GridSpec::set_width_ratios(
  self : GridSpec,
  values : Array[Double],
) -> Unit raise LayoutError {
  let normalized = normalize_ratios(self.columns, values)
  self.width_ratios = normalized.copy()
  self.layout_ref.val.width_ratios = normalized
}

///|
/// Set relative heights for the grid rows.
pub fn GridSpec::set_height_ratios(
  self : GridSpec,
  values : Array[Double],
) -> Unit raise LayoutError {
  let normalized = normalize_ratios(self.rows, values)
  self.height_ratios = normalized.copy()
  self.layout_ref.val.height_ratios = normalized
}

///|
/// Select a half-open row/column span from this grid.
pub fn GridSpec::subplot(
  self : GridSpec,
  row_start : Int32,
  row_end : Int32,
  column_start : Int32,
  column_end : Int32,
) -> SubplotSpec raise LayoutError {
  if row_start < 0 ||
    row_start >= row_end ||
    row_end > self.rows ||
    column_start < 0 ||
    column_start >= column_end ||
    column_end > self.columns {
    raise LayoutError::CellOutOfBounds
  }
  { grid: self, row_start, row_end, column_start, column_end, }
}

///|
/// Select one cell as a SubplotSpec.
///
/// This is the explicit equivalent of Matplotlib's ``gs[row, column]``
/// indexing, while remaining convenient to call from MoonBit.
pub fn GridSpec::cell_spec(
  self : GridSpec,
  row : Int32,
  column : Int32,
) -> SubplotSpec raise LayoutError {
  let normalized_row = if row < 0 { row + self.rows } else { row }
  let normalized_column = if column < 0 {
    column + self.columns
  } else {
    column
  }
  self.subplot(
    normalized_row,
    normalized_row + 1,
    normalized_column,
    normalized_column + 1,
  )
}

///|
/// Return the row and column dimensions of this grid.
pub fn GridSpec::geometry(self : GridSpec) -> (Int32, Int32) {
  (self.rows(), self.columns())
}

///|
/// Matplotlib-compatible name for `geometry`.
pub fn GridSpec::get_geometry(self : GridSpec) -> (Int32, Int32) {
  self.geometry()
}

///|
/// Matplotlib-compatible name for the width ratios getter.
pub fn GridSpec::get_width_ratios(self : GridSpec) -> Array[Double] {
  self.width_ratios()
}

///|
/// Matplotlib-compatible name for the height ratios getter.
pub fn GridSpec::get_height_ratios(self : GridSpec) -> Array[Double] {
  self.height_ratios()
}

///|
/// Select a cell by its row-major flat index.
///
/// Negative indices count from the end, matching Python's indexing rule.
pub fn GridSpec::index(
  self : GridSpec,
  index : Int32,
) -> SubplotSpec raise LayoutError {
  let total = self.rows() * self.columns()
  let normalized = if index < 0 { index + total } else { index }
  if normalized < 0 || normalized >= total {
    raise LayoutError::CellOutOfBounds
  }
  self.cell_spec(normalized / self.columns(), normalized % self.columns())
}

///|
/// Select a nested grid inside this subplot span.
///
/// The returned GridSpec keeps the parent span in its geometry chain, so a
/// child can be passed directly to `Figure::add_subplot` after selecting one
/// of its own cells.
pub fn SubplotSpec::subgridspec(
  self : SubplotSpec,
  rows : Int32,
  columns : Int32,
  hspace? : Double,
  vspace? : Double,
  width_ratios~ : Array[Double] = [],
  height_ratios~ : Array[Double] = [],
) -> GridSpec raise LayoutError {
  let nested = gridspec(
    rows,
    columns,
    hspace?,
    vspace?,
    width_ratios~,
    height_ratios~,
  )
  let parents = copy_parent_chain(self.grid.parent_chain)
  parents.push({
    layout: self.grid.layout_ref,
    row_start: self.row_start,
    row_end: self.row_end,
    column_start: self.column_start,
    column_end: self.column_end,
  })
  nested.parent_chain = parents
  nested
}

///|
/// Copy the parent chain without sharing its ratio arrays.
fn copy_parent_chain(parents : Array[GridParent]) -> Array[GridParent] {
  let result = Array(capacity=parents.length())
  for parent in parents {
    result.push({
      layout: parent.layout,
      row_start: parent.row_start,
      row_end: parent.row_end,
      column_start: parent.column_start,
      column_end: parent.column_end,
    })
  }
  result
}

///|
/// Return the selected row span.
pub fn SubplotSpec::row_start(self : SubplotSpec) -> Int32 {
  self.row_start
}

///|
pub fn SubplotSpec::row_end(self : SubplotSpec) -> Int32 {
  self.row_end
}

///|
/// Return the selected column span.
pub fn SubplotSpec::column_start(self : SubplotSpec) -> Int32 {
  self.column_start
}

///|
pub fn SubplotSpec::column_end(self : SubplotSpec) -> Int32 {
  self.column_end
}

///|
/// Return the inclusive row range occupied by this subplot.
pub fn SubplotSpec::row_span(self : SubplotSpec) -> (Int32, Int32) {
  (self.row_start, self.row_end - 1)
}

///|
/// Return the inclusive column range occupied by this subplot.
pub fn SubplotSpec::column_span(self : SubplotSpec) -> (Int32, Int32) {
  (self.column_start, self.column_end - 1)
}

///|
/// Return whether this subplot touches the first row of its grid.
pub fn SubplotSpec::is_first_row(self : SubplotSpec) -> Bool {
  self.row_start == 0
}

///|
/// Return whether this subplot touches the last row of its grid.
pub fn SubplotSpec::is_last_row(self : SubplotSpec) -> Bool {
  self.row_end == self.grid.rows()
}

///|
/// Return whether this subplot touches the first column of its grid.
pub fn SubplotSpec::is_first_col(self : SubplotSpec) -> Bool {
  self.column_start == 0
}

///|
/// Return whether this subplot touches the last column of its grid.
pub fn SubplotSpec::is_last_col(self : SubplotSpec) -> Bool {
  self.column_end == self.grid.columns()
}

///|
/// Return the GridSpec owning this span.
pub fn SubplotSpec::grid(self : SubplotSpec) -> GridSpec {
  self.grid
}

///|
/// Matplotlib-compatible name for the owning GridSpec getter.
pub fn SubplotSpec::get_gridspec(self : SubplotSpec) -> GridSpec {
  self.grid
}

///|
/// Return grid dimensions and inclusive flat start/end indices.
pub fn SubplotSpec::get_geometry(
  self : SubplotSpec,
) -> (Int32, Int32, Int32, Int32) {
  let rows = self.grid.rows()
  let columns = self.grid.columns()
  let start = self.row_start * columns + self.column_start
  let end = (self.row_end - 1) * columns + self.column_end - 1
  (rows, columns, start, end)
}

///|
/// Return the outermost SubplotSpec that contains this nested span.
pub fn SubplotSpec::get_topmost_subplotspec(self : SubplotSpec) -> SubplotSpec {
  if self.grid.parent_chain.length() == 0 {
    return self
  }
  let parent = self.grid.parent_chain[0]
  let layout = parent.layout.val
  let parent_grid : GridSpec = {
    rows: layout.rows,
    columns: layout.columns,
    hspace: layout.hspace.unwrap_or(0.1),
    vspace: layout.vspace.unwrap_or(0.1),
    width_ratios: layout.width_ratios.copy(),
    height_ratios: layout.height_ratios.copy(),
    parent_chain: [],
    layout_ref: parent.layout,
    share_x: ShareMode::None,
    share_y: ShareMode::None,
  }
  {
    grid: parent_grid,
    row_start: parent.row_start,
    row_end: parent.row_end,
    column_start: parent.column_start,
    column_end: parent.column_end,
  }
}

///|
fn GridSpec::cell_geometry(
  self : GridSpec,
  row_start : Int32,
  row_end : Int32,
  column_start : Int32,
  column_end : Int32,
  figure_width : Int32,
  figure_height : Int32,
  margin : Int32,
) -> Rect {
  let container = {
    x: Double::from_int(margin),
    y: Double::from_int(margin),
    width: Double::from_int(figure_width - margin * 2),
    height: Double::from_int(figure_height - margin * 2),
  }
  self.cell_geometry_in(
    row_start,
    row_end,
    column_start,
    column_end,
    container,
    None,
    None,
  )
}

///|
/// Compute a span inside a caller-provided display rectangle.
fn GridSpec::cell_geometry_in(
  self : GridSpec,
  row_start : Int32,
  row_end : Int32,
  column_start : Int32,
  column_end : Int32,
  container : Rect,
  hspace_override : Double?,
  vspace_override : Double?,
) -> Rect {
  let mut container = container
  for parent in self.parent_chain {
    let layout = parent.layout.val
    container = grid_span_geometry(
      layout,
      container,
      parent.row_start,
      parent.row_end,
      parent.column_start,
      parent.column_end,
      hspace_override,
      vspace_override,
    )
  }
  let layout = self.layout_ref.val
  grid_span_geometry(
    layout, container, row_start, row_end, column_start, column_end, hspace_override,
    vspace_override,
  )
}

///|
/// Compute a span inside an arbitrary display rectangle.
fn grid_span_geometry(
  layout : GridLayout,
  container : Rect,
  row_start : Int32,
  row_end : Int32,
  column_start : Int32,
  column_end : Int32,
  hspace_override : Double?,
  vspace_override : Double?,
) -> Rect {
  let hspace = match layout.hspace {
    Some(value) => value
    None => hspace_override.unwrap_or(0.1)
  }
  let vspace = match layout.vspace {
    Some(value) => value
    None => vspace_override.unwrap_or(0.1)
  }
  let total_width = layout.width_ratios.fold(init=0.0, (sum, ratio) => {
    sum + ratio
  })
  let total_height = layout.height_ratios.fold(init=0.0, (sum, ratio) => {
    sum + ratio
  })
  let base_width = container.width /
    (
      Double::from_int(layout.columns) +
      hspace * Double::from_int(layout.columns - 1)
    )
  let base_height = container.height /
    (Double::from_int(layout.rows) + vspace * Double::from_int(layout.rows - 1))
  let horizontal_gap = hspace * base_width
  let vertical_gap = vspace * base_height
  let width_unit = base_width * Double::from_int(layout.columns) / total_width
  let height_unit = base_height * Double::from_int(layout.rows) / total_height
  let mut x = container.x
  for column in 0.. Rect {
  self.grid.cell_geometry(
    self.row_start,
    self.row_end,
    self.column_start,
    self.column_end,
    figure_width,
    figure_height,
    margin,
  )
}

///|
/// Compute this span inside normalized figure bounds.
///
/// `left`, `bottom`, `right`, and `top` are fractions of the figure size,
/// with the same orientation as Matplotlib's subplot parameters. Display
/// coordinates still use the SVG top-left origin, so `bottom` is converted
/// to a top-down y coordinate here. Figure spacing applies to each nested
/// grid where that grid has not explicitly set its own spacing. `wspace` is
/// horizontal and `hspace` vertical, following Matplotlib's parameter names.
pub fn SubplotSpec::rect_with_bounds(
  self : SubplotSpec,
  figure_width : Int32,
  figure_height : Int32,
  left : Double,
  bottom : Double,
  right : Double,
  top : Double,
  wspace? : Double,
  hspace? : Double,
) -> Rect {
  let width = Double::from_int(figure_width)
  let height = Double::from_int(figure_height)
  let container = {
    x: width * left,
    y: height * (1.0 - top),
    width: width * (right - left),
    height: height * (top - bottom),
  }
  self.grid.cell_geometry_in(
    self.row_start,
    self.row_end,
    self.column_start,
    self.column_end,
    container,
    wspace,
    hspace,
  )
}

///|
/// Compute one cell rectangle inside a figure display area.
pub fn GridSpec::cell(
  self : GridSpec,
  row : Int32,
  column : Int32,
  figure_width : Int32,
  figure_height : Int32,
  margin : Int32,
) -> Rect raise LayoutError {
  if row < 0 || row >= self.rows || column < 0 || column >= self.columns {
    raise LayoutError::CellOutOfBounds
  }
  self.cell_geometry(
    row,
    row + 1,
    column,
    column + 1,
    figure_width,
    figure_height,
    margin,
  )
}

///|
/// Compute all cell rectangles in row-major order.
pub fn GridSpec::cells(
  self : GridSpec,
  figure_width : Int32,
  figure_height : Int32,
  margin : Int32,
) -> Array[Rect] raise LayoutError {
  let result = Array(capacity=self.rows * self.columns)
  for row in 0..