///|
pub struct Workbook {
  /// Opaque identity shared with worksheets currently owned by this workbook.
  /// A non-empty array is used solely to provide stable reference identity.
  priv worksheet_owner_token : Array[Unit]
  priv sheets : Array[Worksheet]
  priv chart_sheets : Array[ChartSheet]
  priv sheet_order : Array[SheetEntry]
  styles : Array[Style]
  conditional_styles : Array[Style]
  defined_names : Array[DefinedName]
  mut core_properties : CoreProperties
  mut app_properties : AppProperties
  custom_properties : Array[CustomProperty]
  mut vba_project : Bytes?
  mut workbook_props : WorkbookPropsOptions
  mut calc_props : CalcPropsOptions
  mut default_font : String
  default_table_style : String
  default_pivot_style : String
  theme_colors : Array[String]?
  theme_xml : String?
  indexed_colors : Array[String]?
  mru_colors_xml : String?
  styles_ext_lst_xml : String?
  mut io_context : WorkbookIOContext
  options : Options
  mut workbook_protection : WorkbookProtection?
  mut active_sheet_index : Int
  /// Kingsoft WPS Office embedded cell images parsed from
  /// xl/cellimages.xml on read, keyed by their DISPIMG identifier.
  cell_images : Array[CellImage]
  /// Modern rich-value ("Place in cell") image metadata parsed from
  /// xl/metadata.xml and xl/richData/* on read.
  rich_value_images : RichValueImages?
  /// Media parts referenced by rich-value images, keyed by archive path.
  rich_value_media : Map[String, Bytes]
  /// Raw-package feature flags captured at read time, before lossy
  /// modeling; `XlsxPackageFeatures::none()` for API-built workbooks.
  package_features : XlsxPackageFeatures
}

///|
enum SheetEntry {
  Worksheet(Int)
  ChartSheet(Int)
} derive(Debug)

///|
let max_sheet_name_length = 31

///|
let ole_identifier_bytes : Bytes = Bytes::from_array(ole_identifier)

///|
fn check_sheet_name(name : StringView) -> Unit raise XlsxError {
  let text = name.to_owned()
  if text == "" {
    raise InvalidSheetName(msg="sheet name is blank")
  }
  if text.length() > max_sheet_name_length {
    raise InvalidSheetName(msg="sheet name too long")
  }
  if text.has_prefix("'") || text.has_suffix("'") {
    raise InvalidSheetName(
      msg="sheet name cannot start or end with a single quote",
    )
  }
  for c in text {
    match c {
      ':' | '\\' | '/' | '?' | '*' | '[' | ']' =>
        raise InvalidSheetName(msg="sheet name contains invalid characters")
      _ => ()
    }
  }
}

///|
fn normalize_sheet_name(name : StringView) -> String {
  name.to_owned().to_lower()
}

///|
fn sheet_name_equal(a : StringView, b : StringView) -> Bool {
  normalize_sheet_name(a) == normalize_sheet_name(b)
}

///|
fn Workbook::sheet_entry_index(self : Workbook, name : StringView) -> Int? {
  let needle = normalize_sheet_name(name)
  for i, entry in self.sheet_order {
    match entry {
      Worksheet(idx) =>
        if normalize_sheet_name(self.sheets[idx].name()) == needle {
          return Some(i)
        }
      ChartSheet(idx) =>
        if normalize_sheet_name(self.chart_sheets[idx].name()) == needle {
          return Some(i)
        }
    }
  }
  None
}

///|
fn Workbook::sheet_entry(self : Workbook, name : StringView) -> SheetEntry? {
  let needle = normalize_sheet_name(name)
  for entry in self.sheet_order {
    match entry {
      Worksheet(idx) =>
        if normalize_sheet_name(self.sheets[idx].name()) == needle {
          return Some(entry)
        }
      ChartSheet(idx) =>
        if normalize_sheet_name(self.chart_sheets[idx].name()) == needle {
          return Some(entry)
        }
    }
  }
  None
}

///|
fn Workbook::sheet_entry_name(self : Workbook, entry : SheetEntry) -> String {
  match entry {
    Worksheet(idx) => self.sheets[idx].name()
    ChartSheet(idx) => self.chart_sheets[idx].name()
  }
}

///|
fn Workbook::sheet_entry_state(
  self : Workbook,
  entry : SheetEntry,
) -> SheetState {
  match entry {
    Worksheet(idx) => self.sheets[idx].state()
    ChartSheet(idx) => self.chart_sheets[idx].state()
  }
}

///|
fn Workbook::set_sheet_entry_state(
  self : Workbook,
  entry : SheetEntry,
  state : SheetState,
) -> Unit {
  match entry {
    Worksheet(idx) => self.sheets[idx].state = state
    ChartSheet(idx) => self.chart_sheets[idx].state = state
  }
}

///|
fn defined_name_scope_key(scope : StringView) -> String {
  let normalized = normalize_defined_name_scope(scope)
  if normalized == "Workbook" {
    "workbook"
  } else {
    normalize_sheet_name(normalized)
  }
}

///|
pub fn Workbook::new(options? : Options = Options::new()) -> Workbook {
  {
    worksheet_owner_token: [()],
    sheets: [],
    chart_sheets: [],
    sheet_order: [],
    styles: [Style::new()],
    conditional_styles: [],
    defined_names: [],
    core_properties: CoreProperties::new(),
    app_properties: AppProperties::new(),
    custom_properties: [],
    vba_project: None,
    workbook_props: WorkbookPropsOptions::new(),
    calc_props: CalcPropsOptions::new(),
    default_font: "Calibri",
    default_table_style: "TableStyleMedium9",
    default_pivot_style: "PivotStyleLight16",
    theme_colors: None,
    theme_xml: None,
    indexed_colors: None,
    mru_colors_xml: None,
    styles_ext_lst_xml: None,
    io_context: empty_workbook_io_context(),
    options,
    workbook_protection: None,
    active_sheet_index: 0,
    cell_images: [],
    rich_value_images: None,
    rich_value_media: Map([]),
    package_features: XlsxPackageFeatures::none(),
  }
}