mod analyze;
mod codegen;
mod emit;
mod ir;
mod lower;
mod opt;

pub use emit::write_generated_modules;
pub use ir::SrcGenIR;

use shifty::shacl_ir::ShapeIR;

pub struct GeneratedRust {
    pub root: String,
    pub files: Vec<(String, String)>,
    pub binary_files: Vec<(String, Vec<u8>)>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SrcGenBackend {
    Specialized,
    Tables,
}

impl SrcGenBackend {
    pub fn as_str(self) -> &'static str {
        match self {
            SrcGenBackend::Specialized => "specialized",
            SrcGenBackend::Tables => "tables",
        }
    }
}

impl GeneratedRust {
    pub fn to_single_file(&self) -> String {
        use std::collections::HashMap;

        let file_map: HashMap<&str, &str> = self
            .files
            .iter()
            .map(|(name, content)| (name.as_str(), content.as_str()))
            .collect();

        let mut out = String::new();
        out.push_str("// Code generated by shacl-srcgen-compiler. DO NOT EDIT.\n");
        out.push_str("#![allow(unused_variables)]\n");
        out.push_str("#![allow(dead_code)]\n");
        out.push_str("#![allow(unused_mut)]\n");
        out.push_str("#![allow(unused_imports)]\n");
        out.push_str("#![allow(unused_comparisons)]\n");

        for (name, content) in &self.files {
            if name.contains('/') {
                continue;
            }

            if let Some(stem) = name.strip_suffix(".rs") {
                let mut chunk_idx = 0usize;
                let mut wrote_chunks = false;
                loop {
                    let chunk_name = format!("{stem}/chunk_{chunk_idx:04}.rs");
                    let Some(chunk_content) = file_map.get(chunk_name.as_str()) else {
                        break;
                    };
                    wrote_chunks = true;
                    out.push_str(chunk_content);
                    if !chunk_content.ends_with('\n') {
                        out.push('\n');
                    }
                    out.push('\n');
                    chunk_idx += 1;
                }
                if wrote_chunks {
                    continue;
                }
            }

            out.push_str(content);
            if !content.ends_with('\n') {
                out.push('\n');
            }
            out.push('\n');
        }
        out
    }
}

pub fn lower_shape_ir(shape_ir: &ShapeIR) -> Result<SrcGenIR, String> {
    lower::lower_shape_ir(shape_ir)
}

pub fn generate_modules_from_ir_with_backend(
    ir: &SrcGenIR,
    backend: SrcGenBackend,
) -> Result<GeneratedRust, String> {
    let analysis = analyze::analyze(ir);
    let _analysis_counts = (
        analysis.shape_count,
        analysis.component_count,
        analysis.fallback_component_count,
    );
    let mut optimized = ir.clone();
    opt::optimize(&mut optimized);
    codegen::generate_modules(&optimized, backend)
}

pub fn generate_modules_from_shape_ir_with_backend(
    shape_ir: &ShapeIR,
    backend: SrcGenBackend,
) -> Result<GeneratedRust, String> {
    let ir = lower_shape_ir(shape_ir)?;
    generate_modules_from_ir_with_backend(&ir, backend)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::ir::{SrcGenIR, SrcGenMeta, SrcGenNodeShape};

    fn base_ir() -> SrcGenIR {
        SrcGenIR {
            meta: SrcGenMeta {
                compiler_track: "srcgen".to_string(),
                schema_version: 1,
                shape_graph_iri: "urn:shape-graph".to_string(),
                data_graph_iri: "urn:data-graph".to_string(),
                rule_count: 0,
                specialization_ready: false,
            },
            node_shapes: Vec::new(),
            property_shapes: Vec::new(),
            components: Vec::new(),
            rules: Vec::new(),
            fallback_annotations: Vec::new(),
            rule_fallback_annotations: Vec::new(),
            embedded_shape_ir_bincode: vec![1, 2, 3, 4],
        }
    }

    #[test]
    fn emits_shape_ir_binary_payload_when_runtime_fallback_is_planned() {
        let mut ir = base_ir();
        ir.node_shapes.push(SrcGenNodeShape {
            id: 1,
            iri: "urn:shape:unsupported".to_string(),
            target_classes: Vec::new(),
            target_nodes: Vec::new(),
            target_subjects_of: Vec::new(),
            target_objects_of: Vec::new(),
            target_advanced_select_queries: Vec::new(),
            constraints: Vec::new(),
            supported_constraints: Vec::new(),
            fallback_constraints: Vec::new(),
            property_shapes: Vec::new(),
            supported: false,
        });

        let generated = generate_modules_from_ir_with_backend(&ir, SrcGenBackend::Specialized)
            .expect("module generation should succeed");

        assert_eq!(generated.binary_files.len(), 1);
        assert_eq!(generated.binary_files[0].0, "shape_ir.bin");
        assert_eq!(generated.binary_files[0].1, ir.embedded_shape_ir_bincode);
        let run = generated
            .files
            .iter()
            .find_map(|(name, content)| (name == "run.rs").then_some(content))
            .expect("run.rs should exist");
        assert!(run.contains("include_bytes!(\"shape_ir.bin\")"));
    }

    #[test]
    fn omits_shape_ir_binary_payload_when_runtime_fallback_is_not_planned() {
        let mut ir = base_ir();
        ir.node_shapes.push(SrcGenNodeShape {
            id: 1,
            iri: "urn:shape:supported".to_string(),
            target_classes: Vec::new(),
            target_nodes: Vec::new(),
            target_subjects_of: Vec::new(),
            target_objects_of: Vec::new(),
            target_advanced_select_queries: Vec::new(),
            constraints: Vec::new(),
            supported_constraints: Vec::new(),
            fallback_constraints: Vec::new(),
            property_shapes: Vec::new(),
            supported: true,
        });

        let generated = generate_modules_from_ir_with_backend(&ir, SrcGenBackend::Specialized)
            .expect("module generation should succeed");

        assert!(generated.binary_files.is_empty());
        let run = generated
            .files
            .iter()
            .find_map(|(name, content)| (name == "run.rs").then_some(content))
            .expect("run.rs should exist");
        assert!(run.contains("const SRCGEN_EMBEDDED_SHAPE_IR_BIN: &[u8] = b\"\";"));
    }

    #[test]
    fn to_single_file_inlines_chunk_files() {
        let generated = GeneratedRust {
            root: String::new(),
            files: vec![
                (
                    "alpha.rs".to_string(),
                    "include!(\"alpha/chunk_0000.rs\");\ninclude!(\"alpha/chunk_0001.rs\");\n"
                        .to_string(),
                ),
                ("alpha/chunk_0000.rs".to_string(), "fn a() {}\n".to_string()),
                ("alpha/chunk_0001.rs".to_string(), "fn b() {}\n".to_string()),
                ("beta.rs".to_string(), "fn c() {}\n".to_string()),
            ],
            binary_files: Vec::new(),
        };

        let single = generated.to_single_file();
        assert!(single.contains("fn a() {}"));
        assert!(single.contains("fn b() {}"));
        assert!(single.contains("fn c() {}"));
        assert!(!single.contains("include!(\"alpha/chunk_0000.rs\")"));
    }
}
