///|
pub(all) struct HorBarChart {
  title : String
  y_labels : Array[String] // Categories (displayed on Y-axis)
  series_list : Array[Series]
  width : Float
  height : Float
  config : ChartConfig
}

///|
pub fn HorBarChart::new() -> HorBarChart {
  {
    title: "",
    y_labels: [],
    series_list: [],
    width: 800.0,
    height: 400.0,
    config: ChartConfig::default(),
  }
}

///|
pub fn HorBarChart::title(self : HorBarChart, t : String) -> HorBarChart {
  { ..self, title: t }
}

///|
pub fn HorBarChart::y_labels(
  self : HorBarChart,
  labels : Array[String],
) -> HorBarChart {
  { ..self, y_labels: labels }
}

///|
pub fn HorBarChart::series(self : HorBarChart, s : Series) -> HorBarChart {
  let new_list = self.series_list
  new_list.push(s)
  { ..self, series_list: new_list }
}

///|
pub fn HorBarChart::width(self : HorBarChart, w : Float) -> HorBarChart {
  { ..self, width: w }
}

///|
pub fn HorBarChart::height(self : HorBarChart, h : Float) -> HorBarChart {
  { ..self, height: h }
}

///|
pub fn HorBarChart::config(self : HorBarChart, c : ChartConfig) -> HorBarChart {
  { ..self, config: c }
}

// Find max value across all series

///|
fn find_data_max(series_list : Array[Series]) -> Float {
  fn loop_series(i : Int, max_val : Float, found : Bool) -> Float {
    if i < series_list.length() {
      let (new_max, new_found) = find_values_max(
        series_list[i].values,
        0,
        max_val,
        found,
      )
      loop_series(i + 1, new_max, new_found)
    } else {
      max_val
    }
  }
  loop_series(0, 0.0, false)
}

///|
fn find_values_max(
  values : Array[Float],
  j : Int,
  max_val : Float,
  found : Bool,
) -> (Float, Bool) {
  if j < values.length() {
    let v = values[j]
    if found {
      find_values_max(
        values,
        j + 1,
        if v > max_val {
          v
        } else {
          max_val
        },
        true,
      )
    } else {
      find_values_max(values, j + 1, v, true)
    }
  } else {
    (max_val, found)
  }
}

// Compute horizontal (X) axis layout: maps data values to SVG x-positions

///|
fn compute_horiz_axis(
  data_max : Float,
  _width : Float,
  _margin_left : Float,
  _margin_right : Float,
) -> AxisLayout {
  let max_ticks = 8
  let zero : Float = Float::from_int(0)
  let one : Float = Float::from_int(1)
  let range = data_max - zero
  if range == zero {
    let step = one
    let x_min = zero - step
    let x_max = data_max + step
    AxisLayout::{ y_min: x_min, y_max: x_max, ticks: [], tick_labels: [] }
  } else {
    let step = nice_step(range, max_ticks)
    let x_min = floor_float(zero / step) * step
    let x_max = (ceil_float(data_max / step) + one) * step
    let ticks = build_ticks(x_min, x_max, step)
    let tick_labels = format_labels(ticks)
    AxisLayout::{ y_min: x_min, y_max: x_max, ticks, tick_labels }
  }
}

// Render vertical grid lines and X-axis tick labels at the bottom

///|
fn render_vert_grid(
  acc : String,
  ticks : Array[Float],
  tick_labels : Array[String],
  x_min : Float,
  x_max : Float,
  width : Float,
  height : Float,
  mt : Float,
  mb : Float,
  ml : Float,
  mr : Float,
  i : Int,
  config : ChartConfig,
) -> String {
  if i < ticks.length() {
    let chart_w = width - ml - mr
    let x_pos = ml + (ticks[i] - x_min) / (x_max - x_min) * chart_w
    let tick_label = text(
      x_pos,
      height - mb + 16.0,
      tick_labels[i],
      config.axis_font_size,
      "middle",
    )
    let new_acc = if config.show_grid {
      let grid_line = line(x_pos, mt, x_pos, height - mb, "#e0e0e0", 1.0)
      acc + grid_line + tick_label
    } else {
      acc + tick_label
    }
    render_vert_grid(
      new_acc,
      ticks,
      tick_labels,
      x_min,
      x_max,
      width,
      height,
      mt,
      mb,
      ml,
      mr,
      i + 1,
      config,
    )
  } else {
    acc
  }
}

// Render bars for a specific category row (inner loop over series)

///|
fn render_horiz_cat_bars(
  acc : String,
  series_list : Array[Series],
  num_series : Int,
  ci : Int,
  bar_h : Float,
  group_y : Float,
  x_min : Float,
  x_max : Float,
  chart_w : Float,
  ml : Float,
  si : Int,
  config : ChartConfig,
) -> String {
  if si < num_series {
    let val = series_list[si].values[ci]
    let si_float = Float::from_int(si)
    let offset : Float = 0.5
    let bar_y = group_y + bar_h * (si_float + offset)
    let bar_w = (val - x_min) / (x_max - x_min) * chart_w
    let color_str = get_chart_color(config, si)
    let rect_str = rect(ml, bar_y, bar_w, bar_h, color_str)
    render_horiz_cat_bars(
      acc + rect_str,
      series_list,
      num_series,
      ci,
      bar_h,
      group_y,
      x_min,
      x_max,
      chart_w,
      ml,
      si + 1,
      config,
    )
  } else {
    acc
  }
}

// Render bars and Y-axis category labels (outer loop over categories)

///|
fn render_categories(
  acc : String,
  series_list : Array[Series],
  y_labels : Array[String],
  num_categories : Int,
  num_series : Int,
  bar_h : Float,
  group_h : Float,
  x_min : Float,
  x_max : Float,
  width : Float,
  height : Float,
  mt : Float,
  mb : Float,
  ml : Float,
  mr : Float,
  ci : Int,
  config : ChartConfig,
) -> String {
  if ci < num_categories {
    let chart_h = height - mt - mb
    let chart_w = width - ml - mr
    let group_y = mt +
      Float::from_int(ci) / Float::from_int(num_categories) * chart_h
    let with_bars = render_horiz_cat_bars(
      acc, series_list, num_series, ci, bar_h, group_y, x_min, x_max, chart_w, ml,
      0, config,
    )
    let label_y = group_y + group_h / 2.0 + 4.0
    let with_label = with_bars +
      text(ml - 8.0, label_y, y_labels[ci], config.axis_font_size, "end")
    render_categories(
      with_label,
      series_list,
      y_labels,
      num_categories,
      num_series,
      bar_h,
      group_h,
      x_min,
      x_max,
      width,
      height,
      mt,
      mb,
      ml,
      mr,
      ci + 1,
      config,
    )
  } else {
    acc
  }
}

///|
pub fn HorBarChart::render(self : HorBarChart) -> String {
  let mt : Float = 40.0
  let mb : Float = 60.0
  let ml : Float = 60.0
  let mr : Float = 30.0

  let data_max_val = find_data_max(self.series_list)
  let axis = compute_horiz_axis(data_max_val, self.width, ml, mr)

  let base = svg_open(self.width, self.height)

  // Title
  let with_title = if self.title != "" {
    base +
    text(
      self.width / 2.0,
      mt - 12.0,
      self.title,
      self.config.title_font_size,
      "middle",
    )
  } else {
    base
  }

  // Vertical grid lines and X-axis labels at bottom
  let with_v_grid = render_vert_grid(
    with_title,
    axis.ticks,
    axis.tick_labels,
    axis.y_min,
    axis.y_max,
    self.width,
    self.height,
    mt,
    mb,
    ml,
    mr,
    0,
    self.config,
  )

  // Baseline (vertical line at left edge of chart area)
  let with_baseline = with_v_grid +
    line(ml, mt, ml, self.height - mb, "#888888", 1.0)

  // Bars and Y-axis category labels
  let num_categories = min_categories(
    self.series_list,
    self.y_labels.length(),
    0,
  )
  let num_series = self.series_list.length()
  let with_bars = if num_categories > 0 && num_series > 0 {
    let chart_h = self.height - mt - mb
    let group_h = chart_h / Float::from_int(num_categories)
    let bar_h = group_h / Float::from_int(num_series + 1)
    render_categories(
      with_baseline,
      self.series_list,
      self.y_labels,
      num_categories,
      num_series,
      bar_h,
      group_h,
      axis.y_min,
      axis.y_max,
      self.width,
      self.height,
      mt,
      mb,
      ml,
      mr,
      0,
      self.config,
    )
  } else {
    with_baseline
  }

  // Legend
  let names : Array[String] = []
  for i = 0; i < self.series_list.length(); i = i + 1 {
    names.push(self.series_list[i].name)
  }
  let with_legend = with_bars +
    render_legend(names, self.width, self.height - 10.0, self.config)

  with_legend + svg_close()
}