///|
fn known_namespace(uri : String) -> Bool {
  uri == "http://www.omg.org/spec/BPMN/20100524/MODEL" || uri == "http://www.omg.org/spec/BPMN/20100524/DI" || uri == "http://www.omg.org/spec/DD/20100524/DC" || uri == "http://www.omg.org/spec/DD/20100524/DI"
}

///|
pub fn Kind::substitutes(self : Kind, head : ExpandedName) -> Bool {
  let mut name = self.name()
  for _ in 0..<(schema_elements.length()+1) {
    if name == head { return true }
    guard schema_elements.iter().find_first(e => e.name == name) is Some(e) && e.substitution_group is Some(parent) else { return false }
    name = parent
  }
  false
}

///|
fn child_type(parent : ExpandedName?, kind : Kind) -> ExpandedName? {
  if parent is Some(name) && schema_type(name) is Some(t) {
    for field in t.fields() {
      if field.kind != Child { continue }
      if field.reference_element is Some(head) && kind.substitutes(head) {
        return match kind.declared_type() { Some(t) => Some(t); None => field.value_type }
      }
      if field.reference_element == None && field.name == Some(kind.name()) { return field.value_type }
    }
  }
  kind.declared_type()
}

///|
priv struct PreparedElement {
  kind : Kind
  values : Array[Attribute]
  value_type : ExpandedName?
  text_value : Scalar?
  preserve_space : Bool
  mixed : Bool
  payload_mode : Bool
  context : @namespaces.HashMap[String,String]
  context_parent : @namespaces.HashMap[String,String]?
}

///|
fn lower_minimal(tree : Cst, map : SourceMap, diagnostics : Array[Diagnostic], maximum : Int, source : String) -> Document? {
  let origin=SourceOrigin::{ source, map }
  let prepared : Array[PreparedElement?] = Array::make(tree.nodes.length(),None)
  let lowered : Array[Element?] = Array::make(tree.nodes.length(),None)
  let mut exhausted = false
  fn report(code : String, message : String, span : Span) -> Unit {
    if diagnostics.length() < maximum { diagnostics.push(finding(code,message,span~,map~)) }
    else if !exhausted { diagnostics[maximum-1] = finding("xml/diagnostic-limit","Diagnostic limit exceeded during typed lowering",span~,map~); exhausted = true }
  }
  fn inspect_scalar(value : Scalar, span : Span) -> Unit {
    match value {
      Invalid(..) => report("bpmn/invalid-scalar","Value does not match its declared schema scalar type",span)
      Reference(r) if r.name == None => report("bpmn/unbound-qname","QName value has an invalid name or an unbound prefix",span)
      _ => ()
    }
  }
  for i in 1.. prepared[p]; None => None }
    let mut context=parent.map(p => p.context).unwrap_or_else(() => @namespaces.HashMap::new().add("xml","http://www.w3.org/XML/1998/namespace").add("",""))
    for (prefix,uri) in node.bindings { if context.get(prefix) != Some(uri) { context=context.add(prefix,uri) } }
    let name=node.name.unwrap()
    let mut payload_mode=parent.map(p => p.payload_mode).unwrap_or(false)
    let mut kind=if payload_mode { Kind::Foreign(name) } else {
      match kind_from_name(name) {
        Some(kind) => kind
        None => {
          payload_mode=true
          if known_namespace(name.namespace_uri) {
            report("bpmn/unsupported-element","Unknown standard element is retained as payload_mode XML",node.span)
            Kind::Unknown(name)
          } else { Kind::Foreign(name) }
        }
      }
    }
    let mut value_type : ExpandedName?=if payload_mode { None } else { child_type(parent.map(p => p.value_type).unwrap_or(None),kind) }
    if !payload_mode {
      for attr in node.attributes {
        if attr.name.namespace_uri == "http://www.w3.org/2001/XMLSchema-instance" && attr.name.local_name == "type" {
          let value=reference_lookup(attr.value,QName,prefix => context.get(prefix))
          inspect_scalar(Reference(value),attr.value_span)
          if value.name is Some(t) { value_type=Some(t) }
        }
      }
      if value_type is Some(t) && t.namespace_uri != "http://www.w3.org/2001/XMLSchema" && schema_type(t) == None {
        report("bpmn/unsupported-type","Unknown type is retained with its complete payload_mode XML",node.span)
        payload_mode=true; kind=Kind::Unknown(name); value_type=None
      }
    }
    let values : Array[Attribute] = []
    let mut preserve_space=parent.map(p => p.preserve_space).unwrap_or(false)
    for attr in node.attributes {
      if exhausted { break }
      if attr.namespace_declaration { continue }
      let named_type=field_type(value_type,attr.name)
      let value=if payload_mode { Scalar::Text(attr.value) } else { parse_scalar_lookup(named_type,attr.value,prefix => context.get(prefix)) }
      if !payload_mode {
        inspect_scalar(value,attr.value_span)
        if (attr.name.namespace_uri == "" || known_namespace(attr.name.namespace_uri)) &&
          !value_type.map(t => schema_type(t).map(t => t.fields().iter().any(f => f.kind == Attribute && f.name == Some(attr.name))).unwrap_or(false)).unwrap_or(false) {
          report("bpmn/unsupported-attribute","Unknown standard attribute is retained",attr.span)
        }
      }
      values.push({ name:Some(attr.name),raw_name:attr.raw_name,value,span:Some(attr.span),origin:Some(origin) })
      if attr.name.namespace_uri == "http://www.w3.org/XML/1998/namespace" && attr.name.local_name == "space" {
        if attr.value == "preserve" { preserve_space=true } else if attr.value == "default" { preserve_space=false }
      }
    }
    let mut text_value : Scalar? = None
    if !payload_mode && simple_type(value_type) {
      let text=StringBuilder()
      for child in node.children { let child=tree.nodes[child]; if child.kind == Text || child.kind == CData { text.write_string(child.value) } }
      let value=parse_scalar_lookup(value_type.unwrap(),text.to_string(),prefix => context.get(prefix))
      inspect_scalar(value,node.span); text_value=Some(value)
    }
    let mixed=payload_mode || (match value_type { Some(t) => schema_type(t).map(t => t.has_mixed_content()).unwrap_or(false); None => false })
    prepared[i]=Some({kind,values,value_type,text_value,preserve_space,mixed,payload_mode,context,context_parent:parent.map(p => p.context)})
  }
  if exhausted { return None }
  for i=tree.nodes.length()-1; i>0; i=i-1 {
    guard prepared[i] is Some(p) else { continue }
    let node=tree.nodes[i]
    let contents : Array[Content] = []
    let spans : Array[Span?] = []
    let mut text_span : Span? = None
    let mut text=StringBuilder()
    fn flush() -> Unit {
      let value=text.to_string()
      if !value.is_empty() && (p.text_value != None || p.mixed || p.preserve_space || !value.iter().all(c => space(c.to_int()))) { contents.push(Text(value)); spans.push(text_span) }
      text=StringBuilder(); text_span=None
    }
    for child in node.children {
      let c=tree.nodes[child]
      if lowered[child] is Some(element) { flush(); contents.push(Element(element)); spans.push(element.span) }
      else { match c.kind {
        Text | CData => { text.write_string(c.value); text_span=Some({start:text_span.map(s => s.start).unwrap_or(c.span.start),end:c.span.end}) }
        Comment => { flush(); contents.push(Comment(c.value)); spans.push(Some(c.span)) }
        ProcessingInstruction => { flush(); contents.push(ProcessingInstruction(target=c.raw_name,data=c.value)); spans.push(Some(c.span)) }
        _ => ()
      } }
    }
    flush()
    lowered[i]=Some(make_element(p.kind,p.values,contents,node.bindings,p.value_type,p.text_value,Some(node.span),context=p.context,captured_context=true,parent_context?=p.context_parent,raw_name=node.raw_name,origin~,content_spans=spans))
  }
  let roots=tree.nodes[0].children.filter(i => tree.nodes[i].kind == Element).iter().to_array()
  guard roots.length() == 1 && lowered[roots[0]] is Some(root) && root.kind.name() == Kind::Definitions.name() else {
    report("bpmn/definitions-required","One BPMN definitions root is required",tree.nodes[0].span); return None
  }
  let prolog : Array[Content] = []; let epilog : Array[Content] = []
  let prolog_spans : Array[Span?] = []; let epilog_spans : Array[Span?] = []
  for child in tree.nodes[0].children {
    let node=tree.nodes[child]
    let target=if child < roots[0] { prolog } else { epilog }
    let spans=if child < roots[0] { prolog_spans } else { epilog_spans }
    match node.kind { Comment => { target.push(Comment(node.value)); spans.push(Some(node.span)) }; ProcessingInstruction => { target.push(ProcessingInstruction(target=node.raw_name,data=node.value)); spans.push(Some(node.span)) }; _ => () }
  }
  Some(Document::{root,prolog:ReadOnlyArray::from_array(prolog),epilog:ReadOnlyArray::from_array(epilog),origin:Some(origin),prolog_spans:ReadOnlyArray::from_array(prolog_spans),epilog_spans:ReadOnlyArray::from_array(epilog_spans)})
}