//! Build script for `iced_ui`.
//!
//! Scans the `iced_widget` source in the cargo registry to discover all
//! `pub trait Catalog` definitions. For each one, it generates a forwarding
//! `impl Catalog for Theme` that delegates styling to the inner
//! `iced::Theme` (stored in `Theme::colors`).
//!
//! This ensures `iced_ui::Theme` is automatically compatible with every
//! iced built-in widget — both current and future — without manual
//! maintenance.

use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};

fn main() {
    let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR not set");
    let out_path = PathBuf::from(&out_dir).join("iced_catalog_impls.rs");

    let iced_widget_src = locate_iced_widget_src();

    // Discover all Catalog traits in iced_widget
    let catalogs = discover_catalogs(&iced_widget_src);

    // Generate the output file (invocations only — the macro is in iced_compat.rs)
    let mut output = String::new();
    output.push_str("// Auto-generated by build.rs — do not edit manually.\n");
    output.push_str("// Regenerate by running `cargo build -p iced_ui`.\n");
    output.push_str("// The `impl_iced_catalog!` macro is defined in iced_compat.rs.\n\n");

    for catalog in &catalogs {
        if let Some(code) = generate_impl(catalog) {
            output.push_str(&code);
            output.push('\n');
        }
    }

    fs::write(&out_path, output).expect("Failed to write generated catalog impls");

    // Tell cargo to rerun if iced_widget source changes (unlikely for registry
    // crates, but correct for path dependencies during development).
    println!("cargo::rerun-if-changed=build.rs");
}

/// Information about a discovered Catalog trait.
#[derive(Debug)]
struct CatalogInfo {
    /// Module path segments, e.g. `["button"]` or `["overlay", "menu"]`.
    module_path: Vec<String>,
    /// Whether `fn style(...)` takes a `Status` parameter.
    has_status: bool,
    /// The default function/expression used in `Box::new(...)` in the
    /// `impl Catalog for Theme` block.
    default_expr: DefaultExpr,
    /// Feature gate, if any (e.g. `"svg"`, `"qr_code"`).
    feature_gate: Option<String>,
    /// Whether this is a "special" trait that can't use the macro
    /// (e.g. combo_box which is a marker trait, or markdown which
    /// has extra methods).
    special: SpecialKind,
}

#[derive(Debug)]
enum DefaultExpr {
    /// A named function, e.g. `primary` or `default`.
    NamedFn(String),
    /// An inline closure like `|_| Style::default()` or `|_theme, _status| Style::default()`.
    StyleDefault,
}

#[derive(Debug, PartialEq)]
enum SpecialKind {
    Normal,
    /// Marker trait with no `type Class` or `fn style` (e.g., combo_box).
    Marker,
    /// Has extra non-default methods that need hand-writing (e.g., markdown).
    ExtraMethods,
}

/// Locate the iced_widget source directory in the cargo registry.
fn locate_iced_widget_src() -> PathBuf {
    let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set");

    // Look in cargo home registry
    let cargo_home = std::env::var("CARGO_HOME").unwrap_or_else(|_| {
        let home = std::env::var("HOME").expect("HOME not set");
        format!("{home}/.cargo")
    });
    let registry_src = PathBuf::from(&cargo_home).join("registry").join("src");
    let index_dir = find_index_dir(&registry_src);

    // Try to get the exact version from Cargo.lock.
    // We check multiple locations because CARGO_MANIFEST_DIR varies:
    //   - In workspace builds: `crates/iced_ui/` → lock is at `../../Cargo.lock`
    //   - In publish verification: `target/package/iced_ui-x.y.z/` → lock may be
    //     next to manifest or not present at all.
    let version = try_lock_version(&manifest_dir, "iced_widget");

    // Fallback: if no lock file was found (e.g. during `cargo publish --dry-run`),
    // scan the registry for the highest iced_widget version present.
    let version = version.unwrap_or_else(|| {
        find_latest_version_in_registry(&index_dir, "iced_widget")
            .expect("No iced_widget-* found in cargo registry")
    });

    let iced_widget_dir = index_dir.join(format!("iced_widget-{version}"));
    assert!(
        iced_widget_dir.exists(),
        "iced_widget source not found at: {}",
        iced_widget_dir.display()
    );

    iced_widget_dir.join("src")
}

/// Try to read the iced_widget version from a Cargo.lock file.
/// Searches several candidate paths relative to the manifest directory.
fn try_lock_version(manifest_dir: &str, package: &str) -> Option<String> {
    let manifest_path = PathBuf::from(manifest_dir);

    // Candidate lock file locations
    let candidates = [
        // Workspace build: crates/iced_ui/ → ../../Cargo.lock
        manifest_path.parent()?.parent()?.join("Cargo.lock"),
        // Standalone or publish verification: next to Cargo.toml
        manifest_path.join("Cargo.lock"),
    ];

    for lock_path in &candidates {
        if let Ok(content) = fs::read_to_string(lock_path)
            && let Some(v) = parse_lock_version(&content, package)
        {
            return Some(v);
        }
    }

    None
}

/// Scan the registry index directory for the highest version of a crate.
fn find_latest_version_in_registry(index_dir: &Path, crate_name: &str) -> Option<String> {
    let prefix = format!("{crate_name}-");
    let mut best: Option<String> = None;

    if let Ok(entries) = fs::read_dir(index_dir) {
        for entry in entries.flatten() {
            let name = entry.file_name();
            let name_str = name.to_string_lossy();
            if let Some(version) = name_str.strip_prefix(&prefix)
                && entry.path().is_dir()
            {
                // Pick the highest version lexicographically (works for semver)
                if best.as_deref().is_none_or(|b| version > b) {
                    best = Some(version.to_string());
                }
            }
        }
    }

    best
}

fn parse_lock_version(content: &str, package_name: &str) -> Option<String> {
    let target = format!("name = \"{package_name}\"");

    let mut lines = content.lines().peekable();
    while let Some(line) = lines.next() {
        if line.trim() == target {
            // Look for version = "..." in following lines
            for next_line in lines.by_ref() {
                if next_line.trim().starts_with("version")
                    && let Some(v) = extract_quoted_value(next_line)
                {
                    return Some(v);
                }
                if next_line.trim().is_empty() || next_line.trim() == "[[package]]" {
                    break;
                }
            }
        }
    }
    None
}

fn extract_quoted_value(line: &str) -> Option<String> {
    let start = line.find('"')? + 1;
    let end = line[start..].find('"')? + start;
    Some(line[start..end].to_string())
}

fn find_index_dir(registry_src: &Path) -> PathBuf {
    if let Ok(entries) = fs::read_dir(registry_src) {
        for entry in entries.flatten() {
            let name = entry.file_name();
            let name_str = name.to_string_lossy();
            if name_str.starts_with("index.crates.io-") {
                return entry.path();
            }
        }
    }
    panic!(
        "Could not find index.crates.io-* directory in {}",
        registry_src.display()
    );
}

/// Feature gates for modules in iced_widget.
fn feature_gates() -> HashMap<&'static str, &'static str> {
    let mut map = HashMap::new();
    map.insert("svg", "svg");
    map.insert("qr_code", "qr_code");
    map.insert("markdown", "markdown");
    map
}

/// Discover all Catalog traits in the iced_widget source tree.
fn discover_catalogs(src_dir: &Path) -> Vec<CatalogInfo> {
    let gates = feature_gates();
    let mut catalogs = Vec::new();

    // Walk the source tree
    walk_rs_files(src_dir, &[], &gates, &mut catalogs);

    catalogs
}

fn walk_rs_files(
    dir: &Path,
    parent_path: &[String],
    gates: &HashMap<&str, &str>,
    catalogs: &mut Vec<CatalogInfo>,
) {
    let Ok(entries) = fs::read_dir(dir) else {
        return;
    };

    for entry in entries.flatten() {
        let path = entry.path();
        let file_name = entry.file_name().to_string_lossy().to_string();

        if path.is_dir() {
            let mut sub_path = parent_path.to_vec();
            sub_path.push(file_name.clone());

            // Look for catalog in submodules (e.g., overlay/menu.rs)
            walk_rs_files(&path, &sub_path, gates, catalogs);
        } else if file_name.ends_with(".rs") && file_name != "mod.rs" && file_name != "lib.rs" {
            let module_name = file_name.trim_end_matches(".rs").to_string();
            let content = match fs::read_to_string(&path) {
                Ok(c) => c,
                Err(_) => continue,
            };

            if !content.contains("pub trait Catalog") {
                continue;
            }

            let mut module_path = parent_path.to_vec();
            module_path.push(module_name.clone());

            // Determine feature gate
            let top_module = module_path.first().unwrap();
            let feature_gate = gates.get(top_module.as_str()).map(|s| s.to_string());

            // Determine special kind
            let special = determine_special_kind(&content);

            if special != SpecialKind::Normal {
                catalogs.push(CatalogInfo {
                    module_path,
                    has_status: false,
                    default_expr: DefaultExpr::StyleDefault,
                    feature_gate,
                    special,
                });
                continue;
            }

            // Parse the impl Catalog for Theme block
            let (has_status, default_expr) = parse_catalog_impl(&content);

            catalogs.push(CatalogInfo {
                module_path,
                has_status,
                default_expr,
                feature_gate,
                special,
            });
        }
    }
}

fn determine_special_kind(content: &str) -> SpecialKind {
    // Find the trait definition
    let trait_start = match content.find("pub trait Catalog") {
        Some(pos) => pos,
        None => return SpecialKind::Normal,
    };

    let trait_body = &content[trait_start..];

    // Check if this is a marker trait (no `type Class` in body)
    // Find the opening brace
    let brace_pos = match trait_body.find('{') {
        Some(pos) => pos,
        None => return SpecialKind::Normal,
    };

    let body_start = &trait_body[brace_pos..];
    let body_content = extract_brace_content(body_start);

    if !body_content.contains("type Class") {
        // No `type Class` — check if all methods (if any) have default bodies.
        // If any method lacks a default body, it needs hand-writing.
        let has_method_without_default = body_content.lines().any(|line| {
            let trimmed = line.trim();
            trimmed.starts_with("fn ") && !has_default_body(body_content, trimmed)
        });

        if has_method_without_default {
            return SpecialKind::ExtraMethods;
        }

        // Either no methods at all, or all methods have defaults → empty impl works.
        return SpecialKind::Marker;
    }

    // Has `type Class` — check for extra methods beyond default() and style()
    for line in body_content.lines() {
        let trimmed = line.trim();
        if trimmed.starts_with("fn ")
            && !trimmed.starts_with("fn default")
            && !trimmed.starts_with("fn style")
            && !has_default_body(body_content, trimmed)
        {
            return SpecialKind::ExtraMethods;
        }
    }

    SpecialKind::Normal
}

fn has_default_body(body: &str, fn_line: &str) -> bool {
    // Find this function in the body. We search for the trimmed fn_line
    // but the body may have indentation, so look for it as a substring.
    // The key indicator is: does the line (or the continuation after the
    // return type) contain `{`? If it ends with `{` it has a body.
    // If it ends with `;` it does not.

    // Simple heuristic: the fn_line itself (trimmed) often already tells us.
    if fn_line.ends_with('{') {
        return true;
    }
    if fn_line.ends_with(';') {
        return false;
    }

    // Multi-line signature: scan body for this fn and look for `{` vs `;`
    for (i, line) in body.lines().enumerate() {
        if line
            .trim()
            .starts_with(fn_line.split('(').next().unwrap_or(fn_line))
        {
            // Found the fn declaration — scan forward for `{` or `;`
            let remaining = body.lines().skip(i).collect::<Vec<_>>().join("\n");
            let first_brace = remaining.find('{');
            let first_semi_after_sig = remaining.find(';');
            return match (first_brace, first_semi_after_sig) {
                (Some(b), Some(s)) => b < s,
                (Some(_), None) => true,
                _ => false,
            };
        }
    }

    // Couldn't determine — assume it has a default
    true
}

fn extract_brace_content(s: &str) -> &str {
    let start = match s.find('{') {
        Some(pos) => pos + 1,
        None => return "",
    };

    let mut depth = 1;
    let bytes = s.as_bytes();
    let mut i = start;
    while i < bytes.len() && depth > 0 {
        match bytes[i] {
            b'{' => depth += 1,
            b'}' => depth -= 1,
            _ => {}
        }
        i += 1;
    }

    &s[start..i.saturating_sub(1)]
}

/// Parse the `impl Catalog for Theme` block to extract status and default info.
fn parse_catalog_impl(content: &str) -> (bool, DefaultExpr) {
    // Find `impl Catalog for Theme` or `impl Catalog for crate::Theme`
    let impl_pattern = ["impl Catalog for Theme", "impl Catalog for crate::Theme"];

    let impl_start = impl_pattern
        .iter()
        .find_map(|p| content.find(p))
        .unwrap_or(0);

    let impl_body_str = &content[impl_start..];
    let impl_body = extract_brace_content(impl_body_str);

    // Check for Status parameter in style() method
    let has_status = impl_body.contains("status: Status")
        || impl_body.contains("status:") && impl_body.contains("Status");

    // Extract the default expression from `Box::new(...)`
    let default_expr = extract_default_expr(impl_body);

    (has_status, default_expr)
}

fn extract_default_expr(impl_body: &str) -> DefaultExpr {
    // Look for the fn default body — find `Box::new(`
    if let Some(box_pos) = impl_body.find("Box::new(") {
        let after_box = &impl_body[box_pos + "Box::new(".len()..];

        // Find the matching closing paren
        let inner = extract_paren_content(after_box);
        let inner = inner.trim();

        // Check if it's a closure that returns Style::default()
        if inner.contains("Style::default()") {
            return DefaultExpr::StyleDefault;
        }

        // Otherwise it's a named function reference
        // e.g., `primary`, `default`
        if !inner.contains('|') && !inner.contains('{') {
            return DefaultExpr::NamedFn(inner.to_string());
        }

        // It's some closure — treat as StyleDefault if we can't parse it
        DefaultExpr::StyleDefault
    } else {
        DefaultExpr::StyleDefault
    }
}

fn extract_paren_content(s: &str) -> &str {
    let mut depth = 1;
    let bytes = s.as_bytes();
    let mut i = 0;
    while i < bytes.len() && depth > 0 {
        match bytes[i] {
            b'(' => depth += 1,
            b')' => depth -= 1,
            _ => {}
        }
        if depth == 0 {
            return &s[..i];
        }
        i += 1;
    }
    s
}

/// Generate the impl code for a single catalog.
fn generate_impl(catalog: &CatalogInfo) -> Option<String> {
    let module_path = catalog.module_path.join("::");

    match catalog.special {
        SpecialKind::Marker => {
            // Marker trait — empty impl
            let mut code = String::new();
            if let Some(ref feat) = catalog.feature_gate {
                code.push_str(&format!("#[cfg(feature = \"{feat}\")]\n"));
            }
            code.push_str(&format!(
                "impl iced::widget::{module_path}::Catalog for Theme {{}}\n"
            ));
            Some(code)
        }
        SpecialKind::ExtraMethods => {
            // Can't auto-generate — emit a comment reminding the developer
            // to hand-write the impl in iced_compat.rs.
            let mut code = String::new();
            if let Some(ref feat) = catalog.feature_gate {
                code.push_str(&format!("// #[cfg(feature = \"{feat}\")]\n"));
            }
            code.push_str(&format!(
                "// NOTE: iced::widget::{module_path}::Catalog requires a hand-written impl\n\
                 // (has extra methods without defaults). See iced_compat.rs.\n"
            ));
            Some(code)
        }
        SpecialKind::Normal => {
            let path_tokens = catalog.module_path.join("::");
            let mut code = String::new();

            if let Some(ref feat) = catalog.feature_gate {
                code.push_str(&format!("#[cfg(feature = \"{feat}\")]\n"));
            }

            match (&catalog.default_expr, catalog.has_status) {
                (DefaultExpr::NamedFn(name), true) => {
                    code.push_str(&format!("impl_iced_catalog!({path_tokens}, {name});\n"));
                }
                (DefaultExpr::NamedFn(name), false) => {
                    code.push_str(&format!(
                        "impl_iced_catalog!(no_status {path_tokens}, {name});\n"
                    ));
                }
                (DefaultExpr::StyleDefault, true) => {
                    code.push_str(&format!(
                        "impl_iced_catalog!(style_default {path_tokens});\n"
                    ));
                }
                (DefaultExpr::StyleDefault, false) => {
                    code.push_str(&format!(
                        "impl_iced_catalog!(no_status style_default {path_tokens});\n"
                    ));
                }
            }

            Some(code)
        }
    }
}
