///|
/// CLI errors.
suberror CliError {
  CliError(String)
}

///|
/// Parsed command line options.
struct Options {
  wave : String
  freq : Double
  rate : Double
  duration : Double
  seed : UInt64
  peaks : Int32
  window_type : @moonbit-dsp.WindowType?
  lowpass : Double?
  stats : Bool
  dominant : Bool
}

///|
fn help_text() -> String {
  let text =
    #|moonbit-dsp-cli - generate a test signal and analyze it with moonbit-dsp
    #|
    #|USAGE
    #|  moon run cmd/main -- [OPTIONS]
    #|
    #|OPTIONS
    #|  --wave        sine | square | triangle | sawtooth | noise (default sine)
    #|  --freq          signal frequency (default 50)
    #|  --rate          sample rate (default 8000)
    #|  --duration     signal duration in seconds (default 1)
    #|  --seed         LCG seed for the noise wave (default 42)
    #|  --peaks          number of strongest spectral peaks to print (default 3)
    #|  --window      hann | hamming | blackman | rectangular (default none)
    #|  --lowpass       FIR low-pass at normalized cutoff in (0, 0.5)
    #|  --stats             print mean / rms / max-abs statistics
    #|  --no-dominant       do not print the dominant frequency
    #|  --help              show this help
  text
}

///|
fn require_next(
  args : ArrayView[String],
  index : Int32,
  flag : String,
) -> String raise CliError {
  if index + 1 >= args.length() {
    raise CliError("Missing value after \{flag}")
  }
  args[index + 1]
}

///|
fn parse_number(
  args : ArrayView[String],
  index : Int32,
  flag : String,
) -> Double raise CliError {
  @string.parse_double(require_next(args, index, flag)) catch {
    _ => raise CliError("Invalid numeric value for \{flag}")
  }
}

///|
fn parse_int_arg(
  args : ArrayView[String],
  index : Int32,
  flag : String,
) -> Int32 raise CliError {
  @string.parse_int(require_next(args, index, flag)) catch {
    _ => raise CliError("Invalid integer value for \{flag}")
  }
}

///|
fn parse_window(s : String) -> @moonbit-dsp.WindowType? raise CliError {
  match s {
    "hann" => Some(@moonbit-dsp.WindowType::Hann)
    "hamming" => Some(@moonbit-dsp.WindowType::Hamming)
    "blackman" => Some(@moonbit-dsp.WindowType::Blackman)
    "rectangular" => Some(@moonbit-dsp.WindowType::Rectangular)
    "none" => None
    _ => raise CliError("Unknown window `\{s}`")
  }
}

///|
fn parse_options(args : ArrayView[String]) -> Options? raise CliError {
  let mut wave = "sine"
  let mut freq = 50.0
  let mut rate = 8000.0
  let mut duration = 1.0
  let mut seed = 42UL
  let mut peaks = 3
  let mut window_type : @moonbit-dsp.WindowType? = None
  let mut lowpass : Double? = None
  let mut stats = false
  let mut dominant = true
  let mut index = 1
  while index < args.length() {
    let arg = args[index]
    match arg {
      "--wave" => {
        wave = require_next(args, index, arg)
        index += 2
      }
      "--freq" => {
        freq = parse_number(args, index, arg)
        index += 2
      }
      "--rate" => {
        rate = parse_number(args, index, arg)
        index += 2
      }
      "--duration" => {
        duration = parse_number(args, index, arg)
        index += 2
      }
      "--seed" => {
        seed = parse_int_arg(args, index, arg).to_uint64()
        index += 2
      }
      "--peaks" => {
        peaks = parse_int_arg(args, index, arg)
        index += 2
      }
      "--window" => {
        window_type = parse_window(require_next(args, index, arg))
        index += 2
      }
      "--lowpass" => {
        lowpass = Some(parse_number(args, index, arg))
        index += 2
      }
      "--stats" => {
        stats = true
        index += 1
      }
      "--no-dominant" => {
        dominant = false
        index += 1
      }
      "--help" | "-h" => {
        println(help_text())
        return None
      }
      _ => raise CliError("Unknown option `\{arg}`; run with --help for usage")
    }
  }
  if rate <= 0.0 || duration <= 0.0 {
    raise CliError("Sample rate and duration must be positive")
  }
  match lowpass {
    Some(cutoff) =>
      if cutoff <= 0.0 || cutoff >= 0.5 {
        raise CliError("Low-pass cutoff must be in (0, 0.5)")
      }
    None => ()
  }
  Some({
    wave,
    freq,
    rate,
    duration,
    seed,
    peaks,
    window_type,
    lowpass,
    stats,
    dominant,
  })
}

///|
fn generate_signal(
  kind : String,
  n : Int32,
  freq : Double,
  rate : Double,
  seed : UInt64,
) -> Array[Double] raise CliError {
  match kind {
    "sine" => @moonbit-dsp.sine_wave(n, freq, rate)
    "square" => @moonbit-dsp.square_wave(n, freq, rate)
    "triangle" => @moonbit-dsp.triangle_wave(n, freq, rate)
    "sawtooth" => @moonbit-dsp.sawtooth_wave(n, freq, rate)
    "noise" => @moonbit-dsp.white_noise(n, seed)
    _ => raise CliError("Unknown wave type `\{kind}`")
  }
}

///|
fn run(args : ArrayView[String]) -> Int32 raise {
  let options = match parse_options(args) {
    Some(options) => options
    None => return 0
  }
  let n = @math.floor(options.duration * options.rate).to_int()
  if n <= 0 {
    raise CliError("Duration too short for the sample rate")
  }
  let signal = generate_signal(
    options.wave,
    n,
    options.freq,
    options.rate,
    options.seed,
  )
  println(
    "Signal: \{options.wave} \{options.freq.to_string()} Hz @ \{options.rate.to_string()} Hz, \{options.duration.to_string()} s (\{n} samples)",
  )
  let filtered = match options.lowpass {
    Some(cutoff) => {
      println("[filter] FIR low-pass, normalized cutoff \{cutoff.to_string()}")
      let h = @moonbit-dsp.fir_lowpass_coeffs(
        cutoff,
        65,
        @moonbit-dsp.WindowType::Hamming,
      )
      @moonbit-dsp.fir_filter(signal, h)
    }
    None => signal
  }
  let analyzed = match options.window_type {
    Some(window_type) => @moonbit-dsp.apply_window(filtered, window_type)
    None => filtered
  }
  let spectrum = @moonbit-dsp.magnitude_spectrum(analyzed)
  let n_fft = @moonbit-dsp.next_power_of_two(analyzed.length())
  let resolution = options.rate / n_fft.to_double()
  println(
    "Spectrum: N=\{n_fft}, frequency resolution \{resolution.to_string()} Hz",
  )
  let tops = @moonbit-dsp.top_k(spectrum, options.peaks)
  println("Top \{tops.length()} spectral peaks:")
  for (idx, mag) in tops {
    let freq = idx.to_double() * resolution
    println(
      "  bin \{idx}  \{freq.to_string()} Hz  magnitude \{mag.to_string()}",
    )
  }
  if options.dominant {
    let (freq, mag) = @moonbit-dsp.dominant_frequency(analyzed, options.rate)
    println(
      "Dominant frequency: \{freq.to_string()} Hz (magnitude \{mag.to_string()})",
    )
  }
  if options.stats {
    println(
      "Stats: mean \{@moonbit-dsp.mean(analyzed).to_string()}, rms \{@moonbit-dsp.rms(analyzed).to_string()}, max-abs \{@moonbit-dsp.max_abs(analyzed).to_string()}",
    )
  }
  0
}

///|
fn main {
  let _ = run(@env.args()) catch {
    error => {
      eprintln("dsp-cli error: \{error}")
      0
    }
  }
}