///|
pub struct ScopeLimits { nodes : Int; work : Int } derive(Eq,Debug)

///|
pub fn ScopeLimits::new(nodes? : Int = 10000, work? : Int = 1000000) -> ScopeLimits { {nodes,work} }

///|
pub struct Scope {
  view : Bpmn
  visible : ReadOnlyArray[Bool]
  containers : ReadOnlyArray[Int?]
  work : Int
  priv nodes : Map[String,Int]
}

///|
pub fn Scope::node_index(self : Scope, id : String) -> Int? { self.nodes.get(id) }

///|
pub struct ScopeResult { scope : Scope?; diagnostics : ReadOnlyArray[@diagrams.Diagnostic] }

///|
priv struct ScopeBudget { limits : ScopeLimits; diagnostics : Array[@diagrams.Diagnostic]; mut work : Int; mut failed : Bool }

///|
fn ScopeBudget::error(self : ScopeBudget, code : String, message : String) -> Unit {
  if !self.failed { self.diagnostics.push(@diagrams.Diagnostic::error(code,message)) }
  self.failed=true
}

///|
fn ScopeBudget::charge(self : ScopeBudget, amount : Int) -> Bool {
  if amount < 0 || amount > self.limits.work-self.work { self.error("bpmn/layout-limit","BPMN scope work limit exceeded"); return false }
  self.work+=amount; true
}

///|
fn scope_activity(kind : NodeKind) -> Bool {
  match kind { Task | UserTask | ServiceTask | SendTask | ReceiveTask | ManualTask | BusinessRuleTask | ScriptTask | SubProcess | AdHocSubProcess | Transaction | CallActivity => true; _ => false }
}

///|
/// Pure selected visibility shared by geometry, motion and structural queries.
/// Group membership is visual grouping and never hides a member.
pub fn Bpmn::scope(self : Bpmn, limits? : ScopeLimits = ScopeLimits::new()) -> ScopeResult {
  let budget=ScopeBudget::{limits,diagnostics:[],work:0,failed:false}
  if limits.nodes <= 0 || limits.work <= 0 { budget.error("bpmn/layout-limit","Scope limits must be positive") }
  let scope : Scope?=if budget.failed { None } else { build_scope(self,budget) }
  {scope,diagnostics:ReadOnlyArray::from_array(budget.diagnostics)}
}

///|
fn build_scope(view : Bpmn, budget : ScopeBudget) -> Scope? {
  if !budget.charge(view.nodes.length()) || !budget.charge(view.flows.length()) { return None }
  let nodes : Map[String,Int]=Map([])
  let ids : Map[String,Bool]=Map([])
  for i,node in view.nodes {
    if !budget.charge(node.id.length()+node.label.length()+node.source.path.length()) { return None }
    if ids.contains(node.id) || node.id.is_empty() { budget.error("bpmn/invalid-reference","Duplicate or empty render identity: \{node.id}"); return None }
    nodes[node.id]=i; ids[node.id]=true
  }
  for flow in view.flows {
    if !budget.charge(flow.id.length()+flow.label.length()) { return None }
    if ids.contains(flow.id) || flow.id.is_empty() { budget.error("bpmn/invalid-reference","Duplicate or empty connection identity: \{flow.id}"); return None }
    ids[flow.id]=true
    if (flow.has_source && !nodes.contains(flow.source)) || !nodes.contains(flow.target) { budget.error("bpmn/invalid-reference","Connection endpoint is absent: \{flow.id}"); return None }
  }
  let containers : Array[Int?]=Array::make(view.nodes.length(),None)
  for i,node in view.nodes {
    for owner in node.ownership.lanes {
      if !budget.charge(1) { return None }
      guard nodes.get(owner) is Some(j) && view.nodes[j].kind == Lane else { budget.error("bpmn/invalid-reference","Invalid lane owner of \{node.id}"); return None }
    }
    if node.ownership.pool is Some(owner) {
      guard nodes.get(owner) is Some(j) && view.nodes[j].kind == Participant else { budget.error("bpmn/invalid-reference","Invalid pool owner of \{node.id}"); return None }
    }
    if node.ownership.subprocess is Some(owner) {
      guard nodes.get(owner) is Some(j) && [NodeKind::SubProcess,AdHocSubProcess,Transaction,CallActivity].contains(view.nodes[j].kind) else { budget.error("bpmn/invalid-reference","Invalid subprocess owner of \{node.id}"); return None }
      containers[i]=Some(j)
    }
    if node.ownership.attached_to is Some(owner) {
      guard nodes.get(owner) is Some(j) && scope_activity(view.nodes[j].kind) && node.kind == BoundaryEvent else { budget.error("bpmn/invalid-reference","Invalid boundary attachment of \{node.id}"); return None }
    } else if node.kind == BoundaryEvent { budget.error("bpmn/invalid-reference","Boundary event has no attached activity: \{node.id}"); return None }
  }
  let owners : Array[Array[Int]]=Array::makei(view.nodes.length(),_ => [])
  for i,node in view.nodes {
    for id in node.ownership.lanes.iter().concat(node.ownership.pool.iter()).concat(node.ownership.subprocess.iter()) { let parent=nodes[id]; if !owners[i].contains(parent) { owners[i].push(parent) } }
  }
  // Attachments inherit visibility from their activity, including when that
  // activity is inside a collapsed scope. Attaching to a visible collapsed
  // subprocess does not hide its outside boundary event.
  for i,node in view.nodes {
    if node.ownership.attached_to is Some(id) { let parent=nodes[id]; if !owners[i].contains(parent) { owners[i].push(parent) } }
  }
  let states=Array::make(view.nodes.length(),0)
  let visible=Array::make(view.nodes.length(),true)
  for start in 0.. visible[p] && (if view.nodes[p].kind == Participant { !view.nodes[p].black_box } else if containers[i] == Some(p) { view.nodes[p].expanded.unwrap_or(false) } else { true }))
        states[i]=2
      } else {
        if states[i] == 2 { continue }
        if states[i] == 1 { budget.error("bpmn/invalid-reference","Containment ownership cycle at \{view.nodes[i].id}"); return None }
        states[i]=1; stack.push((i,true))
        for parent in owners[i] { stack.push((parent,false)) }
      }
    }
  }
  if visible.iter().filter(v => v).count() > budget.limits.nodes { budget.error("bpmn/layout-limit","Visible node limit exceeded"); return None }
  Some({view,nodes,visible:ReadOnlyArray::from_array(visible),containers:ReadOnlyArray::from_array(containers),work:budget.work})
}


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

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