dol 0.8.1

DOL (Design Ontology Language) - A declarative specification language for ontology-first development
//! dol-codegen - Generate code from Metal DOL declarations
//!
//! A command-line tool for generating code from DOL files.
//!
//! # Usage
//!
//! ```bash
//! # Generate Rust code from a single file
//! dol-codegen examples/genes/container.exists.dol
//!
//! # Generate TypeScript code
//! dol-codegen --target typescript examples/genes/container.exists.dol
//!
//! # Generate JSON Schema
//! dol-codegen --target jsonschema examples/genes/container.exists.dol
//!
//! # Write output to file
//! dol-codegen -o generated.rs examples/genes/container.exists.dol
//!
//! # Generate from multiple files
//! dol-codegen --recursive examples/
//!
//! # Specify target language
//! dol-codegen --target rust examples/
//! dol-codegen --target typescript examples/
//! dol-codegen --target jsonschema examples/
//! ```

use clap::{Parser, ValueEnum};
use colored::Colorize;
use std::path::PathBuf;
use std::process::ExitCode;

use metadol::codegen::{JsonSchemaCodegen, RustCodegen, TypeScriptCodegen};
use metadol::parse_file_all;

/// Generate code from Metal DOL declarations
#[derive(Parser, Debug)]
#[command(name = "dol-codegen")]
#[command(author, version, about, long_about = None)]
struct Args {
    /// Files or directories to process
    #[arg(required = true)]
    paths: Vec<PathBuf>,

    /// Target language for code generation
    #[arg(short, long, value_enum, default_value = "rust")]
    target: TargetLanguage,

    /// Output file (defaults to stdout)
    #[arg(short, long)]
    output: Option<PathBuf>,

    /// Recursively process directories
    #[arg(short, long)]
    recursive: bool,

    /// Quiet mode: only show errors
    #[arg(short, long)]
    quiet: bool,

    /// Include derive macros (Rust-specific)
    #[arg(long, value_delimiter = ',')]
    derive: Vec<String>,

    /// Generate public items (default)
    #[arg(long, default_value = "true")]
    public: bool,
}

#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
enum TargetLanguage {
    /// Generate Rust code
    Rust,
    /// Generate TypeScript code
    Typescript,
    /// Generate JSON Schema
    Jsonschema,
}

fn main() -> ExitCode {
    let args = Args::parse();

    // Collect all DOL files
    let files = collect_dol_files(&args.paths, args.recursive);

    if files.is_empty() {
        if !args.quiet {
            eprintln!("{}: No .dol files found", "warning".yellow());
        }
        return ExitCode::SUCCESS;
    }

    let mut generated_code = String::new();
    let mut failed = 0;

    // Add header comment based on target (JSON Schema has no comments)
    if args.target != TargetLanguage::Jsonschema {
        let comment_prefix = match args.target {
            TargetLanguage::Rust => "//",
            TargetLanguage::Typescript => "//",
            TargetLanguage::Jsonschema => unreachable!(),
        };
        generated_code.push_str(&format!(
            "{} Generated by dol-codegen from Metal DOL declarations\n",
            comment_prefix
        ));
        generated_code.push_str(&format!(
            "{} Do not edit manually - regenerate from source .dol files\n\n",
            comment_prefix
        ));
    }

    for path in &files {
        match process_file(path, &args) {
            Ok(code) => {
                if !args.quiet {
                    eprintln!("{} {}", "Generated".green(), path.display());
                }
                // JSON Schema doesn't support comments
                if args.target != TargetLanguage::Jsonschema {
                    generated_code.push_str(&format!("// Source: {}\n", path.display()));
                }
                generated_code.push_str(&code);
                generated_code.push_str("\n\n");
            }
            Err(e) => {
                failed += 1;
                eprintln!("{} {}: {}", "Error".red(), path.display(), e);
            }
        }
    }

    if failed > 0 {
        eprintln!("\n{}: {} file(s) failed to process", "error".red(), failed);
        return ExitCode::FAILURE;
    }

    // Post-process: Add missing enum variants for typechecker compatibility
    if args.target == TargetLanguage::Rust {
        // Add missing Type variants
        let type_enum_marker = "pub enum Type {\n";
        if let Some(pos) = generated_code.find(type_enum_marker) {
            let insert_pos = pos + type_enum_marker.len();
            let missing_variants = "    // Additional variants for typechecker compatibility\n    Unit,\n    Never,\n    Gene(String, Vec<String>),\n    List(Box<Type>),\n    Option(Box<Type>),\n    Result(Box<Type>, Box<Type>),\n    Quoted(Box<Type>),\n";
            generated_code.insert_str(insert_pos, missing_variants);
        }

        // Add missing TokenKind variants
        let token_enum_marker = "pub enum TokenKind {\n";
        if let Some(pos) = generated_code.find(token_enum_marker) {
            let insert_pos = pos + token_enum_marker.len();
            let missing_variants = "    // Additional token kinds\n    At,\n";
            generated_code.insert_str(insert_pos, missing_variants);
        }
    }

    // Output the generated code
    match &args.output {
        Some(output_path) => {
            if let Err(e) = std::fs::write(output_path, &generated_code) {
                eprintln!("{}: Failed to write output: {}", "error".red(), e);
                return ExitCode::FAILURE;
            }
            if !args.quiet {
                eprintln!(
                    "\n{} {} ({} files)",
                    "Wrote".green(),
                    output_path.display(),
                    files.len()
                );
            }
        }
        None => {
            print!("{}", generated_code);
        }
    }

    ExitCode::SUCCESS
}

fn collect_dol_files(paths: &[PathBuf], recursive: bool) -> Vec<PathBuf> {
    let mut files = Vec::new();

    for path in paths {
        if path.is_file() {
            if path.extension().is_some_and(|ext| ext == "dol") {
                files.push(path.clone());
            }
        } else if path.is_dir() {
            if recursive {
                collect_dol_files_recursive(path, &mut files);
            } else {
                // Only immediate children
                if let Ok(entries) = std::fs::read_dir(path) {
                    for entry in entries.flatten() {
                        let p = entry.path();
                        if p.is_file() && p.extension().is_some_and(|ext| ext == "dol") {
                            files.push(p);
                        }
                    }
                }
            }
        }
    }

    files.sort();
    files
}

fn collect_dol_files_recursive(dir: &PathBuf, files: &mut Vec<PathBuf>) {
    if let Ok(entries) = std::fs::read_dir(dir) {
        for entry in entries.flatten() {
            let path = entry.path();
            if path.is_dir() {
                collect_dol_files_recursive(&path, files);
            } else if path.extension().is_some_and(|ext| ext == "dol") {
                files.push(path);
            }
        }
    }
}

fn process_file(path: &PathBuf, args: &Args) -> Result<String, String> {
    let source =
        std::fs::read_to_string(path).map_err(|e| format!("Failed to read file: {}", e))?;

    let decls = parse_file_all(&source).map_err(|e| format!("Parse error: {}", e))?;

    // Generate code based on target
    let code = match args.target {
        TargetLanguage::Rust => RustCodegen::generate_all(&decls),
        TargetLanguage::Typescript => TypeScriptCodegen::generate_all(&decls),
        TargetLanguage::Jsonschema => JsonSchemaCodegen::generate_all(&decls),
    };

    Ok(code)
}

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

    #[test]
    fn test_collect_dol_files_empty() {
        let files = collect_dol_files(&[], false);
        assert!(files.is_empty());
    }

    #[test]
    fn test_target_language_variants() {
        assert_eq!(TargetLanguage::Rust, TargetLanguage::Rust);
    }
}