///|
pub(all) struct TimingQuery {
  cell : String
  pin : String
  related_pin : String
  table_kind : String
  condition : String?
  slew_seconds : Double
  load_farads : Double
  seconds : Double
  raw_value : Double
  clamped : Bool
  extrapolated : Bool
} derive(Eq, Debug)

///|
fn axis_coordinate(
  variable : String?,
  slew : Double,
  load : Double,
  units : Units,
  pos : Position,
) -> Double raise LibertyError {
  match variable {
    Some("input_net_transition") | Some("input_transition_time") =>
      slew / units.time_seconds
    Some("total_output_net_capacitance") => load / units.capacitance_farads
    _ =>
      raise failure(
        "UnsupportedAxis", "SI delay queries require input transition or total output capacitance axes",
        pos,
      )
  }
}

///|
pub fn query_timing(
  library : Library,
  cell_name : String,
  pin_name : String,
  related_pin : String,
  table_kind : String,
  slew_seconds : Double,
  load_farads : Double,
  condition? : String,
  policy~ : RangePolicy = Reject,
) -> TimingQuery raise LibertyError {
  if library.source.text("delay_model") is Some(model) &&
    model != "table_lookup" {
    raise failure(
      "UnsupportedDelayModel",
      "SI queries require table_lookup delay_model",
      library.source.position,
    )
  }
  if !["cell_rise", "cell_fall", "rise_transition", "fall_transition"].contains(
      table_kind,
    ) {
    raise failure(
      "UnsupportedQuery",
      "query supports combinational delay and transition tables",
      library.source.position,
    )
  }
  if slew_seconds < 0.0 ||
    load_farads < 0.0 ||
    slew_seconds.is_nan() ||
    slew_seconds.is_inf() ||
    load_farads.is_nan() ||
    load_farads.is_inf() {
    raise failure(
      "InvalidCoordinate",
      "slew seconds and load farads must be finite and nonnegative",
      no_position(),
    )
  }
  let errors = lint_library(library).filter(f => f.severity == Error)
  if !errors.is_empty() {
    raise failure(
      "InvalidLibrary",
      errors[0].code + ": " + errors[0].message,
      errors[0].position,
    )
  }
  let units = match library.units {
    Some(units) => units
    None =>
      raise failure(
        "MissingUnit",
        "SI queries require explicit supported time and capacitance units",
        library.source.position,
      )
  }
  let cell = match library.cell(cell_name) {
    Some(cell) => cell
    None =>
      raise failure(
        "MissingCell",
        "cell not found: " + cell_name,
        library.source.position,
      )
  }
  let pin = match cell.pin(pin_name) {
    Some(pin) => pin
    None =>
      raise failure(
        "MissingPin",
        "pin not found: " + pin_name,
        cell.source.position,
      )
  }
  let candidates = pin.timings.filter(arc => {
    arc.related_pins.contains(related_pin) &&
    (
      arc.timing_type == None ||
      ["combinational", "combinational_rise", "combinational_fall"].contains(
        arc.timing_type.unwrap(),
      )
    ) &&
    arc.tables.any(table => table.kind == table_kind)
  })
  if candidates.is_empty() {
    raise failure(
      "MissingArc",
      "no matching combinational timing arc/table",
      pin.source.position,
    )
  }
  let selected = candidates.filter(arc => arc.condition == condition)
  if selected.is_empty() && condition == None {
    raise failure(
      "ConditionRequired",
      "only conditional arcs match; select an exact when expression",
      pin.source.position,
    )
  }
  if selected.is_empty() {
    raise failure(
      "MissingCondition",
      "no arc matches the exact when expression",
      pin.source.position,
    )
  }
  let matches = []
  for arc in selected {
    for table in arc.tables {
      if table.kind == table_kind {
        matches.push(table)
      }
    }
  }
  if matches.length() != 1 {
    raise failure(
      "AmbiguousArc",
      "multiple matching arcs/tables; refuse arbitrary selection",
      pin.source.position,
    )
  }
  let table = matches[0]
  let x = if table.index_1.is_empty() {
    0.0
  } else {
    axis_coordinate(
      table.variable_1,
      slew_seconds,
      load_farads,
      units,
      table.position,
    )
  }
  let y = if table.index_2.is_empty() {
    None
  } else {
    Some(
      axis_coordinate(
        table.variable_2,
        slew_seconds,
        load_farads,
        units,
        table.position,
      ),
    )
  }
  let lookup = lookup_table(table, x, y?, policy~)
  if ["rise_transition", "fall_transition"].contains(table_kind) &&
    lookup.value < 0.0 {
    raise failure(
      "NegativeTransition",
      "extrapolation produced a negative transition time",
      table.position,
    )
  }
  let seconds = lookup.value * units.time_seconds
  if seconds.is_nan() || seconds.is_inf() {
    raise failure(
      "NumericRange",
      "normalized query result is not finite",
      table.position,
    )
  }
  {
    cell: cell_name,
    pin: pin_name,
    related_pin,
    table_kind,
    condition,
    slew_seconds,
    load_farads,
    seconds,
    raw_value: lookup.value,
    clamped: lookup.clamped,
    extrapolated: lookup.extrapolated,
  }
}

///|
pub fn query_json(query : TimingQuery) -> Json {
  {
    "schema": "moonliberty.query.v1",
    "cell": Json(query.cell),
    "pin": Json(query.pin),
    "related_pin": Json(query.related_pin),
    "table": Json(query.table_kind),
    "when": nullable_text(query.condition),
    "slew_seconds": Json(query.slew_seconds),
    "load_farads": Json(query.load_farads),
    "seconds": Json(query.seconds),
    "value_in_library_time_unit": Json(query.raw_value),
    "clamped": Json(query.clamped),
    "extrapolated": Json(query.extrapolated),
  }
}