///|
pub struct CoreProperties {
  mut title : String
  mut subject : String
  mut creator : String
  mut keywords : String
  mut description : String
  mut last_modified_by : String
  mut language : String
  mut identifier : String
  mut revision : String
  mut content_status : String
  mut category : String
  mut version : String
  mut created : String
  mut modified : String
}

///|
fn CoreProperties::new() -> CoreProperties {
  {
    title: "",
    subject: "",
    creator: "mbtexcel",
    keywords: "",
    description: "",
    last_modified_by: "mbtexcel",
    language: "",
    identifier: "",
    revision: "",
    content_status: "",
    category: "",
    version: "",
    created: "",
    modified: "",
  }
}

///|
pub fn CoreProperties::with_values(
  title? : String,
  subject? : String,
  creator? : String,
  keywords? : String,
  description? : String,
  last_modified_by? : String,
  language? : String,
  identifier? : String,
  revision? : String,
  content_status? : String,
  category? : String,
  version? : String,
  created? : String,
  modified? : String,
) -> CoreProperties {
  let props = CoreProperties::new()
  match title {
    Some(value) => props.title = value
    None => ()
  }
  match subject {
    Some(value) => props.subject = value
    None => ()
  }
  match creator {
    Some(value) => props.creator = value
    None => ()
  }
  match keywords {
    Some(value) => props.keywords = value
    None => ()
  }
  match description {
    Some(value) => props.description = value
    None => ()
  }
  match last_modified_by {
    Some(value) => props.last_modified_by = value
    None => ()
  }
  match language {
    Some(value) => props.language = value
    None => ()
  }
  match identifier {
    Some(value) => props.identifier = value
    None => ()
  }
  match revision {
    Some(value) => props.revision = value
    None => ()
  }
  match content_status {
    Some(value) => props.content_status = value
    None => ()
  }
  match category {
    Some(value) => props.category = value
    None => ()
  }
  match version {
    Some(value) => props.version = value
    None => ()
  }
  match created {
    Some(value) => props.created = value
    None => ()
  }
  match modified {
    Some(value) => props.modified = value
    None => ()
  }
  props
}

///|
pub struct AppProperties {
  mut application : String
  mut doc_security : Int?
  mut scale_crop : Bool?
  mut company : String
  mut links_up_to_date : Bool?
  mut hyperlinks_changed : Bool?
  mut app_version : String
}

///|
fn AppProperties::new() -> AppProperties {
  {
    application: "mbtexcel",
    doc_security: None,
    scale_crop: None,
    company: "",
    links_up_to_date: None,
    hyperlinks_changed: None,
    app_version: "",
  }
}

///|
pub fn AppProperties::with_values(
  application? : String,
  doc_security? : Int,
  scale_crop? : Bool,
  company? : String,
  links_up_to_date? : Bool,
  hyperlinks_changed? : Bool,
  app_version? : String,
) -> AppProperties {
  let props = AppProperties::new()
  match application {
    Some(value) => props.application = value
    None => ()
  }
  match doc_security {
    Some(value) => props.doc_security = Some(value)
    None => ()
  }
  match scale_crop {
    Some(value) => props.scale_crop = Some(value)
    None => ()
  }
  match company {
    Some(value) => props.company = value
    None => ()
  }
  match links_up_to_date {
    Some(value) => props.links_up_to_date = Some(value)
    None => ()
  }
  match hyperlinks_changed {
    Some(value) => props.hyperlinks_changed = Some(value)
    None => ()
  }
  match app_version {
    Some(value) => props.app_version = value
    None => ()
  }
  props
}

///|
fn bool_text(value : Bool) -> String {
  if value {
    "true"
  } else {
    "false"
  }
}

///|
fn write_core_properties_xml(
  props : CoreProperties,
  budget? : WritePartBudget,
) -> String raise XlsxError {
  let sb = LimitedXmlBuilder::new(budget)
  sb.write_view("\n")
  sb.write_view(
    "\n")
  if props.title != "" {
    sb.write_view("  ")
    sb.write_xml_text(props.title)
    sb.write_view("\n")
  }
  if props.subject != "" {
    sb.write_view("  ")
    sb.write_xml_text(props.subject)
    sb.write_view("\n")
  }
  if props.creator != "" {
    sb.write_view("  ")
    sb.write_xml_text(props.creator)
    sb.write_view("\n")
  }
  if props.keywords != "" {
    sb.write_view("  ")
    sb.write_xml_text(props.keywords)
    sb.write_view("\n")
  }
  if props.description != "" {
    sb.write_view("  ")
    sb.write_xml_text(props.description)
    sb.write_view("\n")
  }
  if props.last_modified_by != "" {
    sb.write_view("  ")
    sb.write_xml_text(props.last_modified_by)
    sb.write_view("\n")
  }
  if props.language != "" {
    sb.write_view("  ")
    sb.write_xml_text(props.language)
    sb.write_view("\n")
  }
  if props.identifier != "" {
    sb.write_view("  ")
    sb.write_xml_text(props.identifier)
    sb.write_view("\n")
  }
  if props.revision != "" {
    sb.write_view("  ")
    sb.write_xml_text(props.revision)
    sb.write_view("\n")
  }
  if props.content_status != "" {
    sb.write_view("  ")
    sb.write_xml_text(props.content_status)
    sb.write_view("\n")
  }
  if props.category != "" {
    sb.write_view("  ")
    sb.write_xml_text(props.category)
    sb.write_view("\n")
  }
  if props.version != "" {
    sb.write_view("  ")
    sb.write_xml_text(props.version)
    sb.write_view("\n")
  }
  if props.created != "" {
    sb.write_view("  ")
    sb.write_xml_text(props.created)
    sb.write_view("\n")
  }
  if props.modified != "" {
    sb.write_view("  ")
    sb.write_xml_text(props.modified)
    sb.write_view("\n")
  }
  sb.write_view("")
  sb.to_string()
}

///|
fn write_app_properties_xml(
  props : AppProperties,
  budget? : WritePartBudget,
) -> String raise XlsxError {
  let sb = LimitedXmlBuilder::new(budget)
  sb.write_view("\n")
  sb.write_view(
    "\n",
  )
  if props.application != "" {
    sb.write_view("  ")
    sb.write_xml_text(props.application)
    sb.write_view("\n")
  }
  match props.scale_crop {
    Some(value) => {
      sb.write_view("  ")
      sb.write_view(bool_text(value))
      sb.write_view("\n")
    }
    None => ()
  }
  match props.doc_security {
    Some(value) => {
      sb.write_view("  ")
      sb.write_view("\{value}")
      sb.write_view("\n")
    }
    None => ()
  }
  if props.company != "" {
    sb.write_view("  ")
    sb.write_xml_text(props.company)
    sb.write_view("\n")
  }
  match props.links_up_to_date {
    Some(value) => {
      sb.write_view("  ")
      sb.write_view(bool_text(value))
      sb.write_view("\n")
    }
    None => ()
  }
  match props.hyperlinks_changed {
    Some(value) => {
      sb.write_view("  ")
      sb.write_view(bool_text(value))
      sb.write_view("\n")
    }
    None => ()
  }
  if props.app_version != "" {
    sb.write_view("  ")
    sb.write_xml_text(props.app_version)
    sb.write_view("\n")
  }
  sb.write_view("")
  sb.to_string()
}

///|
fn parse_bool(text : StringView) -> Bool {
  let value = text.to_owned()
  value == "true" || value == "1"
}

///|
fn parse_core_properties(xml : StringView) -> CoreProperties raise XlsxError {
  let props = CoreProperties::new()
  match extract_tag_body(xml, "dc:title") {
    Some(value) => props.title = unescape_xml_text(value)
    None => ()
  }
  match extract_tag_body(xml, "dc:subject") {
    Some(value) => props.subject = unescape_xml_text(value)
    None => ()
  }
  match extract_tag_body(xml, "dc:creator") {
    Some(value) => props.creator = unescape_xml_text(value)
    None => ()
  }
  match extract_tag_body(xml, "cp:keywords") {
    Some(value) => props.keywords = unescape_xml_text(value)
    None => ()
  }
  match extract_tag_body(xml, "dc:description") {
    Some(value) => props.description = unescape_xml_text(value)
    None => ()
  }
  match extract_tag_body(xml, "cp:lastModifiedBy") {
    Some(value) => props.last_modified_by = unescape_xml_text(value)
    None => ()
  }
  match extract_tag_body(xml, "dc:language") {
    Some(value) => props.language = unescape_xml_text(value)
    None => ()
  }
  match extract_tag_body(xml, "dc:identifier") {
    Some(value) => props.identifier = unescape_xml_text(value)
    None => ()
  }
  match extract_tag_body(xml, "cp:revision") {
    Some(value) => props.revision = unescape_xml_text(value)
    None => ()
  }
  match extract_tag_body(xml, "cp:contentStatus") {
    Some(value) => props.content_status = unescape_xml_text(value)
    None => ()
  }
  match extract_tag_body(xml, "cp:category") {
    Some(value) => props.category = unescape_xml_text(value)
    None => ()
  }
  match extract_tag_body(xml, "cp:version") {
    Some(value) => props.version = unescape_xml_text(value)
    None => ()
  }
  match extract_tag_body(xml, "dcterms:created") {
    Some(value) => props.created = unescape_xml_text(value)
    None => ()
  }
  match extract_tag_body(xml, "dcterms:modified") {
    Some(value) => props.modified = unescape_xml_text(value)
    None => ()
  }
  props
}

///|
fn parse_app_properties(xml : StringView) -> AppProperties raise XlsxError {
  let props = AppProperties::new()
  match extract_tag_body(xml, "Application") {
    Some(value) => props.application = unescape_xml_text(value)
    None => ()
  }
  match extract_tag_body(xml, "ScaleCrop") {
    Some(value) => props.scale_crop = Some(parse_bool(value))
    None => ()
  }
  match extract_tag_body(xml, "DocSecurity") {
    Some(value) =>
      props.doc_security = Some(
        @string.parse_int(unescape_xml_text(value), base=10) catch {
          _ => 0
        },
      )
    None => ()
  }
  match extract_tag_body(xml, "Company") {
    Some(value) => props.company = unescape_xml_text(value)
    None => ()
  }
  match extract_tag_body(xml, "LinksUpToDate") {
    Some(value) => props.links_up_to_date = Some(parse_bool(value))
    None => ()
  }
  match extract_tag_body(xml, "HyperlinksChanged") {
    Some(value) => props.hyperlinks_changed = Some(parse_bool(value))
    None => ()
  }
  match extract_tag_body(xml, "AppVersion") {
    Some(value) => props.app_version = unescape_xml_text(value)
    None => ()
  }
  props
}

///|
let custom_props_namespace = "http://schemas.openxmlformats.org/officeDocument/2006/custom-properties"

///|
let custom_props_vt_namespace = "http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"

///|
let custom_props_fmt_id = "{D5CDD505-2E9C-101B-9397-08002B2CF9AE}"

///|
fn extract_tag_value(xml : StringView, tag_name : StringView) -> String? {
  let open = "<\{tag_name.to_owned()}>"
  let close = ""
  let text = xml.to_owned()
  match text.find(open) {
    None => None
    Some(start) => {
      let rest = text[start + open.length():]
      match rest.find(close) {
        Some(end) => {
          let slice = rest[:end]
          Some(slice.to_owned())
        }
        None => None
      }
    }
  }
}

///|
fn parse_custom_property_value(
  xml : StringView,
) -> CustomPropertyValue? raise XlsxError {
  match extract_tag_value(xml, "vt:lpwstr") {
    Some(value) => return Some(Text(unescape_xml_text(value)))
    None => ()
  }
  match extract_tag_value(xml, "vt:bool") {
    Some(value) => return Some(Boolean(parse_cell_bool(value)))
    None => ()
  }
  match extract_tag_value(xml, "vt:filetime") {
    Some(value) => return Some(DateTime(value))
    None => ()
  }
  let int_tags = [
    "vt:i1", "vt:i2", "vt:i4", "vt:i8", "vt:int", "vt:ui1", "vt:ui2", "vt:ui4", "vt:ui8",
    "vt:uint",
  ]
  for tag in int_tags {
    match extract_tag_value(xml, tag) {
      Some(value) =>
        return Some(
          Integer(
            @string.parse_int(value, base=10) catch {
              _ =>
                raise InvalidWorkbookProperty(msg="custom property int invalid")
            },
          ),
        )
      None => ()
    }
  }
  let float_tags = ["vt:r4", "vt:r8", "vt:decimal"]
  for tag in float_tags {
    match extract_tag_value(xml, tag) {
      Some(value) =>
        return Some(
          Float(
            @string.parse_double(value) catch {
              _ =>
                raise InvalidWorkbookProperty(
                  msg="custom property number invalid",
                )
            },
          ),
        )
      None => ()
    }
  }
  None
}

///|
fn write_custom_property_value(
  sb : LimitedXmlBuilder,
  value : CustomPropertyValue,
) -> Unit raise XlsxError {
  match value {
    Integer(number) => {
      sb.write_view("")
      sb.write_view("\{number}")
      sb.write_view("")
    }
    Float(number) => {
      sb.write_view("")
      sb.write_view("\{number}")
      sb.write_view("")
    }
    Boolean(value) => {
      sb.write_view("")
      sb.write_view(if value { "true" } else { "false" })
      sb.write_view("")
    }
    Text(text) => {
      sb.write_view("")
      sb.write_xml_text(text)
      sb.write_view("")
    }
    DateTime(text) => {
      sb.write_view("")
      sb.write_xml_text(text)
      sb.write_view("")
    }
  }
}

///|
fn write_custom_properties_xml(
  props : ArrayView[CustomProperty],
  budget? : WritePartBudget,
) -> String raise XlsxError {
  let sb = LimitedXmlBuilder::new(budget)
  sb.write_view("\n")
  sb.write_view("\n")
  let mut pid = 2
  for prop in props {
    match prop.value {
      None => ()
      Some(value) => {
        sb.write_view("  ")
        write_custom_property_value(sb, value)
        sb.write_view("\n")
        pid = pid + 1
      }
    }
  }
  sb.write_view("")
  sb.to_string()
}

///|
fn parse_custom_properties(
  xml : StringView,
) -> Array[CustomProperty] raise XlsxError {
  let properties : Array[CustomProperty] = []
  let mut first = true
  for chunk in xml.split("") {
      Some(pos) => pos
      None => raise InvalidXml(msg="property tag not closed")
    }
    let tag = text[:end]
    let name = match attr_value(tag, "name") {
      Some(value) => unescape_xml_text(value)
      None => raise InvalidXml(msg="property name missing")
    }
    let close_tag = ""
    let body = match text.find(close_tag) {
      Some(pos) => text[end + 1:pos]
      None => ""
    }
    let value = parse_custom_property_value(body)
    properties.push({ name, value })
  }
  properties
}