use std::{fs, path::Path};

use quote::ToTokens;
use syn::{FnArg, ForeignItem, Item, Pat, PathArguments, ReturnType, Type};

pub fn generate(bindings: &bindgen::Bindings, out_path: &Path) {
    let source = bindings_to_string(bindings);
    let syntax = syn::parse_file(&source).expect("failed to parse generated bindings");

    let mut declarations = Vec::new();
    for item in syntax.items {
        let Item::ForeignMod(foreign_mod) = item else {
            continue;
        };

        for item in foreign_mod.items {
            let ForeignItem::Fn(function) = item else {
                continue;
            };

            let name = function.sig.ident.to_string();
            if name.starts_with("aic_") {
                declarations.push(render_symbol_declaration(&function));
            }
        }
    }

    assert!(
        !declarations.is_empty(),
        "bindgen did not emit any aic_* function declarations"
    );

    declarations.push("fn aic_set_sdk_wrapper_id(id: u32);".to_string());

    let mut output =
        String::from("// @generated by aic-sdk-sys/build-utils/runtime_linking.rs\n\n");
    output.push_str("aic_symbols! {\n");
    for declaration in declarations {
        output.push_str("    ");
        output.push_str(&declaration);
        output.push('\n');
    }
    output.push_str("}\n");

    fs::write(out_path, output).expect("failed to write runtime-linking symbols");
}

fn bindings_to_string(bindings: &bindgen::Bindings) -> String {
    let mut bytes = Vec::new();
    bindings
        .write(Box::new(&mut bytes))
        .expect("failed to render generated bindings");
    String::from_utf8(bytes).expect("bindgen emitted non-UTF-8 bindings")
}

fn render_symbol_declaration(function: &syn::ForeignItemFn) -> String {
    let name = &function.sig.ident;
    let args = function
        .sig
        .inputs
        .iter()
        .enumerate()
        .map(render_arg)
        .collect::<Vec<_>>()
        .join(", ");
    let output = match &function.sig.output {
        ReturnType::Default => String::new(),
        ReturnType::Type(_, ty) => format!(" -> {}", render_type(ty)),
    };

    format!("fn {name}({args}){output};")
}

fn render_arg((index, arg): (usize, &FnArg)) -> String {
    let FnArg::Typed(arg) = arg else {
        panic!("bindgen emitted a method receiver in a foreign function")
    };

    let name = match arg.pat.as_ref() {
        Pat::Ident(ident) => ident.ident.to_string(),
        _ => format!("arg_{index}"),
    };
    let ty = render_type(&arg.ty);

    format!("{name}: {ty}")
}

fn render_type(ty: &Type) -> String {
    match ty {
        Type::Path(ty) => render_path(&ty.path),
        Type::Ptr(ty) => {
            let mutability = if ty.const_token.is_some() {
                "const"
            } else {
                "mut"
            };
            format!("*{mutability} {}", render_type(&ty.elem))
        }
        _ => render_tokens(ty),
    }
}

fn render_path(path: &syn::Path) -> String {
    let mut rendered = String::new();
    if path.leading_colon.is_some() {
        rendered.push_str("::");
    }

    rendered.push_str(
        &path
            .segments
            .iter()
            .map(|segment| {
                let ident = segment.ident.to_string();
                match &segment.arguments {
                    PathArguments::None => ident,
                    arguments => format!("{ident}{}", render_tokens(arguments)),
                }
            })
            .collect::<Vec<_>>()
            .join("::"),
    );

    rendered
}

fn render_tokens(tokens: &impl ToTokens) -> String {
    let mut rendered = tokens.to_token_stream().to_string();
    for (from, to) in [
        (" :: ", "::"),
        (" ::", "::"),
        (":: ", "::"),
        ("* const", "*const"),
        ("* mut", "*mut"),
    ] {
        rendered = rendered.replace(from, to);
    }
    rendered
}
