///|
/// Product-form center for a single-class, single-server closed network.
/// Fcfs centers model one exponential server; Delay centers have infinite
/// servers and no queue.
pub(all) enum ClosedCenter {
  Fcfs(Double)
  Delay(Double)
} derive(Debug, ToJson)

///|
/// Single-class closed queueing network. Visits are relative visits per
/// reference customer cycle; think_time is the per-customer delay-center time.
pub(all) struct ClosedNetwork {
  centers : Array[ClosedCenter]
  visits : Array[Double]
  think_time : Double
} derive(Debug, ToJson)

///|
/// Per-center mean-value analysis at one population. offered_load is
/// throughput * visits * service time (Erlangs), not a service-time demand.
/// It is busy probability at Fcfs and mean busy servers at Delay.
pub(all) struct ClosedCenterMetrics {
  mean_jobs : Double
  mean_queue : Double
  mean_wait : Double
  mean_response : Double
  offered_load : Double
} derive(Debug, ToJson)

///|
/// Population operating point; conservation_residual compares center jobs,
/// think population and the requested closed population.
pub(all) struct ClosedPopulationMetrics {
  population : Int
  throughput : Double
  mean_network_response : Double
  mean_cycle_time : Double
  mean_think_population : Double
  centers : Array[ClosedCenterMetrics]
  conservation_residual : Double
} derive(Debug, ToJson)

///|
fn ClosedCenter::service_time(self : ClosedCenter) -> Double {
  match self {
    Fcfs(service) => service
    Delay(service) => service
  }
}

///|
fn ClosedCenter::is_fcfs(self : ClosedCenter) -> Bool {
  match self {
    Fcfs(_) => true
    Delay(_) => false
  }
}

///|
fn ClosedNetwork::validate(self : ClosedNetwork) -> Unit raise QueueError {
  let n = self.centers.length()
  if n == 0 || n > 128 || self.visits.length() != n {
    raise Invalid("closed network needs 1..128 centers and matching visits")
  }
  nonnegative(self.think_time, "think time")
  let mut has_visit = false
  for i in 0.. 1.0e12 {
      raise Limit("FCFS service time exceeds numerical range")
    }
    nonnegative(self.visits[i], "center visit ratio")
    if self.visits[i] > 1.0e12 {
      raise Limit("center visit ratio exceeds 1e12")
    }
    if self.visits[i] > 0.0 {
      has_visit = true
    }
  }
  if !has_visit {
    raise Invalid("at least one center must have a positive visit ratio")
  }
}

///|
/// Exact single-class MVA population curve for FCFS single-server and delay
/// centers. Multiserver FCFS centers are deliberately not approximated here.
pub fn ClosedNetwork::population_curve(
  self : ClosedNetwork,
  maximum_population : Int,
) -> Array[ClosedPopulationMetrics] raise QueueError {
  self.validate()
  if maximum_population < 0 || maximum_population > 1000 {
    raise Limit("closed MVA population bound must be 0..1000")
  }
  let center_count = self.centers.length()
  let output = []
  let zero_centers = Array::make(center_count, {
    mean_jobs: 0.0,
    mean_queue: 0.0,
    mean_wait: 0.0,
    mean_response: 0.0,
    offered_load: 0.0,
  })
  output.push({
    population: 0,
    throughput: 0.0,
    mean_network_response: 0.0,
    mean_cycle_time: 0.0,
    mean_think_population: 0.0,
    centers: zero_centers,
    conservation_residual: 0.0,
  })
  let previous_jobs = Array::make(center_count, 0.0)
  for population in 1..<=maximum_population {
    let response_per_visit = Array::make(center_count, 0.0)
    let mut denominator = self.think_time
    for i in 0.. 1.0e-8 * (1.0 + population.to_double()) {
      raise Numerical("closed MVA population conservation failed")
    }
    output.push({
      population,
      throughput,
      mean_network_response: network_response,
      mean_cycle_time: cycle_time,
      mean_think_population: think_population,
      centers: center_metrics,
      conservation_residual: residual,
    })
  }
  output
}

///|
/// Analyze one requested population while retaining the exact MVA curve used
/// for its recurrence.
pub fn ClosedNetwork::analyze_population(
  self : ClosedNetwork,
  population : Int,
) -> ClosedPopulationMetrics raise QueueError {
  let curve = self.population_curve(population)
  curve[population]
}