//! API: the C header (`wai/wai-rs/include/wai.h`, generated by cbindgen from the FFI and
//! committed). A program compiled and linked against the old header must still build and
//! run against the new library. So a function that is gone, or whose return type or
//! parameter types changed (their names are not compared), breaks; so does a struct whose
//! fields changed in any way (its size or layout differs), an opaque type or typedef that
//! is gone or changed, an enumerator or macro constant that is gone or holds another
//! value, and a declaration moved under a preprocessor condition. New declarations are
//! compatible.

use super::{Category, Finding};
use std::collections::BTreeMap;

pub const PATH: &str = "wai/wai-rs/include/wai.h";

/// One declaration of the header.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Decl {
    /// A function: its return type and its parameters' types, in order.
    Function { ret: String, params: Vec<String> },
    /// A struct or union with its fields (`type name`, in order), or none for an opaque one.
    Struct { fields: Option<Vec<String>> },
    /// An enumeration: each enumerator and its value.
    Enum { values: Vec<(String, String)> },
    Typedef { ty: String },
    /// A macro constant, with every value it is defined to (cbindgen repeats some).
    Macro { values: Vec<String> },
    Variable { ty: String },
}

/// A declaration and the preprocessor conditions it sits under (empty for none).
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Item {
    pub decl: Decl,
    pub cond: String,
}

fn strip_comments(src: &str) -> String {
    let mut out = String::with_capacity(src.len());
    let b = src.as_bytes();
    let mut i = 0;
    while i < b.len() {
        if b[i] == b'/' && b.get(i + 1) == Some(&b'*') {
            i += 2;
            while i + 1 < b.len() && !(b[i] == b'*' && b[i + 1] == b'/') {
                if b[i] == b'\n' {
                    out.push('\n');
                }
                i += 1;
            }
            i += 2;
        } else if b[i] == b'/' && b.get(i + 1) == Some(&b'/') {
            while i < b.len() && b[i] != b'\n' {
                i += 1;
            }
        } else {
            let ch = src[i..].chars().next().unwrap_or(' ');
            out.push(ch);
            i += ch.len_utf8();
        }
    }
    out
}

/// Whitespace collapsed, with `*`, `(`, `)`, `[`, `]` and `,` set apart so spellings
/// that differ only in spacing compare equal.
fn norm(s: &str) -> String {
    let spaced: String = s.chars().flat_map(|c| if "*()[],".contains(c) { vec![' ', c, ' '] } else { vec![c] }).collect();
    spaced.split_whitespace().collect::<Vec<_>>().join(" ")
}

fn ident(s: &str) -> bool {
    !s.is_empty() && s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') && !s.starts_with(|c: char| c.is_ascii_digit())
}

/// A parameter's type: its text without the trailing name.
fn param_type(p: &str) -> String {
    let n = norm(p);
    let toks: Vec<&str> = n.split(' ').collect();
    let last = toks.last().copied().unwrap_or("");
    let basic = matches!(last, "void" | "int" | "char" | "long" | "short" | "unsigned" | "signed" | "float" | "double" | "bool");
    if toks.len() > 1 && ident(last) && !basic { toks[..toks.len() - 1].join(" ") } else { n.clone() }
}

fn split_top(s: &str, sep: char) -> Vec<String> {
    let mut out = Vec::new();
    let (mut depth, mut cur) = (0i32, String::new());
    for c in s.chars() {
        match c {
            '(' | '{' | '[' => depth += 1,
            ')' | '}' | ']' => depth -= 1,
            _ => {}
        }
        if c == sep && depth == 0 {
            out.push(std::mem::take(&mut cur));
        } else {
            cur.push(c);
        }
    }
    if !cur.trim().is_empty() {
        out.push(cur);
    }
    out
}

fn declaration(text: &str, cond: &str, out: &mut BTreeMap<String, Item>) {
    let t = norm(text);
    if t.is_empty() {
        return;
    }
    let item = |decl| Item { decl, cond: cond.to_string() };
    let body = |t: &str| -> Option<(String, String, String)> {
        let open = t.find('{')?;
        let close = t.rfind('}')?;
        Some((t[..open].trim().to_string(), t[open + 1..close].trim().to_string(), t[close + 1..].trim().to_string()))
    };
    if let Some((head, inner, tail)) = body(&t) {
        let words: Vec<&str> = head.split(' ').collect();
        let is_enum = words.contains(&"enum");
        let tag = words.last().filter(|w| ident(w) && !matches!(**w, "struct" | "union" | "enum" | "typedef")).map(|w| w.to_string());
        let name = if tail.is_empty() { tag } else { Some(tail.trim_end_matches(';').trim().to_string()) };
        let Some(name) = name else { return };
        if is_enum {
            let mut values = Vec::new();
            let mut next: i128 = 0;
            for e in split_top(&inner, ',') {
                let e = e.trim();
                if e.is_empty() {
                    continue;
                }
                let (n, v) = match e.split_once('=') {
                    Some((n, v)) => (n.trim().to_string(), norm(v)),
                    None => (e.to_string(), next.to_string()),
                };
                next = v.parse::<i128>().map(|x| x + 1).unwrap_or(next + 1);
                values.push((n, v));
            }
            out.insert(name, item(Decl::Enum { values }));
        } else {
            let fields = split_top(&inner, ';').into_iter().map(|f| norm(&f)).filter(|f| !f.is_empty()).collect();
            out.insert(name, item(Decl::Struct { fields: Some(fields) }));
        }
        return;
    }
    if let Some(rest) = t.strip_prefix("typedef ") {
        let words: Vec<&str> = rest.split(' ').collect();
        if matches!(words.first(), Some(&"struct") | Some(&"union")) && words.len() == 3 && !rest.contains('(') {
            out.entry(words[2].to_string()).or_insert_with(|| item(Decl::Struct { fields: None }));
            return;
        }
        // `typedef <type> <name>`, or a function pointer `typedef R ( * name ) ( … )`.
        let name = if let Some(i) = rest.find("( * ") {
            rest[i + 4..].split(' ').next().unwrap_or("").to_string()
        } else {
            words.last().unwrap_or(&"").to_string()
        };
        if ident(&name) {
            out.insert(name, item(Decl::Typedef { ty: rest.to_string() }));
        }
        return;
    }
    if let Some(open) = t.find(" ( ") {
        let head = &t[..open];
        let name = head.split(' ').next_back().unwrap_or("").to_string();
        if !ident(&name) {
            return;
        }
        let ret = head[..head.len() - name.len()].trim().trim_start_matches("extern ").to_string();
        let close = t.rfind(')').unwrap_or(t.len());
        let inner = &t[open + 3..close];
        let params: Vec<String> = split_top(inner, ',').iter().map(|p| param_type(p)).filter(|p| !p.is_empty()).collect();
        let params = if params == ["void"] { Vec::new() } else { params };
        out.insert(name, item(Decl::Function { ret, params }));
        return;
    }
    let words: Vec<&str> = t.split(' ').collect();
    if words.len() >= 2 && ident(words[words.len() - 1]) {
        let name = words[words.len() - 1].to_string();
        let ty = words[..words.len() - 1].join(" ").trim_start_matches("extern ").to_string();
        out.insert(name, item(Decl::Variable { ty }));
    }
}

/// Every declaration of a header, by name.
pub fn parse(src: &str) -> BTreeMap<String, Item> {
    let text = strip_comments(src);
    let mut out: BTreeMap<String, Item> = BTreeMap::new();
    let mut conds: Vec<String> = Vec::new();
    let mut guard: Option<String> = None;
    let mut pending = String::new();
    let mut lines = text.lines().peekable();
    while let Some(line) = lines.next() {
        let mut l = line.trim().to_string();
        while l.ends_with('\\') {
            l.pop();
            l.push(' ');
            l.push_str(lines.next().unwrap_or("").trim());
        }
        if let Some(d) = l.strip_prefix('#') {
            let d = d.trim();
            let (word, rest) = d.split_once(char::is_whitespace).map(|(a, b)| (a, b.trim())).unwrap_or((d, ""));
            match word {
                "ifndef" if guard.is_none() && conds.is_empty() => {
                    guard = Some(rest.to_string());
                    conds.push(String::new());
                }
                "if" | "ifdef" | "ifndef" => conds.push(format!("{word} {rest}")),
                "else" => {
                    if let Some(c) = conds.last_mut() {
                        *c = format!("not ({c})");
                    }
                }
                "elif" => {
                    if let Some(c) = conds.last_mut() {
                        *c = format!("elif {rest}");
                    }
                }
                "endif" => {
                    conds.pop();
                }
                "define" => {
                    let (name, value) = rest.split_once(char::is_whitespace).map(|(a, b)| (a, norm(b))).unwrap_or((rest, String::new()));
                    if Some(name) != guard.as_deref() && ident(name) {
                        let cond = conds.iter().filter(|c| !c.is_empty()).cloned().collect::<Vec<_>>().join(" && ");
                        match out.get_mut(name) {
                            Some(Item { decl: Decl::Macro { values }, .. }) => {
                                if !values.contains(&value) {
                                    values.push(value);
                                    values.sort();
                                }
                            }
                            _ => {
                                out.insert(name.to_string(), Item { decl: Decl::Macro { values: vec![value] }, cond });
                            }
                        }
                    }
                }
                _ => {}
            }
            continue;
        }
        // The `extern "C" {` wrapper sits under `__cplusplus`; nothing there is a declaration.
        if conds.iter().any(|c| c.contains("__cplusplus")) {
            continue;
        }
        let cond = conds.iter().filter(|c| !c.is_empty()).cloned().collect::<Vec<_>>().join(" && ");
        for ch in l.chars().chain(std::iter::once(' ')) {
            pending.push(ch);
            if ch == ';' {
                let depth: i32 = pending.chars().map(|c| match c {
                    '{' => 1,
                    '}' => -1,
                    _ => 0,
                }).sum();
                if depth == 0 {
                    let decl = std::mem::take(&mut pending);
                    declaration(decl.trim().trim_end_matches(';'), &cond, &mut out);
                }
            }
        }
    }
    out
}

fn show(d: &Decl, name: &str) -> String {
    match d {
        Decl::Function { ret, params } => format!("{ret} {name}({})", params.join(", ")),
        Decl::Struct { fields: Some(f) } => format!("struct {name} {{ {} }}", f.join("; ")),
        Decl::Struct { fields: None } => format!("struct {name} (opaque)"),
        Decl::Enum { values } => format!("enum {name} {{ {} }}", values.iter().map(|(n, v)| format!("{n} = {v}")).collect::<Vec<_>>().join(", ")),
        Decl::Typedef { ty } => format!("typedef {ty}"),
        Decl::Macro { values } => format!("#define {name} {}", values.join(" | ")),
        Decl::Variable { ty } => format!("{ty} {name}"),
    }
}

fn kind(d: &Decl) -> &'static str {
    match d {
        Decl::Function { .. } => "function",
        Decl::Struct { .. } => "struct",
        Decl::Enum { .. } => "enum",
        Decl::Typedef { .. } => "typedef",
        Decl::Macro { .. } => "constant",
        Decl::Variable { .. } => "variable",
    }
}

/// The header's findings, from the two headers (`None` where a tree has none).
pub fn compare(old: Option<&str>, new: Option<&str>) -> Vec<Finding> {
    let a = old.map(parse).unwrap_or_default();
    let b = new.map(parse).unwrap_or_default();
    let mut out = Vec::new();
    for (name, x) in &a {
        let k = kind(&x.decl);
        let Some(y) = b.get(name) else {
            out.push(Finding::broke(
                Category::Api,
                "c",
                name.clone(),
                "removed".into(),
                format!("the {k} {} is gone", show(&x.decl, name)),
                format!("a program that uses {name} no longer compiles against the header, or no longer links against the library"),
            ));
            continue;
        };
        let why = match (&x.decl, &y.decl) {
            _ if x.cond != y.cond => Some(format!("now declared under `{}` (was `{}`)", y.cond, x.cond)),
            (Decl::Struct { fields: Some(_) }, Decl::Struct { fields: None }) => Some("its fields are no longer declared".into()),
            (Decl::Struct { fields: None }, Decl::Struct { fields: Some(_) }) => None,
            (Decl::Enum { values: va }, Decl::Enum { values: vb }) => {
                let gone: Vec<String> = va.iter().filter(|e| !vb.contains(e)).map(|(n, v)| format!("{n} = {v}")).collect();
                (!gone.is_empty()).then(|| format!("{} gone or renumbered", gone.join(", ")))
            }
            (Decl::Macro { values: va }, Decl::Macro { values: vb }) => {
                (va.iter().any(|v| !vb.contains(v))).then(|| format!("value {} → {}", va.join(" | "), vb.join(" | ")))
            }
            (p, q) if p != q => Some(format!("{} → {}", show(p, name), show(q, name))),
            _ => None,
        };
        if let Some(what) = why {
            let reason = match &x.decl {
                Decl::Function { .. } => "a caller compiled against the old prototype passes or reads the wrong types: undefined behaviour, or a build that fails",
                Decl::Struct { .. } => "the struct's size or layout differs, so a caller compiled against the old header reads and writes the wrong bytes",
                Decl::Macro { .. } | Decl::Enum { .. } => "a program compiled with the old value passes or compares a value the library now reads differently",
                _ => "a program compiled against the old declaration no longer matches the library",
            };
            out.push(Finding::broke(Category::Api, "c", name.clone(), "changed".into(), format!("the {k} {name}: {what}"), reason.into()));
        } else if x.decl != y.decl {
            out.push(Finding::compatible(Category::Api, "c", name.clone(), format!("{} → {}", show(&x.decl, name), show(&y.decl, name))));
        }
    }
    for (name, y) in &b {
        if !a.contains_key(name) {
            out.push(Finding::compatible(Category::Api, "c", name.clone(), format!("the {} {} is new", kind(&y.decl), show(&y.decl, name))));
        }
    }
    out
}

#[cfg(test)]
mod tests {
    use super::*;

    const H: &str = "#ifndef WAI_H\n#define WAI_H\n#include <stdint.h>\n/** doc */\n#define WAI_OK 0\n#define CREATE 1\n#define CREATE 1\n\
        typedef struct wai_sink_t wai_sink_t;\ntypedef struct wai_buffer_t {\n  uint8_t *data; // the bytes\n  uintptr_t len;\n} wai_buffer_t;\n\
        #ifdef __cplusplus\nextern \"C\" {\n#endif\n int wai_pack(const char *manifest,\n   const uint8_t *payload, uintptr_t len,\n   struct wai_buffer_t *out);\n\
        uint32_t wai_abi(void);\n#ifdef __cplusplus\n}\n#endif\n#endif\n";

    #[test]
    fn parses_what_cbindgen_writes() {
        let m = parse(H);
        assert_eq!(m["wai_pack"].decl, Decl::Function { ret: "int".into(), params: vec!["const char *".into(), "const uint8_t *".into(), "uintptr_t".into(), "struct wai_buffer_t *".into()] });
        assert_eq!(m["wai_abi"].decl, Decl::Function { ret: "uint32_t".into(), params: vec![] });
        assert_eq!(m["wai_buffer_t"].decl, Decl::Struct { fields: Some(vec!["uint8_t * data".into(), "uintptr_t len".into()]) });
        assert_eq!(m["wai_sink_t"].decl, Decl::Struct { fields: None });
        assert_eq!(m["CREATE"].decl, Decl::Macro { values: vec!["1".into()] });
        assert!(!m.contains_key("WAI_H"));
        assert_eq!(m["wai_pack"].cond, "");
    }

    #[test]
    fn a_changed_parameter_type_breaks_and_a_renamed_one_does_not() {
        let renamed = H.replace("uintptr_t len,\n", "uintptr_t payload_len,\n");
        assert!(compare(Some(H), Some(&renamed)).iter().all(|f| !f.breaking));
        let changed = H.replace("uintptr_t len,\n", "uint32_t len,\n");
        let f = compare(Some(H), Some(&changed));
        assert!(f.iter().any(|f| f.breaking && f.id == "api:c:wai_pack"), "{f:?}");
    }
}
