//! C loader generator — renders minijinja templates against a `FeatureSet`.
//!
//! All generation logic lives in the `.j2` template files under
//! `src/generator/c/templates/`.  This module handles environment setup,
//! pre-computation of template data, filter registration, and file I/O.

use std::collections::HashSet;
use std::path::Path;

use anyhow::{Context, Result, anyhow};
use minijinja::{Environment, Value, context};

use crate::cli::CArgs;
use crate::preamble;
use crate::provenance::load::{self, LoadCtx};
use crate::provenance::manifest::{OutputEntry, ProvenancePin, git_blob_sha1};
use crate::resolve::FeatureSet;
use indexmap::IndexMap;

/// What one `generate()` call produced: the provenance pins for every source it
/// used and an output-BOM entry for every file it wrote.  The run loop merges
/// these across feature sets into `.gloam/manifest.json`.
pub struct GeneratedTree {
    pub pins: IndexMap<String, ProvenancePin>,
    pub files: Vec<OutputEntry>,
}

// ---------------------------------------------------------------------------
// Entry point
// ---------------------------------------------------------------------------

pub fn generate(
    fs: &FeatureSet,
    args: &CArgs,
    out: &Path,
    load_ctx: &LoadCtx,
    command_line: &str,
) -> Result<GeneratedTree> {
    let stem = output_stem(fs);
    let env = build_env()?;

    // Resolve provenance pins for every contributing source (cache-warm in
    // --fetch mode after this call, so the aux-header copy below reuses them).
    let source_key_refs: Vec<&str> = fs.source_keys.iter().map(String::as_str).collect();
    let mut pins: IndexMap<String, ProvenancePin> = load::resolve(&source_key_refs, load_ctx)
        .context("resolving source provenance")?
        .into_iter()
        .map(|(key, src)| (key, src.pin))
        .collect();
    let preamble = preamble::build_preamble(fs, &pins, command_line);

    let names = FnNameLayout::build(fs);

    let include_dir = out.join("include");
    let gloam_dir = include_dir.join("gloam");
    let src_dir = out.join("src");
    std::fs::create_dir_all(&gloam_dir)?;
    std::fs::create_dir_all(&src_dir)?;

    let ctx = context! {
        fs                    => fs,
        stem                  => &stem,
        guard                 => format!("{}_H", stem.to_uppercase()),
        alias                 => args.alias,
        loader                => args.loader,
        external_headers      => args.external_headers,
        preamble              => &preamble,
        fn_name_offsets       => &names.offsets,
        fn_name_offset_type   => names.offset_type,
    };

    // The loader's .h/.c derive from every contributing source.
    let derived_from = fs.source_keys.clone();
    let mut files: Vec<OutputEntry> = Vec::new();

    let header = env.get_template("header.h.j2")?.render(&ctx)?;
    std::fs::write(gloam_dir.join(format!("{stem}.h")), &header)?;
    files.push(OutputEntry {
        path: format!("include/gloam/{stem}.h"),
        blob: git_blob_sha1(header.as_bytes()),
        verbatim: false,
        derived_from: derived_from.clone(),
    });

    let source = env.get_template("source.c.j2")?.render(&ctx)?;
    std::fs::write(src_dir.join(format!("{stem}.c")), &source)?;
    files.push(OutputEntry {
        path: format!("src/{stem}.c"),
        blob: git_blob_sha1(source.as_bytes()),
        verbatim: false,
        derived_from,
    });

    // Auxiliary headers, copied verbatim; collect their pins + BOM entries.
    for aux in copy_auxiliary_headers(fs, &include_dir, load_ctx, args.external_headers)? {
        files.push(OutputEntry {
            path: aux.rel_path,
            blob: aux.pin.blob.clone(),
            verbatim: true,
            derived_from: vec![aux.key.clone()],
        });
        pins.insert(aux.key, aux.pin);
    }

    Ok(GeneratedTree { pins, files })
}

// ---------------------------------------------------------------------------
// Function name blob layout
// ---------------------------------------------------------------------------

/// Pre-computed function name string blob layout.
///
/// Each command name is stored as a NUL-terminated string in a single
/// contiguous char array, with a parallel offset table for O(1) indexing.
/// This avoids one pointer + relocation per command (~30 bytes/command on
/// PIC builds).
struct FnNameLayout {
    /// Byte offset of each command's name within the blob.
    offsets: Vec<u32>,
    /// C type for the offset table: "uint16_t" or "uint32_t".
    offset_type: &'static str,
}

impl FnNameLayout {
    fn build(fs: &FeatureSet) -> Self {
        let mut offsets = Vec::with_capacity(fs.commands.len());
        let mut pos = 0u32;
        for cmd in &fs.commands {
            offsets.push(pos);
            pos += cmd.name.len() as u32 + 1; // +1 for NUL
        }
        let offset_type = if pos <= u16::MAX as u32 {
            "uint16_t"
        } else {
            "uint32_t"
        };
        Self {
            offsets,
            offset_type,
        }
    }
}

// ---------------------------------------------------------------------------
// Auxiliary header copying
// ---------------------------------------------------------------------------

/// Copy auxiliary headers (khrplatform.h, vk_platform.h, etc.) to the output
/// include tree, then transitively follow any quoted `#include` directives
/// found inside them.  This catches implicit dependencies like
/// `vulkan_video_codecs_common.h` which are `#include`'d by other vk_video
/// headers but never declared in the XML spec.
///
/// When `external_headers` is true the Vulkan type-definition headers
/// (vk_platform.h, vk_video/*) come from the system include path, so we
/// skip bundling them.  xxhash.h is still needed by the generated .c.
///
/// Returns one [`AuxHeader`] per emitted file (its registry key, output path,
/// and provenance pin) for the manifest BOM.
fn copy_auxiliary_headers(
    fs: &FeatureSet,
    include_dir: &Path,
    ctx: &LoadCtx,
    external_headers: bool,
) -> Result<Vec<AuxHeader>> {
    // xxhash.h is always needed by the generated .c (extension hash search).
    let mut queue: Vec<String> = std::iter::once("xxhash.h".to_string())
        .chain(if external_headers && fs.is_vulkan {
            // External-headers mode: Vulkan auxiliary headers (vk_platform.h,
            // vk_video/*) are provided by the system Vulkan-Headers package.
            Vec::new().into_iter()
        } else {
            fs.required_headers.clone().into_iter()
        })
        .collect();
    let mut visited: HashSet<String> = HashSet::new();
    let mut emitted: Vec<AuxHeader> = Vec::new();

    while let Some(hdr_path) = queue.pop() {
        if !visited.insert(hdr_path.clone()) {
            continue;
        }

        let dest = include_dir.join(&hdr_path);
        if let Some(parent) = dest.parent() {
            std::fs::create_dir_all(parent)?;
        }
        // Resolve content + provenance pin together (cache-warm; instant in
        // bundled mode).
        let resolved = load::resolve(&[hdr_path.as_str()], ctx)
            .with_context(|| format!("loading auxiliary header '{}'", hdr_path))?;
        let src = resolved
            .get(&hdr_path)
            .ok_or_else(|| anyhow!("auxiliary header '{}' was not resolved", hdr_path))?;
        std::fs::write(&dest, &src.content)?;
        let content = String::from_utf8_lossy(&src.content);
        emitted.push(AuxHeader {
            key: hdr_path.clone(),
            rel_path: format!("include/{hdr_path}"),
            pin: src.pin.clone(),
        });

        // Scan for `#include "relative/path.h"` lines and enqueue them,
        // resolved relative to the directory of the current header.
        let hdr_dir = std::path::Path::new(&hdr_path)
            .parent()
            .and_then(|p| p.to_str())
            .unwrap_or("");

        for line in content.lines() {
            let trimmed = line.trim();
            if !trimmed.starts_with("#include") {
                continue;
            }
            // Match the quoted form only — angle-bracket system headers are
            // not bundled and don't need copying.
            if let Some(rest) = trimmed.strip_prefix("#include") {
                let rest = rest.trim();
                if rest.starts_with('"')
                    && let Some(end) = rest[1..].find('"')
                {
                    let included = &rest[1..1 + end];
                    let resolved = if hdr_dir.is_empty() {
                        included.to_string()
                    } else {
                        format!("{}/{}", hdr_dir, included)
                    };
                    if !visited.contains(&resolved) {
                        queue.push(resolved);
                    }
                }
            }
        }
    }

    Ok(emitted)
}

/// One emitted auxiliary header: its registry key, output path (relative to the
/// tree root), and provenance pin.
struct AuxHeader {
    key: String,
    rel_path: String,
    pin: ProvenancePin,
}

// ---------------------------------------------------------------------------
// Output stem
// ---------------------------------------------------------------------------

fn output_stem(fs: &FeatureSet) -> String {
    if fs.is_merged {
        fs.spec_name.clone()
    } else {
        fs.apis
            .first()
            .cloned()
            .unwrap_or_else(|| fs.spec_name.clone())
    }
}

// ---------------------------------------------------------------------------
// Environment
// ---------------------------------------------------------------------------

fn build_env() -> Result<Environment<'static>> {
    let mut env = Environment::new();

    env.set_keep_trailing_newline(true);

    env.add_template("utils.j2", include_str!("templates/utils.j2"))?;
    env.add_template("impl_util.j2", include_str!("templates/impl_util.j2"))?;
    env.add_template("hash_search.j2", include_str!("templates/hash_search.j2"))?;
    env.add_template("library.j2", include_str!("templates/library.j2"))?;
    env.add_template("loader.j2", include_str!("templates/loader.j2"))?;
    env.add_template("header.h.j2", include_str!("templates/header.h.j2"))?;
    env.add_template("source.c.j2", include_str!("templates/source.c.j2"))?;

    env.add_filter("rjust", filter_rjust);
    env.add_filter("ljust", filter_ljust);
    env.add_filter("hex4", filter_hex4);
    env.add_filter("api_display", filter_api_display);
    env.add_filter("spec_display", filter_spec_display);
    env.add_filter("c_ident", filter_c_ident);
    env.add_filter("vk_max_enum_name", filter_enum_max_name);
    env.add_filter("ull", filter_ull);

    Ok(env)
}

// ---------------------------------------------------------------------------
// Custom filters
// ---------------------------------------------------------------------------

/// Right-justify a value to `width` characters, padding with spaces on the left.
/// Usage in templates: `{{ value | rjust(4) }}`
fn filter_rjust(value: Value, width: usize) -> String {
    let s = value.to_string();
    format!("{s:>width$}")
}

/// Left-justify a value to `width` characters, padding with spaces on the right.
/// Usage in templates: `{{ value | ljust(4) }}`
fn filter_ljust(value: Value, width: usize) -> String {
    let s = value.to_string();
    format!("{s:<width$}")
}

/// Format a u16 packed version as a 4-digit lowercase hex literal: `0x0303`.
/// Used for packed version constants in `find_core_*` comparisons.
fn filter_hex4(value: Value) -> String {
    let n = value.as_i64().unwrap_or(0) as u64;
    format!("0x{n:04x}")
}

/// Ensure a string is a valid C identifier by prefixing with `_` if it starts
/// with a digit.  Used for struct member names: `3DFX_multisample` → `_3DFX_multisample`.
/// The macro names (e.g. `GL_3DFX_multisample`) don't need this because they
/// don't start with a digit themselves.
fn filter_c_ident(value: Value) -> String {
    let s = value.as_str().unwrap_or("");
    if s.starts_with(|c: char| c.is_ascii_digit()) {
        format!("_{s}")
    } else {
        s.to_string()
    }
}

/// Append `ULL` suffix to a value if it is a numeric literal (decimal, hex,
/// or negative).  Alias references (identifiers) are left unchanged.
/// Used for 64-bit enum constants in the pre-C23 `#define` path.
fn filter_ull(value: Value) -> String {
    let s = value.as_str().unwrap_or("");
    let trimmed = s.strip_prefix('-').unwrap_or(s);
    let is_numeric = trimmed.starts_with(|c: char| c.is_ascii_digit())
        || trimmed.starts_with("0x")
        || trimmed.starts_with("0X");
    if is_numeric {
        format!("{s}ULL")
    } else {
        s.to_string()
    }
}

/// Used to build public function names like `gloamLoadGLES2Context`.
fn filter_spec_display(value: Value) -> String {
    match value.as_str().unwrap_or("") {
        "gles1" | "gles2" | "gl" | "glcore" => "GL",
        "egl" => "EGL",
        "glx" => "GLX",
        "wgl" => "WGL",
        "vk" | "vulkan" => "Vulkan",
        other => return other.to_string(),
    }
    .to_string()
}

/// Used to build public function names like `gloamLoadGLES2Context`.
fn filter_api_display(value: Value) -> String {
    match value.as_str().unwrap_or("") {
        "gl" | "glcore" => "GL",
        "gles1" => "GLES1",
        "gles2" => "GLES2",
        "egl" => "EGL",
        "glx" => "GLX",
        "wgl" => "WGL",
        "vk" | "vulkan" => "Vulkan",
        other => return other.to_string(),
    }
    .to_string()
}

/// Convert a CamelCase Vulkan type name to its SCREAMING_SNAKE_CASE MAX_ENUM
/// sentinel name.  e.g. `VkDriverId` → `VK_DRIVER_ID_MAX_ENUM`.
///
/// Rule: insert `_` before any uppercase letter that is either:
///   - preceded by a lowercase letter or digit  (e.g. Driver→_Driver)
///   - preceded by an uppercase letter AND followed by a lowercase letter
///     (handles acronyms: `VkEGL` → `VK_EGL`, not `VK_E_G_L`)
fn filter_enum_max_name(value: Value) -> String {
    let name = value.as_str().unwrap_or("");
    let chars: Vec<char> = name.chars().collect();
    let mut out = String::with_capacity(name.len() + 8);

    for (i, &c) in chars.iter().enumerate() {
        if c.is_ascii_uppercase() && i > 0 {
            let prev = chars[i - 1];
            let next = chars.get(i + 1).copied();
            let split = prev.is_ascii_lowercase()
                || prev.is_ascii_digit()
                || (prev.is_ascii_uppercase() && next.is_some_and(|n| n.is_ascii_lowercase()));
            if split {
                out.push('_');
            }
        }
        out.push(c.to_ascii_uppercase());
    }

    out.push_str("_MAX_ENUM");
    out
}

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

    // ---- filter_enum_max_name ----

    fn max(s: &str) -> String {
        filter_enum_max_name(Value::from(s))
    }

    #[test]
    fn enum_max_simple_camel() {
        assert_eq!(max("VkDriverId"), "VK_DRIVER_ID_MAX_ENUM");
    }

    #[test]
    fn enum_max_acronym_not_split() {
        // "EGL" should stay together; "Image" triggers a split after.
        assert_eq!(
            max("VkEGLImageCreateFlagBitsKHR"),
            "VK_EGL_IMAGE_CREATE_FLAG_BITS_KHR_MAX_ENUM"
        );
    }

    #[test]
    fn enum_max_trailing_acronym_not_split() {
        // Trailing uppercase run (KHR, EXT) should not be internally split.
        assert_eq!(
            max("VkSamplerAddressMode"),
            "VK_SAMPLER_ADDRESS_MODE_MAX_ENUM"
        );
    }

    #[test]
    fn enum_max_single_word() {
        assert_eq!(max("VkFormat"), "VK_FORMAT_MAX_ENUM");
    }

    // ---- filter_c_ident ----

    fn c_ident(s: &str) -> String {
        filter_c_ident(Value::from(s))
    }

    #[test]
    fn c_ident_digit_prefix_gets_underscore() {
        assert_eq!(c_ident("3DFX_multisample"), "_3DFX_multisample");
    }

    #[test]
    fn c_ident_normal_name_unchanged() {
        assert_eq!(c_ident("ARB_sync"), "ARB_sync");
        assert_eq!(c_ident("ANGLE_framebuffer_blit"), "ANGLE_framebuffer_blit");
    }

    #[test]
    fn c_ident_empty_string_unchanged() {
        assert_eq!(c_ident(""), "");
    }

    // ---- filter_api_display / filter_spec_display ----

    fn api_disp(s: &str) -> String {
        filter_api_display(Value::from(s))
    }

    fn spec_disp(s: &str) -> String {
        filter_spec_display(Value::from(s))
    }

    #[test]
    fn api_display_gl_variants() {
        assert_eq!(api_disp("gl"), "GL");
        assert_eq!(api_disp("gles1"), "GLES1");
        assert_eq!(api_disp("gles2"), "GLES2");
        assert_eq!(api_disp("glcore"), "GL");
    }

    #[test]
    fn api_display_other() {
        assert_eq!(api_disp("egl"), "EGL");
        assert_eq!(api_disp("vk"), "Vulkan");
        assert_eq!(api_disp("vulkan"), "Vulkan");
    }

    #[test]
    fn spec_display_gl_family_all_map_to_gl() {
        for api in &["gl", "gles1", "gles2", "glcore"] {
            assert_eq!(spec_disp(api), "GL", "failed for '{api}'");
        }
    }

    // ---- filter_hex4 ----

    #[test]
    fn hex4_formats_correctly() {
        assert_eq!(filter_hex4(Value::from(0x0303_i64)), "0x0303");
        assert_eq!(filter_hex4(Value::from(0x0100_i64)), "0x0100");
        assert_eq!(filter_hex4(Value::from(0_i64)), "0x0000");
    }

    // ---- filter_ull ----

    fn ull(s: &str) -> String {
        filter_ull(Value::from(s))
    }

    #[test]
    fn ull_appends_to_decimal() {
        assert_eq!(ull("0"), "0ULL");
        assert_eq!(ull("42"), "42ULL");
    }

    #[test]
    fn ull_appends_to_hex() {
        assert_eq!(ull("0x0000000000000001"), "0x0000000000000001ULL");
        assert_eq!(ull("0X1F"), "0X1FULL");
    }

    #[test]
    fn ull_appends_to_negative() {
        assert_eq!(ull("-1"), "-1ULL");
    }

    #[test]
    fn ull_leaves_identifier_unchanged() {
        assert_eq!(ull("VK_PIPELINE_STAGE_2_NONE"), "VK_PIPELINE_STAGE_2_NONE");
        assert_eq!(ull("VK_ACCESS_2_NONE_KHR"), "VK_ACCESS_2_NONE_KHR");
    }
}
