// Statistics: turn a `Device` into aggregate, quantified facts about how the
// Modbus address space is used. Where `tools.mbt` gives narrow per-area/type
// counts, this module builds a single `DeviceStats` snapshot you can render or
// reason about, plus a few density helpers that drive allocation advice.

///|
/// Per-area summarised allocation.
pub struct AreaStats {
  area : Int
  /// Number of registers mapped into this area.
  registers : Int
  /// Total words occupied by those registers (accounting for each type width).
  words : Int
  /// Words that remain unused out of the area's 9999-word capacity.
  free_words : Int
  /// Utilisation as parts per thousand (0..1000).
  utilization_permille : Int
  /// Total bytes consumed (words * 2).
  bytes : Int
}

///|
/// Whole-device stats snapshot.
pub struct DeviceStats {
  total_registers : Int
  /// Sum of `word_count` over all registers.
  total_words : Int
  /// `total_words` in bytes (Modbus word = 2 bytes).
  total_bytes : Int
  /// Number of coil/discrete-input (bit) registers.
  bit_registers : Int
  /// Number of registers that carry a usable jsonb mapping.
  jsonb_mapped : Int
  /// Number of registers without an explicit unit string.
  unitless : Int
  /// Shortest register-name length observed (0 for an empty device).
  min_name_len : Int
  /// Longest register-name length observed (0 for an empty device).
  max_name_len : Int
  /// Average name length, rounded down.
  avg_name_len : Int
  /// Per-area rows, in the standard 0..4 order (area 2 omitted).
  areas : Array[AreaStats]
}

///|
/// Accumulate utilisation info for one area; capacity is 9999 words.
fn area_usage(dev : Device, area : Int) -> AreaStats {
  let mut registers = 0
  let mut words = 0
  for r in dev.registers {
    if area_of(r.address) == area {
      registers = registers + 1
      words = words + word_count(r.rtype)
    }
  }
  let free = 9999 - words
  let permille = if words > 0 { words * 1000 / 9999 } else { 0 }
  {
    area,
    registers,
    words,
    free_words: free,
    utilization_permille: permille,
    bytes: words * 2,
  }
}

///|
/// Build a full statistics snapshot for a `Device`.
pub fn compute_stats(dev : Device) -> DeviceStats {
  let _all : Array[Register] = dev.registers
  let mut total_words = 0
  let mut bit_registers = 0
  let mut jsonb_mapped = 0
  let mut unitless = 0
  let mut min_len = 0
  let mut max_len = 0
  let mut sum_len = 0
  let mut first = true
  for r in dev.registers {
    total_words = total_words + word_count(r.rtype)
    if area_is_bit(area_of(r.address)) {
      bit_registers = bit_registers + 1
    }
    if !jsonb_is_unset(r.jsonb) {
      jsonb_mapped = jsonb_mapped + 1
    }
    if r.unit.length() == 0 {
      unitless = unitless + 1
    }
    let l = r.name.length()
    if first {
      min_len = l
      max_len = l
      first = false
    } else {
      if l < min_len {
        min_len = l
      }
      if l > max_len {
        max_len = l
      }
    }
    sum_len = sum_len + l
  }
  let total = dev.registers.length()
  let avg = if total > 0 { sum_len / total } else { 0 }
  let areas : Array[AreaStats] = []
  // Treat only the four real areas; area 2 is reserved/nonexistent.
  for i = 0; i < 5; i = i + 1 {
    if i == 2 {
      continue
    }
    areas.push(area_usage(dev, i))
  }
  {
    total_registers: total,
    total_words,
    total_bytes: total_words * 2,
    bit_registers,
    jsonb_mapped,
    unitless,
    min_name_len: min_len,
    max_name_len: max_len,
    avg_name_len: avg,
    areas,
  }
}

///|
/// Total free words across every real area.
pub fn total_free_words(stats : DeviceStats) -> Int {
  let mut sum = 0
  for a in stats.areas {
    sum = sum + a.free_words
  }
  sum
}

///|
/// The area with the largest absolute word consumption.
/// Returns `(-1, 0)` when the device is empty.
pub fn dominant_area(dev : Device) -> (Int, Int) {
  let mut best_area = -1
  let mut best_words = 0
  let areas = compute_stats(dev).areas
  for a in areas {
    if a.words > best_words {
      best_words = a.words
      best_area = a.area
    }
  }
  (best_area, best_words)
}

///|
/// Smallest and largest address gaps (in words) between consecutive registers
/// in the same area, computed from the whole device. `(0, 0)` when there are
/// fewer than two registers in any single area.
pub fn gap_bounds(dev : Device) -> (Int, Int) {
  let areas = count_by_area(dev)
  let mut min_gap = -1
  let mut max_gap = 0
  for (area, _count) in areas {
    let sorted = sort_by_address(filter_area(dev, area)).registers
    if sorted.length() < 2 {
      continue
    }
    let base = area_base(area)
    for i = 1; i < sorted.length(); i = i + 1 {
      let prev_hi = sorted[i - 1].address -
        base +
        word_count(sorted[i - 1].rtype)
      let cur_lo = sorted[i].address - base + 0
      let gap = cur_lo - prev_hi
      if gap < 0 {
        continue // registers overlap; not treated as a gap
      }
      if min_gap < 0 || gap < min_gap {
        min_gap = gap
      }
      if gap > max_gap {
        max_gap = gap
      }
    }
  }
  if min_gap < 0 {
    min_gap = 0
  }
  (min_gap, max_gap)
}

///|
/// Render the stats snapshot as a compact text block (used by the CLI summary).
pub fn stats_text(dev : Device) -> String {
  let s = compute_stats(dev)
  let out = StringBuilder()
  out.write_string("total registers: " + s.total_registers.to_string() + "\n")
  out.write_string("total words: " + s.total_words.to_string() + "\n")
  out.write_string("total bytes: " + s.total_bytes.to_string() + "\n")
  out.write_string("bit registers: " + s.bit_registers.to_string() + "\n")
  out.write_string("jsonb mapped: " + s.jsonb_mapped.to_string() + "\n")
  out.write_string("unitless: " + s.unitless.to_string() + "\n")
  out.write_string(
    "name length (min/max/avg): " +
    s.min_name_len.to_string() +
    "/" +
    s.max_name_len.to_string() +
    "/" +
    s.avg_name_len.to_string() +
    "\n",
  )
  for a in s.areas {
    out.write_string(
      area_name(a.area) +
      ": " +
      a.registers.to_string() +
      " reg, " +
      a.words.to_string() +
      "/" +
      a.free_words.to_string() +
      " words free (" +
      (a.utilization_permille / 10).to_string() +
      "%)\n",
    )
  }
  out.to_string()
}

///|
test "compute_stats tallies the whole device" {
  let dev : Device = {
    registers: [
      {
        name: "a",
        address: 40001,
        rtype: TFloat32,
        access: Read,
        unit: "degC",
        jsonb: "jsonb->a",
        line: 1,
      },
      {
        name: "bb",
        address: 1001,
        rtype: TBit,
        access: Read,
        unit: "",
        jsonb: "",
        line: 2,
      },
    ],
  }
  let s = compute_stats(dev)
  assert_eq(s.total_registers, 2)
  assert_eq(s.total_words, 3) // float32 = 2 words + coil = 1 word
  assert_eq(s.total_bytes, 6)
  assert_eq(s.bit_registers, 1)
  assert_eq(s.jsonb_mapped, 1)
  assert_eq(s.unitless, 1)
}

///|
test "area usage matches per-area free words" {
  let dev : Device = {
    registers: [
      {
        name: "a",
        address: 40001,
        rtype: TInt16,
        access: Read,
        unit: "",
        jsonb: "",
        line: 1,
      },
      {
        name: "b",
        address: 40002,
        rtype: TFloat32,
        access: Read,
        unit: "",
        jsonb: "",
        line: 2,
      },
    ],
  }
  let s = compute_stats(dev)
  // Holding area: 1 + 2 = 3 words used, 9996 free.
  let mut hr : AreaStats = {
    area: 0,
    registers: 0,
    words: 0,
    free_words: 0,
    utilization_permille: 0,
    bytes: 0,
  }
  for a in s.areas {
    if a.area == 4 {
      hr = a
    }
  }
  assert_eq(hr.words, 3)
  assert_eq(hr.free_words, 9996)
  assert_eq(hr.bytes, 6)
}

///|
test "gap_bounds returns min and max intra-area gaps" {
  let dev : Device = {
    registers: [
      {
        name: "a",
        address: 40001,
        rtype: TInt16,
        access: Read,
        unit: "",
        jsonb: "",
        line: 1,
      },
      {
        name: "b",
        address: 40004,
        rtype: TInt16,
        access: Read,
        unit: "",
        jsonb: "",
        line: 2,
      },
    ],
  }
  // a occupies word 0; b starts at word 3 => gap of 2 words.
  assert_eq(gap_bounds(dev), (2, 2))
}

///|
test "dominant_area picks the area with the most words" {
  let dev : Device = {
    registers: [
      {
        name: "a",
        address: 40001,
        rtype: TFloat64,
        access: Read,
        unit: "",
        jsonb: "",
        line: 1,
      },
      {
        name: "b",
        address: 1001,
        rtype: TBit,
        access: Read,
        unit: "",
        jsonb: "",
        line: 2,
      },
    ],
  }
  assert_eq(dominant_area(dev), (4, 4)) // float64 = 4 words in holding area
}

///|
test "stats_text includes the summary lines" {
  let dev : Device = { registers: [], }
  let t = stats_text(dev)
  assert_eq(t.contains("total registers: 0"), true)
  assert_eq(t.contains("total words: 0"), true)
  assert_eq(t.contains("coils: 0 reg"), true)
}

///|
test "name length bounds are tracked across registers" {
  let dev : Device = {
    registers: [
      {
        name: "x",
        address: 40001,
        rtype: TInt16,
        access: Read,
        unit: "",
        jsonb: "",
        line: 1,
      },
      {
        name: "longer_name",
        address: 40002,
        rtype: TInt16,
        access: Read,
        unit: "",
        jsonb: "",
        line: 2,
      },
    ],
  }
  let s = compute_stats(dev)
  assert_eq(s.min_name_len, 1)
  assert_eq(s.max_name_len, 11)
  assert_eq(s.avg_name_len, 6)
}