///|
pub struct PivotTableField {
  data : String
  name : String
  subtotal : String
  num_fmt : Int
  compact : Bool
  outline : Bool
  show_all : Bool
  insert_blank_row : Bool
  default_subtotal : Bool
} derive(Debug)

///|
pub fn PivotTableField::new(
  data : String,
  name? : String = "",
  subtotal? : String = "Sum",
  num_fmt? : Int = 0,
  compact? : Bool = false,
  outline? : Bool = false,
  show_all? : Bool = false,
  insert_blank_row? : Bool = false,
  default_subtotal? : Bool = false,
) -> PivotTableField raise XlsxError {
  if data == "" {
    raise InvalidPivotTable(msg="pivot field data empty")
  }
  {
    data,
    name,
    subtotal,
    num_fmt,
    compact,
    outline,
    show_all,
    insert_blank_row,
    default_subtotal,
  }
}

///|
/// Canonical dataField subtotal enum values accepted by the OOXML schema.
let pivot_subtotal_enums : Array[String] = [
  "average", "count", "countNums", "max", "min", "product", "stdDev", "stdDevp",
  "sum", "var", "varp",
]

///|
/// Normalizes a user-provided subtotal to its canonical schema spelling via
/// case-insensitive match, falling back to "sum" like Excelize.
fn normalize_pivot_subtotal(value : String) -> String {
  let lowered = value.to_lower()
  for enum_value in pivot_subtotal_enums {
    if enum_value.to_lower() == lowered {
      return enum_value
    }
  }
  "sum"
}

///|
/// Returns the number format ID to emit for a pivot data field: built-in
/// format IDs (including the language-specific 27-36 and 50-81 ranges) pass
/// through, anything else maps to 0 and is omitted, mirroring Excelize.
fn pivot_data_field_num_fmt_id(num_fmt : Int) -> Int {
  match num_fmt {
    0..=4 | 9..=22 | 37..=49 => num_fmt
    27..=36 | 50..=81 => num_fmt
    _ => 0
  }
}

///|
/// Truncates a pivot field display name to Excelize's MaxFieldLength of
/// 255 UTF-16 code units.
fn pivot_field_display_name(name : String) -> String {
  if count_utf16_units(name) > max_field_length {
    truncate_utf16_units(name, max_field_length)
  } else {
    name
  }
}

///|
pub(all) struct PivotTableOptions {
  data_range : String
  pivot_table_range : String
  mut name : String
  rows : Array[PivotTableField]
  columns : Array[PivotTableField]
  data : Array[PivotTableField]
  filter : Array[PivotTableField]
  mut row_grand_totals : Bool
  mut col_grand_totals : Bool
  mut show_drill : Bool
  mut use_auto_formatting : Bool
  mut page_over_then_down : Bool
  mut merge_item : Bool
  mut classic_layout : Bool
  mut compact_data : Bool
  mut show_error : Bool
  mut show_row_headers : Bool
  mut show_col_headers : Bool
  mut show_row_stripes : Bool
  mut show_col_stripes : Bool
  mut show_last_column : Bool
  mut field_print_titles : Bool
  mut item_print_titles : Bool
  mut pivot_table_style_name : String
} derive(Debug)

///|
pub fn PivotTableOptions::new(
  data_range : String,
  pivot_table_range : String,
) -> PivotTableOptions raise XlsxError {
  if data_range == "" {
    raise InvalidPivotTable(msg="pivot data range empty")
  }
  if pivot_table_range == "" {
    raise InvalidPivotTable(msg="pivot table range empty")
  }
  {
    data_range,
    pivot_table_range,
    name: "",
    rows: [],
    columns: [],
    data: [],
    filter: [],
    row_grand_totals: true,
    col_grand_totals: true,
    show_drill: true,
    use_auto_formatting: false,
    page_over_then_down: false,
    merge_item: false,
    classic_layout: false,
    compact_data: false,
    show_error: false,
    show_row_headers: true,
    show_col_headers: true,
    show_row_stripes: true,
    show_col_stripes: false,
    show_last_column: false,
    field_print_titles: false,
    item_print_titles: false,
    pivot_table_style_name: "PivotStyleLight16",
  }
}

///|
fn split_sheet_range(value : StringView) -> (String, String) raise XlsxError {
  let text = value.to_owned()
  match text.find("!") {
    Some(pos) => {
      if pos <= 0 || pos + 1 >= text.length() {
        raise InvalidPivotTable(msg="range must be Sheet!A1:B2")
      }
      let sheet = text[:pos]
      let range_ref = text[pos + 1:]
      (sheet.to_owned(), range_ref.to_owned())
    }
    None => raise InvalidPivotTable(msg="range must be Sheet!A1:B2")
  }
}

///|
fn write_pivot_cache_definition_xml(
  data_sheet : String,
  data_ref : String,
  fields : ArrayView[String],
) -> String {
  let sb = StringBuilder::new()
  sb.write_view("\n")
  sb.write_view(
    "\n",
  )
  sb.write_view("  \n")
  sb.write_view("    \n")
  sb.write_view("  \n")
  sb.write_view("  \n")
  for name in fields {
    sb.write_view("    \n")
    sb.write_view("      \n")
    sb.write_view("    \n")
  }
  sb.write_view("  \n")
  sb.write_view("")
  sb.to_string()
}

///|
fn write_pivot_table_definition_xml(
  name : String,
  cache_id : Int,
  pivot_ref : String,
  field_names : ArrayView[String],
  row_fields : ArrayView[(Int, PivotTableField)],
  col_fields : ArrayView[(Int, PivotTableField)],
  page_fields : ArrayView[(Int, PivotTableField)],
  data_fields : ArrayView[(Int, PivotTableField)],
  opts : PivotTableOptions,
) -> String {
  let sb = StringBuilder::new()
  sb.write_view("\n")
  sb.write_view(
    "\n")
  sb.write_view("  \n",
  )
  let row_opts : Map[Int, PivotTableField] = Map([])
  for entry in row_fields {
    let (idx, field) = entry
    row_opts[idx] = field
  }
  let col_opts : Map[Int, PivotTableField] = Map([])
  for entry in col_fields {
    let (idx, field) = entry
    col_opts[idx] = field
  }
  let page_opts : Map[Int, PivotTableField] = Map([])
  for entry in page_fields {
    let (idx, field) = entry
    page_opts[idx] = field
  }
  let data_opts : Map[Int, PivotTableField] = Map([])
  for entry in data_fields {
    let (idx, field) = entry
    data_opts[idx] = field
  }
  sb.write_view("  \n")
  for i, _fname in field_names {
    sb.write_view("     {
        let pivot_name = if field.name == "" { field.data } else { field.name }
        sb.write_view(" name=\"")
        sb.write_view(escape_xml_attr(pivot_field_display_name(pivot_name)))
        sb.write_view("\" axis=\"axisRow\"")
        let compact = if opts.classic_layout { false } else { field.compact }
        let outline = if opts.classic_layout { false } else { field.outline }
        sb.write_view(" compact=\"")
        sb.write_view(bool_attr(compact))
        sb.write_view("\" outline=\"")
        sb.write_view(bool_attr(outline))
        sb.write_view("\" showAll=\"")
        sb.write_view(bool_attr(field.show_all))
        sb.write_view("\" defaultSubtotal=\"")
        sb.write_view(bool_attr(field.default_subtotal))
        sb.write_view("\"")
        if field.insert_blank_row {
          sb.write_view(" insertBlankRow=\"1\"")
        }
        match data_opts.get(i) {
          Some(_) => sb.write_view(" dataField=\"1\"")
          None => ()
        }
        sb.write_view(">")
        if field.default_subtotal {
          sb.write_view("")
        } else {
          sb.write_view("")
        }
        sb.write_view("\n")
        continue
      }
      None => ()
    }
    match col_opts.get(i) {
      Some(field) => {
        let pivot_name = if field.name == "" { field.data } else { field.name }
        sb.write_view(" name=\"")
        sb.write_view(escape_xml_attr(pivot_field_display_name(pivot_name)))
        sb.write_view("\" axis=\"axisCol\"")
        let compact = if opts.classic_layout { false } else { field.compact }
        let outline = if opts.classic_layout { false } else { field.outline }
        sb.write_view(" compact=\"")
        sb.write_view(bool_attr(compact))
        sb.write_view("\" outline=\"")
        sb.write_view(bool_attr(outline))
        sb.write_view("\" showAll=\"")
        sb.write_view(bool_attr(field.show_all))
        sb.write_view("\" defaultSubtotal=\"")
        sb.write_view(bool_attr(field.default_subtotal))
        sb.write_view("\"")
        if field.insert_blank_row {
          sb.write_view(" insertBlankRow=\"1\"")
        }
        match data_opts.get(i) {
          Some(_) => sb.write_view(" dataField=\"1\"")
          None => ()
        }
        sb.write_view(">")
        if field.default_subtotal {
          sb.write_view("")
        } else {
          sb.write_view("")
        }
        sb.write_view("\n")
        continue
      }
      None => ()
    }
    match page_opts.get(i) {
      Some(field) => {
        let pivot_name = if field.name == "" { field.data } else { field.name }
        sb.write_view(" name=\"")
        sb.write_view(escape_xml_attr(pivot_field_display_name(pivot_name)))
        sb.write_view("\" axis=\"axisPage\"")
        sb.write_view(" showAll=\"")
        sb.write_view(bool_attr(field.show_all))
        sb.write_view("\"")
        match data_opts.get(i) {
          Some(_) => sb.write_view(" dataField=\"1\"")
          None => ()
        }
        sb.write_view(
          ">\n",
        )
        continue
      }
      None => ()
    }
    match data_opts.get(i) {
      Some(_) => {
        if opts.classic_layout {
          sb.write_view(" compact=\"0\" outline=\"0\"")
        }
        sb.write_view(" dataField=\"1\" showAll=\"0\"/>\n")
        continue
      }
      None => ()
    }
    if opts.classic_layout {
      sb.write_view(" compact=\"0\" outline=\"0\"")
    }
    sb.write_view(" showAll=\"0\"/>\n")
  }
  sb.write_view("  \n")
  if row_fields.length() > 0 {
    sb.write_view("  \n")
    for entry in row_fields {
      let (idx, _field) = entry
      sb.write_view("    \n")
    }
    sb.write_view("  \n")
    sb.write_view("  \n")
  }
  if col_fields.length() > 0 {
    sb.write_view("  \n")
    for entry in col_fields {
      let (idx, _field) = entry
      sb.write_view("    \n")
    }
    sb.write_view("  \n")
    sb.write_view("  \n")
  }
  if page_fields.length() > 0 {
    sb.write_view("  \n")
    for entry in page_fields {
      let (idx, _field) = entry
      sb.write_view("    \n")
    }
    sb.write_view("  \n")
  }
  if data_fields.length() > 0 {
    sb.write_view("  \n")
    for entry in data_fields {
      let (idx, field) = entry
      sb.write_view("    \n")
    }
    sb.write_view("  \n")
  }
  sb.write_view("  \n")
  sb.write_view("")
  sb.to_string()
}