///|
fn validate_multiseries_bars(
  categories : Array[String],
  series : Array[Array[Double]],
  labels : Array[String],
  colors : Array[String],
  width : Double,
  gap : Double,
  baseline : Double,
) -> Unit raise @core.DataError {
  for count in [labels.length(), colors.length()] {
    if count != 0 && count != series.length() {
      raise @core.DataError::LengthMismatch(
        expected=series.length(),
        actual=count,
      )
    }
  }
  if width.is_nan() || width.is_inf() || width <= 0.0 {
    raise @core.DataError::InvalidStatistics(
      message="bar width must be positive and finite",
    )
  }
  if gap.is_nan() || gap.is_inf() || gap < 0.0 || gap >= 1.0 {
    raise @core.DataError::InvalidStatistics(
      message="grouped bar gap must be finite in [0, 1)",
    )
  }
  if baseline.is_nan() || baseline.is_inf() {
    raise @core.DataError::InvalidStatistics(
      message="bar baseline must be finite",
    )
  }
  for values in series {
    if values.length() != categories.length() {
      raise @core.DataError::LengthMismatch(
        expected=categories.length(),
        actual=values.length(),
      )
    }
    for value in values {
      if value.is_inf() {
        raise @core.DataError::InvalidStatistics(
          message="bar values must be finite or NaN for a missing segment",
        )
      }
    }
  }
}

///|
fn Axes::multiseries_bars(
  self : Axes,
  categories : Array[String],
  series : Array[Array[Double]],
  labels : Array[String],
  colors : Array[String],
  horizontal : Bool,
  width : Double,
  gap : Double,
  baseline : Double,
  stacked : Bool,
) -> Unit raise @core.DataError {
  validate_multiseries_bars(
    categories, series, labels, colors, width, gap, baseline,
  )
  if series.is_empty() || categories.is_empty() {
    return
  }
  let slot = if stacked { width } else { width / series.length().to_double() }
  let bar_width = slot * (1.0 - gap)
  if bar_width <= 0.0 {
    raise @core.DataError::InvalidStatistics(
      message="grouped bar width is too small for this series count and gap",
    )
  }
  // Validate every accumulated endpoint before modifying artists, units or colors.
  // NaN leaves both accumulators unchanged and keeps its original source index.
  let positive = Array::make(categories.length(), baseline)
  let negative = Array::make(categories.length(), baseline)
  let baselines : Array[Array[Double]] = []
  for values in series {
    let starts = []
    for index, value in values {
      let start = if stacked {
        if value < 0.0 {
          negative[index]
        } else {
          positive[index]
        }
      } else {
        baseline
      }
      let end = start + value
      if end.is_inf() {
        raise @core.DataError::InvalidStatistics(
          message="bar endpoint overflowed; reduce the values or baseline",
        )
      }
      starts.push(start)
      if stacked && !value.is_nan() {
        if value < 0.0 {
          negative[index] = end
        } else {
          positive[index] = end
        }
      }
    }
    baselines.push(starts)
  }
  let positions = if horizontal {
    category_y_points(self, positive, categories).map(point => point.y())
  } else {
    category_x_points(self, categories, positive).map(point => point.x())
  }
  for series_index, values in series {
    let offset = if stacked {
      0.0
    } else {
      -width / 2.0 + (series_index.to_double() + 0.5) * slot
    }
    let segments = []
    for index, value in values {
      segments.push(
        @core.bar_segment(
          positions[index] + offset,
          value,
          baseline=baselines[series_index][index],
          width=bar_width,
          category=Some(categories[index]),
        ),
      )
    }
    let color = self.resolve_color(
      if colors.is_empty() {
        ""
      } else {
        colors[series_index]
      },
    )
    let label = if labels.is_empty() { "" } else { labels[series_index] }
    let start = self.state.val.artists.length()
    self.push_artist(
      Artist::BarSegments(
        segments,
        horizontal,
        @core.style(color, width=0.0, fill=color),
      ),
    )
    self.add_auto_legend(label, color, LegendKind::Patch)
    self.add_container(ContainerKind::Bar, start, label)
  }
}

///|
/// Add side-by-side series at each category, vertically or horizontally.
/// `series` is series-major: every row has one value per category. `width` is
/// the total category-axis group span; `gap` is the fraction of each series slot
/// left empty. NaN skips one bar without changing source indices. All lengths,
/// finite options and endpoints are checked before artists are attached.
pub fn Axes::grouped_bar(
  self : Axes,
  categories : Array[String],
  series : Array[Array[Double]],
  labels~ : Array[String] = [],
  colors~ : Array[String] = [],
  horizontal~ : Bool = false,
  width~ : Double = 0.8,
  gap~ : Double = 0.1,
  baseline~ : Double = 0.0,
) -> Unit raise @core.DataError {
  self.multiseries_bars(
    categories, series, labels, colors, horizontal, width, gap, baseline, false,
  )
}

///|
/// Add stacked series with independent positive and negative accumulators per
/// category, both beginning at `baseline`. Tooltip values remain the original
/// segment values rather than their accumulated endpoints. NaN skips a segment.
pub fn Axes::stacked_bar(
  self : Axes,
  categories : Array[String],
  series : Array[Array[Double]],
  labels~ : Array[String] = [],
  colors~ : Array[String] = [],
  horizontal~ : Bool = false,
  width~ : Double = 0.8,
  baseline~ : Double = 0.0,
) -> Unit raise @core.DataError {
  self.multiseries_bars(
    categories, series, labels, colors, horizontal, width, 0.0, baseline, true,
  )
}

///|
/// Add grouped category bars to the default axes. See `Axes::grouped_bar`.
pub fn Figure::grouped_bar(
  self : Figure,
  categories : Array[String],
  series : Array[Array[Double]],
  labels~ : Array[String] = [],
  colors~ : Array[String] = [],
  horizontal~ : Bool = false,
  width~ : Double = 0.8,
  gap~ : Double = 0.1,
  baseline~ : Double = 0.0,
) -> Unit raise @core.DataError {
  self
  .axes()
  .grouped_bar(
    categories,
    series,
    labels~,
    colors~,
    horizontal~,
    width~,
    gap~,
    baseline~,
  )
}

///|
/// Add stacked category bars to the default axes. See `Axes::stacked_bar`.
pub fn Figure::stacked_bar(
  self : Figure,
  categories : Array[String],
  series : Array[Array[Double]],
  labels~ : Array[String] = [],
  colors~ : Array[String] = [],
  horizontal~ : Bool = false,
  width~ : Double = 0.8,
  baseline~ : Double = 0.0,
) -> Unit raise @core.DataError {
  self
  .axes()
  .stacked_bar(
    categories,
    series,
    labels~,
    colors~,
    horizontal~,
    width~,
    baseline~,
  )
}