///|
priv struct XmlNames {
  scope : @namespaces.HashMap[String,String]
  logical : @namespaces.HashMap[String,String]
  fallbacks : @namespaces.HashMap[String,String]
  declarations : Map[String,String]
  element_prefix : String
  attribute_prefixes : Map[(String,String),String]
  reference_prefixes : Map[(String,String,String),String]
  preserve : Bool
}

///|
fn XmlOutput::global_scope(self : XmlOutput) -> @namespaces.HashMap[String,String] {
  let mut scope=@namespaces.HashMap::new().add("xml","http://www.w3.org/XML/1998/namespace").add("","")
  for uri,prefix in self.uris { scope=scope.add(prefix,uri) }
  scope
}

///|
fn XmlOutput::names_for(self : XmlOutput, node : Element, parent : @namespaces.HashMap[String,String], parent_names : XmlNames?, root : Bool) -> XmlNames {
  let logical=namespace_context(node,parent_names.map(n => n.logical).unwrap_or_else(() => @namespaces.HashMap::new()))
  let preserve=parent_names.map(n => n.preserve).unwrap_or(false) || namespace_sensitive(node)
  let mut scope=if root { self.global_scope() } else { parent }
  let mut fallbacks=parent_names.map(n => n.fallbacks).unwrap_or_else(() => @namespaces.HashMap::from_iter(self.uris.iter()))
  let declarations : Map[String,String] = Map([])
  let remaining=self.limits.nodes-self.output_records
  fn bind(prefix : String, uri : String) -> Unit {
    if self.stopped { return }
    if prefix != "xml" && namespace_uri(parent,prefix) != Some(uri) {
      if !declarations.contains(prefix) && declarations.length() >= remaining { self.fail("bpmn/write-node-limit","Preserved namespace declarations exceed the writer node limit",node.span); return }
      declarations[prefix]=uri
    } else { declarations.remove(prefix) }
    if scope.get(prefix) != Some(uri) { scope=scope.add(prefix,uri) }
  }
  if root {
    for uri,prefix in self.uris { if self.stopped { break }; bind(prefix,uri) }
  }
  if preserve {
    let delta=parent_names.map(p => p.preserve && (!node.captured_context || node.context_parent.map(old => physical_equal(old,p.logical)).unwrap_or(false))).unwrap_or(false)
    if delta {
      for (prefix,uri) in node.bindings { if self.stopped { break }; bind(prefix,uri) }
    } else {
      for prefix,uri in logical { if self.stopped { break }; bind(prefix,uri) }
      bind("",namespace_uri(logical,"").unwrap())
    }
    if self.stopped { return {scope,logical,fallbacks,declarations,element_prefix:"",attribute_prefixes:Map([]),reference_prefixes:Map([]),preserve} }
    for text in opaque_strings(node) {
      for prefix in text_prefixes(text) {
        if namespace_uri(logical,prefix) != namespace_uri(scope,prefix) { self.fail("bpmn/namespace-conflict","An inherited prefix would bind an opaque token that was unbound in its captured scope",node.span) }
      }
    }
  }
  let mut next=1
  fn choose(name : ExpandedName, attribute : Bool, spelling : String?) -> String {
    if self.stopped { return "" }
    if name.namespace_uri == "" {
      if !attribute && namespace_uri(scope,"") != Some("") { self.fail("bpmn/namespace-conflict","An empty namespace identity conflicts with the preserved default namespace",node.span) }
      return ""
    }
    if preserve && spelling is Some(raw) && qname(xml_trim(raw)) is Some((prefix,_)) && (prefix != "" || !attribute) && namespace_uri(scope,prefix) == Some(name.namespace_uri) {
      if prefix != "" { fallbacks=fallbacks.add(name.namespace_uri,prefix) }
      return prefix
    }
    if preserve && !attribute && namespace_uri(scope,"") == Some(name.namespace_uri) { return "" }
    if self.uris.get(name.namespace_uri) is Some(prefix) && namespace_uri(scope,prefix) == Some(name.namespace_uri) { return prefix }
    if fallbacks.get(name.namespace_uri) is Some(prefix) && namespace_uri(scope,prefix) == Some(name.namespace_uri) { return prefix }
    let preferred=namespace_prefix(name.namespace_uri)
    let prefix=if preferred != "" && namespace_uri(scope,preferred) == Some(name.namespace_uri) { preferred }
    else if preferred != "" && !scope.contains(preferred) && !self.reserved.contains(preferred) { preferred }
    else {
      while scope.contains("ns\{next}") || self.reserved.contains("ns\{next}") { next+=1 }
      let prefix="ns\{next}"; next+=1; prefix
    }
    bind(prefix,name.namespace_uri); fallbacks=fallbacks.add(name.namespace_uri,prefix)
    prefix
  }
  let element_prefix=choose(node.kind.name(),false,node.raw_name)
  let attribute_prefixes : Map[(String,String),String] = Map([])
  let reference_prefixes : Map[(String,String,String),String] = Map([])
  fn reference(value : Scalar) -> Unit {
    if value is Reference(r) && r.kind == QName && r.name is Some(name) {
      reference_prefixes[(name.namespace_uri,name.local_name,r.spelling)]=choose(name,false,Some(r.spelling))
    }
  }
  for attr in node.values {
    if attr.name is Some(name) { attribute_prefixes[(name.namespace_uri,name.local_name)]=choose(name,true,Some(attr.raw_name)) }
    reference(attr.value)
  }
  if node.text_value is Some(value) { reference(value) }
  {scope,logical,fallbacks,declarations,element_prefix,attribute_prefixes,reference_prefixes,preserve}
}

///|
fn XmlOutput::name_with_prefix(self : XmlOutput, prefix : String, local_name : String) -> Unit {
  if self.stopped { return }
  let extra=if prefix == "" { 0 } else { prefix.length()+1 }
  if extra > self.limits.token_units || local_name.length() > self.limits.token_units-extra { self.fail("bpmn/write-token-limit","Qualified name exceeds its token limit",None); return }
  if prefix != "" { self.chunk(prefix); self.chunk(":") }
  self.chunk(local_name)
}

///|
fn XmlOutput::qualified_names(self : XmlOutput, names : XmlNames, name : ExpandedName, attribute : Bool) -> Unit {
  let prefix=if attribute { names.attribute_prefixes.get((name.namespace_uri,name.local_name)).unwrap() } else { names.element_prefix }
  self.name_with_prefix(prefix,name.local_name)
}

///|
fn XmlOutput::scalar_names(self : XmlOutput, names : XmlNames, value : Scalar, attribute : Bool) -> Unit {
  match value {
    Reference(r) if r.kind == QName => {
      let name=r.name.unwrap()
      self.name_with_prefix(names.reference_prefixes.get((name.namespace_uri,name.local_name,r.spelling)).unwrap(),name.local_name)
    }
    Reference(r) => self.escaped(r.name.unwrap().local_name,attribute)
    _ => self.escaped(value.token(),attribute)
  }
}

///|
fn XmlOutput::namespace_declarations(self : XmlOutput, names : XmlNames, _parent : @namespaces.HashMap[String,String]) -> Unit {
  let keys=names.declarations.keys().to_array(); keys.sort_by((a,b) => a.lexical_compare(b))
  for prefix in keys {
    if self.stopped { break }
    let uri=names.declarations.get(prefix).unwrap()
    let extra=if prefix == "" { 5 } else { 6 }
    if prefix.length() > self.limits.token_units-extra { self.fail("bpmn/write-token-limit","Namespace declaration name exceeds its token limit",None); break }
    self.records(1,true,None); self.chunk(" xmlns")
    if prefix != "" { self.chunk(":"); self.chunk(prefix) }
    self.chunk("=\""); self.escaped(uri,true); self.chunk("\"")
  }
}

///|
fn opaque_unknown_type(element : Element, scope : @namespaces.HashMap[String,String]) -> Bool {
  let type_name=ExpandedName::new("http://www.w3.org/2001/XMLSchema-instance","type")
  guard element.scalar(type_name) is Some(Text(raw)) else { return false }
  let value=reference_lookup(raw,QName,prefix => scope.get(prefix))
  match value.name { Some(name) => name.namespace_uri != "http://www.w3.org/2001/XMLSchema" && schema_type(name) == None; None => false }
}