// Hand-maintained: NOT generated by mmtorust.
//
// Pure-Rust port of the parts of
// `OMCompiler/SimulationRuntime/c/util/read_matlab4.c` that the compiler's
// `SimulationResults` package needs: opening an OpenModelica MATLAB v4 result
// file (`Aclass`/`name`/`description`/`dataInfo`/`data_1`/`data_2`), looking up
// variables, and reading their trajectories (with linear interpolation).
//
// Both storage layouts emitted by the runtime are supported: `binTrans` (the
// default, data_2 stored variable-major) and `binNormal` (time-major, as some
// Dymola-produced reference files use). Comparison/diff readers (CSV, PLT) are
// not ported here; see SimulationResults.rs.

#![allow(non_snake_case)]

use std::cmp::Ordering;
use std::io::{Read, Seek, SeekFrom};

// The result file is read lazily via the fs facade's `Read + Seek` handle: a real
// `std::fs::File` natively, an in-memory `Cursor` on the web target.
type Src = openmodelica_wasi::fs::Reader;

/// One entry from the `name`/`description`/`dataInfo` metadata tables.
#[derive(Clone)]
pub struct MatVariable {
    pub name: String,
    pub descr: String,
    pub isParam: bool,
    /// 1-based index into data_1 (params) or data_2 (vars); negative selects the
    /// sign-inverted alias of that column.
    pub index: i32,
}

/// An opened MATLAB v4 result file.
pub struct MatReader {
    file: Src,
    /// Variable/parameter descriptors, sorted by [`iws_cmp`] for binary search.
    pub allInfo: Vec<MatVariable>,
    pub params: Vec<f64>,
    /// Number of parameter columns in data_1 (the C reader's `nparam`).
    pub nparam: usize,
    pub nvar: usize,
    pub nrows: usize,
    /// File offset (bytes) where the data_2 payload begins.
    var_offset: u64,
    /// data_2 stored variable-major (`binTrans`) vs. time-major (`binNormal`).
    binTrans: bool,
    /// True if data_2 is stored as IEEE double, false if single precision.
    doublePrecision: bool,
    /// data_2 loaded lazily and transposed to time-major order: `data2[i*nvar+v]`.
    data2: Option<Vec<f64>>,
    startTime: Option<f64>,
    stopTime: Option<f64>,
}

/// MATLAB v4 matrix header (5 little-endian u32 fields).
struct MHeader {
    ty: u32,
    mrows: u32,
    ncols: u32,
    imagf: u32,
    namelen: u32,
}

/// Decode a fixed-width MATLAB char field. The runtime stores names/descriptions
/// NUL-terminated (C-string semantics) and space-padded, so cut at the first NUL
/// and trim surrounding whitespace, matching the C reader + `trimWhitespace`.
fn fixed_str(bytes: &[u8]) -> String {
    let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
    String::from_utf8_lossy(&bytes[..end]).trim().to_string()
}

/// Compare two names ignoring ASCII whitespace, matching the C `strcmp_iws`.
/// The metadata is sorted/searched with this so that e.g. "a [1, 2]" and
/// "a[1,2]" compare equal.
fn iws_cmp(a: &str, b: &str) -> Ordering {
    let mut ai = a.bytes().peekable();
    let mut bi = b.bytes().peekable();
    loop {
        match (ai.peek().copied(), bi.peek().copied()) {
            (Some(ca), _) if ca.is_ascii_whitespace() => {
                ai.next();
            }
            (_, Some(cb)) if cb.is_ascii_whitespace() => {
                bi.next();
            }
            (Some(ca), Some(cb)) => {
                if ca != cb {
                    return ca.cmp(&cb);
                }
                ai.next();
                bi.next();
            }
            (None, None) => return Ordering::Equal,
            (Some(_), None) => return Ordering::Greater,
            (None, Some(_)) => return Ordering::Less,
        }
    }
}

fn mat_element_length(ty: i32) -> Option<usize> {
    let m = ty / 1000;
    let o = (ty % 1000) / 100;
    let p = (ty % 100) / 10;
    let t = ty % 10;
    if m != 0 {
        return None; // require IEEE little-endian
    }
    if o != 0 {
        return None;
    }
    if t == 1 && p == 0 {
        return Some(8);
    }
    if t == 1 && p != 5 {
        return None;
    }
    if t == 2 {
        return None; // sparse
    }
    match p {
        0 => Some(8),
        1 => Some(4),
        2 => Some(4),
        3 => Some(2),
        4 => Some(2),
        5 => Some(1),
        _ => None,
    }
}

/// A second reader handle: the caller parses metadata with one copy and keeps the
/// other for lazy `seek`+read of the data payload.
fn clone_src(f: &Src) -> Result<Src, String> {
    f.try_clone().map_err(|e| e.to_string())
}

fn read_exact_vec(file: &mut Src, n: usize) -> Result<Vec<u8>, String> {
    let mut buf = vec![0u8; n];
    file.read_exact(&mut buf).map_err(|e| e.to_string())?;
    Ok(buf)
}

fn read_header(file: &mut Src) -> Result<MHeader, String> {
    let buf = read_exact_vec(file, 20)?;
    let g = |i: usize| u32::from_le_bytes([buf[i], buf[i + 1], buf[i + 2], buf[i + 3]]);
    Ok(MHeader {
        ty: g(0),
        mrows: g(4),
        ncols: g(8),
        imagf: g(12),
        namelen: g(16),
    })
}

/// Read `n` character elements (stored either as doubles or raw bytes).
fn read_chars(ty: i32, n: usize, file: &mut Src) -> Result<Vec<u8>, String> {
    let p = (ty % 100) / 10;
    if p == 0 {
        let raw = read_exact_vec(file, n * 8)?;
        Ok((0..n)
            .map(|k| {
                let b: [u8; 8] = raw[k * 8..k * 8 + 8].try_into().unwrap();
                f64::from_le_bytes(b) as u8
            })
            .collect())
    } else if p == 5 {
        read_exact_vec(file, n)
    } else {
        Err("Could not read matrix as text".into())
    }
}

fn read_int32(ty: i32, n: usize, file: &mut Src) -> Result<Vec<i32>, String> {
    let p = (ty % 100) / 10;
    if p == 0 {
        let raw = read_exact_vec(file, n * 8)?;
        Ok((0..n)
            .map(|k| {
                let b: [u8; 8] = raw[k * 8..k * 8 + 8].try_into().unwrap();
                f64::from_le_bytes(b) as i32
            })
            .collect())
    } else if p == 2 {
        let raw = read_exact_vec(file, n * 4)?;
        Ok((0..n)
            .map(|k| {
                let b: [u8; 4] = raw[k * 4..k * 4 + 4].try_into().unwrap();
                i32::from_le_bytes(b)
            })
            .collect())
    } else {
        Err("Corrupt header: integer matrix".into())
    }
}

fn read_doubles(ty: i32, n: usize, file: &mut Src) -> Result<Vec<f64>, String> {
    let p = (ty % 100) / 10;
    if n == 0 {
        return Ok(Vec::new());
    }
    if p == 0 {
        let raw = read_exact_vec(file, n * 8)?;
        Ok((0..n)
            .map(|k| {
                let b: [u8; 8] = raw[k * 8..k * 8 + 8].try_into().unwrap();
                f64::from_le_bytes(b)
            })
            .collect())
    } else if p == 1 {
        let raw = read_exact_vec(file, n * 4)?;
        Ok((0..n)
            .map(|k| {
                let b: [u8; 4] = raw[k * 4..k * 4 + 4].try_into().unwrap();
                f32::from_le_bytes(b) as f64
            })
            .collect())
    } else {
        Err("Corrupt header: floating-point matrix".into())
    }
}

/// "der(a.b.c)" -> "a.b.der(c)" (Dymola style), or None if no conversion needed.
fn dymola_style_name(var: &str) -> Option<String> {
    if !var.starts_with("der(") {
        return None;
    }
    let dot = var.rfind('.')?;
    // prefix up to and including the last dot, then der( + tail-after-dot.
    Some(format!("{}der({}", &var[4..=dot], &var[dot + 1..]))
}

/// "a.b.der(c)" -> "der(a.b.c)" (OpenModelica style), or None if not applicable.
pub fn openmodelica_style_name(var: &str) -> Option<String> {
    let der = var.find("der(")?;
    if der == 0 {
        return None;
    }
    Some(format!("der({}{}", &var[..der], &var[der + 4..]))
}

impl MatReader {
    /// Open and parse `filename`. Returns a textual error on the first problem,
    /// mirroring `omc_new_matlab4_reader`.
    pub fn open(filename: &str) -> Result<MatReader, String> {
        let mut file = openmodelica_wasi::fs::open_read(filename).map_err(|e| e.to_string())?;
        const MATRIX_NAMES: [&str; 6] =
            ["Aclass", "name", "description", "dataInfo", "data_1", "data_2"];

        let mut reader = MatReader {
            file: clone_src(&file)?,
            allInfo: Vec::new(),
            params: Vec::new(),
            nparam: 0,
            nvar: 0,
            nrows: 0,
            var_offset: 0,
            binTrans: true,
            doublePrecision: true,
            data2: None,
            startTime: None,
            stopTime: None,
        };
        // Parse sequentially using `file`; `reader.file` is the clone kept for
        // the lazy data_2 read.
        let mut binTrans = true;

        for i in 0..6 {
            let hdr = read_header(&mut file).map_err(|_| "Corrupt header (1)".to_string())?;
            if hdr.imagf > 1 {
                return Err("Matrix uses imaginary numbers".into());
            }
            let element_length = mat_element_length(hdr.ty as i32)
                .ok_or("Could not determine size of matrix elements")?;
            let namelen = hdr.namelen as usize;
            let name_bytes = read_exact_vec(&mut file, namelen)
                .map_err(|_| "Corrupt header (2)".to_string())?;
            if namelen == 0 || name_bytes[namelen - 1] != 0 {
                return Err("Corrupt header (3)".into());
            }
            let name = String::from_utf8_lossy(&name_bytes[..namelen - 1]).into_owned();
            let matrix_length = hdr.mrows as usize
                * hdr.ncols as usize
                * (1 + hdr.imagf as usize)
                * element_length;
            if name != MATRIX_NAMES[i] {
                return Err(format!("Matrix name mismatch: {}", MATRIX_NAMES[i]));
            }
            match i {
                0 => {
                    // Aclass: 4x11 char matrix; row 3 encodes binTrans/binNormal.
                    if hdr.mrows != 4 {
                        return Err("Aclass matrix does not have 4 rows".into());
                    }
                    if hdr.ncols != 11 {
                        return Err("Aclass matrix does not have 11 cols".into());
                    }
                    let tmp = read_chars(hdr.ty as i32, (hdr.ncols * hdr.mrows) as usize, &mut file)?;
                    let mrows = hdr.mrows as usize;
                    let ncols = hdr.ncols as usize;
                    // Row 3 (0-based) selects the storage format.
                    let row3: Vec<u8> = (0..ncols).map(|j| tmp[j * mrows + 3]).collect();
                    if row3.starts_with(b"binTrans") {
                        binTrans = true;
                    } else if row3.starts_with(b"binNormal") {
                        binTrans = false;
                    } else {
                        return Err("Aclass matrix does not match binTrans or binNormal format".into());
                    }
                }
                1 => {
                    // name
                    let mrows = hdr.mrows as usize;
                    let ncols = hdr.ncols as usize;
                    let nall = if binTrans { ncols } else { mrows };
                    reader.allInfo = Vec::with_capacity(nall);
                    let tmp = read_chars(hdr.ty as i32, ncols * mrows, &mut file)?;
                    for k in 0..nall {
                        let bytes: Vec<u8> = if binTrans {
                            (0..mrows).map(|r| tmp[k * mrows + r]).collect()
                        } else {
                            (0..ncols).map(|j| tmp[j * mrows + k]).collect()
                        };
                        reader.allInfo.push(MatVariable {
                            name: fixed_str(&bytes),
                            descr: String::new(),
                            isParam: false,
                            index: -1,
                        });
                    }
                }
                2 => {
                    // description
                    let mrows = hdr.mrows as usize;
                    let ncols = hdr.ncols as usize;
                    let nall = reader.allInfo.len();
                    let tmp = read_chars(hdr.ty as i32, ncols * mrows, &mut file)?;
                    for k in 0..nall {
                        let bytes: Vec<u8> = if binTrans {
                            (0..mrows).map(|r| tmp[k * mrows + r]).collect()
                        } else {
                            (0..ncols).map(|j| tmp[j * mrows + k]).collect()
                        };
                        reader.allInfo[k].descr = fixed_str(&bytes);
                    }
                }
                3 => {
                    // dataInfo: per variable {isParam, index, interpolation, defined}
                    let mrows = hdr.mrows as usize;
                    let ncols = hdr.ncols as usize;
                    let tmp = read_int32(hdr.ty as i32, ncols * mrows, &mut file)
                        .map_err(|_| "Corrupt header: dataInfo matrix".to_string())?;
                    let nall = reader.allInfo.len();
                    for k in 0..nall {
                        let (isparam_cell, index_cell) = if binTrans {
                            (tmp[k * mrows], tmp[k * mrows + 1])
                        } else {
                            (tmp[k], tmp[k + mrows])
                        };
                        reader.allInfo[k].isParam = isparam_cell == 1;
                        reader.allInfo[k].index = index_cell;
                    }
                    reader
                        .allInfo
                        .sort_by(|a, b| iws_cmp(&a.name, &b.name));
                }
                4 => {
                    // data_1: parameter values (start/stop columns)
                    let mrows = hdr.mrows as usize;
                    let ncols = hdr.ncols as usize;
                    // nparam = parameter count: rows in binTrans, columns in binNormal.
                    reader.nparam = if binTrans { mrows } else { ncols };
                    if binTrans {
                        if (hdr.mrows != 0 || hdr.ncols != 0) && ncols != 2 && ncols != 1 {
                            return Err("data_1 matrix does not have 1 or 2 cols (or 0 rows/columns)".into());
                        }
                        reader.params = read_doubles(hdr.ty as i32, mrows * ncols, &mut file)
                            .map_err(|_| "Corrupt header: data_1 matrix".to_string())?;
                    } else {
                        if (hdr.mrows != 0 || hdr.ncols != 0) && mrows != 2 && mrows != 1 {
                            return Err("data_1 matrix does not have 1 or 2 rows (or 0 rows/columns)".into());
                        }
                        let tmp = read_doubles(hdr.ty as i32, mrows * ncols, &mut file)
                            .map_err(|_| "Corrupt header: data_1 matrix".to_string())?;
                        // Transpose to params[k*ncols + j] = tmp[k + j*mrows].
                        let mut params = vec![0f64; mrows * ncols];
                        for k in 0..mrows {
                            for j in 0..ncols {
                                params[k * ncols + j] = tmp[k + j * mrows];
                            }
                        }
                        reader.params = params;
                    }
                }
                5 => {
                    // data_2: the time series. Record the offset; load lazily.
                    let p = (hdr.ty as i32 / 10) % 10;
                    reader.doublePrecision = match p {
                        0 => true,
                        1 => false,
                        _ => return Err("data_2 matrix not in double/float representation".into()),
                    };
                    reader.binTrans = binTrans;
                    if binTrans {
                        reader.nrows = hdr.ncols as usize;
                        reader.nvar = hdr.mrows as usize;
                    } else {
                        reader.nrows = hdr.mrows as usize;
                        reader.nvar = hdr.ncols as usize;
                    }
                    reader.var_offset = file.stream_position().map_err(|e| e.to_string())?;
                    let _ = matrix_length; // data_2 is the final matrix; nothing follows.
                }
                _ => unreachable!(),
            }
        }
        Ok(reader)
    }

    /// Load and transpose data_2 into time-major order on first access.
    fn ensure_data2(&mut self) -> bool {
        if self.data2.is_some() {
            return true;
        }
        if self.nrows == 0 || self.nvar == 0 {
            return false;
        }
        let n = self.nrows * self.nvar;
        if self.file.seek(SeekFrom::Start(self.var_offset)).is_err() {
            return false;
        }
        // read_doubles selects double vs. float on the type's precision digit
        // p = (ty%100)/10: 0 = double, 1 = float.
        let ty = if self.doublePrecision { 0 } else { 10 };
        let raw = match read_doubles(ty, n, &mut self.file) {
            Ok(v) => v,
            Err(_) => return false,
        };
        let data2 = if self.binTrans {
            // On disk: raw[i*nvar + v] already time-major.
            raw
        } else {
            // On disk: raw[v*nrows + i]; transpose to data2[i*nvar + v].
            let mut t = vec![0f64; n];
            for v in 0..self.nvar {
                for i in 0..self.nrows {
                    t[i * self.nvar + v] = raw[v * self.nrows + i];
                }
            }
            t
        };
        self.data2 = Some(data2);
        true
    }

    /// Single value of `varIndex` (1-based, sign for negative alias) at row
    /// `timeIndex`. Returns None on read error.
    fn single_val(&mut self, varIndex: i32, timeIndex: usize) -> Option<f64> {
        if !self.ensure_data2() {
            return None;
        }
        let absv = varIndex.unsigned_abs() as usize;
        let data2 = self.data2.as_ref()?;
        let v = *data2.get(timeIndex * self.nvar + (absv - 1))?;
        Some(if varIndex < 0 { -v } else { v })
    }

    /// Full trajectory of a variable column (1-based index, sign for alias).
    pub fn read_vals(&mut self, varIndex: i32) -> Option<Vec<f64>> {
        if !self.ensure_data2() {
            return None;
        }
        let absv = varIndex.unsigned_abs() as usize;
        let nvar = self.nvar;
        let nrows = self.nrows;
        let data2 = self.data2.as_ref()?;
        Some(
            (0..nrows)
                .map(|i| {
                    let v = data2[i * nvar + (absv - 1)];
                    if varIndex < 0 { -v } else { v }
                })
                .collect(),
        )
    }

    pub fn start_time(&mut self) -> f64 {
        if self.startTime.is_none() {
            self.startTime = self.single_val(1, 0).or(Some(f64::NAN));
        }
        self.startTime.unwrap()
    }

    pub fn stop_time(&mut self) -> f64 {
        if self.stopTime.is_none() {
            let last = self.nrows.saturating_sub(1);
            self.stopTime = self.single_val(1, last).or(Some(f64::NAN));
        }
        self.stopTime.unwrap()
    }

    /// Look up a variable by name, with the same fallbacks as
    /// `omc_matlab4_find_var` (time/Time, Dymola/OMC der-style). Returns the
    /// index into `allInfo`.
    pub fn find_var(&self, varName: &str) -> Option<usize> {
        if let Ok(idx) = self.allInfo.binary_search_by(|v| iws_cmp(&v.name, varName)) {
            return Some(idx);
        }
        if varName == "time" {
            return self
                .allInfo
                .binary_search_by(|v| iws_cmp(&v.name, "Time"))
                .ok();
        } else if varName == "Time" {
            return self
                .allInfo
                .binary_search_by(|v| iws_cmp(&v.name, "time"))
                .ok();
        }
        let converted = dymola_style_name(varName).or_else(|| openmodelica_style_name(varName))?;
        self.allInfo
            .binary_search_by(|v| iws_cmp(&v.name, &converted))
            .ok()
    }

    /// Evaluate variable/parameter `var` at `time` with linear interpolation,
    /// mirroring `omc_matlab4_val`. Returns None when `time` is out of range.
    pub fn val(&mut self, varIdx: usize, time: f64) -> Option<f64> {
        let isParam = self.allInfo[varIdx].isParam;
        let index = self.allInfo[varIdx].index;
        if isParam {
            return Some(if index < 0 {
                -self.params[(index.unsigned_abs() as usize) - 1]
            } else {
                self.params[(index as usize) - 1]
            });
        }
        if time > self.stop_time() || time < self.start_time() {
            return None;
        }
        self.interp_val(index, time)
    }

    /// Interpolate a data_2 column directly by its 1-based index (sign selects
    /// the negated alias) at `time`, mirroring the variable branch of
    /// `omc_matlab4_val`. Used by the resampling path of filterSimulationResults,
    /// which works with raw column indices rather than `allInfo` entries.
    pub fn interp_val(&mut self, index: i32, time: f64) -> Option<f64> {
        let timevec = self.read_vals(1)?;
        let (i1, w1, i2, w2) = find_closest_points(time, &timevec);
        if i2 < 0 {
            self.single_val(index, i1 as usize)
        } else if i1 < 0 {
            self.single_val(index, i2 as usize)
        } else {
            let y1 = self.single_val(index, i1 as usize)?;
            let y2 = self.single_val(index, i2 as usize)?;
            Some(w1 * y1 + w2 * y2)
        }
    }
}

/// Port of `find_closest_points`: returns `(index1, weight1, index2, weight2)`
/// such that `value(key) = weight1*vec[index1] + weight2*vec[index2]`. On an
/// exact match `index2` is -1 and `weight1` is 1.
fn find_closest_points(key: f64, vec: &[f64]) -> (i32, f64, i32, f64) {
    let mut min: i32 = 0;
    let mut max: i32 = vec.len() as i32 - 1;
    loop {
        let mid = min + (max - min) / 2;
        if key == vec[mid as usize] {
            // On repeated timestamps (events) take the right limit.
            let mut mid = mid;
            while mid < max && vec[mid as usize] == vec[(mid + 1) as usize] {
                mid += 1;
            }
            return (mid, 1.0, -1, 0.0);
        } else if key > vec[mid as usize] {
            min = mid + 1;
        } else {
            max = mid - 1;
        }
        if max <= min {
            break;
        }
    }
    if max == min {
        if key > vec[max as usize] {
            max += 1;
        } else {
            min -= 1;
        }
    }
    // When the key lies outside the sampled range one of the indices walks
    // off the vector (`min == -1` before the first point, `max == len`
    // after the last). The C version still computes the weights from the
    // out-of-bounds neighbour — a garbage read whose result the caller
    // never uses, because it only reads the in-range index when the other
    // one is -1. In Rust that read panics, so report the out-of-range side
    // as -1 with the full weight on the valid index instead.
    if min < 0 {
        return (max, 1.0, -1, 0.0);
    }
    if max >= vec.len() as i32 {
        return (min, 1.0, -1, 0.0);
    }
    let w1 = (key - vec[min as usize]) / (vec[max as usize] - vec[min as usize]);
    (max, w1, min, 1.0 - w1)
}
