use std::{collections::HashMap, ffi::OsStr, io::Write, os::unix::ffi::OsStrExt, path::{Path, PathBuf}, process::ExitCode};
use camino::Utf8PathBuf;
use clap::Parser;
use mclangc;

#[macro_use]
mod logger;

/// Testing program for mclangc, taken inspiration from porth, which was made by tsoding :3
#[derive(Debug, clap::Parser)]
#[command(version, about, long_about = None)]
struct CliArgs {
    /// Path to the test folder
    #[arg(long, short, default_value="./tests")]
    path: Utf8PathBuf,
    #[clap(subcommand)]
    cmd: CliCmd
}

#[derive(Debug, clap::Subcommand)]
pub enum CliCmd {
    /// Run the tests
    Run,
    /// Run the tests and set the output as the expected output
    Compile
}

struct CollectedFiles {
    tokeniser: HashMap<String, (String, ExpTyp)>,
    parser: HashMap<String, (String, ExpTyp)>,
}

enum ExpTyp {
    Text((PathBuf, String)),
    Path(PathBuf),
}

impl ExpTyp {
    pub fn path(&self) -> &Path {
        match self {
            Self::Text((p, _)) => p,
            Self::Path(p) => p,
        }
    }
}

fn collect_files_for_single_type(path: &Path) -> anyhow::Result<HashMap<String, (String, ExpTyp)>> {
    let mut files = HashMap::new();
    for file in path.read_dir()? {
        let file = file?;
        if file.file_type()?.is_file() {
            if file.path().extension() != Some(OsStr::from_bytes(b"mcl")) {
                continue;
            }
            let src = std::fs::read_to_string(file.path())?;
            let exp_p = file.path().with_extension("exp");
            let name = file.path().with_extension("").file_name().unwrap().to_string_lossy().to_string();
            if exp_p.exists() {
                let exp = std::fs::read_to_string(&exp_p)?;
                files.insert(name, (src, ExpTyp::Text((exp_p, exp))));
            } else {
                files.insert(name, (src, ExpTyp::Path(exp_p)));
            }
        }
    }
    Ok(files)
}

fn collect_all_files(path: &Path) -> anyhow::Result<CollectedFiles> {
    let path = path.to_path_buf();
    let mut tkn = path.clone();
    tkn.push("tokeniser");
    let mut parser = path.clone();
    parser.push("parser");
    Ok(CollectedFiles {
        tokeniser: collect_files_for_single_type(&tkn)?,
        parser: collect_files_for_single_type(&parser)?,
    })
}

fn test_tokeniser(cf: &CollectedFiles, compile: bool) -> anyhow::Result<usize> {
    let mut err_count = 0;
    for (name, (src, expected)) in &cf.tokeniser {
        let tokens = match mclangc::tokeniser::tokenise(src, &format!("tokeniser/{name}.mcl")) {
            Ok(v) => v,
            Err(e) => {
                crate::error!("Test tokeniser/{name} had an error: {e}");
                err_count += 1;
                continue;
            }
        };
        if compile {
            let path = expected.path();
            if path.exists() {
                crate::info!("Test tokeniser/{name} already has a *.exp file, overwriting");
            } else {
                crate::info!("Test tokeniser/{name} doesnt a *.exp file, creating");
            }
            let mut fp = std::fs::File::options()
                .write(true)
                .truncate(true)
                .create(true)
                .open(path)?;
            write!(fp, "{tokens:#?}")?;
        } else {
            let ExpTyp::Text((_, exp)) = expected else {
                crate::warn!("Test tokeniser/{name} doesnt have a *.exp file, please make it by running 'test compile'");
                continue;
            };
            if format!("{tokens:#?}") == *exp {
                crate::info!("Test tokeniser/{name}: OK");
            } else {
                crate::error!("Test tokeniser/{name}: FAIL");
                crate::debug!("Expected: {exp}");
                crate::debug!("Got: {tokens:#?}");
                err_count += 1;
            }
        } 
    }

    
    Ok(err_count)
}

fn test_parser(cf: &CollectedFiles, compile: bool) -> anyhow::Result<usize> {
    let mut err_count = 0;
    for (name, (src, expected)) in &cf.parser {
        let tokens = match mclangc::tokeniser::tokenise(src, &format!("parser/{name}.mcl")) {
            Ok(v) => v,
            Err(e) => {
                crate::error!("Test parser/{name} had an error: {e}");
                err_count += 1;
                continue;
            }
        };
        let ast = match mclangc::parser::parse_program(tokens) {
            Ok(v) => v,
            Err(e) => {
                crate::error!("Test parser/{name} had an error: {e}");
                err_count += 1;
                continue;
            }
        };
        if compile {
            let path = expected.path();
            if path.exists() {
                crate::info!("Test parser/{name} already has a *.exp file, overwriting");
            } else {
                crate::info!("Test parser/{name} doesnt a *.exp file, creating");
            }
            let mut fp = std::fs::File::options()
                .write(true)
                .truncate(true)
                .create(true)
                .open(path)?;
            write!(fp, "{ast:#?}")?;
        } else {
            let ExpTyp::Text((_, exp)) = expected else {
                crate::warn!("Test parser/{name} doesnt have a *.exp file, please make it by running 'test compile'");
                continue;
            };
            if format!("{ast:#?}") == *exp {
                crate::info!("Test parser/{name}: OK");
            } else {
                crate::error!("Test parser/{name}: FAIL");
                crate::debug!("Expected: {exp}");
                crate::debug!("Got: {ast:#?}");
                err_count += 1;
            }
        } 
    }

    Ok(err_count)
}

fn test(cf: &CollectedFiles, compile: bool) -> anyhow::Result<usize> {
   let mut err_count = test_tokeniser(&cf, compile)?;
    err_count += test_parser(&cf, compile)?;

    Ok(err_count)
}

fn main() -> ExitCode {
    let cli = CliArgs::parse();
    let cf = match collect_all_files(cli.path.as_std_path()) {
        Ok(v) => v,
        Err(e) => {
            crate::error!("Failed to read directory '{}', do you have permission to read it?: {e}", cli.path);
            return ExitCode::FAILURE;
        }
    };
    let ec = match cli.cmd {
        CliCmd::Run => {
            match test(&cf, false) {
                Ok(v) => v,
                Err(e) => {
                    crate::error!("Had an error: {e}");
                    return ExitCode::FAILURE;
                }
            }
        }
        CliCmd::Compile => {
            match test(&cf, true) {
                Ok(v) => v,
                Err(e) => {
                    crate::error!("Had an error: {e}");
                    return ExitCode::FAILURE;
                }
            }
        }
    };

    if ec > 0 {
        crate::error!("Testing FAILED, had {ec} errors");
        return ExitCode::FAILURE;
    } else {
        crate::info!("Testing SUCCEEDED, had 0 errors");
    }
    ExitCode::SUCCESS
}