use std::env;
use std::error::Error;
use std::fmt::Write as _;
use std::fs;
use std::path::PathBuf;

use sdl3_sys::metadata::{self, Hint, Property, PropertyType as MetadataPropertyType};

fn main() {
    #[cfg(any(target_os = "linux", target_os = "openbsd", target_os = "freebsd"))]
    println!(r"cargo:rustc-link-search=/usr/local/lib");

    if let Err(err) = generate_metadata_sources() {
        panic!("failed to generate metadata sources: {err}");
    }
}

fn generate_metadata_sources() -> Result<(), Box<dyn Error>> {
    let out_dir = PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR is not set"));
    println!("cargo:rerun-if-changed=build.rs");

    generate_hint_names(&out_dir)?;
    generate_property_names(&out_dir)?;

    Ok(())
}

fn generate_hint_names(out_dir: &PathBuf) -> Result<(), Box<dyn Error>> {
    let mut output = String::new();
    output.push_str("// @generated by build.rs; do not edit.\n\n");

    let mut identifiers = Vec::new();
    for hint in metadata::HINTS {
        identifiers.push(generate_hint_definition(&mut output, hint)?);
    }

    writeln!(output, "pub const ALL: [&str; {}] = [", identifiers.len())?;
    for ident in &identifiers {
        writeln!(output, "    {ident},")?;
    }
    output.push_str("];\n");

    fs::write(out_dir.join("sdl3_hint_names.rs"), output)?;
    Ok(())
}

fn generate_hint_definition(buf: &mut String, hint: &Hint) -> Result<String, Box<dyn Error>> {
    if let Some(doc) = hint.doc {
        append_doc_comment(buf, doc);
    }
    writeln!(buf, "#[doc(alias = {:?})]", hint.name)?;
    writeln!(
        buf,
        "pub const {}: &str = {:?};",
        hint.short_name,
        hint.value_str()
    )?;
    buf.push('\n');
    Ok(hint.short_name.to_string())
}

fn generate_property_names(out_dir: &PathBuf) -> Result<(), Box<dyn Error>> {
    let mut output = String::new();
    output.push_str("// @generated by build.rs; do not edit.\n");
    output.push_str("use super::{PropertyName, PropertyType, VersionTriple};\n\n");

    let mut identifiers = Vec::new();
    for property in metadata::PROPERTIES {
        identifiers.push(generate_property_definition(&mut output, property)?);
    }

    output.push_str("pub const ALL: &[PropertyName] = &[\n");
    for ident in &identifiers {
        writeln!(output, "    {ident},")?;
    }
    output.push_str("];\n");

    fs::write(out_dir.join("sdl3_property_names.rs"), output)?;
    Ok(())
}

fn generate_property_definition(
    buf: &mut String,
    property: &Property,
) -> Result<String, Box<dyn Error>> {
    if let Some(doc) = property.doc {
        append_doc_comment(buf, doc);
    }
    writeln!(buf, "#[doc(alias = {:?})]", property.name)?;
    writeln!(
        buf,
        "pub const {}: PropertyName = PropertyName {{",
        property.short_name
    )?;
    writeln!(buf, "    module: {:?},", property.module)?;
    writeln!(buf, "    name: {:?},", property.name)?;
    writeln!(buf, "    short_name: {:?},", property.short_name)?;
    writeln!(buf, "    value: {:?},", property.value_str())?;
    writeln!(
        buf,
        "    raw: crate::sys::{}::{},",
        property.module, property.name
    )?;
    writeln!(buf, "    ty: {},", property_type_path(property.ty))?;
    if let Some(doc) = property.doc {
        writeln!(buf, "    doc: Some({:?}),", doc)?;
    } else {
        buf.push_str("    doc: None,\n");
    }
    if let Some(version) = property.available_since {
        let (major, minor, patch) = version_tuple(version);
        writeln!(
            buf,
            "    available_since: Some(VersionTriple {{ major: {major}, minor: {minor}, patch: {patch} }}),"
        )?;
    } else {
        buf.push_str("    available_since: None,\n");
    }
    buf.push_str("};\n\n");
    Ok(property.short_name.to_string())
}

fn append_doc_comment(buf: &mut String, doc: &str) {
    for line in doc.trim_end().lines() {
        if line.is_empty() {
            buf.push_str("///\n");
        } else {
            buf.push_str("/// ");
            buf.push_str(line);
            buf.push('\n');
        }
    }
}

fn version_tuple(version: i32) -> (i32, i32, i32) {
    let major = version / 1_000_000;
    let minor = (version / 1_000) % 1_000;
    let patch = version % 1_000;
    (major, minor, patch)
}

fn property_type_path(ty: MetadataPropertyType) -> &'static str {
    if ty == MetadataPropertyType::INVALID {
        "PropertyType::INVALID"
    } else if ty == MetadataPropertyType::POINTER {
        "PropertyType::POINTER"
    } else if ty == MetadataPropertyType::STRING {
        "PropertyType::STRING"
    } else if ty == MetadataPropertyType::NUMBER {
        "PropertyType::NUMBER"
    } else if ty == MetadataPropertyType::FLOAT {
        "PropertyType::FLOAT"
    } else if ty == MetadataPropertyType::BOOLEAN {
        "PropertyType::BOOLEAN"
    } else {
        panic!("unknown property type {}", ty.0);
    }
}
