///|
pub struct Conversion { diagram : @render.Diagram?; diagnostics : ReadOnlyArray[@diagrams.Diagnostic] } derive(Eq, Debug)

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

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

///|
priv struct Instance { resource : Int; root : Int; tag : String; pool : String?; container : String?; members : Map[Int,Bool] }

///|
priv struct Projection {
  converter : Converter; instances : Array[Instance]; nodes : Array[@model.Node]; flows : Array[@model.Flow]
  visuals : Map[(Int,Int,String),Int]; selected : Map[(Int,Int),Bool]
  di : Map[(Int,Int),Array[Int]]; di_order : Array[(Int,Int)]; used_di : Map[Int,Bool]; shapes : Map[Int,Array[Int]]; visual_order : Array[(Int,Int,String)]
}

///|
fn Projection::include_scope(self : Projection, resource : Int, root : Int, tag : String, pool : String?, container? : String) -> Unit {
  let c=self.converter
  let index=c.sources[resource].index
  let members=Map([(root,true)])
  if !c.charge(index.nodes.length()) { return }
  for n in index.nodes { if n.parent.map(p => members.contains(p)).unwrap_or(false) { members[n.ordinal]=true } }
  self.instances.push({resource,root,tag,pool,container,members})
  for ordinal in members.keys() { self.selected[(resource,ordinal)]=true }
}

///|
fn Projection::selected_di(self : Projection, selected : Selected) -> Unit {
  let c=self.converter
  if selected.plane is Some(plane) {
    self.selected[(0,plane)]=true
    let index=c.sources[0].index
    let members=Map([(plane,true)])
    for n in index.nodes { if n.parent.map(p => members.contains(p)).unwrap_or(false) { members[n.ordinal]=true; self.selected[(0,n.ordinal)]=true } }
    for ordinal in index.nodes[plane].children {
      let e=index.nodes[ordinal].element
      if actual(e) != Some(BpmnBPMNShape) && actual(e) != Some(BpmnBPMNEdge) {
        c.add("bpmn/unsupported-element","Selected plane contains an unsupported graphical element",e~); continue
      }
      let targets=c.resolve_field(0,ordinal,"bpmnElement")
      if targets.length() != 1 { c.add("bpmn/invalid-reference","Selected DI must identify one graphical element",e~); continue }
      let key=targets[0]
      if !self.di.contains(key) { self.di_order.push(key) }
      match self.di.get(key) { Some(values) => values.push(ordinal); None => self.di[key]=[ordinal] }
    }
  }
}

///|
fn Projection::geometry_for(self : Projection, key : (Int,Int), shape : Bool) -> Int? {
  let candidates=self.di.get(key).unwrap_or([])
  let expected=if shape { @bpmn.TypeId::BpmnBPMNShape } else { BpmnBPMNEdge }
  let values=candidates.filter(i => actual(self.converter.sources[0].index.nodes[i].element) == Some(expected))
  if values.length() > 1 { self.converter.add("bpmn/ambiguous-di","Multiple DI elements depict one source identity; select an unambiguous plane",e=self.converter.sources[key.0].index.nodes[key.1].element); return None }
  values.get(0)
}

///|
fn Projection::add_node(self : Projection, instance : Instance, ordinal : Int, kind : @model.NodeKind) -> Unit {
  let c=self.converter; let e=c.sources[instance.resource].index.nodes[ordinal].element
  let id=c.render_id(instance.resource,ordinal,instance.tag)
  let geometry_ordinal=self.geometry_for((instance.resource,ordinal),true)
  let geometry=geometry_ordinal.map(i => c.shape(i))
  let ownership=@model.Ownership::new(pool?=instance.pool)
  let mut label=if kind == TextAnnotation { e.children.iter().find_first(c => actual(c) == Some(Text)).map(c => c.content).unwrap_or("") } else { e.attribute("name").unwrap_or("") }
  let label_ref=if kind == Group { "categoryValueRef" } else if kind == DataObjectReference { "dataObjectRef" } else if kind == DataStoreReference { "dataStoreRef" } else { "" }
  if label_ref != "" && label.is_empty() {
    for key in c.resolve_field(instance.resource,ordinal,label_ref) { let element=c.sources[key.0].index.nodes[key.1].element; label=element.attribute(if kind == Group { "value" } else { "name" }).unwrap_or("") }
  }
  let markers=c.markers(instance.resource,ordinal)
  self.visuals[(instance.resource,ordinal,instance.tag)]=self.nodes.length()
  self.visual_order.push((instance.resource,ordinal,instance.tag))
  if geometry_ordinal is Some(i) { self.used_di[i]=true; match self.shapes.get(i) { Some(values) => values.push(self.nodes.length()); None => self.shapes[i]=[self.nodes.length()] } }
  self.nodes.push(@model.Node::new(id,kind,bounds=integer_bounds(geometry),source=c.identity(instance.resource,ordinal),label~,geometry?,ownership~,markers~,expanded?=geometry.bind(g => g.expanded),black_box=kind == Participant && e.attribute("processRef") == None))
}

///|
fn Projection::collect(self : Projection, instance : Instance) -> Unit {
  let c=self.converter; let index=c.sources[instance.resource].index
  let referenced_data=Map([])
  for n in index.nodes {
    if !instance.members.contains(n.ordinal) { continue }
    if actual(n.element) == Some(DataObjectReference) { for key in c.resolve_field(instance.resource,n.ordinal,"dataObjectRef") { referenced_data[key]=true } }
  }
  for n in index.nodes {
    if !instance.members.contains(n.ordinal) || (n.ordinal == instance.root && !(is_type(n.element,SubProcess) && self.di.contains((instance.resource,n.ordinal)))) { continue }
    let e=n.element
    if e.kind is Unknown(_) { c.add("bpmn/unsupported-element","Selected standard element is unknown",e~); continue }
    if n.payload { continue }
    if actual(e) == Some(ImplicitThrowEvent) {
      if self.di.contains((instance.resource,n.ordinal)) { c.add("bpmn/unsupported-element","Implicit throw events are non-graphical",e~) }
      continue
    }
    if is_type(e,ChoreographyActivity) || is_type(e,ConversationNode) || actual(e) == Some(ConversationLink) || actual(e) == Some(ConversationAssociation) {
      c.add("bpmn/unsupported-view","Choreography and conversation rendering is deferred; the document remains intact",e~); continue
    }
    if node_kind(e) is Some(kind) {
      let has_shape=self.di.contains((instance.resource,n.ordinal))
      // Data definitions can have explicit depictions; references carry the usual
      // data symbol. Activity-local IO stays a semantic endpoint, never a shape.
      if kind == DataObject && referenced_data.contains((instance.resource,n.ordinal)) && !has_shape { continue }
      if kind == DataInput || kind == DataOutput {
        let parent=n.parent.map(p => index.nodes[p])
        let process_io=parent.bind(p => p.parent).map(p => p == instance.root && actual(index.nodes[p].element) == Some(Process)).unwrap_or(false)
        if !process_io {
          if has_shape { c.add("bpmn/unsupported-element","Only process data inputs and outputs have graphical depictions",e~) }
          continue
        }
      }
      self.add_node(instance,n.ordinal,kind)
    } else if connection_kind(e) == None && (e.kind is Unknown(_) || is_type(e,FlowNode) || is_type(e,Artifact)) {
      c.add("bpmn/unsupported-element","Selected graphical kind \{e.kind.local_name()} has no conversion",e~)
    }
  }
}

///|
fn Projection::visual(self : Projection, key : (Int,Int), instance : String, owner : @bpmn.Element) -> Int? {
  if self.visuals.get((key.0,key.1,instance)) is Some(i) { return Some(i) }
  let candidates=[]
  for ((resource,ordinal,_),i) in self.visuals { if resource == key.0 && ordinal == key.1 { candidates.push(i) } }
  if candidates.length() == 1 { return Some(candidates[0]) }
  if candidates.length() > 1 { self.converter.add("bpmn/ambiguous-endpoint","Endpoint has multiple visible participant instances; supply an explicit DI endpoint",e=owner); return None }
  let index=self.converter.sources[key.0].index
  let mut parent=index.nodes[key.1].parent
  while parent is Some(ordinal) {
    if self.visuals.get((key.0,ordinal,instance)) is Some(i) { return Some(i) }
    parent=index.nodes[ordinal].parent
  }
  self.converter.add("bpmn/invalid-reference","Selected endpoint has no graphical owner",e=owner); None
}

///|
fn Projection::display_endpoint(self : Projection, semantic : (Int,Int), tag : String, di : Int?, field : String, owner : @bpmn.Element) -> Int? {
  if di is Some(di) {
    let refs=self.converter.resolve_field(0,di,field)
    if refs.length() == 1 {
      let key=refs[0]
      if key.0 != 0 { self.converter.add("bpmn/invalid-reference","DI endpoint must belong to the selected plane",e=owner); return None }
      let candidates=self.shapes.get(key.1).unwrap_or([])
      if candidates.length() != 1 { self.converter.add("bpmn/ambiguous-endpoint","DI endpoint does not identify one selected shape",e=owner); return None }
      return Some(candidates[0])
    }
  }
  let visual=self.visual(semantic,tag,owner)
  if actual(owner) == Some(MessageFlow) && visual is Some(i) && self.nodes[i].ownership.pool is Some(pool) {
    if !self.converter.charge(self.nodes.length()) { return None }
    for index,node in self.nodes { if node.id == pool && node.black_box { return Some(index) } }
  }
  visual
}

///|
fn Projection::ownership(self : Projection) -> Unit {
  let c=self.converter
  for instance in self.instances {
    let index=c.sources[instance.resource].index
    let lane_members : Map[Int,Array[String]]=Map([])
    let category_groups : Map[(Int,Int),Array[String]]=Map([])
    for (resource,ordinal,tag) in self.visual_order {
      let node=self.nodes[self.visuals[(resource,ordinal,tag)]]
      if resource != instance.resource || tag != instance.tag || node.kind != Group { continue }
      for key in c.resolve_field(resource,ordinal,"categoryValueRef") {
        match category_groups.get(key) { Some(groups) => groups.push(node.id); None => category_groups[key]=[node.id] }
      }
    }
    for n in index.nodes {
      if !instance.members.contains(n.ordinal) || actual(n.element) != Some(Lane) { continue }
      let mut lanes=[c.render_id(instance.resource,n.ordinal,instance.tag)]
      let mut parent=n.parent
      while parent is Some(p) { if actual(index.nodes[p].element) == Some(Lane) { lanes.insert(0,c.render_id(instance.resource,p,instance.tag)) }; parent=index.nodes[p].parent }
      for key in c.resolve_field(instance.resource,n.ordinal,"flowNodeRef") {
        if key.0 != instance.resource || !instance.members.contains(key.1) { c.add("bpmn/invalid-reference","Lane member is outside its participant process",e=n.element); continue }
        match lane_members.get(key.1) { Some(existing) => for lane in lanes { if !existing.contains(lane) { existing.push(lane) } }; None => lane_members[key.1]=lanes.copy() }
      }
      lanes=[]
    }
    for (resource,ordinal,tag) in self.visual_order {
      let i=self.visuals[(resource,ordinal,tag)]
      if resource != instance.resource || tag != instance.tag || !instance.members.contains(ordinal) { continue }
      let n=index.nodes[ordinal]; let node=self.nodes[i]
      let mut subprocess=None; let mut parent=n.parent
      let lanes=lane_members.get(ordinal).unwrap_or([])
      while parent is Some(p) {
        if p == instance.root && !self.visuals.contains((resource,p,tag)) { break }
        if subprocess == None && is_type(index.nodes[p].element,SubProcess) { subprocess=Some(c.render_id(resource,p,tag)) }
        if node.kind == Lane && actual(index.nodes[p].element) == Some(Lane) { let lane=c.render_id(resource,p,tag); if !lanes.contains(lane) { lanes.insert(0,lane) } }
        if p == instance.root { break }
        parent=index.nodes[p].parent
      }
      if subprocess == None { subprocess=instance.container }
      let attached=c.resolve_field(resource,ordinal,"attachedToRef").get(0).bind(key => self.visual(key,tag,n.element)).map(i => self.nodes[i].id)
      let groups : Array[String]=[]
      if node.kind != Group {
        for key in c.resolve_field(resource,ordinal,"categoryValueRef") {
          for group in category_groups.get(key).unwrap_or([]) { if !groups.contains(group) { groups.push(group) } }
        }
      }
      self.nodes[i]=node.with_ownership(@model.Ownership::new(pool?=instance.pool,lanes~,subprocess?,attached_to?=attached,groups~))
    }
  }
}

///|
fn Projection::connections(self : Projection, instance : Instance) -> Unit {
  let c=self.converter; let index=c.sources[instance.resource].index
  for n in index.nodes {
    if !instance.members.contains(n.ordinal) || n.payload { continue }
    let e=n.element
    guard connection_kind(e) is Some(kind) else { continue }
    let sources=c.resolve_field(instance.resource,n.ordinal,"sourceRef")
    let targets=c.resolve_field(instance.resource,n.ordinal,"targetRef")
    guard targets.length() == 1 else { c.add("bpmn/invalid-reference","Connection requires one target",e~); continue }
    let data=kind == DataAssociation || kind == DataInputAssociation || kind == DataOutputAssociation
    if !data && sources.length() != 1 { c.add("bpmn/invalid-reference","Connection requires one source",e~); continue }
    let di=self.geometry_for((instance.resource,n.ordinal),false)
    let geometry=di.map(i => { self.used_di[i]=true; c.edge(i) })
    guard self.display_endpoint(targets[0],instance.tag,di,"targetElement",e) is Some(target) else { continue }
    let semantic_target=c.identity(targets[0].0,targets[0].1)
    let condition=e.children.iter().find_first(c => c.kind == ConditionExpression).map(c => c.content)
    let count=Int::max(1,sources.length())
    for j in 0.. self.display_endpoint(key,instance.tag,di,"sourceElement",e))
      if source_key != None && source == None { continue }
      let identity=c.identity(instance.resource,n.ordinal)
      let base=c.render_id(instance.resource,n.ordinal,instance.tag)
      let id=if count == 1 { base } else { base+"@source-\{j}" }
      let is_default=source_key.map(key => c.resolve_field(key.0,key.1,"default").contains((instance.resource,n.ordinal))).unwrap_or(false)
      let direction : @model.AssociationDirection=match e.attribute("associationDirection") { Some("One") => One; Some("Both") => Both; _ => None }
      self.flows.push(@model.Flow::new(id,source.map(i => self.nodes[i].id).unwrap_or(""),self.nodes[target].id,points=integer_points(geometry),identity~,kind~,has_source=source_key != None,semantic_source?=source_key.map(key => c.identity(key.0,key.1)),semantic_target~,geometry?,label=e.attribute("name").unwrap_or(""),condition?,is_default~,direction~))
    }
  }
}

///|
/// Pure view conversion over caller-supplied resources. Geometry remains optional;
/// the SVG renderer owns complete-DI versus whole-view layout policy.
pub fn to_diagram(document : @bpmn.Document, selector? : ViewSelector = Automatic, resources? : @bpmn.ResourceSet = @bpmn.ResourceSet::new(), resource_id? : String, limits? : @bpmn.Limits = @bpmn.Limits::new(), geometry_policy? : @model.GeometryPolicy = PreferDi) -> Conversion {
  let documents=resources.documents.iter().to_array()
  if resource_id is Some(id) && !documents.iter().any(d => d.location == id) { documents.push(@bpmn.DocumentResource::new(id,document)) }
  let resources=@bpmn.ResourceSet::new(documents~,external=resources.external)
  let c=Converter::{ignore_di:geometry_policy == @model.GeometryPolicy::Automatic,sources:[],dependencies:Map([]),resources,limits,diagnostics:[],failed:false,work:0}
  fn result(diagram : @render.Diagram?) -> Conversion { {diagram:if c.failed { None } else { diagram },diagnostics:ReadOnlyArray::from_array(c.diagnostics)} }
  if limits.nodes <= 0 || limits.depth <= 0 || limits.diagnostics <= 0 || limits.token_units <= 0 || limits.source_units <= 0 || limits.output_units <= 0 { return {diagram:None,diagnostics:[@diagrams.Diagnostic::error("bpmn/conversion-limit","All limits must be positive")]} }
  let root=make_source(document,resource_id,resources,limits)
  c.sources.push(root)
  if !c.charge(root.references.references.length()) { return result(None) }
  if !root.index.complete || !root.references.complete { c.add("bpmn/conversion-limit","View validation exceeded its limits"); return result(None) }
  guard c.select(selector) is Some(selected) else { return result(None) }
  let view=c.sources[selected.resource].index.nodes[selected.node].element
  if actual(view) != Some(Process) && !is_type(view,SubProcess) && actual(view) != Some(Collaboration) { c.add("bpmn/unsupported-view","Choreography and conversation rendering is deferred",e=view); return result(None) }
  let p=Projection::{converter:c,instances:[],nodes:[],flows:[],visuals:Map([]),selected:Map([]),di:Map([]),di_order:[],used_di:Map([]),shapes:Map([]),visual_order:[]}
  if !c.ignore_di { p.selected_di(selected) }
  if c.failed { return result(None) }
  p.include_scope(selected.resource,selected.node,"",None)
  if c.failed { return result(None) }
  guard p.instances.get(0) is Some(root_instance) else {
    c.add("bpmn/conversion-limit","Root view scope was not completed"); return result(None)
  }
  if actual(view) == Some(Collaboration) {
    let index=c.sources[selected.resource].index
    for n in index.nodes {
      if !root_instance.members.contains(n.ordinal) || actual(n.element) != Some(Participant) { continue }
      let pool=c.render_id(selected.resource,n.ordinal,"")
      let processes=c.resolve_field(selected.resource,n.ordinal,"processRef")
      if processes.length() > 1 { c.add("bpmn/ambiguous-endpoint","Participant has multiple process targets",e=n.element) }
      for key in processes {
        if actual(c.sources[key.0].index.nodes[key.1].element) != Some(Process) { c.add("bpmn/invalid-reference","Participant must identify a process",e=n.element); continue }
        p.include_scope(key.0,key.1,pool,Some(pool))
      }
    }
  }
  let mut instance_index=0
  while instance_index < p.instances.length() && !c.failed {
    let instance=p.instances[instance_index]; instance_index+=1
    p.collect(instance)
    let index=c.sources[instance.resource].index
    for n in index.nodes {
      if !instance.members.contains(n.ordinal) || actual(n.element) != Some(CallActivity) { continue }
      let visual=p.visuals.get((instance.resource,n.ordinal,instance.tag))
      if visual.map(i => p.nodes[i].expanded == Some(true)).unwrap_or(false) {
        for key in c.resolve_field(instance.resource,n.ordinal,"calledElement") {
          if actual(c.sources[key.0].index.nodes[key.1].element) == Some(Process) {
            let call=p.nodes[visual.unwrap()].id
            p.include_scope(key.0,key.1,call,instance.pool,container=call)
          }
        }
      }
    }
  }
  // Root-level messages and stores can have explicit selected-plane shapes.
  for key in p.di_order {
    if p.selected.contains(key) { continue }
    let element=c.sources[key.0].index.nodes[key.1].element
    if actual(element) == Some(Message) || actual(element) == Some(DataStore) {
      p.selected[key]=true
      p.add_node({resource:key.0,root:key.1,tag:"",pool:None,container:None,members:Map([(key.1,true)])},key.1,node_kind(element).unwrap())
    } else { c.add("bpmn/invalid-reference","DI element is outside the selected view",e=element) }
  }
  if selected.plane != None {
    for instance in p.instances {
      if instance.pool is Some(pool) {
        let has_internal_di=p.di_order.iter().any(key => key.0 == instance.resource && instance.members.contains(key.1))
        if !has_internal_di { for i,node in p.nodes { if node.id == pool && node.geometry != None { p.nodes[i]=node.with_black_box(true) } } }
      }
    }
  }
  p.ownership()
  for instance in p.instances { p.connections(instance) }
  for key in p.di_order { for di in p.di[key] { if !p.used_di.contains(di) { c.add("bpmn/unsupported-element","Selected DI has no graphical projection",e=c.sources[0].index.nodes[di].element) } } }
  let seen : Array[@bpmn.ValidationFinding]=[]
  for source in c.sources {
    for finding in source.findings {
      if seen.contains(finding) { continue }; seen.push(finding)
      let mut resource : Int?=None
      for r,s in c.sources { if s.id == finding.resource_id { resource=Some(r); break } }
      let selected=resource.map(r => finding.node.map(n => p.selected.contains((r,n)) || c.dependencies.contains((r,n)) || n == 0).unwrap_or(false)).unwrap_or(false)
      let fatal=selected && finding.diagnostic.code != "bpmn/unresolved-import"
      let diagnostic=map_diagnostic(finding.diagnostic)
      if c.diagnostics.length() < limits.diagnostics { c.diagnostics.push(if fatal { diagnostic } else { {..diagnostic,severity:Warning} }) } else { c.add("bpmn/conversion-limit","Combined diagnostic limit exceeded") }
      if fatal { c.failed=true }
    }
  }
  let ids=Map([])
  for node in p.nodes { if ids.contains(node.id) { c.add("bpmn/duplicate-id","Duplicate generated render identity") }; ids[node.id]=true }
  for flow in p.flows { if ids.contains(flow.id) { c.add("bpmn/duplicate-id","Duplicate generated render identity") }; ids[flow.id]=true }
  let id=c.render_id(selected.resource,selected.node,"")
  let bpmn=@model.Bpmn::new(id,p.nodes,p.flows,label=view.attribute("name").unwrap_or(id),source=c.identity(selected.resource,selected.node),kind=if actual(view) == Some(Collaboration) { Collaboration } else { Process },plane?=selected.plane.map(i => c.identity(0,i)))
  result(Some(Bpmn(bpmn)))
}

///|
/// Read, select, convert and render without performing filesystem/network I/O.
pub fn render_svg(source : String, selector? : ViewSelector = Automatic, resources? : @bpmn.ResourceSet = @bpmn.ResourceSet::new(), geometry_policy? : @model.GeometryPolicy = PreferDi) -> @render.Rendered {
  let read=@bpmn.read(source)
  guard read.document is Some(document) else { return @render.Rendered::failed("image/svg+xml",read.diagnostics.map(map_diagnostic)) }
  let converted=to_diagram(document,selector~,resources~,geometry_policy~)
  // A successful selected view has already checked its semantic fields. Lowering
  // findings in other views or explicitly ignored DI remain visible as warnings.
  let diagnostics=read.diagnostics.map(d => { let mapped=map_diagnostic(d); if converted.diagram != None { {..mapped,severity:Warning} } else { mapped } }).iter().to_array()
  diagnostics.append(converted.diagnostics)
  guard converted.diagram is Some(diagram) else { return @render.Rendered::failed("image/svg+xml",diagnostics) }
  let rendered=@svg.SvgRenderer::new(bpmn_policy=geometry_policy).render(diagram)
  diagnostics.append(rendered.diagnostics)
  match rendered.output { Some(output) => @render.Rendered::ok(output,rendered.media_type,diagnostics~); None => @render.Rendered::failed(rendered.media_type,diagnostics) }
}