///|
pub struct Affix {
  prefix : String
  suffix : String
  delimiter : String
  text_case : String
  font_style : String
  font_weight : String
  vertical_align : String
}

///|
pub struct TextNode {
  variable : String
  value : String
  macro_name : String
  term : String
  form : String
}

///|
pub struct NameNode {
  variable : String
  form : String
  and_word : String
  delimiter : String
  initialize_with : String
  initialize : Bool
  name_as_sort_order : String
  delimiter_precedes_last : String
  et_al_min : Int32
  et_al_use_first : Int32
  label_variable : String
  options : Map[String, String]
  name_affix : Affix
  label : XmlElem?
  substitute : Array[Node]
}

///|
pub struct LabelNode {
  variable : String
  form : String
  plural : String
}

///|
pub struct NumberNode {
  variable : String
  form : String
  prefix : String
  suffix : String
  delimiter : String
  text_case : String
}

///|
pub struct DatePart {
  name : String
  form : String
  prefix : String
  suffix : String
  range_delimiter : String
  text_case : String
}

///|
pub struct DateNode {
  variable : String
  parts : Array[DatePart]
  delimiter : String
}

///|
pub enum Condition {
  Variable(String)
  Type(String)
  Numeric(String)
  IsUncertain(String)
  IsPlural(String)
  Position(String)
}

///|
pub struct ChooseBranch {
  conditions : Array[Condition]
  match_mode : String
  children : Array[Node]
}

///|
pub enum Node {
  Label(Affix, LabelNode)
  Text(Affix, TextNode)
  Names(Affix, NameNode)
  Number(Affix, NumberNode)
  Date(Affix, DateNode)
  Group(Affix, Array[Node])
  Choose(Affix, Array[ChooseBranch])
}

///|
pub struct Layout {
  affix : Affix
  children : Array[Node]
}

///|
pub struct SortKey {
  variable : String
  macro_name : String
  names_min : Int32
  names_use_first : Int32
  sort_order : String
}

///|
pub struct Style {
  title : String
  version : String
  default_locale : String
  macros : Map[String, Array[Node]]
  citation : Layout?
  bibliography : Layout?
  citation_sort : Array[SortKey]
  bibliography_sort : Array[SortKey]
  citation_disambiguate : Bool
  citation_collapse : String
  citation_delimiter : String
  after_collapse_delimiter : String?
  name_options : Map[String, String]
  citation_name_options : Map[String, String]
  bibliography_name_options : Map[String, String]
  locale : Locale
}

///|
fn read_affix(el : XmlElem) -> Affix {
  {
    prefix: el.attr("prefix"),
    suffix: el.attr("suffix"),
    delimiter: el.attr("delimiter"),
    text_case: el.attr("text-case"),
    font_style: el.attr("font-style"),
    font_weight: el.attr("font-weight"),
    vertical_align: el.attr("vertical-align"),
  }
}

///|
fn parse_conditions(el : XmlElem) -> Array[Condition] {
  let out = []
  for variable in el.attr("variable").split(" ") {
    if !variable.is_empty() {
      out.push(Variable(variable.to_owned()))
    }
  }
  for typ in el.attr("type").split(" ") {
    if !typ.is_empty() {
      out.push(Type(typ.to_owned()))
    }
  }
  for value in el.attr("is-numeric").split(" ") {
    if !value.is_empty() {
      out.push(Numeric(value.to_owned()))
    }
  }
  for value in el.attr("is-uncertain-date").split(" ") {
    if !value.is_empty() {
      out.push(IsUncertain(value.to_owned()))
    }
  }
  for value in el.attr("is-plural").split(" ") {
    if !value.is_empty() {
      out.push(IsPlural(value.to_owned()))
    }
  }
  for value in el.attr("position").split(" ") {
    if !value.is_empty() {
      out.push(Position(value.to_owned()))
    }
  }
  out
}

///|
fn parse_node(el : XmlElem) -> Node? {
  let affix = read_affix(el)
  match el.name {
    "text" =>
      Some(
        Text(affix, {
          variable: el.attr("variable"),
          value: el.attr("value"),
          macro_name: el.attr("macro"),
          term: el.attr("term"),
          form: el.attr("form"),
        }),
      )
    "names" => {
      let options = read_name_options(el)
      let name_el = el.first("name")
      if name_el is Some(child) {
        read_name_options(child).each(fn(key, value) { options[key] = value })
      }
      Some(
        Names(affix, {
          variable: el.attr("variable"),
          form: options.get("form").unwrap_or("long"),
          and_word: options.get("and").unwrap_or(""),
          delimiter: options.get("delimiter").unwrap_or(", "),
          initialize_with: options.get("initialize-with").unwrap_or(""),
          initialize: options.get("initialize").unwrap_or("true") != "false" &&
          options.contains("initialize-with"),
          name_as_sort_order: options.get("name-as-sort-order").unwrap_or(""),
          delimiter_precedes_last: options
          .get("delimiter-precedes-last")
          .unwrap_or("contextual"),
          et_al_min: parse_int(options.get("et-al-min").unwrap_or("0")).unwrap_or(
            0,
          ),
          et_al_use_first: parse_int(
            options.get("et-al-use-first").unwrap_or("1"),
          ).unwrap_or(1),
          label_variable: el.attr("variable"),
          options,
          name_affix: match name_el {
            Some(child) => read_affix(child)
            None => empty_affix()
          },
          label: el.first("label"),
          substitute: match el.first("substitute") {
            Some(child) => parse_children(child)
            None => []
          },
        }),
      )
    }
    "label" =>
      Some(
        Label(affix, {
          variable: el.attr("variable"),
          form: el.attr("form"),
          plural: el.attr("plural"),
        }),
      )
    "number" =>
      Some(
        Number(affix, {
          variable: el.attr("variable"),
          form: el.attr("form"),
          prefix: "",
          suffix: "",
          delimiter: "",
          text_case: "",
        }),
      )
    "date" =>
      Some(
        Date(affix, {
          variable: el.attr("variable"),
          parts: parse_date_parts(el),
          delimiter: el.attr("delimiter"),
        }),
      )
    "group" => Some(Group(affix, parse_children(el)))
    "choose" => Some(Choose(affix, parse_choose(el)))
    _ => None
  }
}

///|
fn parse_date_parts(el : XmlElem) -> Array[DatePart] {
  let out = []
  for child in el.kids() {
    if child.name == "date-part" {
      out.push({
        name: child.attr("name"),
        form: child.attr("form"),
        prefix: child.attr("prefix"),
        suffix: child.attr("suffix"),
        range_delimiter: child.attr("range-delimiter"),
        text_case: child.attr("text-case"),
      })
    }
  }
  out
}

///|
fn parse_children(el : XmlElem) -> Array[Node] {
  let out = []
  for child in el.kids() {
    match parse_node(child) {
      Some(n) => out.push(n)
      None => ()
    }
  }
  out
}

///|
fn parse_choose(el : XmlElem) -> Array[ChooseBranch] {
  let out = []
  for child in el.kids() {
    if child.name == "if" || child.name == "else-if" || child.name == "else" {
      out.push({
        conditions: parse_conditions(child),
        match_mode: child.attr("match"),
        children: parse_children(child),
      })
    }
  }
  out
}

///|
fn parse_sort_keys(el : XmlElem?) -> Array[SortKey] {
  match el {
    None => []
    Some(s) => {
      let out = []
      for child in s.kids() {
        if child.name == "key" {
          let min = match parse_int(child.attr("names-min")) {
            Some(x) => x
            None => 0
          }
          let first = match parse_int(child.attr("names-use-first")) {
            Some(x) => x
            None => 1
          }
          out.push({
            variable: child.attr("variable"),
            macro_name: child.attr("macro"),
            names_min: min,
            names_use_first: first,
            sort_order: child.attr("sort"),
          })
        }
      }
      out
    }
  }
}

///|
fn parse_layout(el : XmlElem) -> Layout {
  { affix: read_affix(el), children: parse_children(el), }
}

///|
fn parse_style(style_xml : String) -> Result[Style, CslError] {
  let root = match parse_xml(style_xml) {
    Ok(x) => x
    Err(e) => return Err(e)
  }
  guard root.name == "style" else {
    return Err(BadStyle("root element must be style"))
  }
  let compatibility = profile_issues(root)
  if !compatibility.is_empty() {
    return Err(BadStyle(join_nonempty(compatibility, "; ")))
  }
  let mut citation = None
  let mut bibliography = None
  let macros = Map([])
  let mut citation_sort = []
  let mut bibliography_sort = []
  let mut collapse = ""
  let mut delimiter = "; "
  let mut after_collapse_delimiter = None
  let mut citation_disambiguate = false
  let name_options = read_name_options(root)
  let mut citation_name_options = Map([])
  let mut bibliography_name_options = Map([])
  let locale = if root.attr("default-locale").has_prefix("zh") {
    Locale::chinese()
  } else {
    Locale::english()
  }
  for child in root.kids() {
    match child.name {
      "macro" => macros[child.attr("name")] = parse_children(child)
      "citation" => {
        citation_sort = parse_sort_keys(child.first("sort"))
        collapse = child.attr("collapse")
        after_collapse_delimiter = child.attrs.get("after-collapse-delimiter")
        citation_disambiguate = bool_word(
          child.attr("disambiguate-add-year-suffix"),
        )
        citation_name_options = read_name_options(child)
        match child.first("layout") {
          Some(x) => {
            citation = Some(parse_layout(x))
            delimiter = x.attr("delimiter")
          }
          None => ()
        }
      }
      "bibliography" => {
        bibliography_sort = parse_sort_keys(child.first("sort"))
        bibliography_name_options = read_name_options(child)
        match child.first("layout") {
          Some(x) => bibliography = Some(parse_layout(x))
          None => ()
        }
      }
      "locale" => {
        let language = child.attrs.get("xml:lang").unwrap_or(child.attr("lang"))
        if language.is_empty() || language == root.attr("default-locale") {
          if child.first("terms") is Some(terms) {
            for term in terms.kids_named("term") {
              locale.terms[term.attr("name")] = term.text()
            }
          }
        }
      }
      _ => ()
    }
  }
  if !collapse.is_empty() && collapse != "citation-number" {
    return Err(BadStyle("unsupported citation collapse: " + collapse))
  }
  let title = match root.first("info") {
    Some(info) =>
      match info.first("title") {
        Some(title) => title.text()
        None => root.attr("title")
      }
    None => root.attr("title")
  }
  let style : Style = {
    title,
    version: root.attr("version"),
    default_locale: root.attr("default-locale"),
    macros,
    citation,
    bibliography,
    citation_sort,
    bibliography_sort,
    citation_disambiguate,
    citation_collapse: collapse,
    citation_delimiter: delimiter,
    after_collapse_delimiter,
    name_options,
    citation_name_options,
    bibliography_name_options,
    locale,
  }
  match validate_macro_graph(style) {
    Some(message) => Err(BadStyle(message))
    None => {
      if style.citation_disambiguate && !explicit_year_suffix(style) {
        if style.citation is Some(layout) {
          if automatic_suffix_dates(layout.children, style) != 1 {
            return Err(
              BadStyle(
                "automatic year-suffix requires exactly one issued/year date and no conditional/substitute placement; use explicit year-suffix text instead",
              ),
            )
          }
        } else {
          return Err(
            BadStyle("year-suffix disambiguation requires citation layout"),
          )
        }
        if style.bibliography is Some(layout) {
          if automatic_suffix_dates(layout.children, style) != 1 {
            return Err(
              BadStyle(
                "automatic year-suffix bibliography requires exactly one issued/year date; use explicit year-suffix text instead",
              ),
            )
          }
        }
      }
      Ok(style)
    }
  }
}

///|
fn empty_affix() -> Affix {
  {
    prefix: "",
    suffix: "",
    delimiter: "",
    text_case: "",
    font_style: "",
    font_weight: "",
    vertical_align: "",
  }
}

///|
fn read_name_options(el : XmlElem) -> Map[String, String] {
  let out = Map([])
  for
    key in [
      "form", "and", "delimiter", "initialize-with", "initialize", "name-as-sort-order",
      "sort-separator", "delimiter-precedes-last", "delimiter-precedes-et-al", "et-al-min",
      "et-al-use-first", "et-al-use-last", "names-delimiter",
    ] {
    if el.attrs.get(key) is Some(value) {
      out[key] = value
    }
  }
  // cs:names delimiter separates variables; cs:name delimiter separates names.
  if el.name == "names" {
    if el.attrs.get("delimiter") is Some(value) {
      out["names-delimiter"] = value
    }
    out.remove("delimiter") |> ignore
  }
  // Root/context delimiter does not inherit as a name delimiter.
  if el.name == "style" || el.name == "citation" || el.name == "bibliography" {
    out.remove("delimiter") |> ignore
    if el.attrs.get("name-delimiter") is Some(value) {
      out["delimiter"] = value
    }
    if el.attrs.get("name-form") is Some(value) {
      out["form"] = value
    }
  }
  out
}

///|
fn context_style(style : Style, citation : Bool) -> Style {
  let options = style.name_options.copy()
  let overrides = if citation {
    style.citation_name_options
  } else {
    style.bibliography_name_options
  }
  overrides.each(fn(key, value) { options[key] = value })
  { ..style, name_options: options, }
}

///|
fn resolve_name_node(node : NameNode, style : Style) -> NameNode {
  let options = style.name_options.copy()
  node.options.each(fn(key, value) { options[key] = value })
  {
    ..node,
    form: options.get("form").unwrap_or("long"),
    and_word: options.get("and").unwrap_or(""),
    delimiter: options.get("delimiter").unwrap_or(", "),
    initialize_with: options.get("initialize-with").unwrap_or(""),
    initialize: options.get("initialize").unwrap_or("true") != "false" &&
    options.contains("initialize-with"),
    name_as_sort_order: options.get("name-as-sort-order").unwrap_or(""),
    delimiter_precedes_last: options
    .get("delimiter-precedes-last")
    .unwrap_or("contextual"),
    et_al_min: parse_int(options.get("et-al-min").unwrap_or("0")).unwrap_or(0),
    et_al_use_first: parse_int(options.get("et-al-use-first").unwrap_or("1")).unwrap_or(
      1,
    ),
    options,
  }
}

///|
fn validate_macro_nodes(
  nodes : Array[Node],
  style : Style,
  path : Array[String],
) -> String? {
  for node in nodes {
    let children = match node {
      Text(_, spec) => {
        if spec.macro_name.is_empty() {
          continue
        }
        if path.contains(spec.macro_name) {
          return Some("cyclic macro: " + spec.macro_name)
        }
        let body = match style.macros.get(spec.macro_name) {
          Some(body) => body
          None => return Some("undefined macro: " + spec.macro_name)
        }
        let next = path.copy()
        next.push(spec.macro_name)
        if validate_macro_nodes(body, style, next) is Some(message) {
          return Some(message)
        }
        continue
      }
      Group(_, children) => children
      Names(_, spec) => spec.substitute
      Choose(_, branches) => {
        for branch in branches {
          for condition in branch.conditions {
            if condition is Position(_) {
              return Some(
                "position conditions require citation history and are not supported",
              )
            }
          }
          if validate_macro_nodes(branch.children, style, path) is Some(message) {
            return Some(message)
          }
        }
        continue
      }
      _ => continue
    }
    if validate_macro_nodes(children, style, path) is Some(message) {
      return Some(message)
    }
  }
  None
}

///|
fn validate_macro_graph(style : Style) -> String? {
  for name, body in style.macros {
    if validate_macro_nodes(body, style, [name]) is Some(message) {
      return Some(message)
    }
  }
  if style.citation is Some(layout) {
    if validate_macro_nodes(layout.children, style, []) is Some(message) {
      return Some(message)
    }
  }
  if style.bibliography is Some(layout) {
    if validate_macro_nodes(layout.children, style, []) is Some(message) {
      return Some(message)
    }
  }
  for key in style.citation_sort + style.bibliography_sort {
    if !key.macro_name.is_empty() && !style.macros.contains(key.macro_name) {
      return Some("undefined sort macro: " + key.macro_name)
    }
  }
  None
}