Skip to content
//! The table data model: column types inferred from a sampled grid, plus
//! synthetic-row generation. This is the "generate similar synthetic data"
//! half of the roadmap and deliberately lives in wasm rather than JS.

use crate::wasm::rng::Rng;
use crate::wasm::wire::{RS, US};

/// AG Grid's row-model types. Determines how data is read and re-injected.
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum RowModel {
    ClientSide,
    ServerSide,
    Infinite,
    Viewport,
}

impl RowModel {
    pub fn from_str(s: &str) -> Self {
        match s {
            "serverSide" => RowModel::ServerSide,
            "infinite" => RowModel::Infinite,
            "viewport" => RowModel::Viewport,
            _ => RowModel::ClientSide,
        }
    }

    pub fn as_str(&self) -> &'static str {
        match self {
            RowModel::ClientSide => "clientSide",
            RowModel::ServerSide => "serverSide",
            RowModel::Infinite => "infinite",
            RowModel::Viewport => "viewport",
        }
    }

    /// Human label for the UI badge.
    pub fn label(&self) -> &'static str {
        match self {
            RowModel::ClientSide => "client-side",
            RowModel::ServerSide => "server-side",
            RowModel::Infinite => "infinite",
            RowModel::Viewport => "viewport",
        }
    }
}

/// Inferred type + parameters for one column, driving both generation and the
/// JS-side value coercion on injection.
pub enum ColKind {
    Bool,
    Int {
        min: i64,
        max: i64,
    },
    Float {
        min: f64,
        max: f64,
    },
    /// Epoch-millis range; emitted as millis, coerced back to `Date` in JS.
    Date {
        min: i64,
        max: i64,
    },
    /// Small set of repeated values.
    Enum {
        values: Vec<String>,
    },
    /// Free text; generated from a sampled pool or random characters.
    Str {
        pool: Vec<String>,
        min_len: usize,
        max_len: usize,
    },
}

impl ColKind {
    /// Token telling the JS side how to coerce generated cell strings back into
    /// native values before handing them to AG Grid.
    pub fn coerce(&self) -> &'static str {
        match self {
            ColKind::Bool => "bool",
            ColKind::Int { .. } | ColKind::Float { .. } => "number",
            ColKind::Date { .. } => "date",
            ColKind::Enum { .. } | ColKind::Str { .. } => "string",
        }
    }

    /// Produce one synthetic cell value as a string.
    fn gen_cell(&self, rng: &mut Rng) -> String {
        match self {
            ColKind::Bool => if rng.bool() { "true" } else { "false" }.to_string(),
            ColKind::Int { min, max } => rng.range_i64(*min, *max).to_string(),
            ColKind::Float { min, max } => fmt_f64(rng.range_f64(*min, *max)),
            ColKind::Date { min, max } => rng.range_i64(*min, *max).to_string(),
            ColKind::Enum { values } => rng.pick(values).cloned().unwrap_or_default(),
            ColKind::Str {
                pool,
                min_len,
                max_len,
            } => match rng.pick(pool) {
                Some(s) => s.clone(),
                None => {
                    let lo = (*min_len).max(1);
                    let hi = (*max_len).max(lo);
                    let len = rng.range_i64(lo as i64, hi as i64) as usize;
                    rand_alnum(rng, len)
                }
            },
        }
    }
}

pub struct Column {
    pub field: String,
    pub header: String,
    pub kind: ColKind,
}

pub struct Table {
    pub id: String,
    pub row_model: RowModel,
    /// Rows observed when sampling the live grid.
    pub row_count: usize,
    /// Desired number of rows to generate (config-driven; defaults to observed).
    pub gen_count: usize,
    pub columns: Vec<Column>,
}

impl Table {
    /// Parse the schema payload produced by the JS `ag_grid_schema` import.
    ///
    /// Record 0 is meta `rowModel US rowCount US gridId`; each following record
    /// is a column `field US header US typeofHint US sample US sample ...`.
    pub fn from_schema(text: &str) -> Table {
        let mut records = text.split(RS);

        let meta = records.next().unwrap_or("");
        let mut meta_fields = meta.split(US);
        let row_model = RowModel::from_str(meta_fields.next().unwrap_or(""));
        let row_count = meta_fields.next().unwrap_or("0").parse().unwrap_or(0);
        let id = meta_fields.next().unwrap_or("").to_string();

        let mut columns = Vec::new();
        for record in records {
            if record.is_empty() {
                continue;
            }
            let mut fields = record.split(US);
            let field = fields.next().unwrap_or("").to_string();
            if field.is_empty() {
                continue;
            }
            let header = fields.next().unwrap_or(&field).to_string();
            let hint = fields.next().unwrap_or("string");
            let samples: Vec<String> = fields.map(|s| s.to_string()).collect();
            let kind = infer_kind(hint, &samples);
            columns.push(Column {
                field,
                header,
                kind,
            });
        }

        Table {
            id,
            row_model,
            row_count,
            gen_count: row_count,
            columns,
        }
    }

    /// Build the injection payload: a header record of `field US coerce`
    /// pairs, followed by one record per generated row (cells in column order).
    pub fn generate(&self, count: usize, rng: &mut Rng) -> String {
        let mut header = String::new();
        for (i, col) in self.columns.iter().enumerate() {
            if i > 0 {
                header.push(US);
            }
            header.push_str(&col.field);
            header.push(US);
            header.push_str(col.kind.coerce());
        }

        let mut out = header;
        for _ in 0..count {
            out.push(RS);
            for (i, col) in self.columns.iter().enumerate() {
                if i > 0 {
                    out.push(US);
                }
                out.push_str(&col.kind.gen_cell(rng));
            }
        }
        out
    }
}

/// Infer a column type from a JS `typeof` hint plus sampled string values.
fn infer_kind(hint: &str, samples: &[String]) -> ColKind {
    match hint {
        "boolean" => ColKind::Bool,
        "number" => {
            let nums: Vec<f64> = samples
                .iter()
                .filter_map(|s| s.parse::<f64>().ok())
                .collect();
            if nums.is_empty() {
                return ColKind::Int { min: 0, max: 1000 };
            }
            let min = nums.iter().cloned().fold(f64::INFINITY, f64::min);
            let max = nums.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
            let all_int = nums.iter().all(|v| v.fract() == 0.0 && v.abs() < 9e18);
            if all_int {
                ColKind::Int {
                    min: min as i64,
                    max: max as i64,
                }
            } else {
                ColKind::Float { min, max }
            }
        }
        "date" => {
            let ms: Vec<i64> = samples
                .iter()
                .filter_map(|s| s.parse::<i64>().ok())
                .collect();
            if ms.is_empty() {
                // 2020-01-01 .. 2024-01-01
                ColKind::Date {
                    min: 1_577_836_800_000,
                    max: 1_704_067_200_000,
                }
            } else {
                let min = *ms.iter().min().unwrap();
                let max = *ms.iter().max().unwrap();
                ColKind::Date { min, max }
            }
        }
        _ => str_kind(samples),
    }
}

/// Decide between a small enum and free text for string columns.
fn str_kind(samples: &[String]) -> ColKind {
    let mut distinct: Vec<String> = Vec::new();
    for s in samples {
        if !distinct.iter().any(|d| d == s) {
            distinct.push(s.clone());
        }
    }
    if !distinct.is_empty() && distinct.len() <= 12 {
        ColKind::Enum { values: distinct }
    } else {
        let min_len = samples.iter().map(|s| s.chars().count()).min().unwrap_or(3);
        let max_len = samples
            .iter()
            .map(|s| s.chars().count())
            .max()
            .unwrap_or(10);
        ColKind::Str {
            pool: distinct,
            min_len,
            max_len,
        }
    }
}

/// Format a float with up to two decimals, trimming trailing zeros.
fn fmt_f64(v: f64) -> String {
    let s = format!("{:.2}", v);
    let trimmed = s.trim_end_matches('0').trim_end_matches('.');
    if trimmed.is_empty() {
        "0".to_string()
    } else {
        trimmed.to_string()
    }
}

fn rand_alnum(rng: &mut Rng, len: usize) -> String {
    const CH: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
    (0..len.max(1))
        .map(|_| CH[rng.below(CH.len() as u64) as usize] as char)
        .collect()
}