// vecplot — text-dump Monitor data.
//
// Julia's `vecplot(pop, :v)` returns a `Plots.plot` object. Since we
// don't have Makie/Plots in the MoonBit port, we instead provide:
//   - `Monitor::dump_csv(m, path)`  : write time-series to CSV
//   - `Monitor::dump_summary(m)`    : print min/max/mean to stdout
//   - `Monitor::count_spikes(m)`    : count spike transitions in :fire
//
// This is a v0.0.x stand-in for vecplot. When the user wires up
// their own Python/Makie post-processing, they can call these
// dump functions to get the data out.

///|
/// Count rising-edge transitions in a fire monitor.
/// Returns the number of times the monitored neuron's fire flag
/// went from false to true. Approximates spike count.
pub fn Monitor::count_spikes(m : Monitor) -> Int {
  let n = m.data.length()
  if n == 0 {
    return 0
  }
  let mut count = 0
  let mut prev = 0.0F
  for i in 0..= 0.5F {
      count = count + 1
    }
    prev = cur
  }
  count
}

///|
/// Compute the firing rate (Hz) of a fire monitor over the full
/// recorded duration. Returns `count_spikes(m) / duration_ms * 1000.0F`.
/// Returns 0.0F if the buffer is empty or the duration is zero.
pub fn Monitor::firing_rate(m : Monitor) -> Float {
  let n = m.data.length()
  if n < 2 {
    return 0.0F
  }
  let n_t = m.times.length()
  if n_t < 2 {
    return 0.0F
  }
  let total_ms = m.times[n_t - 1] - m.times[0]
  if total_ms <= 0.0F {
    return 0.0F
  }
  let n_spikes = m.count_spikes()
  Float::from_int(n_spikes) * 1000.0F / total_ms
}

///|
/// Count rising-edge transitions in the fire buffer that occur
/// within the time window `[t_start, t_end]` (ms, inclusive on both
/// ends). Returns 0 if no spikes in the window. Used to compute
/// firing rates over sub-intervals of the simulation.
pub fn Monitor::count_spikes_interval(
  m : Monitor,
  t_start : Float,
  t_end : Float,
) -> Int {
  let n = m.data.length()
  let n_t = m.times.length()
  let lim = if n < n_t { n } else { n_t }
  let mut count = 0
  let mut prev = 0.0F
  for i in 0..= t_start && t <= t_end && prev < 0.5F && cur >= 0.5F {
      count = count + 1
    }
    prev = cur
  }
  count
}

///|
/// Clear all recorded data and timestamps. Mirrors Julia's
/// `clear_records!(pop)` API. Used to reset monitoring between
/// sim!() calls without re-allocating monitors.
pub fn Monitor::clear_records(m : Monitor) -> Unit {
  m.data.clear()
  m.times.clear()
}

///|
/// Return the mean of the monitor's data buffer.
/// Returns 0.0F if the buffer is empty.
pub fn Monitor::mean(m : Monitor) -> Float {
  let n = m.data.length()
  if n == 0 {
    return 0.0F
  }
  let mut sum = 0.0F
  for i in 0.. Float {
  let n = m.data.length()
  if n == 0 {
    return 0.0F
  }
  let mut mn = m.data[0]
  for i in 1.. Float {
  let n = m.data.length()
  if n == 0 {
    return 0.0F
  }
  let mut mx = m.data[0]
  for i in 1.. mx { mx = m.data[i] }
  }
  mx
}

///|
/// Compute the firing rate (Hz) within a sub-interval `[t_start, t_end]`.
/// Returns `count_spikes_interval(m, t_start, t_end) / duration * 1000.0F`.
/// Returns 0.0F if the interval is empty or duration is zero.
pub fn Monitor::firing_rate_interval(
  m : Monitor,
  t_start : Float,
  t_end : Float,
) -> Float {
  let duration = t_end - t_start
  if duration <= 0.0F {
    return 0.0F
  }
  let n_spikes = m.count_spikes_interval(t_start, t_end)
  Float::from_int(n_spikes) * 1000.0F / duration
}

///|
/// Return the spike times (in ms) as an array of timestamps at which
/// the monitored neuron fired (rising-edge transitions in the fire
/// buffer). Returns an empty array if no spikes. Mirrors Julia's
/// `spiketimes(pop, neuron)` API at the per-neuron level.
pub fn Monitor::spike_times(m : Monitor) -> Array[Float] {
  let n = m.data.length()
  let n_t = m.times.length()
  let lim = if n < n_t { n } else { n_t }
  let result : Array[Float] = []
  let mut prev = 0.0F
  for i in 0..= 0.5F {
      result.push(m.times[i])
    }
    prev = cur
  }
  result
}

///|
/// Print a one-line summary of the monitor buffer (min/max/mean).
pub fn Monitor::dump_summary(m : Monitor) -> Unit {
  let n = m.data.length()
  if n == 0 {
    println("Monitor[\{m.sym}] (empty)")
    return
  }
  let mut mn = m.data[0]
  let mut mx = m.data[0]
  let mut sum = 0.0F
  for i in 0.. mx { mx = v }
    sum = sum + v
  }
  let mean = sum / Float::from_int(n)
  println("Monitor[\{m.sym}] n=" + n.to_string() +
          " min=" + mn.to_string() +
          " max=" + mx.to_string() +
          " mean=" + mean.to_string())
}

///|
/// Dump the monitor buffer as CSV to stdout: `time,value` lines.
/// Useful for piping to a Python plotting script.
pub fn Monitor::dump_csv_stdout(m : Monitor) -> Unit {
  let n = m.data.length()
  let n_t = m.times.length()
  let lim = if n < n_t { n } else { n_t }
  for i in 0.. Float {
  if m.times.length() < 2 {
    return 0.0F
  }
  m.times[m.times.length() - 1] - m.times[0]
}

///|
/// Print a text-mode ASCII plot of the monitor buffer. Each
/// data point becomes a single character placed in a column at the
/// appropriate vertical position. Default width is 80 columns
/// (down-sampled if buffer is larger); default height is 15 rows.
///
/// `width` and `height` can be passed to override the canvas size.
/// The plot is rendered with the value axis on the left (max at top,
/// min at bottom) and the time axis on the x-axis (oldest at left,
/// newest at right). Spike markers (*) are added for fire monitors
/// where the value crosses 0.5.
pub fn Monitor::ascii_plot(
  m : Monitor,
  width? : Int = 80,
  height? : Int = 15,
) -> Unit {
  let n = m.data.length()
  if n == 0 {
    println("Monitor[\{m.sym}] (empty)")
    return
  }
  // Find min/max of data.
  let mut mn = m.data[0]
  let mut mx = m.data[0]
  for i in 0.. mx { mx = v }
  }
  // Avoid degenerate range.
  let vrange = if mx - mn < 1.0e-6F { 1.0e-6F } else { mx - mn }
  let n_cols = if width > n { n } else { width }
  let n_rows = height
  // Down-sample: pick `n_cols` indices evenly spaced across [0, n).
  let col_to_idx : Array[Int] = Array::make(n_cols, 0)
  if n_cols == 1 {
    col_to_idx[0] = 0
  } else {
    for c in 0..= n_rows {
      n_rows - 1
    } else {
      row_from_top
    }
    // Place a character. Use '*' for fire spikes (>=0.5), '.' for others.
    let ch = if m.sym == "fire" {
      if v >= 0.5F { '*' } else { '.' }
    } else {
      '*'
    }
    let row_str = canvas[r]
    let new_row = row_int_set_char(row_str, c + row_init.length(), ch)
    canvas[r] = new_row
  }
  // Add value-axis labels at top, middle, bottom.
  // Render header (max).
  let max_label = format_axis_label(mx)
  let mid_label = format_axis_label((mx + mn) / 2.0F)
  let min_label = format_axis_label(mn)
  // Find the longest axis label so we can left-align.
  let label_width = {
    let a = max_label.length()
    let b = mid_label.length()
    let c = min_label.length()
    let m = if a > b { a } else { b }
    if m > c { m } else { c }
  }
  // Print from top row to bottom.
  for r in 0.. label.length() {
      label_width - label.length()
    } else {
      0
    }
    let padded = String::make(pad_count, ' ')
    println(padded + label + " |" + canvas[r])
  }
  // Footer: x-axis labels (start/end times).
  let n_t = m.times.length()
  if n_t >= 2 {
    let t_start = m.times[0]
    let t_end = m.times[n_t - 1]
    let total_chars = row_init.length() + n_cols + max_label.length() + 3
    let pad_str = String::make(total_chars - 4, ' ')
    println(pad_str + "t=" + t_start.to_string() + " → " + t_end.to_string())
  }
  let _ = m.duration()  // ensure method accessible
}

///|
/// Format a number with up to 6 significant digits for axis labels.
fn format_axis_label(v : Float) -> String {
  // Simple format: round to 4 decimal places, then strip trailing zeros.
  let scaled = v * 10000.0F
  let rounded = scaled.round().to_int()
  let scaled_back = Float::from_int(rounded) / 10000.0F
  scaled_back.to_string()
}

///|
/// Replace the character at position `pos` in `s` with `ch`.
/// Returns the new string. Builds via StringBuilder.
fn row_int_set_char(s : String, pos : Int, ch : Char) -> String {
  let sb = StringBuilder::new(size_hint=s.length())
  let len = s.length()
  // Write prefix [0, pos), then ch, then suffix [pos+1, len).
  let prefix_end = if pos < len { pos } else { len }
  if prefix_end > 0 {
    // Use String::substring to extract the prefix (0..prefix_end).
    let prefix = String::make(prefix_end, ' ')
    // Easier: just iterate by char position.
    let mut k = 0
    while k < prefix_end {
      // Hack: pull chars one at a time using string view.
      // Actually, MoonBit doesn't have s[k] indexing on String; use views.
      // For simplicity, fall back to single-char writes.
      sb.write_char(s[k].unsafe_to_char())
      k = k + 1
    }
    let _ = prefix
  }
  sb.write_char(ch)
  if pos + 1 < len {
    let mut k = pos + 1
    while k < len {
      sb.write_char(s[k].unsafe_to_char())
      k = k + 1
    }
  }
  sb.to_string()
}