///|
pub(all) enum ChartType {
  Area
  AreaStacked
  AreaPercentStacked
  Area3D
  Area3DStacked
  Area3DPercentStacked
  Bar
  BarStacked
  BarPercentStacked
  Bar3DClustered
  Bar3DStacked
  Bar3DPercentStacked
  Bar3DConeClustered
  Bar3DConeStacked
  Bar3DConePercentStacked
  Bar3DPyramidClustered
  Bar3DPyramidStacked
  Bar3DPyramidPercentStacked
  Bar3DCylinderClustered
  Bar3DCylinderStacked
  Bar3DCylinderPercentStacked
  Col
  ColStacked
  ColPercentStacked
  Col3D
  Col3DClustered
  Col3DStacked
  Col3DPercentStacked
  Col3DCone
  Col3DConeClustered
  Col3DConeStacked
  Col3DConePercentStacked
  Col3DPyramid
  Col3DPyramidClustered
  Col3DPyramidStacked
  Col3DPyramidPercentStacked
  Col3DCylinder
  Col3DCylinderClustered
  Col3DCylinderStacked
  Col3DCylinderPercentStacked
  Doughnut
  Line
  Line3D
  Pie
  Pie3D
  PieOfPie
  BarOfPie
  Radar
  Scatter
  Surface3D
  WireframeSurface3D
  Contour
  WireframeContour
  Bubble
  Bubble3D
  StockHighLowClose
  StockOpenHighLowClose
} derive(Eq, Debug)

///|
pub(all) enum ChartDashType {
  Unset
  Solid
  Dot
  Dash
  LgDash
  SashDot
  LgDashDot
  LgDashDotDot
  SysDash
  SysDot
  SysDashDot
  SysDashDotDot
} derive(Eq, Debug)

///|
pub(all) enum ChartLineType {
  Unset
  Solid
  NoLine
  Automatic
} derive(Eq, Debug)

///|
/// Emits the default solid line fill Excelize uses for `ChartLineSolid`
/// when no explicit line color is given: scheme color tx1 with
/// lumMod 15000 / lumOff 85000.
fn write_chart_line_solid_scheme(sb : StringBuilder, indent : String) -> Unit {
  sb.write_view(indent)
  sb.write_view(
    "\n",
  )
}

///|
pub(all) enum ChartTickLabelPositionType {
  NextToAxis
  High
  Low
  NoTickLabels
} derive(Debug)

///|
pub(all) enum ChartDataLabelPositionType {
  Center
  Left
  Right
  Top
  Bottom
  BestFit
  InEnd
  OutEnd
} derive(Eq, Debug)

///|
struct ChartNumFmt {
  custom_num_fmt : String
  source_linked : Bool
} derive(Debug)

///|
pub fn ChartNumFmt::new(
  custom_num_fmt? : String = "",
  source_linked? : Bool = true,
) -> ChartNumFmt {
  { custom_num_fmt, source_linked }
}

///|
pub(all) struct ChartAxis {
  mut none : Bool
  mut drop_lines : Bool
  mut high_low_lines : Bool
  mut major_grid_lines : Bool
  mut minor_grid_lines : Bool
  mut major_unit : Double
  mut tick_label_position : ChartTickLabelPositionType
  mut tick_label_skip : Int
  mut reverse_order : Bool
  mut secondary : Bool
  mut maximum : Double?
  mut minimum : Double?
  mut alignment : Alignment
  mut font : Font
  mut log_base : Double
  mut num_fmt : ChartNumFmt
  mut title : String
  mut title_rich : Array[RichTextRun]?
  mut ax_id : Int
} derive(Debug)

///|
fn ChartAxis::new() -> ChartAxis {
  {
    none: false,
    drop_lines: false,
    high_low_lines: false,
    major_grid_lines: false,
    minor_grid_lines: false,
    major_unit: 0.0,
    tick_label_position: NextToAxis,
    tick_label_skip: 0,
    reverse_order: false,
    secondary: false,
    maximum: None,
    minimum: None,
    alignment: Alignment::new(),
    font: Font::new(),
    log_base: 0.0,
    num_fmt: ChartNumFmt::new(),
    title: "",
    title_rich: None,
    ax_id: 0,
  }
}

///|
/// Emits `` / `` for chart groups that honor the
/// Excelize `ChartAxis` DropLines/HighLowLines options: drop lines apply to
/// area, 3D area, line, and 3D line charts; high-low lines apply to 2D line
/// charts only (stock charts emit theirs unconditionally elsewhere).
fn write_chart_axis_lines(
  sb : StringBuilder,
  plot_tag : String,
  axis : ChartAxis,
) -> Unit {
  let supports_drop_lines = plot_tag == "lineChart" ||
    plot_tag == "line3DChart" ||
    plot_tag == "areaChart" ||
    plot_tag == "area3DChart"
  if supports_drop_lines && axis.drop_lines {
    sb.write_view("        \n")
  }
  if plot_tag == "lineChart" && axis.high_low_lines {
    sb.write_view("        \n")
  }
}

///|
struct ChartDimension {
  width : Int
  height : Int
} derive(Debug)

///|
fn ChartDimension::new() -> ChartDimension {
  { width: 480, height: 260 }
}

///|
pub fn ChartDimension::with_values(
  width : Int,
  height : Int,
) -> ChartDimension raise XlsxError {
  if width <= 0 || height <= 0 {
    raise InvalidSheetOperation(msg="chart dimension must be > 0")
  }
  { width, height }
}

///|
pub(all) struct ChartLine {
  mut typ : ChartLineType
  mut dash : ChartDashType
  mut color : String
  mut transparency : Int
  mut smooth : Bool
  mut width : Double
} derive(Debug)

///|
fn ChartLine::new() -> ChartLine {
  {
    typ: Unset,
    dash: Unset,
    color: "",
    transparency: 0,
    smooth: false,
    width: 0.0,
  }
}

///|
pub(all) struct ChartUpDownBar {
  mut fill_color : String
  mut border : ChartLine
  mut fill : ChartFill?
} derive(Debug)

///|
fn ChartUpDownBar::new() -> ChartUpDownBar {
  { fill_color: "", border: ChartLine::new(), fill: None }
}

///|
pub(all) struct ChartPlotArea {
  mut show_val : Bool
  mut show_cat_name : Bool
  mut show_ser_name : Bool
  mut show_percent : Bool
  mut show_leader_lines : Bool
  mut show_bubble_size : Bool
  mut show_data_table : Bool
  mut show_data_table_keys : Bool
  mut second_plot_values : Int
  mut num_fmt : ChartNumFmt
  mut up_bars : ChartUpDownBar
  mut down_bars : ChartUpDownBar
  mut fill_color : String
  mut fill_transparency : Int
  mut fill : ChartFill?
} derive(Debug)

///|
fn ChartPlotArea::new() -> ChartPlotArea {
  {
    show_val: false,
    show_cat_name: false,
    show_ser_name: false,
    show_percent: false,
    show_leader_lines: false,
    show_bubble_size: false,
    show_data_table: false,
    show_data_table_keys: false,
    second_plot_values: 0,
    num_fmt: ChartNumFmt::new(),
    up_bars: ChartUpDownBar::new(),
    down_bars: ChartUpDownBar::new(),
    fill_color: "",
    fill_transparency: 0,
    fill: None,
  }
}

///|
pub(all) struct ChartLegend {
  mut position : String
  mut show_legend_key : Bool
  mut font : Font?
} derive(Debug)

///|
fn ChartLegend::new(
  position? : String = "bottom",
  show_legend_key? : Bool = false,
) -> ChartLegend {
  { position, show_legend_key, font: None }
}

///|
pub(all) struct ChartMarker {
  mut symbol : String
  mut size : Int
  mut fill_color : String
  mut fill_transparency : Int
  mut fill : ChartFill?
  mut border : ChartLine
} derive(Debug)

///|
fn ChartMarker::new(symbol? : String = "", size? : Int = 0) -> ChartMarker {
  {
    symbol,
    size,
    fill_color: "",
    fill_transparency: 0,
    fill: None,
    border: ChartLine::new(),
  }
}

///|
pub(all) struct ChartDataLabel {
  mut font : Font
  mut alignment : Alignment
  mut fill_color : String
  mut fill_transparency : Int
  mut fill : ChartFill?
} derive(Debug)

///|
fn ChartDataLabel::new() -> ChartDataLabel {
  {
    font: Font::new(),
    alignment: Alignment::new(),
    fill_color: "",
    fill_transparency: 0,
    fill: None,
  }
}

///|
struct ChartDataPoint {
  index : Int
  fill_color : String
  fill : ChartFill?
} derive(Debug)

///|
pub fn ChartDataPoint::new(
  index : Int,
  fill_color? : String = "",
  fill? : ChartFill,
) -> ChartDataPoint {
  { index, fill_color, fill }
}

///|
/// Explicit fill override for a chart element, mirroring Excelize's
/// `Fill` struct. Only `typ == "pattern"` with `pattern == 1` is acted
/// on (like Go's `drawShapeFill`): a single `color` yields a solid fill
/// with optional `transparency`, and any other color count yields an
/// explicit no-fill. Other configurations are inert, matching Go.
/// `shading` is accepted for API parity but, like Go's chart path, is
/// not emitted.
pub(all) struct ChartFill {
  mut typ : String
  mut pattern : Int
  mut color : Array[String]
  mut shading : Int
  mut transparency : Int
} derive(Debug)

///|
pub fn ChartFill::new(
  typ? : String = "",
  pattern? : Int = 0,
  color? : Array[String] = [],
  shading? : Int = 0,
  transparency? : Int = 0,
) -> ChartFill {
  { typ, pattern, color, shading, transparency }
}

///|
/// Effective fill directive derived from an optional `ChartFill`
/// override and the flattened `fill_color`/`fill_transparency` fields.
priv enum ChartFillDirective {
  SolidFill(color~ : String, transparency~ : Int)
  NoFill
  NoOverride
}

///|
fn chart_fill_directive(
  fill : ChartFill?,
  fallback_color : String,
  fallback_transparency : Int,
) -> ChartFillDirective {
  match fill {
    // Excelize's drawShapeFill only acts on pattern fills with index 1.
    Some(f) if f.typ == "pattern" && f.pattern == 1 =>
      if f.color.length() == 1 {
        SolidFill(color=f.color[0], transparency=f.transparency)
      } else {
        NoFill
      }
    _ =>
      if fallback_color != "" {
        SolidFill(color=fallback_color, transparency=fallback_transparency)
      } else {
        NoOverride
      }
  }
}

///|
/// Emits a solid-fill element with an optional alpha channel, the shared
/// shape used for series/marker/data-label/data-point/chart-area fills.
fn write_solid_fill(
  sb : StringBuilder,
  indent : String,
  color : String,
  transparency : Int,
) -> Unit {
  let value = color.replace_all(old="#", new="").to_upper()
  sb.write_view(indent)
  sb.write_view(" 100 {
    100
  } else {
    transparency
  }
  if clamped > 0 {
    let alpha = (100 - clamped) * 1000
    sb.write_view("\">\n")
  } else {
    sb.write_view("\"/>\n")
  }
}

///|
pub(all) struct ChartSeries {
  name : String
  categories : String
  values : String
  mut bubble_size : String?
  mut sizes : String
  mut fill_color : String
  mut fill_transparency : Int
  mut fill : ChartFill?
  mut legend : ChartLegend
  mut line : ChartLine
  mut marker : ChartMarker
  mut data_label : ChartDataLabel
  mut data_label_position : ChartDataLabelPositionType
  data_point : Array[ChartDataPoint]
} derive(Debug)

///|
pub fn ChartSeries::new(
  categories : String,
  values : String,
  name? : String = "",
) -> ChartSeries raise XlsxError {
  if categories == "" || values == "" {
    raise InvalidSheetOperation(
      msg="chart series categories/values must be non-empty",
    )
  }
  {
    name,
    categories,
    values,
    bubble_size: None,
    sizes: "",
    fill_color: "",
    fill_transparency: 0,
    fill: None,
    legend: ChartLegend::new(),
    line: ChartLine::new(),
    marker: ChartMarker::new(),
    data_label: ChartDataLabel::new(),
    data_label_position: BestFit,
    data_point: [],
  }
}

///|
pub fn ChartSeries::set_bubble_size(
  self : ChartSeries,
  bubble_size : String,
) -> Unit raise XlsxError {
  if bubble_size == "" {
    raise InvalidSheetOperation(msg="bubble size range empty")
  }
  self.bubble_size = Some(bubble_size)
  self.sizes = bubble_size
}

///|
fn chart_series_bubble_size(series : ChartSeries) -> String? {
  match series.bubble_size {
    Some(value) => Some(value)
    None => if series.sizes == "" { None } else { Some(series.sizes) }
  }
}

///|
pub(all) struct ChartOptions {
  typ : ChartType
  series : Array[ChartSeries]
  combo_charts : Array[ChartComboOptions]
  mut format : GraphicOptions
  mut dimension : ChartDimension
  mut legend : ChartLegend
  mut title : String
  mut title_rich : Array[RichTextRun]?
  mut fill_color : String
  mut fill_transparency : Int
  mut fill : ChartFill?
  mut border : ChartLine
  mut vary_colors : Bool
  mut x_axis : ChartAxis
  mut y_axis : ChartAxis
  mut plot_area : ChartPlotArea
  mut show_blanks_as : String
  mut gap_width : Int?
  mut overlap : Int?
  mut hole_size : Int?
  mut bubble_scale : Int?
} derive(Debug)

///|
struct ChartComboOptions {
  typ : ChartType
  series : Array[ChartSeries]
  x_axis : ChartAxis
  y_axis : ChartAxis
  plot_area : ChartPlotArea
  vary_colors : Bool
  gap_width : Int?
  overlap : Int?
  hole_size : Int?
  bubble_scale : Int?
} derive(Debug)

///|
fn ChartComboOptions::from_chart_options(
  opts : ChartOptions,
) -> ChartComboOptions {
  {
    typ: opts.typ,
    series: opts.series,
    x_axis: opts.x_axis,
    y_axis: opts.y_axis,
    plot_area: opts.plot_area,
    vary_colors: opts.vary_colors,
    gap_width: opts.gap_width,
    overlap: opts.overlap,
    hole_size: opts.hole_size,
    bubble_scale: opts.bubble_scale,
  }
}

///|
pub fn ChartOptions::new(typ : ChartType) -> ChartOptions {
  {
    typ,
    series: [],
    combo_charts: [],
    format: GraphicOptions::new(),
    dimension: ChartDimension::new(),
    legend: ChartLegend::new(),
    title: "",
    title_rich: None,
    fill_color: "",
    fill_transparency: 0,
    fill: None,
    border: ChartLine::new(),
    vary_colors: true,
    x_axis: ChartAxis::new(),
    y_axis: ChartAxis::new(),
    plot_area: ChartPlotArea::new(),
    show_blanks_as: "gap",
    gap_width: None,
    overlap: None,
    hole_size: None,
    bubble_scale: None,
  }
}

///|
pub fn ChartOptions::add_combo_chart(
  self : ChartOptions,
  combo : ChartOptions,
) -> Unit {
  self.combo_charts.push(ChartComboOptions::from_chart_options(combo))
}

///|
fn chart_type_to_plot(kind : ChartType) -> (String, String?) {
  match kind {
    Col => ("barChart", Some("col"))
    ColStacked => ("barChart", Some("col"))
    ColPercentStacked => ("barChart", Some("col"))
    Bar => ("barChart", Some("bar"))
    BarStacked => ("barChart", Some("bar"))
    BarPercentStacked => ("barChart", Some("bar"))
    Col3D
    | Col3DClustered
    | Col3DStacked
    | Col3DPercentStacked
    | Col3DCone
    | Col3DConeClustered
    | Col3DConeStacked
    | Col3DConePercentStacked
    | Col3DPyramid
    | Col3DPyramidClustered
    | Col3DPyramidStacked
    | Col3DPyramidPercentStacked
    | Col3DCylinder
    | Col3DCylinderClustered
    | Col3DCylinderStacked
    | Col3DCylinderPercentStacked => ("bar3DChart", Some("col"))
    Bar3DClustered
    | Bar3DStacked
    | Bar3DPercentStacked
    | Bar3DConeClustered
    | Bar3DConeStacked
    | Bar3DConePercentStacked
    | Bar3DPyramidClustered
    | Bar3DPyramidStacked
    | Bar3DPyramidPercentStacked
    | Bar3DCylinderClustered
    | Bar3DCylinderStacked
    | Bar3DCylinderPercentStacked => ("bar3DChart", Some("bar"))
    Line => ("lineChart", None)
    Line3D => ("line3DChart", None)
    Area => ("areaChart", None)
    AreaStacked => ("areaChart", None)
    AreaPercentStacked => ("areaChart", None)
    Area3D => ("area3DChart", None)
    Area3DStacked => ("area3DChart", None)
    Area3DPercentStacked => ("area3DChart", None)
    Pie => ("pieChart", None)
    Pie3D => ("pie3DChart", None)
    PieOfPie => ("ofPieChart", None)
    BarOfPie => ("ofPieChart", None)
    Doughnut => ("doughnutChart", None)
    Radar => ("radarChart", None)
    Scatter => ("scatterChart", None)
    Bubble => ("bubbleChart", None)
    Bubble3D => ("bubbleChart", None)
    Surface3D | WireframeSurface3D => ("surface3DChart", None)
    Contour | WireframeContour => ("surfaceChart", None)
    StockHighLowClose => ("stockChart", None)
    StockOpenHighLowClose => ("stockChart", None)
  }
}

///|
fn chart_grouping(kind : ChartType) -> String? {
  match kind {
    Line | Line3D | Area | Area3D => Some("standard")
    Col | Bar | Col3DClustered | Bar3DClustered => Some("clustered")
    Col3DConeClustered
    | Bar3DConeClustered
    | Col3DPyramidClustered
    | Bar3DPyramidClustered
    | Col3DCylinderClustered
    | Bar3DCylinderClustered => Some("clustered")
    Col3D | Col3DCone | Col3DPyramid | Col3DCylinder => Some("standard")
    ColStacked
    | BarStacked
    | AreaStacked
    | Col3DStacked
    | Bar3DStacked
    | Area3DStacked
    | Col3DConeStacked
    | Bar3DConeStacked
    | Col3DPyramidStacked
    | Bar3DPyramidStacked
    | Col3DCylinderStacked
    | Bar3DCylinderStacked => Some("stacked")
    ColPercentStacked
    | BarPercentStacked
    | AreaPercentStacked
    | Col3DPercentStacked
    | Bar3DPercentStacked
    | Area3DPercentStacked
    | Col3DConePercentStacked
    | Bar3DConePercentStacked
    | Col3DPyramidPercentStacked
    | Bar3DPyramidPercentStacked
    | Col3DCylinderPercentStacked
    | Bar3DCylinderPercentStacked => Some("percentStacked")
    _ => None
  }
}

///|
fn plot_supports_vary_colors(plot_tag : StringView) -> Bool {
  plot_tag == "areaChart" ||
  plot_tag == "area3DChart" ||
  plot_tag == "barChart" ||
  plot_tag == "bar3DChart" ||
  plot_tag == "bubbleChart" ||
  plot_tag == "doughnutChart" ||
  plot_tag == "lineChart" ||
  plot_tag == "line3DChart" ||
  plot_tag == "ofPieChart" ||
  plot_tag == "pieChart" ||
  plot_tag == "pie3DChart" ||
  plot_tag == "radarChart" ||
  plot_tag == "scatterChart" ||
  plot_tag == "surface3DChart"
}

///|
fn write_plot_vary_colors(sb : StringBuilder, vary_colors : Bool) -> Unit {
  let value = if vary_colors { "1" } else { "0" }
  sb.write_view("        \n")
}

///|
fn legend_position_value(position : String) -> String? {
  let normalized = position.trim().to_lower().to_owned()
  match normalized {
    "none" => None
    "b" | "bottom" => Some("b")
    "t" | "top" => Some("t")
    "l" | "left" => Some("l")
    "r" | "right" => Some("r")
    "tr" | "topright" | "top_right" | "top-right" | "top right" => Some("tr")
    _ => Some(normalized)
  }
}

///|
fn chart_shape(kind : ChartType) -> String? {
  match kind {
    Bar3DConeClustered
    | Bar3DConeStacked
    | Bar3DConePercentStacked
    | Col3DCone
    | Col3DConeClustered
    | Col3DConeStacked
    | Col3DConePercentStacked => Some("cone")
    Bar3DPyramidClustered
    | Bar3DPyramidStacked
    | Bar3DPyramidPercentStacked
    | Col3DPyramid
    | Col3DPyramidClustered
    | Col3DPyramidStacked
    | Col3DPyramidPercentStacked => Some("pyramid")
    Bar3DCylinderClustered
    | Bar3DCylinderStacked
    | Bar3DCylinderPercentStacked
    | Col3DCylinder
    | Col3DCylinderClustered
    | Col3DCylinderStacked
    | Col3DCylinderPercentStacked => Some("cylinder")
    _ => None
  }
}

///|
fn write_drawingml_r_pr(
  sb : StringBuilder,
  indent : StringView,
  font : RichTextFont?,
) -> Unit {
  match font {
    Some(value) => {
      sb.write_view(indent)
      sb.write_view(" {
          sb.write_view(" u=\"")
          sb.write_view(escape_xml_attr(u))
          sb.write_view("\"")
        }
        None => ()
      }
      match value.size {
        Some(size) =>
          sb.write_view(" sz=\"\{(size * 100.0).round().to_int()}\"")
        None => ()
      }
      sb.write_view(">")
      match value.family {
        Some(family) => {
          sb.write_view("")
          sb.write_view("")
          sb.write_view("")
        }
        None => ()
      }
      match value.color {
        Some(color) => {
          let normalized = color.replace_all(old="#", new="").to_upper()
          sb.write_view("")
        }
        None => ()
      }
      sb.write_view("\n")
    }
    None => {
      sb.write_view(indent)
      sb.write_view("\n")
    }
  }
}

///|
fn write_drawingml_run(
  sb : StringBuilder,
  indent : StringView,
  run : RichTextRun,
) -> Unit {
  sb.write_view(indent)
  sb.write_view("\n")
  write_drawingml_r_pr(sb, "\{indent.to_owned()}  ", run.font)
  let text_value = if run.text == "" { " " } else { run.text }
  sb.write_view(indent)
  if text_value.has_prefix(" ") ||
    text_value.has_suffix(" ") ||
    text_value.contains("\n") {
    sb.write_view("  ")
  } else {
    sb.write_view("  ")
  }
  sb.write_view(escape_xml_text(text_value))
  sb.write_view("\n")
  sb.write_view(indent)
  sb.write_view("\n")
}

///|
fn write_chart_rich_tx(
  sb : StringBuilder,
  tx_indent : StringView,
  runs : ArrayView[RichTextRun],
) -> Unit {
  let base = tx_indent.to_owned()
  sb.write_view(base)
  sb.write_view("\n")
  sb.write_view("\{base}  \n")
  sb.write_view("\{base}    \n")
  sb.write_view("\{base}    \n")
  sb.write_view("\{base}    \n")
  let run_indent = "\{base}      "
  for run in runs {
    write_drawingml_run(sb, run_indent, run)
  }
  sb.write_view("\{base}    \n")
  sb.write_view("\{base}  \n")
  sb.write_view(base)
  sb.write_view("\n")
}

///|
fn write_chart_title(
  sb : StringBuilder,
  title : String,
  title_rich : Array[RichTextRun]?,
) -> Unit {
  match title_rich {
    Some(runs) => {
      if runs.length() == 0 {
        return
      }
      sb.write_view("    \n")
      write_chart_rich_tx(sb, "      ", runs)
      sb.write_view("    \n")
    }
    None =>
      if title != "" {
        sb.write_view("    \n")
        write_chart_rich_tx(sb, "      ", [{ text: title, font: None }])
        sb.write_view("    \n")
      }
  }
}

///|
fn write_chart_series_tx(sb : StringBuilder, name : String) -> Unit {
  if name == "" {
    return
  }
  sb.write_view("          ")
  if name.contains("!") {
    sb.write_view("")
    sb.write_view(escape_xml_text(name))
    sb.write_view("")
  } else {
    sb.write_view("")
    sb.write_view(escape_xml_text(name))
    sb.write_view("")
  }
  sb.write_view("\n")
}

///|
fn chart_dash_value(dash : ChartDashType) -> StringView? {
  match dash {
    Unset => None
    Solid => Some("solid")
    Dot => Some("dot")
    Dash => Some("dash")
    LgDash => Some("lgDash")
    SashDot => Some("dashDot")
    LgDashDot => Some("lgDashDot")
    LgDashDotDot => Some("lgDashDotDot")
    SysDash => Some("sysDash")
    SysDot => Some("sysDot")
    SysDashDot => Some("sysDashDot")
    SysDashDotDot => Some("sysDashDotDot")
  }
}

///|
fn data_label_pos_value(pos : ChartDataLabelPositionType) -> StringView {
  match pos {
    Center => "ctr"
    Left => "l"
    Right => "r"
    Top => "t"
    Bottom => "b"
    BestFit => "bestFit"
    InEnd => "inEnd"
    OutEnd => "outEnd"
  }
}

///|
fn write_chart_series_sppr(
  sb : StringBuilder,
  series : ChartSeries,
  global_idx : Int,
) -> Unit {
  let line = series.line
  let fill_dir = chart_fill_directive(
    series.fill,
    series.fill_color,
    series.fill_transparency,
  )
  if fill_dir is NoOverride &&
    line.typ == Unset &&
    line.dash == Unset &&
    line.color == "" &&
    line.transparency == 0 &&
    line.width == 0.0 {
    return
  }
  sb.write_view("          \n")
  match fill_dir {
    SolidFill(color~, transparency~) =>
      write_solid_fill(sb, "            ", color, transparency)
    NoFill => sb.write_view("            \n")
    NoOverride => ()
  }
  if line.typ == NoLine {
    sb.write_view("            \n")
    sb.write_view("          \n")
    return
  }
  sb.write_view("             0.0 {
    let w = (line.width * 12700.0).round().to_int()
    sb.write_view(" w=\"\{w}\"")
  }
  let dash = chart_dash_value(line.dash)
  if line.typ != Solid && line.color == "" && dash is None {
    sb.write_view("/>\n")
    sb.write_view("          \n")
    return
  }
  sb.write_view(">\n")
  if line.typ == Solid && line.color == "" {
    // Excelize series lines cycle the theme accent palette
    // (drawChartSeriesSpPr); tx1 is only used for border lines.
    let accent = global_idx % 6 + 1
    sb.write_view(
      "              \n",
    )
  }
  if line.color != "" {
    let color = line.color.replace_all(old="#", new="").to_upper()
    sb.write_view("               100 { 100 } else { t }
    if transparency > 0 {
      let alpha = (100 - transparency) * 1000
      sb.write_view(
        "\">\n",
      )
    } else {
      sb.write_view("\"/>\n")
    }
  }
  match dash {
    Some(value) => {
      sb.write_view("              \n")
    }
    None => ()
  }
  sb.write_view("            \n")
  sb.write_view("          \n")
}

///|
fn write_chart_series_marker(sb : StringBuilder, marker : ChartMarker) -> Unit {
  let border = marker.border
  let has_border = border.typ != Unset ||
    border.dash != Unset ||
    border.color != "" ||
    border.width != 0.0
  let fill_dir = chart_fill_directive(
    marker.fill,
    marker.fill_color,
    marker.fill_transparency,
  )
  let has_fill = !(fill_dir is NoOverride)
  if marker.symbol == "" && marker.size == 0 && !has_border && !has_fill {
    return
  }
  let sym = if marker.symbol == "" { "auto" } else { marker.symbol }
  sb.write_view("          \n")
  sb.write_view("            \n")
  if marker.size > 0 {
    sb.write_view("            \n")
  }
  if has_border || has_fill {
    sb.write_view("            \n")
    match fill_dir {
      SolidFill(color~, transparency~) =>
        write_solid_fill(sb, "              ", color, transparency)
      NoFill => sb.write_view("              \n")
      NoOverride => ()
    }
    if has_border {
      if border.typ == NoLine {
        sb.write_view("              \n")
      } else {
        sb.write_view("               0.0 {
          let w = (border.width * 12700.0).round().to_int()
          sb.write_view(" w=\"\{w}\"")
        }
        let dash = chart_dash_value(border.dash)
        if border.typ != Solid && border.color == "" && dash is None {
          sb.write_view("/>\n")
        } else {
          sb.write_view(">\n")
          if border.typ == Solid && border.color == "" {
            write_chart_line_solid_scheme(sb, "                ")
          }
          if border.color != "" {
            let color = border.color.replace_all(old="#", new="").to_upper()
            sb.write_view("                 100 { 100 } else { t }
            if transparency > 0 {
              let alpha = (100 - transparency) * 1000
              sb.write_view(
                "\">\n",
              )
            } else {
              sb.write_view("\"/>\n")
            }
          }
          match dash {
            Some(value) => {
              sb.write_view("                \n")
            }
            None => ()
          }
          sb.write_view("              \n")
        }
      }
    }
    sb.write_view("            \n")
  }
  sb.write_view("          \n")
}

///|
fn write_chart_series_dlbls(sb : StringBuilder, series : ChartSeries) -> Unit {
  let pos_set = series.data_label_position != BestFit
  let has_font = series.data_label.font != Font::new()
  let has_alignment = series.data_label.alignment != Alignment::new()
  let fill_dir = chart_fill_directive(
    series.data_label.fill,
    series.data_label.fill_color,
    series.data_label.fill_transparency,
  )
  let has_fill = !(fill_dir is NoOverride)
  if !pos_set && !has_font && !has_alignment && !has_fill {
    return
  }
  sb.write_view("          \n")
  if pos_set {
    sb.write_view("            \n")
  }
  if has_fill {
    sb.write_view("            \n")
    match fill_dir {
      SolidFill(color~, transparency~) =>
        write_solid_fill(sb, "              ", color, transparency)
      NoFill => sb.write_view("              \n")
      NoOverride => ()
    }
    sb.write_view("            \n")
  }
  if has_font || has_alignment {
    write_drawingml_tx_pr_with_font_and_alignment(
      sb,
      "            ",
      series.data_label.font,
      series.data_label.alignment,
    )
  }
  sb.write_view("          \n")
}

///|
fn write_chart_series_dpt(sb : StringBuilder, point : ChartDataPoint) -> Unit {
  let fill_dir = chart_fill_directive(point.fill, point.fill_color, 0)
  if fill_dir is NoOverride {
    return
  }
  sb.write_view("          \n")
  sb.write_view("            \n")
  sb.write_view("            \n")
  match fill_dir {
    SolidFill(color~, transparency~) =>
      write_solid_fill(sb, "              ", color, transparency)
    NoFill => sb.write_view("              \n")
    NoOverride => ()
  }
  sb.write_view("            \n")
  sb.write_view("          \n")
}

///|
fn write_chart_series(
  sb : StringBuilder,
  series : ChartSeries,
  idx : Int,
  order_base : Int,
) -> Unit {
  let global_idx = order_base + idx
  sb.write_view("        \n")
  sb.write_view("          \n")
  sb.write_view("          \n")
  write_chart_series_tx(sb, series.name)
  write_chart_series_sppr(sb, series, global_idx)
  write_chart_series_marker(sb, series.marker)
  if series.line.smooth {
    sb.write_view("          \n")
  }
  for point in series.data_point {
    write_chart_series_dpt(sb, point)
  }
  write_chart_series_dlbls(sb, series)
  sb.write_view("          ")
  sb.write_view(escape_xml_text(series.categories))
  sb.write_view("\n")
  sb.write_view("          ")
  sb.write_view(escape_xml_text(series.values))
  sb.write_view("\n")
  sb.write_view("        \n")
}

///|
fn write_chart_series_scatter(
  sb : StringBuilder,
  series : ChartSeries,
  idx : Int,
  order_base : Int,
) -> Unit {
  let global_idx = order_base + idx
  sb.write_view("        \n")
  sb.write_view("          \n")
  sb.write_view("          \n")
  write_chart_series_tx(sb, series.name)
  write_chart_series_sppr(sb, series, global_idx)
  write_chart_series_marker(sb, series.marker)
  if series.line.smooth {
    sb.write_view("          \n")
  }
  for point in series.data_point {
    write_chart_series_dpt(sb, point)
  }
  write_chart_series_dlbls(sb, series)
  sb.write_view("          ")
  sb.write_view(escape_xml_text(series.categories))
  sb.write_view("\n")
  sb.write_view("          ")
  sb.write_view(escape_xml_text(series.values))
  sb.write_view("\n")
  sb.write_view("        \n")
}

///|
fn write_chart_series_for_plot(
  sb : StringBuilder,
  plot_tag : StringView,
  series : ChartSeries,
  idx : Int,
  order_base : Int,
) -> Unit raise XlsxError {
  if plot_tag == "scatterChart" {
    write_chart_series_scatter(sb, series, idx, order_base)
  } else if plot_tag == "bubbleChart" {
    let bubble_size = match chart_series_bubble_size(series) {
      Some(value) => value
      None => raise InvalidSheetOperation(msg="bubble size missing")
    }
    let global_idx = order_base + idx
    sb.write_view("        \n")
    sb.write_view("          \n")
    sb.write_view("          \n")
    write_chart_series_tx(sb, series.name)
    write_chart_series_sppr(sb, series, global_idx)
    write_chart_series_marker(sb, series.marker)
    if series.line.smooth {
      sb.write_view("          \n")
    }
    for point in series.data_point {
      write_chart_series_dpt(sb, point)
    }
    write_chart_series_dlbls(sb, series)
    sb.write_view("          ")
    sb.write_view(escape_xml_text(series.categories))
    sb.write_view("\n")
    sb.write_view("          ")
    sb.write_view(escape_xml_text(series.values))
    sb.write_view("\n")
    sb.write_view("          ")
    sb.write_view(escape_xml_text(bubble_size))
    sb.write_view("\n")
    sb.write_view("        \n")
  } else {
    write_chart_series(sb, series, idx, order_base)
  }
}

///|
fn write_chart_sppr(
  sb : StringBuilder,
  fill_color : String,
  border : ChartLine,
  fill? : ChartFill,
) -> Unit {
  let fill_dir = chart_fill_directive(fill, fill_color, 0)
  if fill_dir is NoOverride &&
    (border.typ == Unset || border.typ == Automatic) &&
    border.dash == Unset &&
    border.color == "" &&
    border.transparency == 0 &&
    border.width == 0.0 {
    return
  }
  sb.write_view("            \n")
  match fill_dir {
    SolidFill(color~, transparency~) =>
      write_solid_fill(sb, "              ", color, transparency)
    NoFill => sb.write_view("              \n")
    NoOverride => ()
  }
  if border.typ == NoLine {
    sb.write_view("              \n")
  } else if border.typ == Solid ||
    border.color != "" ||
    border.width != 0.0 ||
    border.dash != Unset {
    sb.write_view("               0.0 {
      let w = (border.width * 12700.0).round().to_int()
      sb.write_view(" w=\"\{w}\"")
    }
    let dash = chart_dash_value(border.dash)
    if border.typ != Solid && border.color == "" && dash is None {
      sb.write_view("/>\n")
    } else {
      sb.write_view(">\n")
      if border.typ == Solid && border.color == "" {
        write_chart_line_solid_scheme(sb, "                ")
      }
      if border.color != "" {
        let color = border.color.replace_all(old="#", new="").to_upper()
        sb.write_view("                 100 { 100 } else { t }
        if transparency > 0 {
          let alpha = (100 - transparency) * 1000
          sb.write_view(
            "\">\n",
          )
        } else {
          sb.write_view("\"/>\n")
        }
      }
      match dash {
        Some(value) => {
          sb.write_view("                \n")
        }
        None => ()
      }
      sb.write_view("              \n")
    }
  }
  sb.write_view("            \n")
}

///|
fn tick_label_pos_value(pos : ChartTickLabelPositionType) -> StringView {
  match pos {
    NextToAxis => "nextTo"
    High => "high"
    Low => "low"
    NoTickLabels => "none"
  }
}

///|
fn val_axis_tick_label_pos(
  kind : ChartType,
  pos : ChartTickLabelPositionType,
) -> StringView {
  match kind {
    Contour | WireframeContour => "none"
    _ => tick_label_pos_value(pos)
  }
}

///|
fn val_axis_cross_between(kind : ChartType) -> StringView {
  match kind {
    Area
    | AreaStacked
    | AreaPercentStacked
    | Area3D
    | Area3DStacked
    | Area3DPercentStacked
    | Surface3D
    | WireframeSurface3D
    | Contour
    | WireframeContour
    | Bubble
    | Bubble3D => "midCat"
    _ => "between"
  }
}

///|
fn axis_position_value(
  axis_tag : StringView,
  axis : ChartAxis,
  axis_pos : StringView,
) -> StringView {
  if !axis.reverse_order {
    return axis_pos
  }
  if axis_pos == "b" &&
    (axis_tag == "catAx" || axis_tag == "dateAx" || axis_tag == "serAx") {
    return "t"
  }
  if axis_pos == "l" && axis_tag == "valAx" {
    return "r"
  }
  axis_pos
}

///|
fn axis_crosses_value(
  axis_tag : StringView,
  axis_pos : StringView,
) -> StringView? {
  if axis_tag == "valAx" {
    if axis_pos == "r" {
      Some("max")
    } else {
      Some("autoZero")
    }
  } else if axis_tag == "catAx" || axis_tag == "dateAx" {
    Some("autoZero")
  } else {
    None
  }
}

///|
fn axis_id_or_default(axis : ChartAxis, fallback : Int) -> Int {
  if axis.ax_id > 0 {
    axis.ax_id
  } else {
    fallback
  }
}

///|
fn write_axis_title(
  sb : StringBuilder,
  title : String,
  title_rich : Array[RichTextRun]?,
) -> Unit {
  match title_rich {
    Some(runs) => {
      if runs.length() == 0 {
        return
      }
      sb.write_view("        \n")
      write_chart_rich_tx(sb, "          ", runs)
      sb.write_view("        \n")
    }
    None =>
      if title != "" {
        sb.write_view("        \n")
        write_chart_rich_tx(sb, "          ", [{ text: title, font: None }])
        sb.write_view("        \n")
      }
  }
}

///|
fn write_axis_num_fmt(sb : StringBuilder, fmt : ChartNumFmt) -> Unit {
  if fmt.custom_num_fmt == "" && fmt.source_linked {
    return
  }
  let code = if fmt.custom_num_fmt == "" {
    "General"
  } else {
    fmt.custom_num_fmt
  }
  sb.write_view("        \n")
}

///|
fn drawingml_text_vertical_value(value : StringView) -> StringView? {
  match value {
    "horz" => Some("horz")
    "vert" => Some("vert")
    "vert270" => Some("vert270")
    "wordArtVert" => Some("wordArtVert")
    "eaVert" => Some("eaVert")
    "mongolianVert" => Some("mongolianVert")
    "wordArtVertRtl" => Some("wordArtVertRtl")
    _ => None
  }
}

///|
fn drawingml_paragraph_align_value(value : StringView) -> StringView? {
  match value {
    "left" => Some("l")
    "center" => Some("ctr")
    "right" => Some("r")
    "justify" => Some("just")
    "distributed" => Some("dist")
    "centerContinuous" => Some("ctr")
    _ => None
  }
}

///|
fn drawingml_underline_value(value : StringView) -> StringView? {
  match value {
    "none" => Some("none")
    "words" => Some("words")
    "sng" => Some("sng")
    "dbl" => Some("dbl")
    "heavy" => Some("heavy")
    "dotted" => Some("dotted")
    "dottedHeavy" => Some("dottedHeavy")
    "dash" => Some("dash")
    "dashHeavy" => Some("dashHeavy")
    "dashLong" => Some("dashLong")
    "dashLongHeavy" => Some("dashLongHeavy")
    "dotDash" => Some("dotDash")
    "dotDashHeavy" => Some("dotDashHeavy")
    "dotDotDash" => Some("dotDotDash")
    "dotDotDashHeavy" => Some("dotDotDashHeavy")
    "wavy" => Some("wavy")
    "wavyHeavy" => Some("wavyHeavy")
    "wavyDbl" => Some("wavyDbl")
    _ => None
  }
}

///|
fn write_drawingml_def_r_pr_from_font(
  sb : StringBuilder,
  indent : StringView,
  font : Font,
) -> Unit {
  sb.write_view(indent)
  sb.write_view(" sb.write_view(" b=\"1\"")
    _ => ()
  }
  match font.italic {
    Some(true) => sb.write_view(" i=\"1\"")
    _ => ()
  }
  match font.strike {
    Some(true) => sb.write_view(" strike=\"sngStrike\"")
    _ => ()
  }
  match font.underline {
    Some(u) =>
      match drawingml_underline_value(u) {
        Some(v) => sb.write_view(" u=\"\{v.to_owned()}\"")
        None => ()
      }
    None => ()
  }
  match font.size {
    Some(size) =>
      if size > 0.0 {
        sb.write_view(" sz=\"\{(size * 100.0).round().to_int()}\"")
      }
    None => ()
  }
  sb.write_view(">")
  match font.family {
    Some(family) => {
      sb.write_view("")
      sb.write_view("")
      sb.write_view("")
    }
    None => ()
  }
  match font.color {
    Some(color) => {
      let normalized = color.replace_all(old="#", new="").to_upper()
      sb.write_view("")
    }
    None => ()
  }
  sb.write_view("\n")
}

///|
fn write_drawingml_tx_pr_with_font(
  sb : StringBuilder,
  tx_pr_indent : StringView,
  font : Font,
) -> Unit {
  let base = tx_pr_indent.to_owned()
  sb.write_view(base)
  sb.write_view("\n")
  sb.write_view("\{base}  \n")
  sb.write_view("\{base}  \n")
  sb.write_view("\{base}  \n")
  sb.write_view("\{base}    \n")
  write_drawingml_def_r_pr_from_font(sb, "\{base}      ", font)
  sb.write_view("\{base}    \n")
  sb.write_view("\{base}    \n")
  sb.write_view("\{base}  \n")
  sb.write_view(base)
  sb.write_view("\n")
}

///|
fn write_drawingml_tx_pr_with_font_and_alignment(
  sb : StringBuilder,
  tx_pr_indent : StringView,
  font : Font,
  alignment : Alignment,
) -> Unit {
  let base = tx_pr_indent.to_owned()
  sb.write_view(base)
  sb.write_view("\n")
  sb.write_view("\{base}  
      if value >= -90 && value <= 90 {
        sb.write_view(" rot=\"\{value * 60000}\"")
      }
    None => ()
  }
  match alignment.vertical {
    Some(value) =>
      match drawingml_text_vertical_value(value) {
        Some(v) => sb.write_view(" vert=\"\{v.to_owned()}\"")
        None => ()
      }
    None => ()
  }
  sb.write_view("/>\n")
  sb.write_view("\{base}  \n")
  sb.write_view("\{base}  \n")
  let algn = match alignment.horizontal {
    Some(v) => drawingml_paragraph_align_value(v)
    None => None
  }
  match algn {
    Some(v) => sb.write_view("\{base}    \n")
    None => sb.write_view("\{base}    \n")
  }
  if font == Font::new() {
    sb.write_view("\{base}      \n")
  } else {
    write_drawingml_def_r_pr_from_font(sb, "\{base}      ", font)
  }
  sb.write_view("\{base}    \n")
  sb.write_view("\{base}    \n")
  sb.write_view("\{base}  \n")
  sb.write_view(base)
  sb.write_view("\n")
}

///|
fn write_axis_tx_pr(sb : StringBuilder, axis : ChartAxis) -> Unit {
  let has_font = axis.font != Font::new()
  let mut needs = has_font
  match axis.alignment.text_rotation {
    Some(_) => needs = true
    None => ()
  }
  match axis.alignment.vertical {
    Some(_) => needs = true
    None => ()
  }
  if !needs {
    return
  }
  sb.write_view("        \n")
  sb.write_view("          
      if value >= -90 && value <= 90 {
        sb.write_view(" rot=\"\{value * 60000}\"")
      }
    None => ()
  }
  match axis.alignment.vertical {
    Some(value) =>
      match drawingml_text_vertical_value(value) {
        Some(v) => sb.write_view(" vert=\"\{v.to_owned()}\"")
        None => ()
      }
    None => ()
  }
  sb.write_view("/>\n")
  sb.write_view("          \n")
  sb.write_view("          \n")
  sb.write_view("            \n")
  if !has_font {
    sb.write_view("              \n")
  } else {
    write_drawingml_def_r_pr_from_font(sb, "              ", axis.font)
  }
  sb.write_view("            \n")
  sb.write_view("            \n")
  sb.write_view("          \n")
  sb.write_view("        \n")
}

///|
fn write_axis(
  sb : StringBuilder,
  axis_tag : StringView,
  axis : ChartAxis,
  ax_id : Int,
  cross_ax_id : Int,
  axis_pos : StringView,
  kind : ChartType,
) -> Unit {
  let is_category_like = axis_tag == "catAx" || axis_tag == "dateAx"
  let is_value_like = axis_tag == "valAx"
  let resolved_axis_pos = axis_position_value(axis_tag, axis, axis_pos)
  sb.write_view("      \n")
  sb.write_view("        \n")
  sb.write_view("        ")
  if axis.reverse_order {
    sb.write_view("")
  } else {
    sb.write_view("")
  }
  match axis.log_base {
    0.0 => ()
    value => sb.write_view("")
  }
  match axis.maximum {
    Some(value) => sb.write_view("")
    None => ()
  }
  match axis.minimum {
    Some(value) => sb.write_view("")
    None => ()
  }
  sb.write_view("\n")
  sb.write_view("        \n")
  sb.write_view("        \n")
  if axis.major_grid_lines {
    sb.write_view("        \n")
  }
  if axis.minor_grid_lines {
    sb.write_view("        \n")
  }
  write_axis_title(sb, axis.title, axis.title_rich)
  write_axis_num_fmt(sb, axis.num_fmt)
  if axis_tag != "serAx" {
    sb.write_view("        \n")
    sb.write_view("        \n")
  }
  let tick_lbl_pos = if is_value_like {
    val_axis_tick_label_pos(kind, axis.tick_label_position)
  } else {
    tick_label_pos_value(axis.tick_label_position)
  }
  sb.write_view("        \n")
  write_axis_tx_pr(sb, axis)
  sb.write_view("        \n")
  match axis_crosses_value(axis_tag, resolved_axis_pos) {
    Some(value) => {
      sb.write_view("        \n")
    }
    None => ()
  }
  if is_value_like {
    sb.write_view("        \n")
  }
  if is_category_like {
    sb.write_view("        \n")
    sb.write_view("        \n")
    sb.write_view("        \n")
    sb.write_view("        \n")
  }
  if is_category_like && axis.tick_label_skip > 0 {
    sb.write_view("        \n")
  }
  if is_value_like && axis.major_unit > 0.0 {
    sb.write_view("        \n")
  }
  sb.write_view("      \n")
}

///|
fn write_chart_dlbls(
  sb : StringBuilder,
  plot_tag : StringView,
  show_legend_key : Bool,
  plot_area : ChartPlotArea,
) -> Unit {
  if plot_tag == "surface3DChart" || plot_tag == "surfaceChart" {
    return
  }
  let fmt = plot_area.num_fmt
  let need_num_fmt = fmt.custom_num_fmt != "" || !fmt.source_linked
  let has_flags = show_legend_key ||
    plot_area.show_val ||
    plot_area.show_cat_name ||
    plot_area.show_ser_name ||
    plot_area.show_percent ||
    plot_area.show_leader_lines ||
    plot_area.show_bubble_size ||
    need_num_fmt
  if !has_flags {
    return
  }
  sb.write_view("        \n")
  if need_num_fmt {
    let code = if fmt.custom_num_fmt == "" {
      "General"
    } else {
      fmt.custom_num_fmt
    }
    sb.write_view("          \n")
  }
  if show_legend_key {
    sb.write_view("          \n")
  }
  if plot_area.show_val {
    sb.write_view("          \n")
  }
  if plot_area.show_cat_name {
    sb.write_view("          \n")
  }
  if plot_area.show_ser_name {
    sb.write_view("          \n")
  }
  if plot_area.show_percent {
    sb.write_view("          \n")
  }
  if plot_area.show_leader_lines {
    sb.write_view("          \n")
  }
  if plot_area.show_bubble_size {
    sb.write_view("          \n")
  }
  sb.write_view("        \n")
}

///|
fn write_plot_area_sppr(sb : StringBuilder, opts : ChartOptions) -> Unit {
  let fill_dir = chart_fill_directive(
    opts.plot_area.fill,
    opts.plot_area.fill_color,
    opts.plot_area.fill_transparency,
  )
  if fill_dir is NoOverride {
    return
  }
  sb.write_view("      \n")
  match fill_dir {
    SolidFill(color~, transparency~) =>
      write_solid_fill(sb, "        ", color, transparency)
    NoFill => sb.write_view("        \n")
    NoOverride => ()
  }
  sb.write_view("      \n")
}

///|
fn write_plot_area_d_table(
  sb : StringBuilder,
  plot_tag : StringView,
  opts : ChartOptions,
) -> Unit {
  if !opts.plot_area.show_data_table {
    return
  }
  if plot_tag != "barChart" &&
    plot_tag != "bar3DChart" &&
    plot_tag != "areaChart" &&
    plot_tag != "area3DChart" &&
    plot_tag != "lineChart" &&
    plot_tag != "line3DChart" {
    return
  }
  sb.write_view("      \n")
  sb.write_view("        \n")
  sb.write_view("        \n")
  sb.write_view("        \n")
  if opts.plot_area.show_data_table_keys {
    sb.write_view("        \n")
  } else {
    sb.write_view("        \n")
  }
  sb.write_view("      \n")
}

///|
fn write_chart_sppr_root(sb : StringBuilder, opts : ChartOptions) -> Unit {
  let fill_dir = chart_fill_directive(
    opts.fill,
    opts.fill_color,
    opts.fill_transparency,
  )
  if fill_dir is NoOverride &&
    (opts.border.typ == Unset || opts.border.typ == Automatic) &&
    opts.border.dash == Unset &&
    opts.border.color == "" &&
    opts.border.transparency == 0 &&
    opts.border.width == 0.0 {
    return
  }
  sb.write_view("    \n")
  match fill_dir {
    SolidFill(color~, transparency~) =>
      write_solid_fill(sb, "      ", color, transparency)
    NoFill => sb.write_view("      \n")
    NoOverride => ()
  }
  let border = opts.border
  if border.typ == NoLine {
    sb.write_view("      \n")
  } else if border.color != "" || border.width != 0.0 || border.dash != Unset {
    sb.write_view("       0.0 {
      let w = (border.width * 12700.0).round().to_int()
      sb.write_view(" w=\"\{w}\"")
    }
    let dash = chart_dash_value(border.dash)
    if border.color == "" && dash is None {
      sb.write_view("/>\n")
    } else {
      sb.write_view(">\n")
      if border.color != "" {
        let color = border.color.replace_all(old="#", new="").to_upper()
        sb.write_view("         100 { 100 } else { t }
        if transparency > 0 {
          let alpha = (100 - transparency) * 1000
          sb.write_view(
            "\">\n",
          )
        } else {
          sb.write_view("\"/>\n")
        }
      }
      match dash {
        Some(value) => {
          sb.write_view("        \n")
        }
        None => ()
      }
      sb.write_view("      \n")
    }
  }
  sb.write_view("    \n")
}

///|
fn chart_options_to_xml(opts : ChartOptions) -> String raise XlsxError {
  if opts.series.length() == 0 {
    raise InvalidSheetOperation(msg="chart series empty")
  }
  if opts.combo_charts.length() > 0 {
    for combo in opts.combo_charts {
      if combo.series.length() == 0 {
        raise InvalidSheetOperation(msg="combo chart series empty")
      }
    }
  }
  let sb = StringBuilder::new()
  sb.write_view("\n")
  sb.write_view(
    "\n",
  )
  sb.write_view("  \n")
  write_chart_title(sb, opts.title, opts.title_rich)
  sb.write_view("    \n")
  sb.write_view("      \n")
  write_plot_area_sppr(sb, opts)
  let has_combo = opts.combo_charts.length() > 0
  if !has_combo {
    let (plot_tag, bar_dir) = chart_type_to_plot(opts.typ)
    if plot_tag == "stockChart" {
      let expected = if opts.typ == StockHighLowClose { 3 } else { 4 }
      if opts.series.length() != expected {
        raise InvalidSheetOperation(
          msg="stock chart requires \{expected} series",
        )
      }
    }
    sb.write_view("      \n")
    if plot_tag == "barChart" || plot_tag == "bar3DChart" {
      if bar_dir is Some(dir) {
        sb.write_view("        \n")
      }
    }
    if plot_tag == "barChart" ||
      plot_tag == "bar3DChart" ||
      plot_tag == "lineChart" ||
      plot_tag == "line3DChart" ||
      plot_tag == "areaChart" ||
      plot_tag == "area3DChart" {
      if chart_grouping(opts.typ) is Some(value) {
        sb.write_view("        \n")
      }
    }
    if plot_tag == "scatterChart" {
      sb.write_view("        \n")
    }
    if plot_tag == "radarChart" {
      sb.write_view("        \n")
    }
    if plot_tag == "ofPieChart" {
      let typ = if opts.typ == BarOfPie { "bar" } else { "pie" }
      sb.write_view("        \n")
    }
    if plot_tag == "surface3DChart" && opts.typ == WireframeSurface3D {
      sb.write_view("        \n")
    }
    if plot_tag == "surfaceChart" && opts.typ == WireframeContour {
      sb.write_view("        \n")
    }
    if plot_supports_vary_colors(plot_tag) {
      write_plot_vary_colors(sb, opts.vary_colors)
    }
    let cat_ax_id = 1
    let val_ax_id = 2
    let ser_ax_id = 3
    for i, series in opts.series {
      write_chart_series_for_plot(sb, plot_tag, series, i, 0)
    }
    write_chart_dlbls(sb, plot_tag, opts.legend.show_legend_key, opts.plot_area)
    write_chart_axis_lines(sb, plot_tag, opts.x_axis)
    if plot_tag == "stockChart" {
      sb.write_view("        \n")
      if opts.typ == StockOpenHighLowClose {
        sb.write_view("        \n")
        sb.write_view("          \n")
        sb.write_view("          \n")
        write_chart_sppr(
          sb,
          opts.plot_area.up_bars.fill_color,
          opts.plot_area.up_bars.border,
          fill?=opts.plot_area.up_bars.fill,
        )
        sb.write_view("          \n")
        sb.write_view("          \n")
        write_chart_sppr(
          sb,
          opts.plot_area.down_bars.fill_color,
          opts.plot_area.down_bars.border,
          fill?=opts.plot_area.down_bars.fill,
        )
        sb.write_view("          \n")
        sb.write_view("        \n")
      }
    }
    if plot_tag == "bubbleChart" && opts.typ == Bubble3D {
      sb.write_view("        \n")
    }
    if plot_tag == "bubbleChart" {
      match opts.bubble_scale {
        Some(value) =>
          if value > 0 && value <= 300 {
            sb.write_view("        \n")
          }
        None => ()
      }
    }
    if plot_tag == "ofPieChart" {
      if opts.plot_area.second_plot_values > 0 {
        sb.write_view(
          "        \n",
        )
      }
      sb.write_view("        \n")
    }
    if plot_tag == "doughnutChart" {
      let raw = match opts.hole_size {
        Some(v) => v
        None => 75
      }
      let hole = if raw > 0 && raw <= 90 { raw } else { 75 }
      sb.write_view("        \n")
    }
    match opts.gap_width {
      Some(value) =>
        if plot_tag == "barChart" || plot_tag == "bar3DChart" {
          sb.write_view("        \n")
        }
      None => ()
    }
    match opts.overlap {
      Some(value) =>
        if plot_tag == "barChart" || plot_tag == "bar3DChart" {
          sb.write_view("        \n")
        }
      None => ()
    }
    if plot_tag == "bar3DChart" {
      match chart_shape(opts.typ) {
        Some(value) => {
          sb.write_view("        \n")
        }
        None => ()
      }
    }
    if plot_tag != "pieChart" &&
      plot_tag != "pie3DChart" &&
      plot_tag != "ofPieChart" &&
      plot_tag != "doughnutChart" {
      if plot_tag == "surface3DChart" || plot_tag == "surfaceChart" {
        sb.write_view("        \n")
        sb.write_view("        \n")
        sb.write_view("        \n")
      } else {
        sb.write_view("        \n")
        sb.write_view("        \n")
      }
    }
    sb.write_view("      \n")
    if plot_tag != "pieChart" &&
      plot_tag != "pie3DChart" &&
      plot_tag != "ofPieChart" &&
      plot_tag != "doughnutChart" {
      let x_id = axis_id_or_default(opts.x_axis, cat_ax_id)
      let y_id = axis_id_or_default(opts.y_axis, val_ax_id)
      let x_axis_tag = if plot_tag == "scatterChart" ||
        plot_tag == "bubbleChart" {
        "valAx"
      } else if plot_tag == "stockChart" {
        "dateAx"
      } else {
        "catAx"
      }
      write_axis(sb, x_axis_tag, opts.x_axis, x_id, y_id, "b", opts.typ)
      write_axis(sb, "valAx", opts.y_axis, y_id, x_id, "l", opts.typ)
      if plot_tag == "surface3DChart" || plot_tag == "surfaceChart" {
        write_axis(sb, "serAx", opts.y_axis, ser_ax_id, y_id, "r", opts.typ)
      }
    }
    write_plot_area_d_table(sb, plot_tag, opts)
  } else {
    let plots : Array[ChartComboOptions] = [
      ChartComboOptions::from_chart_options(opts),
    ]
    for combo in opts.combo_charts {
      plots.push(combo)
    }
    let (main_plot_tag, _main_bar_dir) = chart_type_to_plot(opts.typ)
    let mut has_cat_val = false
    let mut has_xy = false
    let mut has_stock = false
    let mut data_table_tag : String? = None
    for plot in plots {
      let (plot_tag, _bar_dir) = chart_type_to_plot(plot.typ)
      if plot_tag == "pieChart" ||
        plot_tag == "pie3DChart" ||
        plot_tag == "ofPieChart" ||
        plot_tag == "doughnutChart" ||
        plot_tag == "surface3DChart" ||
        plot_tag == "surfaceChart" {
        raise UnsupportedFeature(msg="combo chart type unsupported")
      }
      if plot_tag == "scatterChart" || plot_tag == "bubbleChart" {
        has_xy = true
      } else {
        has_cat_val = true
        if plot_tag == "stockChart" {
          has_stock = true
        }
      }
      if data_table_tag is None &&
        (
          plot_tag == "barChart" ||
          plot_tag == "bar3DChart" ||
          plot_tag == "areaChart" ||
          plot_tag == "area3DChart" ||
          plot_tag == "lineChart" ||
          plot_tag == "line3DChart"
        ) {
        data_table_tag = Some(plot_tag.to_string())
      }
    }
    let cat_axis_tag = if has_stock { "dateAx" } else { "catAx" }
    let primary_cat_id = axis_id_or_default(opts.x_axis, 1)
    let primary_val_id = axis_id_or_default(opts.y_axis, 2)
    let mut secondary_plot : ChartComboOptions? = None
    if has_cat_val {
      for i in 1..
        (axis_id_or_default(plot.x_axis, 3), axis_id_or_default(plot.y_axis, 4))
      None => (0, 0)
    }
    let mut next_xy_axis_id = if secondary_plot is Some(_) { 5 } else { 3 }
    let mut xy_primary_plot : ChartComboOptions? = None
    let mut xy_secondary_plot : ChartComboOptions? = None
    if has_xy {
      for plot_idx in 0.. 0 && plot.y_axis.secondary {
            if xy_secondary_plot is None {
              xy_secondary_plot = Some(plot)
            }
          } else if xy_primary_plot is None {
            xy_primary_plot = Some(plot)
          }
        }
      }
      if xy_primary_plot is None {
        xy_primary_plot = xy_secondary_plot
      }
    }
    let (xy_primary_x_id, xy_primary_y_id) = match xy_primary_plot {
      Some(plot) => {
        let x_id = axis_id_or_default(plot.x_axis, next_xy_axis_id)
        let y_id = axis_id_or_default(plot.y_axis, next_xy_axis_id + 1)
        next_xy_axis_id = next_xy_axis_id + 2
        (x_id, y_id)
      }
      None => (0, 0)
    }
    let (xy_secondary_x_id, xy_secondary_y_id) = match xy_secondary_plot {
      Some(plot) => {
        let x_id = axis_id_or_default(plot.x_axis, next_xy_axis_id)
        let y_id = axis_id_or_default(plot.y_axis, next_xy_axis_id + 1)
        (x_id, y_id)
      }
      None => (0, 0)
    }
    let mut order_base = 0
    for plot_idx in 0.. 0 && plot.y_axis.secondary
      let (x_ax_id, y_ax_id) = if is_xy {
        if use_secondary && xy_secondary_x_id > 0 && xy_secondary_y_id > 0 {
          (xy_secondary_x_id, xy_secondary_y_id)
        } else {
          (xy_primary_x_id, xy_primary_y_id)
        }
      } else if use_secondary && secondary_cat_id > 0 && secondary_val_id > 0 {
        (secondary_cat_id, secondary_val_id)
      } else {
        (primary_cat_id, primary_val_id)
      }
      sb.write_view("      \n")
      if plot_tag == "barChart" || plot_tag == "bar3DChart" {
        if bar_dir is Some(dir) {
          sb.write_view("        \n")
        }
      }
      if plot_tag == "barChart" ||
        plot_tag == "bar3DChart" ||
        plot_tag == "lineChart" ||
        plot_tag == "line3DChart" ||
        plot_tag == "areaChart" ||
        plot_tag == "area3DChart" {
        if chart_grouping(plot.typ) is Some(value) {
          sb.write_view("        \n")
        }
      }
      if plot_tag == "scatterChart" {
        sb.write_view("        \n")
      }
      if plot_tag == "radarChart" {
        sb.write_view("        \n")
      }
      if plot_supports_vary_colors(plot_tag) {
        write_plot_vary_colors(sb, plot.vary_colors)
      }
      for i, series in plot.series {
        write_chart_series_for_plot(sb, plot_tag, series, i, order_base)
      }
      write_chart_dlbls(
        sb,
        plot_tag,
        opts.legend.show_legend_key,
        plot.plot_area,
      )
      write_chart_axis_lines(sb, plot_tag, plot.x_axis)
      if plot_tag == "stockChart" {
        sb.write_view("        \n")
        if plot.typ == StockOpenHighLowClose {
          sb.write_view("        \n")
          sb.write_view("          \n")
          sb.write_view("          \n")
          write_chart_sppr(
            sb,
            plot.plot_area.up_bars.fill_color,
            plot.plot_area.up_bars.border,
            fill?=plot.plot_area.up_bars.fill,
          )
          sb.write_view("          \n")
          sb.write_view("          \n")
          write_chart_sppr(
            sb,
            plot.plot_area.down_bars.fill_color,
            plot.plot_area.down_bars.border,
            fill?=plot.plot_area.down_bars.fill,
          )
          sb.write_view("          \n")
          sb.write_view("        \n")
        }
      }
      if plot_tag == "bubbleChart" && plot.typ == Bubble3D {
        sb.write_view("        \n")
      }
      if plot_tag == "bubbleChart" {
        match plot.bubble_scale {
          Some(value) =>
            if value > 0 && value <= 300 {
              sb.write_view("        \n")
            }
          None => ()
        }
      }
      match plot.gap_width {
        Some(value) =>
          if plot_tag == "barChart" || plot_tag == "bar3DChart" {
            sb.write_view("        \n")
          }
        None => ()
      }
      match plot.overlap {
        Some(value) =>
          if plot_tag == "barChart" || plot_tag == "bar3DChart" {
            sb.write_view("        \n")
          }
        None => ()
      }
      if plot_tag == "bar3DChart" {
        match chart_shape(plot.typ) {
          Some(value) => {
            sb.write_view("        \n")
          }
          None => ()
        }
      }
      sb.write_view("        \n")
      sb.write_view("        \n")
      sb.write_view("      \n")
      order_base = order_base + plot.series.length()
    }
    if has_cat_val {
      write_axis(
        sb,
        cat_axis_tag,
        opts.x_axis,
        primary_cat_id,
        primary_val_id,
        "b",
        opts.typ,
      )
      write_axis(
        sb,
        "valAx",
        opts.y_axis,
        primary_val_id,
        primary_cat_id,
        "l",
        opts.typ,
      )
      match secondary_plot {
        Some(value) => {
          write_axis(
            sb,
            cat_axis_tag,
            opts.x_axis,
            secondary_cat_id,
            secondary_val_id,
            "b",
            value.typ,
          )
          write_axis(
            sb,
            "valAx",
            value.y_axis,
            secondary_val_id,
            secondary_cat_id,
            "r",
            value.typ,
          )
        }
        None => ()
      }
    }
    match xy_primary_plot {
      Some(value) => {
        write_axis(
          sb,
          "valAx",
          value.x_axis,
          xy_primary_x_id,
          xy_primary_y_id,
          "b",
          value.typ,
        )
        write_axis(
          sb,
          "valAx",
          value.y_axis,
          xy_primary_y_id,
          xy_primary_x_id,
          "l",
          value.typ,
        )
      }
      None => ()
    }
    match xy_secondary_plot {
      Some(value) =>
        if xy_secondary_x_id > 0 && xy_secondary_y_id > 0 {
          write_axis(
            sb,
            "valAx",
            value.x_axis,
            xy_secondary_x_id,
            xy_secondary_y_id,
            "t",
            value.typ,
          )
          write_axis(
            sb,
            "valAx",
            value.y_axis,
            xy_secondary_y_id,
            xy_secondary_x_id,
            "r",
            value.typ,
          )
        }
      None => ()
    }
    match data_table_tag {
      Some(tag) => write_plot_area_d_table(sb, tag, opts)
      None => write_plot_area_d_table(sb, main_plot_tag, opts)
    }
  }
  sb.write_view("    \n")
  match legend_position_value(opts.legend.position) {
    Some(value) => {
      sb.write_view("    \n")
      sb.write_view("      \n")
      match opts.legend.font {
        Some(font) =>
          if font != Font::new() {
            write_drawingml_tx_pr_with_font(sb, "      ", font)
          }
        None => ()
      }
      sb.write_view("    \n")
    }
    None => ()
  }
  sb.write_view("    \n")
  if opts.show_blanks_as != "" {
    sb.write_view("    \n")
  }
  write_chart_sppr_root(sb, opts)
  sb.write_view("  \n")
  sb.write_view("")
  sb.to_string()
}

///|
test "chart options: setter coverage and basic validation" {
  let bad_dim : Result[ChartDimension, Error] = Ok(
    ChartDimension::with_values(0, 1),
  ) catch {
    e => Err(e)
  }
  inspect(bad_dim is Err(XlsxError::InvalidSheetOperation(_)), content="true")
  let bad_series : Result[ChartSeries, Error] = Ok(
    ChartSeries::new("", "Sheet1!$A$1:$A$2"),
  ) catch {
    e => Err(e)
  }
  inspect(
    bad_series is Err(XlsxError::InvalidSheetOperation(_)),
    content="true",
  )
  let series = ChartSeries::new(
    "Sheet1!$A$1:$A$2",
    "Sheet1!$B$1:$B$2",
    name="S1",
  )
  let bad_bubble : Result[Unit, Error] = Ok(series.set_bubble_size("")) catch {
    e => Err(e)
  }
  inspect(
    bad_bubble is Err(XlsxError::InvalidSheetOperation(_)),
    content="true",
  )
  series.set_bubble_size("Sheet1!$C$1:$C$2")
  inspect(series.sizes, content="Sheet1!$C$1:$C$2")
  inspect(series.legend.position, content="bottom")
  let opts = ChartOptions::new(Scatter)
  opts.series.push(series)
  opts.format = GraphicOptions::new()
  opts.dimension = ChartDimension::with_values(320, 240)
  opts.legend = ChartLegend::new(position="top", show_legend_key=true)
  opts.title = "T"
  opts.vary_colors = false
  let x_axis = ChartAxis::new()
  x_axis.major_grid_lines = true
  x_axis.tick_label_position = Low
  x_axis.title = "X"
  opts.x_axis = x_axis
  let y_axis = ChartAxis::new()
  y_axis.minor_grid_lines = true
  y_axis.reverse_order = true
  y_axis.title = "Y"
  opts.y_axis = y_axis
  let plot = ChartPlotArea::new()
  plot.show_val = true
  plot.show_cat_name = true
  plot.show_percent = true
  opts.plot_area = plot
  opts.show_blanks_as = "span"
  opts.gap_width = Some(150)
  opts.overlap = Some(25)
  let xml = chart_options_to_xml(opts)
  inspect(xml.contains("scatterChart"), content="true")
  inspect(xml.contains(""), content="true")
}