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

//! Indexed storage: turns a [`WaveReader`] into [`Curve`]s.
//!
//! The store is a shared service, deliberately not owned by a widget.  One
//! store, N views — which is what makes "several plot windows on the same
//! data" cost nothing, and what stops a second window from re-reading a
//! multi-gigabyte file.
//!
//! Two storage strategies, chosen by the file rather than by configuration:
//!
//! * **Reduced** — real data (`.tran`, `.dc`).  Gigabytes, one scalar per
//!   sample, so build a min/max pyramid once and never read in bulk again.
//! * **Exact** — complex data (`.ac`).  Small, but each sample backs several
//!   different curves (magnitude, phase, dB, real, imag), and those
//!   projections are not monotone in the sample, so they must be computed
//!   from samples *before* any reduction.  See [`complex`].

pub mod complex;
pub mod index;

use std::path::Path;
use std::sync::Arc;

use wave_core::{Component, Curve, CurveId, Poll, Reduced, Span, Unit, ValueKind, XScale};
use wave_io::{RawReader, WaveReader};

pub use complex::{ComplexCurve, ComplexStore};
pub use index::{Index, IndexCfg, RowCache};

enum Backend {
    Reduced {
        index: Arc<Index>,
        /// Shared by every curve of this file, so one repaint reads any exact
        /// window once rather than once per curve.
        cache: Arc<RowCache>,
        from_sidecar: bool,
    },
    Exact {
        store: Arc<ComplexStore>,
    },
}

/// An opened file plus whatever storage its analysis type calls for.
pub struct WaveSource {
    reader: Arc<dyn WaveReader>,
    backend: Backend,
    path_hash: u64,
}

impl WaveSource {
    /// Open a raw file, reusing the sidecar index when it is present and not
    /// older than the data.  Complex plots ignore the sidecar: they are held
    /// exactly, and rebuilding costs milliseconds.
    pub fn open_raw<P: AsRef<Path>>(
        path: P,
        cfg: IndexCfg,
        use_sidecar: bool,
    ) -> std::io::Result<WaveSource> {
        let path = path.as_ref();
        let reader: Arc<dyn WaveReader> = Arc::new(RawReader::open(path)?);
        Self::from_reader(reader, path, cfg, use_sidecar)
    }

    pub fn from_reader<P: AsRef<Path>>(
        reader: Arc<dyn WaveReader>,
        path: P,
        cfg: IndexCfg,
        use_sidecar: bool,
    ) -> std::io::Result<WaveSource> {
        let path = path.as_ref();
        let path_hash = fnv1a(path.to_string_lossy().as_bytes());

        if reader.kind() == ValueKind::Complex {
            let store = ComplexStore::build(&reader)?;
            return Ok(WaveSource {
                reader,
                backend: Backend::Exact {
                    store: Arc::new(store),
                },
                path_hash,
            });
        }

        let side = sidecar_path(path);
        let mut from_sidecar = false;
        let index = if use_sidecar && sidecar_is_fresh(path, &side) {
            match Index::load(&side) {
                Ok(ix) if ix.n_points == reader.n_points() => {
                    from_sidecar = true;
                    ix
                }
                _ => Index::build(&reader, cfg)?,
            }
        } else {
            Index::build(&reader, cfg)?
        };
        if use_sidecar && !from_sidecar {
            // Best effort: an unwritable directory must not fail the open.
            let _ = index.save(&side);
        }
        Ok(WaveSource {
            reader,
            backend: Backend::Reduced {
                index: Arc::new(index),
                cache: Arc::new(RowCache::new()),
                from_sidecar,
            },
            path_hash,
        })
    }

    pub fn reader(&self) -> &Arc<dyn WaveReader> {
        &self.reader
    }

    pub fn kind(&self) -> ValueKind {
        self.reader.kind()
    }

    pub fn x_scale(&self) -> XScale {
        self.reader.x_scale()
    }

    pub fn index(&self) -> Option<&Arc<Index>> {
        match &self.backend {
            Backend::Reduced { index, .. } => Some(index),
            Backend::Exact { .. } => None,
        }
    }

    pub fn cache(&self) -> Option<&Arc<RowCache>> {
        match &self.backend {
            Backend::Reduced { cache, .. } => Some(cache),
            Backend::Exact { .. } => None,
        }
    }

    pub fn complex_store(&self) -> Option<&Arc<ComplexStore>> {
        match &self.backend {
            Backend::Exact { store } => Some(store),
            Backend::Reduced { .. } => None,
        }
    }

    pub fn loaded_from_sidecar(&self) -> bool {
        matches!(self.backend, Backend::Reduced { from_sidecar: true, .. })
    }

    /// Resident bytes of whatever this source built.
    pub fn size_bytes(&self) -> usize {
        match &self.backend {
            Backend::Reduced { index, .. } => index.size_bytes(),
            Backend::Exact { store } => store.size_bytes(),
        }
    }

    pub fn find(&self, name: &str) -> Option<usize> {
        self.reader
            .variables()
            .iter()
            .position(|v| v.name.eq_ignore_ascii_case(name))
    }

    /// Full X extent of the data, independent of any curve — needed to set
    /// the initial view when nothing is plotted yet.
    pub fn x_span(&self) -> Span {
        match &self.backend {
            Backend::Reduced { index, .. } => index.x_span(),
            Backend::Exact { store } => store.x_span(),
        }
    }

    /// Rows covered by `span`, whichever backend is in use.
    pub fn row_range(&self, span: Span) -> (usize, usize) {
        match &self.backend {
            Backend::Reduced { index, .. } => index.row_range(span),
            Backend::Exact { store } => store.row_range(span),
        }
    }

    /// The default curve for a variable: the stored scalar for real data,
    /// magnitude for complex.
    pub fn curve(&self, var: usize) -> Arc<dyn Curve> {
        match &self.backend {
            Backend::Exact { .. } => self
                .curve_component(var, Component::Magnitude)
                .expect("magnitude is always available for complex data"),
            Backend::Reduced { index, cache, .. } => {
                let vars = self.reader.variables();
                let v = &vars[var];
                Arc::new(IndexedCurve {
                    id: self.curve_id(var, Component::Real),
                    name: v.name.clone(),
                    unit: v.unit,
                    x_unit: vars[index.x_var].unit,
                    x_scale: self.reader.x_scale(),
                    kind: self.reader.kind(),
                    var,
                    reader: Arc::clone(&self.reader),
                    index: Arc::clone(index),
                    cache: Arc::clone(cache),
                })
            }
            #[allow(unreachable_patterns)]
            _ => unreachable!(),
        }
    }

    /// A specific projection of a variable.
    ///
    /// Returns `None` for real data with a nonlinear component: `|v|` and
    /// `20log10|v|` cannot be derived from a stored min/max envelope, because
    /// `min(|v|) != |min(v)|` once a bucket straddles zero.  Serving those
    /// would need their own pyramids; refusing is better than being quietly
    /// wrong at every zoom level except the deepest.
    pub fn curve_component(&self, var: usize, comp: Component) -> Option<Arc<dyn Curve>> {
        let vars = self.reader.variables();
        if var >= vars.len() {
            return None;
        }
        match &self.backend {
            Backend::Exact { store } => Some(Arc::new(ComplexCurve::new(
                self.curve_id(var, comp),
                &vars[var].name,
                vars[var].unit,
                vars[0].unit,
                var,
                comp,
                Arc::clone(store),
            ))),
            Backend::Reduced { .. } => match comp {
                Component::Real => Some(self.curve(var)),
                _ => None,
            },
        }
    }

    /// Every projection worth plotting for this file — one curve per variable
    /// for real data, the full Bode set for complex.
    pub fn curves(&self) -> Vec<Arc<dyn Curve>> {
        (0..self.reader.n_vars()).map(|v| self.curve(v)).collect()
    }

    /// Batched reduction for a pane full of traces.  See
    /// [`Index::reduce_many`] for why this is not just a loop.
    pub fn reduce_many(&self, vars: &[usize], span: Span, n_buckets: usize) -> Vec<Reduced> {
        match &self.backend {
            Backend::Reduced { index, cache, .. } => {
                index.reduce_many(vars, span, n_buckets, &*self.reader, cache)
            }
            Backend::Exact { store } => vars
                .iter()
                .map(|v| store.reduce(*v, Component::Magnitude, span, n_buckets))
                .collect(),
        }
    }

    fn curve_id(&self, var: usize, comp: Component) -> CurveId {
        self.path_hash
            ^ (var as u64).wrapping_mul(0x9E3779B97F4A7C15)
            ^ (comp as u64).wrapping_mul(0xD1B54A32D192ED03)
    }
}

/// A file-backed curve served from the min/max pyramid.  Indistinguishable,
/// from above, from an expression result or a complex projection.
pub struct IndexedCurve {
    id: CurveId,
    name: String,
    unit: Unit,
    x_unit: Unit,
    x_scale: XScale,
    kind: ValueKind,
    var: usize,
    reader: Arc<dyn WaveReader>,
    index: Arc<Index>,
    cache: Arc<RowCache>,
}

impl Curve for IndexedCurve {
    fn id(&self) -> CurveId {
        self.id
    }
    fn name(&self) -> &str {
        &self.name
    }
    fn unit(&self) -> Unit {
        self.unit
    }
    fn x_unit(&self) -> Unit {
        self.x_unit
    }
    fn kind(&self) -> ValueKind {
        self.kind
    }
    fn x_span(&self) -> Span {
        self.index.x_span()
    }
    fn x_scale(&self) -> XScale {
        self.x_scale
    }

    fn reduce(&self, span: Span, n_buckets: usize) -> Poll<Reduced> {
        // Synchronous today because every path is either RAM-resident index
        // or a bounded read.  The signature is already `Poll` so that moving
        // the exact path onto a worker queue later changes this function and
        // nothing above it.
        Poll::Ready(self.index.reduce_var(
            self.var,
            span,
            n_buckets,
            &*self.reader,
            &self.cache,
        ))
    }

    fn value_at(&self, x: f64) -> Poll<Option<f64>> {
        Poll::Ready(
            self.index
                .value_at(self.var, x, &*self.reader)
                .unwrap_or(None),
        )
    }

    fn for_each_sample(&self, span: Span, f: &mut dyn FnMut(f64, f64)) -> Poll<usize> {
        let (r0, r1) = self.index.row_range(span);
        let rl = self.reader.row_len();
        let comps = self.reader.components();
        let x_var = self.index.x_var;
        // Bounded working set: stream 4 MB of file at a time regardless of
        // how wide the run is.
        let block = ((4 << 20) / (rl * 8).max(1)).max(1);
        let mut buf = vec![0f64; block * rl];
        let mut n = 0usize;
        let mut row = r0;
        while row < r1 {
            let want = block.min(r1 - row);
            let got = match self.reader.read_rows(row, want, &mut buf) {
                Ok(g) => g,
                Err(_) => return Poll::Ready(n),
            };
            if got == 0 {
                break;
            }
            for r in 0..got {
                let base = r * rl;
                let x = if comps == 2 { buf[base + x_var * 2] } else { buf[base + x_var] };
                // row_range is bucket-quantised, so trim to the real span.
                if x < span.x0 || x > span.x1 {
                    continue;
                }
                let v = if comps == 2 {
                    buf[base + self.var * 2].hypot(buf[base + self.var * 2 + 1])
                } else {
                    buf[base + self.var]
                };
                f(x, v);
                n += 1;
            }
            row += got;
        }
        Poll::Ready(n)
    }
}

fn sidecar_path(path: &Path) -> std::path::PathBuf {
    let mut s = path.as_os_str().to_os_string();
    s.push(".wvidx");
    std::path::PathBuf::from(s)
}

fn sidecar_is_fresh(data: &Path, side: &Path) -> bool {
    let (dm, sm) = match (std::fs::metadata(data), std::fs::metadata(side)) {
        (Ok(d), Ok(s)) => (d, s),
        _ => return false,
    };
    match (dm.modified(), sm.modified()) {
        (Ok(d), Ok(s)) => s >= d,
        _ => false,
    }
}

fn fnv1a(bytes: &[u8]) -> u64 {
    let mut h: u64 = 0xcbf29ce484222325;
    for b in bytes {
        h ^= *b as u64;
        h = h.wrapping_mul(0x100000001b3);
    }
    h
}
