///|
pub(all) struct ChartSize {
  width : Int
  height : Int
  padding : Int
} derive(Eq, Debug)

///|
pub(all) struct DataPoint {
  label : String
  value : Int
} derive(Eq, Debug)

///|
pub(all) struct ChartTheme {
  background : String
  foreground : String
  accent : String
  accent_alt : String
  grid : String
  font_family : String
  radius : Int
} derive(Eq, Debug)

///|
pub(all) struct PlotArea {
  x : Int
  y : Int
  width : Int
  height : Int
} derive(Eq, Debug)

///|
pub(all) struct ChartSummary {
  count : Int
  total : Int
  max : Int
  average : Int
} derive(Eq, Debug)

///|
pub fn ChartSize::new(width : Int, height : Int, padding : Int) -> ChartSize {
  {
    width: normalize_positive(width, 640),
    height: normalize_positive(height, 360),
    padding: normalize_positive(padding, 32),
  }
}

///|
pub fn DataPoint::new(label : String, value : Int) -> DataPoint {
  { label, value }
}

///|
pub fn ChartTheme::atelier() -> ChartTheme {
  {
    background: "#fbfaf7",
    foreground: "#243b53",
    accent: "#2f80ed",
    accent_alt: "#f2994a",
    grid: "#d9e2ec",
    font_family: "Inter, Segoe UI, sans-serif",
    radius: 6,
  }
}

///|
pub fn ChartTheme::midnight() -> ChartTheme {
  {
    background: "#111827",
    foreground: "#f9fafb",
    accent: "#38bdf8",
    accent_alt: "#f472b6",
    grid: "#374151",
    font_family: "Inter, Segoe UI, sans-serif",
    radius: 6,
  }
}

///|
pub fn ChartSize::plot_area(self : ChartSize) -> PlotArea {
  {
    x: self.padding,
    y: self.padding,
    width: self.width - self.padding * 2,
    height: self.height - self.padding * 2,
  }
}

///|
pub fn max_value(points : Array[DataPoint]) -> Int {
  if points.length() == 0 {
    return 0
  }
  let mut best = points[0].value
  for i = 1; i < points.length(); i = i + 1 {
    if points[i].value > best {
      best = points[i].value
    }
  }
  best
}

///|
pub fn total_value(points : Array[DataPoint]) -> Int {
  let mut total = 0
  for i = 0; i < points.length(); i = i + 1 {
    total = total + points[i].value
  }
  total
}

///|
pub fn ChartSummary::from_points(points : Array[DataPoint]) -> ChartSummary {
  let total = total_value(points)
  let count = points.length()
  {
    count,
    total,
    max: max_value(points),
    average: if count == 0 {
      0
    } else {
      total / count
    },
  }
}

///|
pub fn ChartSummary::to_json(self : ChartSummary) -> String {
  "{\"count\":\{self.count},\"total\":\{self.total},\"max\":\{self.max},\"average\":\{self.average}}"
}

///|
pub fn percent_of(value : Int, total : Int) -> Int {
  if value <= 0 || total <= 0 {
    0
  } else {
    value * 100 / total
  }
}

///|
fn scale_value(value : Int, max : Int, height : Int) -> Int {
  if max <= 0 || value <= 0 {
    0
  } else {
    value * height / max
  }
}

///|
fn escape_xml(value : String) -> String {
  value
  .replace(old="&", new="&")
  .replace(old="<", new="<")
  .replace(old=">", new=">")
  .replace(old="\"", new=""")
}

///|
pub fn escape_label(value : String) -> String {
  escape_xml(value)
}

///|
pub fn render_bar_chart(
  points : Array[DataPoint],
  title? : String = "",
  size? : ChartSize = ChartSize::new(640, 360, 44),
  theme? : ChartTheme = ChartTheme::atelier(),
) -> String {
  let area = size.plot_area()
  let max = normalize_positive(max_value(points), 1)
  let buf = StringBuilder()
  buf.write_string(svg_header(size, theme))
  buf.write_string(svg_background(size, theme))
  buf.write_string(svg_title(title, size, theme))
  buf.write_string(
    "",
  )
  if points.length() > 0 {
    let gap = 10
    let slot = area.width / points.length()
    let bar_width = normalize_positive(slot - gap, 1)
    for i = 0; i < points.length(); i = i + 1 {
      let bar_height = scale_value(points[i].value, max, area.height)
      let x = area.x + i * slot + gap / 2
      let y = area.y + area.height - bar_height
      let fill = if i % 2 == 0 { theme.accent } else { theme.accent_alt }
      buf.write_string(
        "",
      )
      buf.write_string(
        "\{escape_xml(points[i].label)}",
      )
    }
  }
  buf.write_string("")
  buf.to_string()
}

///|
pub fn render_line_chart(
  points : Array[DataPoint],
  title? : String = "",
  size? : ChartSize = ChartSize::new(640, 360, 44),
  theme? : ChartTheme = ChartTheme::atelier(),
) -> String {
  let area = size.plot_area()
  let max = normalize_positive(max_value(points), 1)
  let buf = StringBuilder()
  buf.write_string(svg_header(size, theme))
  buf.write_string(svg_background(size, theme))
  buf.write_string(svg_title(title, size, theme))
  buf.write_string(
    "",
  )
  if points.length() > 0 {
    let step = if points.length() == 1 {
      0
    } else {
      area.width / (points.length() - 1)
    }
    let path = StringBuilder()
    for i = 0; i < points.length(); i = i + 1 {
      let x = area.x + i * step
      let y = area.y +
        area.height -
        scale_value(points[i].value, max, area.height)
      if i == 0 {
        path.write_string("M \{x} \{y}")
      } else {
        path.write_string(" L \{x} \{y}")
      }
      buf.write_string(
        "",
      )
    }
    buf.write_string(
      "",
    )
  }
  buf.write_string("")
  buf.to_string()
}

///|
pub fn render_sparkline(
  values : Array[Int],
  size? : ChartSize = ChartSize::new(180, 48, 6),
  theme? : ChartTheme = ChartTheme::atelier(),
) -> String {
  let points : Array[DataPoint] = []
  for i = 0; i < values.length(); i = i + 1 {
    points.push(DataPoint::new("", values[i]))
  }
  let area = size.plot_area()
  let max = normalize_positive(max_value(points), 1)
  let buf = StringBuilder()
  buf.write_string(svg_header(size, theme))
  buf.write_string(svg_background(size, theme))
  if values.length() > 0 {
    let step = if values.length() == 1 {
      0
    } else {
      area.width / (values.length() - 1)
    }
    let path = StringBuilder()
    for i = 0; i < values.length(); i = i + 1 {
      let x = area.x + i * step
      let y = area.y + area.height - scale_value(values[i], max, area.height)
      if i == 0 {
        path.write_string("M \{x} \{y}")
      } else {
        path.write_string(" L \{x} \{y}")
      }
    }
    buf.write_string(
      "",
    )
  }
  buf.write_string("")
  buf.to_string()
}

///|
pub fn render_metric_card(
  title : String,
  value : String,
  caption? : String = "",
  size? : ChartSize = ChartSize::new(360, 180, 28),
  theme? : ChartTheme = ChartTheme::atelier(),
) -> String {
  let buf = StringBuilder()
  buf.write_string(svg_header(size, theme))
  buf.write_string(svg_background(size, theme))
  buf.write_string(
    "",
  )
  buf.write_string(
    "\{escape_xml(title)}",
  )
  buf.write_string(
    "\{escape_xml(value)}",
  )
  if caption != "" {
    buf.write_string(
      "\{escape_xml(caption)}",
    )
  }
  buf.write_string("")
  buf.to_string()
}

///|
pub fn render_progress_bar(
  label : String,
  value : Int,
  max : Int,
  size? : ChartSize = ChartSize::new(420, 110, 24),
  theme? : ChartTheme = ChartTheme::atelier(),
) -> String {
  let area = size.plot_area()
  let safe_max = normalize_positive(max, 1)
  let filled = scale_value(value, safe_max, area.width)
  let pct = percent_of(value, safe_max)
  let buf = StringBuilder()
  buf.write_string(svg_header(size, theme))
  buf.write_string(svg_background(size, theme))
  buf.write_string(
    "\{escape_xml(label)}",
  )
  buf.write_string(
    "",
  )
  buf.write_string(
    "",
  )
  buf.write_string(
    "\{pct}%",
  )
  buf.write_string("")
  buf.to_string()
}

///|
pub fn render_dashboard(
  title : String,
  points : Array[DataPoint],
  trend : Array[Int],
  size? : ChartSize = ChartSize::new(760, 420, 32),
  theme? : ChartTheme = ChartTheme::atelier(),
) -> String {
  let summary = ChartSummary::from_points(points)
  let area = size.plot_area()
  let bar_top = area.y + 156
  let bar_height = area.height - 176
  let bar_width_area = area.width / 2
  let trend_left = area.x + bar_width_area + 52
  let trend_width = area.width - bar_width_area - 52
  let max = normalize_positive(summary.max, 1)
  let buf = StringBuilder()
  buf.write_string(svg_header(size, theme))
  buf.write_string(svg_background(size, theme))
  buf.write_string(svg_title(title, size, theme))
  write_kpi(buf, area.x, area.y + 36, "Total", summary.total.to_string(), theme)
  write_kpi(
    buf,
    area.x + 152,
    area.y + 36,
    "Average",
    summary.average.to_string(),
    theme,
  )
  write_kpi(
    buf,
    area.x + 304,
    area.y + 36,
    "Max",
    summary.max.to_string(),
    theme,
  )
  buf.write_string(
    "Breakdown",
  )
  if points.length() > 0 {
    let slot = bar_width_area / points.length()
    for i = 0; i < points.length(); i = i + 1 {
      let h = scale_value(points[i].value, max, bar_height)
      let x = area.x + i * slot + 6
      let y = bar_top + bar_height - h
      let w = normalize_positive(slot - 12, 1)
      let fill = if i % 2 == 0 { theme.accent } else { theme.accent_alt }
      buf.write_string(
        "",
      )
      buf.write_string(
        "\{escape_xml(points[i].label)}",
      )
    }
  }
  buf.write_string(
    "Trend",
  )
  write_trend_path(
    buf,
    trend,
    { x: trend_left, y: bar_top, width: trend_width, height: bar_height },
    theme,
  )
  buf.write_string("")
  buf.to_string()
}

///|
fn write_kpi(
  buf : StringBuilder,
  x : Int,
  y : Int,
  label : String,
  value : String,
  theme : ChartTheme,
) -> Unit {
  buf.write_string(
    "\{escape_xml(label)}",
  )
  buf.write_string(
    "\{escape_xml(value)}",
  )
}

///|
fn write_trend_path(
  buf : StringBuilder,
  values : Array[Int],
  area : PlotArea,
  theme : ChartTheme,
) -> Unit {
  buf.write_string(
    "",
  )
  if values.length() == 0 {
    return
  }
  let max = normalize_positive(max_int(values), 1)
  let step = if values.length() == 1 {
    0
  } else {
    area.width / (values.length() - 1)
  }
  let path = StringBuilder()
  for i = 0; i < values.length(); i = i + 1 {
    let x = area.x + i * step
    let y = area.y + area.height - scale_value(values[i], max, area.height)
    if i == 0 {
      path.write_string("M \{x} \{y}")
    } else {
      path.write_string(" L \{x} \{y}")
    }
  }
  buf.write_string(
    "",
  )
}

///|
fn max_int(values : Array[Int]) -> Int {
  if values.length() == 0 {
    return 0
  }
  let mut best = values[0]
  for i = 1; i < values.length(); i = i + 1 {
    if values[i] > best {
      best = values[i]
    }
  }
  best
}

///|
fn svg_header(size : ChartSize, theme : ChartTheme) -> String {
  ""
}

///|
fn svg_background(size : ChartSize, theme : ChartTheme) -> String {
  ""
}

///|
fn svg_title(title : String, size : ChartSize, theme : ChartTheme) -> String {
  if title == "" {
    ""
  } else {
    "\{escape_xml(title)}"
  }
}

///|
fn normalize_positive(value : Int, fallback : Int) -> Int {
  if value > 0 {
    value
  } else {
    fallback
  }
}