///|
pub(all) struct TimingArc {
  related_pins : Array[String]
  timing_type : String?
  timing_sense : String?
  condition : String?
  tables : Array[Table]
  position : Position
} derive(Debug)

///|
pub(all) struct Pin {
  name : String
  direction : String?
  capacitance : Double?
  max_capacitance : Double?
  max_transition : Double?
  function : String?
  timings : Array[TimingArc]
  source : Group
} derive(Debug)

///|
pub(all) struct Cell {
  name : String
  area : Double?
  pins : Array[Pin]
  source : Group
} derive(Debug)

///|
pub(all) struct Library {
  name : String
  units : Units?
  cells : Array[Cell]
  templates : Array[Group]
  findings : Array[Finding]
  source : Group
} derive(Debug)

///|
pub(all) struct Inventory {
  cells : Int32
  pins : Int32
  arcs : Int32
  tables : Int32
  errors : Int32
  warnings : Int32
} derive(Eq, Debug)

///|
/// Bounds expansion of shared pin declarations and repeated validation work.
pub(all) struct ModelLimits {
  max_pins : Int32
  max_work : Int32
} derive(Eq, Debug)

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

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

///|
pub fn ModelLimits::default() -> ModelLimits {
  { max_pins: 100000, max_work: 4000000, }
}

///|
fn model_work(
  timings : Array[TimingArc],
  function : String?,
  source : Group,
  limit : Int32,
  pos : Position,
) -> Int32 raise LibertyError {
  let mut work = 1 +
    source.items.length() +
    function.map(s => s.length()).unwrap_or(0)
  for item in source.items {
    if item is Section(timing) && timing.kind == "timing" {
      work += timing.items.length()
      for node in timing.items {
        if node is Section(table) && timing_table_kind(table.kind) {
          work += table.items.length()
          if work > limit {
            raise failure(
              "ModelLimit", "expanded validation work exceeds budget", pos,
            )
          }
        }
      }
    }
  }
  for arc in timings {
    work += 1 +
      arc.related_pins.length() +
      arc.condition.map(s => s.length()).unwrap_or(0)
    for table in arc.tables {
      work += 1 +
        table.index_1.length() +
        table.index_2.length() +
        table.values.length()
      if work > limit {
        raise failure(
          "ModelLimit", "expanded validation work exceeds budget", pos,
        )
      }
    }
  }
  if work > limit {
    raise failure("ModelLimit", "expanded validation work exceeds budget", pos)
  }
  work
}

///|
fn finding(
  error : LibertyError,
  path : String,
  severity~ : Severity = Error,
) -> Finding {
  let LibertyError(code, message, position) = error
  { code, severity, path, message, position, }
}

///|
fn simple_position(group : Group, key : String) -> Position {
  for item in group.items {
    if item is Attribute(name, _, pos) && name == key {
      return pos
    }
  }
  group.position
}

///|
fn optional_number(
  group : Group,
  key : String,
  path : String,
  findings : Array[Finding],
) -> Double? {
  match group.text(key) {
    None => None
    Some(text) => {
      let value = finite_number(text, position=simple_position(group, key)) catch {
        error => {
          findings.push(finding(error, path + "/" + key))
          return None
        }
      }
      Some(value)
    }
  }
}

///|
fn words(value : String) -> Array[String] {
  let result = []
  let mut start = 0
  for i = 0; i <= value.length(); i = i + 1 {
    if i == value.length() || whitespace(value[i]) {
      if i > start {
        result.push(value[start:i].to_owned())
      }
      start = i + 1
    }
  }
  result
}

///|
fn timing_table_kind(kind : String) -> Bool {
  [
    "cell_rise", "cell_fall", "rise_transition", "fall_transition", "rise_constraint",
    "fall_constraint",
  ].contains(kind)
}

///|
pub fn build_library(
  root : Group,
  model_limits~ : ModelLimits = ModelLimits::default(),
) -> Library raise LibertyError {
  if root.kind != "library" || root.name().is_empty() {
    raise failure(
      "InvalidRoot",
      "typed model needs one named library",
      root.position,
    )
  }
  if model_limits.max_pins < 1 ||
    model_limits.max_pins > 1000000 ||
    model_limits.max_work < 1 ||
    model_limits.max_work > 16000000 {
    raise failure(
      "InvalidLimit",
      "model limits require 1..1000000 pins and 1..16000000 work units",
      root.position,
    )
  }
  let mut expanded_pins = 0
  let mut expanded_work = 0
  let findings = []
  let units = Some(read_units(root)) catch {
    error => {
      findings.push(finding(error, root.name(), severity=Warning))
      None
    }
  }
  let templates = root.groups("lu_table_template")
  let by_name : Map[String, Group] = Map([])
  for template in templates {
    if by_name.contains(template.name()) {
      findings.push({
        code: "DuplicateTemplate",
        severity: Error,
        path: root.name() + "/" + template.name(),
        message: "lookup template name is duplicated",
        position: template.position,
      })
    } else {
      by_name[template.name()] = template
    }
  }
  let cells = []
  for cell in root.groups("cell") {
    let cell_path = root.name() + "/cell:" + cell.name()
    let pins = []
    for container in cell.groups("bus") + cell.groups("bundle") {
      add_finding(
        findings,
        "UnexpandedPinContainer",
        cell_path + "/" + container.kind + ":" + container.name(),
        "bus/bundle data is retained; typed inventory and SI queries cover directly declared scalar pins only",
        container.position,
        severity=Warning,
      )
    }
    for pin in cell.groups("pin") {
      let pin_path = cell_path + "/pin:" + pin.name()
      let timings = []
      for timing in pin.groups("timing") {
        let tables = []
        for node in timing.items {
          if node is Section(group) && timing_table_kind(group.kind) {
            let table = read_table(group, template?=by_name.get(group.name())) catch {
              error => {
                let LibertyError(code, _, _) = error
                findings.push(
                  finding(
                    error,
                    pin_path + "/" + group.kind,
                    severity=if code == "UnsupportedDimension" {
                      Warning
                    } else {
                      Error
                    },
                  ),
                )
                continue
              }
            }
            tables.push(table)
          }
        }
        timings.push({
          related_pins: match timing.text("related_pin") {
            Some(value) => words(value)
            None => []
          },
          timing_type: timing.text("timing_type"),
          timing_sense: timing.text("timing_sense"),
          condition: timing.text("when"),
          tables,
          position: timing.position,
        })
      }
      let names = if !pin.arguments.is_empty() &&
        pin.arguments.all(args => {
          args.length() == 1 && !atom_text(args[0]).is_empty()
        }) {
        pin.arguments.map(args => atom_text(args[0]))
      } else {
        [""]
      }
      let capacitance = optional_number(pin, "capacitance", pin_path, findings)
      let max_capacitance = optional_number(
        pin, "max_capacitance", pin_path, findings,
      )
      let max_transition = optional_number(
        pin, "max_transition", pin_path, findings,
      )
      let function = pin.text("function")
      let cost = model_work(
        timings,
        function,
        pin,
        model_limits.max_work,
        pin.position,
      )
      for name in names {
        if expanded_pins >= model_limits.max_pins ||
          cost > model_limits.max_work - expanded_work {
          raise failure(
            "ModelLimit",
            "expanded pin count or validation work exceeds budget",
            pin.position,
          )
        }
        expanded_pins += 1
        expanded_work += cost
        pins.push({
          name,
          direction: pin.text("direction"),
          capacitance,
          max_capacitance,
          max_transition,
          function,
          timings,
          source: pin,
        })
      }
    }
    cells.push({
      name: cell.name(),
      area: optional_number(cell, "area", cell_path, findings),
      pins,
      source: cell,
    })
  }
  { name: root.name(), units, cells, templates, findings, source: root, }
}

///|
pub fn load_library(
  text : String,
  limits~ : Limits = Limits::default(),
  model_limits~ : ModelLimits = ModelLimits::default(),
) -> Library raise LibertyError {
  build_library(parse_liberty(text, limits~), model_limits~)
}

///|
pub fn Library::cell(self : Library, name : String) -> Cell? {
  self.cells.iter().find_first(cell => cell.name == name)
}

///|
pub fn Cell::pin(self : Cell, name : String) -> Pin? {
  self.pins.iter().find_first(pin => pin.name == name)
}

///|
pub fn Library::inventory(self : Library) -> Inventory {
  let mut pins = 0
  let mut arcs = 0
  let mut tables = 0
  for cell in self.cells {
    pins += cell.pins.length()
    for pin in cell.pins {
      arcs += pin.timings.length()
      for timing in pin.timings {
        tables += timing.tables.length()
      }
    }
  }
  {
    cells: self.cells.length(),
    pins,
    arcs,
    tables,
    errors: self.findings.filter(f => f.severity == Error).length(),
    warnings: self.findings.filter(f => f.severity == Warning).length(),
  }
}

///|
fn nullable_number(value : Double?) -> Json {
  match value {
    Some(number) => Json(number)
    None => Json::null()
  }
}

///|
fn nullable_text(value : String?) -> Json {
  match value {
    Some(text) => Json(text)
    None => Json::null()
  }
}

///|
pub fn inventory_json(library : Library) -> Json {
  let inventory = library.inventory()
  {
    "schema": "moonliberty.inventory.v1",
    "library": Json(library.name),
    "delay_model": nullable_text(library.source.text("delay_model")),
    "cells": Json(inventory.cells),
    "pins": Json(inventory.pins),
    "arcs": Json(inventory.arcs),
    "tables": Json(inventory.tables),
    "model_errors": Json(inventory.errors),
    "model_warnings": Json(inventory.warnings),
    "time_unit_seconds": nullable_number(
      library.units.map(units => units.time_seconds),
    ),
    "capacitance_unit_farads": nullable_number(
      library.units.map(units => units.capacitance_farads),
    ),
    "cell_names": Json(library.cells.map(cell => cell.name)),
  }
}