// xml:id processing is a non-fatal layer after XML attribute normalization.
// W3C xml:id 1.0 §§4–6: assign ID type even when lexical/type/unique checks fail.
// No DTD is parsed here. Declaration information is supplied explicitly by the
// enclosing XML processor; absence is not evidence that all DTDs were checked.

///|
let xml_id_expanded_name = "http://www.w3.org/XML/1998/namespace#id"

///|
/// XML element indices are zero-based preorder within document_part.
/// Type assignment is always ID, including values with reported xml:id errors.
/// This is import metadata, not a promise to preserve IDs on data-only export.
#derive(Eq, Debug)
pub(all) struct XmlIdAssignment {
  document_part : String
  element_index : Int32
  element_name : String
  attribute_name : String
  value : String
  attribute_type : String
  valid_ncname : Bool
  unique : Bool
  declared_type : String?
}

///|
/// Non-fatal xml:id constraint diagnostic; XML well-formedness errors remain
/// the responsibility of the enclosing XML parser, not this diagnostic layer.
#derive(Eq, Debug)
pub(all) struct XmlIdDiagnostic {
  document_part : String
  element_index : Int32
  value : String
  code : String
  message : String
}

///|
priv struct XmlIdReport {
  assignments : Array[XmlIdAssignment]
  errors : Array[XmlIdDiagnostic]
}

///|
// Input has already undergone XML 1.0 §3.3.3 attribute normalization. The
// additional ID step collapses #x20 only: TAB/LF/CR from character references
// must survive and cause a lexical error, not be silently repaired to a name.
fn xml_id_normalize(value : String) -> String {
  let out = StringBuilder()
  let mut started = false
  let mut pending_space = false
  for c in value {
    if c == ' ' {
      if started {
        pending_space = true
      }
    } else {
      if pending_space {
        out.write_char(' ')
      }
      out.write_char(c)
      started = true
      pending_space = false
    }
  }
  out.to_string()
}

///|
fn xml_id_ncname(value : String) -> Bool {
  let mut first = true
  for c in value {
    if c == ':' || (if first { !xml_name_start(c) } else { !xml_name_char(c) }) {
      return false
    }
    first = false
  }
  !first
}

///|
// declared_types keys: (preorder element index, expanded attribute name).
// other_ids: (preorder element index, expanded attribute name, normalized ID).
// They must originate in actual parser/type information, not guessed schemas.
// The caller supplies every other ID-typed attribute for cross-type uniqueness;
// xml:id entries are ignored here to avoid double-counting the same attribute.
// A fresh invocation is required for each XML document (not each ZIP package).
fn process_xml_ids(
  root : ReadElement,
  document_part~ : String = "",
  declared_types~ : Map[(Int32, String), String] = Map([]),
  other_ids~ : Array[(Int32, String, String)] = [],
) -> XmlIdReport {
  let pending = [root]
  let found : Array[XmlIdAssignment] = []
  let counts : Map[String, Int32] = Map([])
  let mut index = 0
  while !pending.is_empty() {
    let element = pending.remove(pending.length() - 1)
    match element.attrs.get(xml_id_expanded_name) {
      Some(raw) => {
        let value = xml_id_normalize(raw)
        element.attrs.set(xml_id_expanded_name, value)
        counts.set(value, counts.get(value).unwrap_or(0) + 1)
        found.push({
          document_part,
          element_index: index,
          element_name: element.name,
          attribute_name: xml_id_expanded_name,
          value,
          attribute_type: "ID",
          valid_ncname: xml_id_ncname(value),
          unique: true,
          declared_type: declared_types.get((index, xml_id_expanded_name)),
        })
      }
      None => ()
    }
    for i = element.parts.length() - 1; i >= 0; i = i - 1 {
      match element.parts[i] {
        ReadElement(child) => pending.push(child)
        ReadText(_) => ()
      }
    }
    index += 1
  }
  let other_seen : Map[(Int32, String), Bool] = Map([])
  for (element_index, attribute_name, value) in other_ids {
    let key = (element_index, attribute_name)
    if attribute_name != xml_id_expanded_name && !other_seen.contains(key) {
      other_seen.set(key, true)
      counts.set(value, counts.get(value).unwrap_or(0) + 1)
    }
  }
  let assignments : Array[XmlIdAssignment] = []
  let errors : Array[XmlIdDiagnostic] = []
  for item in found {
    let unique = counts.get(item.value) == Some(1)
    assignments.push({ ..item, unique, })
    if !item.valid_ncname {
      errors.push({
        document_part,
        element_index: item.element_index,
        value: item.value,
        code: "XML_ID_INVALID_NCNAME",
        message: "xml:id is assigned ID type, but its normalized value is not an XML 1.0 NCName.",
      })
    }
    if item.declared_type is Some(kind) && kind != "ID" {
      errors.push({
        document_part,
        element_index: item.element_index,
        value: item.value,
        code: "XML_ID_DECLARED_TYPE",
        message: "xml:id is assigned ID type despite a supplied declaration with a non-ID type.",
      })
    }
    if !unique {
      errors.push({
        document_part,
        element_index: item.element_index,
        value: item.value,
        code: "XML_ID_DUPLICATE",
        message: "xml:id is assigned ID type, but its value is not unique among reported ID attributes in this XML document.",
      })
    }
  }
  { assignments, errors, }
}

///|
pub extend XmlIdAssignment with Eq::{equal, not_equal}

///|
pub extend XmlIdAssignment with @debug.Debug::{to_repr}

///|
pub extend XmlIdDiagnostic with Eq::{equal, not_equal}

///|
pub extend XmlIdDiagnostic with @debug.Debug::{to_repr}