///|
/// Mutable state owned by one independent axes panel.
struct AxesData {
  artists : Array[Artist]
  line_originals : Array[Line2D]
  x_axis : @axis.Axis
  y_axis : @axis.Axis
  mut title : String?
  mut explicit_bounds : @core.Bounds?
  mut legend : Array[LegendEntry]
  auto_legend : Array[LegendEntry]
  mut explicit_legend : Bool
  mut legend_position : LegendPosition
  containers : Array[Container]
  rect : @gridspec.Rect?
  mut position_override : @gridspec.Rect?
  mut active_position : @gridspec.Rect?
  mut subplot_spec : @gridspec.SubplotSpec?
  mut share_x_group : Int32?
  mut share_y_group : Int32?
  mut share_x_order : Int64
  mut share_y_order : Int64
  mut share_x_target : @ref.Ref[AxesData]?
  mut share_y_target : @ref.Ref[AxesData]?
  mut share_x_members : SharedAxesGroup?
  mut share_y_members : SharedAxesGroup?
  mut show_x_tick_labels : Bool
  mut show_y_tick_labels : Bool
  mut frame_visible : Bool
  spines : Array[SpineState]
  mut x_margin : Double
  mut y_margin : Double
  mut raw_x_data : @core.Limits
  mut raw_y_data : @core.Limits
  mut x_data_minpos : Double
  mut y_data_minpos : Double
  mut ignore_existing_data_limits : Bool
  mut tight : Bool?
  mut use_sticky_edges : Bool
  color_cycle : Array[String]
  mut color_cycle_index : Int32
  mut data_aspect : Double?
  mut anchor : Anchor
  mut adjustable : String
  mut box_aspect : Double?
  secondary_x : Array[SecondaryAxis]
  secondary_y : Array[SecondaryAxis]
}

///|
/// State retained by a SubFigure handle and consumed by the renderer.
struct SubFigureData {
  spec : @gridspec.SubplotSpec
  mut title : String?
  axes : Array[@ref.Ref[AxesData]]
  subfigures : Array[@ref.Ref[SubFigureData]]
}

///|
/// Clear the presentation state retained by a nested figure tree.
fn SubFigureData::clear_titles(self : SubFigureData) -> Unit {
  for child in self.subfigures {
    child.val.clear_titles()
  }
  self.title = None
}

///|
fn AxesData::clear_presentation(self : AxesData) -> Unit {
  for line in self.line_originals {
    line.clear_attachment()
  }
  self.artists.clear()
  self.line_originals.clear()
  self.title = None
  self.explicit_bounds = None
  self.legend.clear()
  self.auto_legend.clear()
  self.containers.clear()
  self.explicit_legend = false
  self.legend_position = UpperRight
  self.show_x_tick_labels = true
  self.show_y_tick_labels = true
  self.frame_visible = true
  self.spines.clear()
  for spine in default_spines() {
    self.spines.push(spine)
  }
  self.x_margin = @config.x_margin()
  self.y_margin = @config.y_margin()
  self.ignore_existing_data_limits = true
  self.tight = None
  self.use_sticky_edges = true
  self.color_cycle.clear()
  for color in default_color_cycle() {
    self.color_cycle.push(color)
  }
  self.color_cycle_index = 0
  self.data_aspect = None
  self.secondary_x.clear()
  self.secondary_y.clear()
}

///|
/// Allocate a never-reused projection id for a retained sharing group.
fn Figure::allocate_share_group(self : Figure, count : Int32) -> Int32 {
  let result = self.share_group_seed
  self.share_group_seed -= count
  result
}

///|
fn Figure::default_axes_data(self : Figure) -> @ref.Ref[AxesData] {
  if self.axes.is_empty() {
    return self.add_subplot(try! @gridspec.gridspec(1, 1).cell_spec(0, 0)).state
  }
  self.axes[0]
}

///|
fn Axes::push_artist(self : Axes, artist : Artist) -> Unit {
  self.state.val.artists.push(artist)
  self.update_artist_data_limits(artist)
  match artist {
    Artist::Text(_, _, _, _) | Artist::Annotation(_) | Artist::Table(_) => ()
    Artist::HLine(_, _) | Artist::HSpan(_, _, _) =>
      self.state.val.y_axis.request_autoscale()
    Artist::VLine(_, _) | Artist::VSpan(_, _, _) =>
      self.state.val.x_axis.request_autoscale()
    _ => {
      self.state.val.x_axis.request_autoscale()
      self.state.val.y_axis.request_autoscale()
    }
  }
}

///|
fn Figure::insert_axes_state(self : Figure, state : @ref.Ref[AxesData]) -> Unit {
  if self.axes.length() == 1 {
    let current = self.axes[0].val
    let empty = current.artists.length() == 0 &&
      current.title is None &&
      current.explicit_bounds is None &&
      current.legend.length() == 0 &&
      current.auto_legend.length() == 0 &&
      current.rect is None &&
      current.position_override is None &&
      current.x_axis.label() is None &&
      current.y_axis.label() is None &&
      current.x_axis.limits() is None &&
      current.y_axis.limits() is None &&
      self.colorbars.length() == 0
    if empty {
      self.axes_selection.retain(item => !physical_equal(item, self.axes[0]))
      self.axes[0] = state
      self.axes_selection.push(state)
      self.initialize_axes_state(state)
      return
    }
  }
  self.axes.push(state)
  self.axes_selection.push(state)
  self.initialize_axes_state(state)
}

///|
/// Allocate an independent axes state with an explicit display rectangle.
fn new_axes_state(
  rect : @gridspec.Rect?,
  spec : @gridspec.SubplotSpec?,
) -> @ref.Ref[AxesData] {
  @ref.new({
    artists: [],
    line_originals: [],
    x_axis: @axis.axis(),
    y_axis: @axis.axis(horizontal=false),
    title: None,
    explicit_bounds: None,
    legend: [],
    auto_legend: [],
    explicit_legend: false,
    legend_position: UpperRight,
    containers: [],
    rect,
    position_override: None,
    active_position: None,
    subplot_spec: spec,
    share_x_group: None,
    share_y_group: None,
    share_x_order: 0L,
    share_y_order: 0L,
    share_x_target: None,
    share_y_target: None,
    share_x_members: None,
    share_y_members: None,
    show_x_tick_labels: true,
    show_y_tick_labels: true,
    frame_visible: true,
    spines: default_spines(),
    x_margin: @config.x_margin(),
    y_margin: @config.y_margin(),
    raw_x_data: @core.limits(1.0 / 0.0, -1.0 / 0.0),
    raw_y_data: @core.limits(1.0 / 0.0, -1.0 / 0.0),
    x_data_minpos: 1.0 / 0.0,
    y_data_minpos: 1.0 / 0.0,
    ignore_existing_data_limits: true,
    tight: None,
    use_sticky_edges: true,
    color_cycle: default_color_cycle(),
    color_cycle_index: 0,
    data_aspect: None,
    anchor: Anchor::Named("C"),
    adjustable: "box",
    box_aspect: None,
    secondary_x: [],
    secondary_y: [],
  })
}

///|
fn Figure::add_subplot_with_targets(
  self : Figure,
  spec : @gridspec.SubplotSpec,
  x_target : @ref.Ref[AxesData]?,
  y_target : @ref.Ref[AxesData]?,
) -> Axes {
  let rect = self.subplot_rect(spec)
  let state = new_axes_state(Some(rect), Some(spec))
  state.val.share_x_target = x_target
  state.val.share_y_target = y_target
  self.insert_axes_state(state)
  { figure: self, state, }
}

///|
fn Figure::initialize_axes_state(
  self : Figure,
  state : @ref.Ref[AxesData],
) -> Unit {
  let axes : Axes = { figure: self, state, }
  axes.bind_axis_data(true)
  axes.bind_axis_data(false)
  // Only fresh panels and fresh construction targets reach this private path.
  // Public clear_checked retains checked failures from later user providers.
  try! axes.clear_checked()
}