// Copyright (C) 2026 alto555
// SPDX-License-Identifier: GPL-2.0-only

//! Headless plotter — the render seam's proof, and a useful tool on its own.
//!
//! Producing a PNG from a raw file with no GUI is exactly what you want after
//! a batch regression run, and it forces `wave-render` to stay free of any
//! dependency on files, readers or toolkits.
//!
//! Usage:
//!   wave-plot <file> -o out.png [options]
//!     -s <spec>        add a trace to the current pane.  `v(out)` plots the
//!                      variable; `db(v(out))`, `ph(...)`, `cph(...)`,
//!                      `mag(...)`, `re(...)`, `im(...)` select a projection
//!                      of a complex variable
//!     --sub <a> <b>    add a derived a-b trace (exercises wave-expr)
//!     --pane           start a new stacked pane, sharing the X axis
//!     --bode           complex files: db in one pane, unwrapped phase below
//!     --cursor <x>     vertical marker; two of them print the delta
//!     --hcursor <y>    horizontal marker on the current pane
//!     --from/--to <x>  X range (default: the whole sweep)
//!     --size <WxH>     image size, default 1400x800
//!     --title <text>

use std::sync::Arc;
use std::time::Instant;

use wave_core::{Component, Curve, Span, Unit};
use wave_expr::{BinOpCurve, Op};
use wave_render::{Pane, PlotSpec, Trace, PALETTE};
use wave_store::{IndexCfg, WaveSource};

fn main() {
    if let Err(e) = run() {
        eprintln!("wave-plot: {}", e);
        std::process::exit(1);
    }
}

fn parse_spec(s: &str) -> (Component, &str) {
    for (p, c) in [
        ("mag(", Component::Magnitude),
        ("db(", Component::Db),
        ("cph(", Component::PhaseUnwrapped),
        ("ph(", Component::Phase),
        ("re(", Component::Real),
        ("im(", Component::Imag),
    ] {
        if s.len() > p.len() && s[..p.len()].eq_ignore_ascii_case(p) && s.ends_with(')') {
            return (c, &s[p.len()..s.len() - 1]);
        }
    }
    (Component::Real, s)
}

const USAGE: &str = "\
wave-plot - render SPICE raw files to a PNG, with no window

  wave-plot <file.raw> -o out.png [options]

Needs no display, so it works over ssh and in batch scripts.  Same renderer
as the interactive viewer, so the plots are identical.

  -o <file.png>      output image (default plot.png)
  -s <spec>          add a trace to the current pane.  `v(out)` plots the
                     variable; on .ac files `db(v(out))`, `ph(...)`,
                     `cph(...)`, `mag(...)`, `re(...)`, `im(...)` select a
                     projection of the stored complex value
  --sub <a> <b>      add a derived a-b trace
  --pane             start a new stacked pane, sharing the X axis
  --bode             .ac files: db in one pane, unwrapped phase below
  --cursor <x>       vertical marker; two of them print the delta
  --hcursor <y>      horizontal marker on the current pane
  --from <x> --to <x>  X range (default: the whole sweep)
  --size <WxH>       image size, default 1400x800
  --title <text>     title above the plot
  --font-size <px>   text size; baked sizes are 13, 16, 20, 26
  -h, --help         this text

Examples
  wave-plot out.raw -o a.png -s 'v(xafe.xi0.outp)' -s 'v(xafe.xi0.outn)'
  wave-plot out.raw -o z.png -s 'v(clk)' --from 4.99e-5 --to 5.01e-5
  wave-plot ac.raw  -o b.png -s 'v(out)' --bode
  for f in sweep_*.raw; do wave-plot $f -o ${f%.raw}.png -s 'v(out)'; done

waveview and wave-plot.  Copyright (C) 2026 alto555.
Free software under the GNU GPL version 2; see the LICENSE file.
There is NO WARRANTY, to the extent permitted by law.
";

fn run() -> Result<(), String> {
    let args: Vec<String> = std::env::args().skip(1).collect();
    if args.is_empty()
        || args[0] == "-h"
        || args[0] == "--help"
    {
        print!("{}", USAGE);
        return Ok(());
    }
    let path = args[0].clone();

    let mut out = "plot.png".to_string();
    let mut w = 1400usize;
    let mut h = 800usize;
    let mut title: Option<String> = None;
    let mut from: Option<f64> = None;
    let mut to: Option<f64> = None;
    let mut cursors: Vec<f64> = Vec::new();
    let mut bode = false;
    // Trace requests grouped by pane; resolved after the source is open.
    let mut panes_req: Vec<Vec<Req>> = vec![Vec::new()];
    let mut hcur: Vec<Vec<f64>> = vec![Vec::new()];

    enum Req {
        Sig(String),
        Sub(String, String),
    }

    let mut i = 1usize;
    while i < args.len() {
        let a = args[i].as_str();
        let next = |i: &mut usize| -> Result<String, String> {
            *i += 1;
            args.get(*i).cloned().ok_or_else(|| format!("{} needs a value", a))
        };
        match a {
            "-o" => out = next(&mut i)?,
            "--title" => title = Some(next(&mut i)?),
            "--font-size" => {
                let v = next(&mut i)?;
                if let Ok(px) = v.parse::<usize>() {
                    let got = wave_render::font::set_size(px);
                    if got != px {
                        eprintln!(
                            "wave-plot: font size {} not baked, using {} (available: {:?})",
                            px,
                            got,
                            wave_render::font::sizes()
                        );
                    }
                }
            }
            "-s" => {
                let v = next(&mut i)?;
                panes_req.last_mut().unwrap().push(Req::Sig(v));
            }
            "--sub" => {
                let x = next(&mut i)?;
                let y = next(&mut i)?;
                panes_req.last_mut().unwrap().push(Req::Sub(x, y));
            }
            "--pane" => {
                panes_req.push(Vec::new());
                hcur.push(Vec::new());
            }
            "--bode" => bode = true,
            "--cursor" => cursors.push(parse_num(&next(&mut i)?)?),
            "--hcursor" => {
                let v = parse_num(&next(&mut i)?)?;
                hcur.last_mut().unwrap().push(v);
            }
            "--from" => from = Some(parse_num(&next(&mut i)?)?),
            "--to" => to = Some(parse_num(&next(&mut i)?)?),
            "--size" => {
                let v = next(&mut i)?;
                let mut it = v.split('x');
                w = it.next().and_then(|s| s.parse().ok()).unwrap_or(w);
                h = it.next().and_then(|s| s.parse().ok()).unwrap_or(h);
            }
            other => return Err(format!("unknown option {}", other)),
        }
        i += 1;
    }

    let t_open = Instant::now();
    let src = WaveSource::open_raw(&path, IndexCfg::default(), true).map_err(|e| e.to_string())?;
    let open_ms = t_open.elapsed().as_secs_f64() * 1e3;

    // --bode expands each requested signal into two panes.
    if bode {
        let names: Vec<String> = panes_req
            .iter()
            .flatten()
            .filter_map(|r| match r {
                Req::Sig(s) => Some(s.clone()),
                _ => None,
            })
            .collect();
        panes_req = vec![
            names.iter().map(|n| Req::Sig(format!("db({})", n))).collect(),
            names.iter().map(|n| Req::Sig(format!("cph({})", n))).collect(),
        ];
        hcur = vec![vec![-3.0], Vec::new()];
    }

    // Resolve every request to a curve, keeping them alive for the render.
    let mut owned: Vec<Arc<dyn Curve>> = Vec::new();
    let mut pane_idx: Vec<Vec<usize>> = Vec::new();
    for reqs in &panes_req {
        let mut idx = Vec::new();
        for r in reqs {
            let c: Arc<dyn Curve> = match r {
                Req::Sig(spec) => {
                    let (comp, base) = parse_spec(spec);
                    let v = src
                        .find(base)
                        .ok_or_else(|| format!("no variable named {}", base))?;
                    src.curve_component(v, comp).ok_or_else(|| {
                        format!(
                            "{:?} is not available for this plot's data (nonlinear component on reduced real data)",
                            comp
                        )
                    })?
                }
                Req::Sub(x, y) => {
                    let a = src.find(x).ok_or_else(|| format!("no variable {}", x))?;
                    let b = src.find(y).ok_or_else(|| format!("no variable {}", y))?;
                    Arc::new(BinOpCurve::new(Op::Sub, src.curve(a), src.curve(b)))
                }
            };
            idx.push(owned.len());
            owned.push(c);
        }
        pane_idx.push(idx);
    }
    if owned.is_empty() {
        // Nothing asked for: show the first few signals, which is the useful
        // default for "just show me what is in this file".
        let n = src.reader().n_vars().min(5);
        for v in 1..n {
            owned.push(src.curve(v));
        }
        pane_idx = vec![(0..owned.len()).collect()];
    }

    let full = owned[0].x_span();
    let span = Span::new(from.unwrap_or(full.x0), to.unwrap_or(full.x1));
    let x_scale = owned[0].x_scale();
    let x_label = match owned[0].x_unit() {
        Unit::Hertz => "frequency (Hz)",
        Unit::Second => "time (s)",
        _ => "x",
    };

    let panes: Vec<Pane> = pane_idx
        .iter()
        .enumerate()
        .filter(|(_, idx)| !idx.is_empty())
        .map(|(pi, idx)| {
            let traces = idx
                .iter()
                .enumerate()
                .map(|(k, &oi)| Trace {
                    curve: &*owned[oi],
                    color: PALETTE[(k + pi) % PALETTE.len()],
                    label: None,
                })
                .collect();
            let mut p = Pane::new(traces);
            p.h_cursors = hcur.get(pi).cloned().unwrap_or_default();
            p
        })
        .collect();

    let spec = PlotSpec {
        width: w,
        height: h,
        title: title.unwrap_or_else(|| {
            format!("{}  -  {}", src.reader().plot_name(), file_name(&path))
        }),
        span,
        x_scale,
        x_label: x_label.to_string(),
        x_unit: owned[0].x_unit(),
        show_deltas: true,
        panes,
        cursors: cursors.iter().copied().enumerate().collect(),
    };

    let t = Instant::now();
    let canvas = wave_render::render(&spec).canvas;
    let render_ms = t.elapsed().as_secs_f64() * 1e3;

    let t = Instant::now();
    let png = canvas.to_png();
    let encode_ms = t.elapsed().as_secs_f64() * 1e3;
    std::fs::write(&out, &png).map_err(|e| e.to_string())?;

    let n_traces: usize = spec.panes.iter().map(|p| p.traces.len()).sum();
    println!(
        "{}  {}x{}  {} traces in {} pane(s)  x={:?}",
        out,
        w,
        h,
        n_traces,
        spec.panes.len(),
        x_scale
    );
    println!(
        "open {:.1} ms   render {:.1} ms   png encode {:.1} ms   {} KB",
        open_ms,
        render_ms,
        encode_ms,
        png.len() / 1024
    );
    Ok(())
}

fn file_name(p: &str) -> &str {
    p.rsplit('/').next().unwrap_or(p)
}

fn parse_num(s: &str) -> Result<f64, String> {
    s.parse::<f64>().map_err(|_| format!("bad number: {}", s))
}
