///|
pub(all) enum ReductionStatus {
  ExactTree
  ApproximateGraph
  EmptyNetwork
  NoDriver
  MultipleDrivers
  Disconnected
  InvalidElectricalValue
} derive(Eq, Debug)

///|
pub(all) struct SinkEstimate {
  node_id : Int
  node_name : String
  direction : @model.Direction
  pin_load_capacitance : Double
  local_parasitic_capacitance : Double
  path_resistance : Double
  elmore_delay : Double?
  reachable : Bool
} derive(Debug)

///|
pub(all) struct PiModel {
  near_capacitance : Double
  far_capacitance : Double
  series_resistance : Double
  total_capacitance : Double
  first_moment : Double
} derive(Debug)

///|
pub(all) struct NetReduction {
  status : ReductionStatus
  net_name : String
  driver_node : String?
  pi : PiModel
  critical_path : @path.PathResult?
  sinks : Array[SinkEstimate]
  local_node_count : Int
  reachable_node_count : Int
  coupling_policy : String
} derive(Debug)

///|
pub(all) struct DesignReduction {
  nets : Array[NetReduction]
  exact_tree_count : Int
  approximate_count : Int
  unavailable_count : Int
  total_near_capacitance : Double
  total_far_capacitance : Double
} derive(Debug)

///|
fn empty_pi() -> PiModel {
  {
    near_capacitance: 0.0,
    far_capacitance: 0.0,
    series_resistance: 0.0,
    total_capacitance: 0.0,
    first_moment: 0.0,
  }
}

///|
fn local_node_count(net : @model.Net) -> Int {
  let mut count = 0
  for node in net.nodes {
    if node.kind != @model.External {
      count += 1
    }
  }
  count
}

///|
fn unavailable(status : ReductionStatus, net : @model.Net) -> NetReduction {
  {
    status,
    net_name: net.name,
    driver_node: None,
    pi: empty_pi(),
    critical_path: None,
    sinks: [],
    local_node_count: local_node_count(net),
    reachable_node_count: 0,
    coupling_policy: "split coupling capacitance equally between endpoints",
  }
}

///|
fn valid_value(value : Double) -> Bool {
  value >= 0.0 && !value.is_nan() && !value.is_inf()
}

///|
fn electrical_values_valid(net : @model.Net) -> Bool {
  for resistor in net.resistors {
    if !valid_value(resistor.resistance) {
      return false
    }
  }
  for capacitor in net.capacitors {
    if !valid_value(capacitor.capacitance) {
      return false
    }
  }
  for connection in net.connections {
    if connection.load_capacitance is Some(value) && !valid_value(value) {
      return false
    }
  }
  true
}

///|
fn node_capacitances(net : @model.Net) -> Array[Double] {
  let values = Array::make(net.nodes.length(), 0.0)
  for capacitor in net.capacitors {
    if capacitor.from_node >= 0 && capacitor.from_node < values.length() {
      match capacitor.to_node {
        None => values[capacitor.from_node] += capacitor.capacitance
        Some(other) => {
          values[capacitor.from_node] += capacitor.capacitance * 0.5
          if other >= 0 &&
            other < values.length() &&
            net.nodes[other].kind != @model.External {
            values[other] += capacitor.capacitance * 0.5
          }
        }
      }
    }
  }
  values
}

///|
fn pin_loads(net : @model.Net) -> Array[Double] {
  let values = Array::make(net.nodes.length(), 0.0)
  for connection in net.connections {
    if connection.node_id >= 0 && connection.node_id < values.length() {
      if connection.load_capacitance is Some(value) {
        values[connection.node_id] += value
      }
    }
  }
  values
}

///|
fn delay_for_node(result : @analyzer.ElmoreResult, node_id : Int) -> Double? {
  for delay in result.node_delays {
    if delay.node_id == node_id {
      return Some(delay.delay_seconds)
    }
  }
  None
}

///|
fn sink_estimates(
  net : @model.Net,
  driver_name : String,
  parasitics : Array[Double],
  loads : Array[Double],
  delay : @analyzer.ElmoreResult,
) -> Array[SinkEstimate] {
  let sinks : Array[SinkEstimate] = []
  for connection in net.connections {
    if connection.direction == @model.Input ||
      connection.direction == @model.Inout {
      match net.find_node(connection.node_id) {
        Some(node) => {
          let path = @path.minimum_resistance_path(net, driver_name, node.name)
          sinks.push({
            node_id: node.id,
            node_name: node.name,
            direction: connection.direction,
            pin_load_capacitance: loads[node.id],
            local_parasitic_capacitance: parasitics[node.id],
            path_resistance: path.total_resistance,
            elmore_delay: delay_for_node(delay, node.id),
            reachable: path.status == @path.Found,
          })
        }
        None => ()
      }
    }
  }
  sinks.sort_by(fn(left, right) {
    let delay_order = right.path_resistance.compare(left.path_resistance)
    if delay_order == 0 {
      left.node_name.compare(right.node_name)
    } else {
      delay_order
    }
  })
  sinks
}

///|
fn build_pi(
  net : @model.Net,
  driver_name : String,
  driver_id : Int,
  parasitics : Array[Double],
  loads : Array[Double],
) -> (PiModel, Int) {
  let mut near_capacitance = parasitics[driver_id] + loads[driver_id]
  let mut far_capacitance = 0.0
  let mut first_moment = 0.0
  let mut reachable_count = 0
  for node in net.nodes {
    if node.kind != @model.External {
      let local_capacitance = parasitics[node.id] + loads[node.id]
      let path = @path.minimum_resistance_path(net, driver_name, node.name)
      if path.status == @path.Found {
        reachable_count += 1
        if node.id != driver_id {
          far_capacitance += local_capacitance
          first_moment += local_capacitance * path.total_resistance
        }
      }
    }
  }
  if near_capacitance < 0.0 {
    near_capacitance = 0.0
  }
  let series_resistance = if far_capacitance > 0.0 {
    first_moment / far_capacitance
  } else {
    0.0
  }
  (
    {
      near_capacitance,
      far_capacitance,
      series_resistance,
      total_capacitance: near_capacitance + far_capacitance,
      first_moment,
    },
    reachable_count,
  )
}

///|
/// Produces a first-moment-preserving two-capacitance RC approximation.
///
/// For a tree, shortest resistance paths are unique and the model is marked
/// exact-tree. Cyclic graphs are accepted as a screening approximation using
/// minimum-resistance paths. Disconnected and invalid networks are explicit.
pub fn reduce_net(net : @model.Net) -> NetReduction {
  if net.nodes.length() == 0 {
    return unavailable(EmptyNetwork, net)
  }
  if !electrical_values_valid(net) {
    return unavailable(InvalidElectricalValue, net)
  }
  let drivers = net.driver_nodes()
  if drivers.length() == 0 {
    return unavailable(NoDriver, net)
  }
  if drivers.length() > 1 {
    return unavailable(MultipleDrivers, net)
  }
  let topology = @graph.summarize(net)
  if topology.connected_components > 1 {
    return unavailable(Disconnected, net)
  }
  let driver_id = drivers[0]
  let driver_name = net.nodes[driver_id].name
  let parasitics = node_capacitances(net)
  let loads = pin_loads(net)
  let (pi, reachable_count) = build_pi(
    net, driver_name, driver_id, parasitics, loads,
  )
  let delay = @analyzer.elmore_delay(net)
  {
    status: if topology.has_cycle {
      ApproximateGraph
    } else {
      ExactTree
    },
    net_name: net.name,
    driver_node: Some(driver_name),
    pi,
    critical_path: Some(@path.critical_resistance_path(net)),
    sinks: sink_estimates(net, driver_name, parasitics, loads, delay),
    local_node_count: local_node_count(net),
    reachable_node_count: reachable_count,
    coupling_policy: "split coupling capacitance equally between endpoints",
  }
}

///|
pub fn reduce_design(network : @model.RCNetwork) -> DesignReduction {
  let nets : Array[NetReduction] = []
  let mut exact_tree_count = 0
  let mut approximate_count = 0
  let mut unavailable_count = 0
  let mut total_near_capacitance = 0.0
  let mut total_far_capacitance = 0.0
  for net in network.nets {
    let reduction = reduce_net(net)
    match reduction.status {
      ExactTree => exact_tree_count += 1
      ApproximateGraph => approximate_count += 1
      _ => unavailable_count += 1
    }
    total_near_capacitance += reduction.pi.near_capacitance
    total_far_capacitance += reduction.pi.far_capacitance
    nets.push(reduction)
  }
  {
    nets,
    exact_tree_count,
    approximate_count,
    unavailable_count,
    total_near_capacitance,
    total_far_capacitance,
  }
}

///|
pub fn ReductionStatus::label(self : ReductionStatus) -> String {
  match self {
    ExactTree => "exact-tree"
    ApproximateGraph => "approximate-graph"
    EmptyNetwork => "empty-network"
    NoDriver => "no-driver"
    MultipleDrivers => "multiple-drivers"
    Disconnected => "disconnected"
    InvalidElectricalValue => "invalid-electrical-value"
  }
}

///|
pub fn NetReduction::worst_sink(self : NetReduction) -> SinkEstimate? {
  if self.sinks.length() == 0 {
    None
  } else {
    Some(self.sinks[0])
  }
}