use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use std::process::Command;
use target_lexicon::Triple;
use which::which;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolInfo {
    path: PathBuf,
    version: Option<String>,
}

impl ToolInfo {
    pub fn path(&self) -> &Path {
        &self.path
    }

    pub fn version(&self) -> Option<&str> {
        self.version.as_deref()
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Tool {
    name: String,
    info: Option<ToolInfo>,
}

impl Tool {
    pub fn name(&self) -> &String {
        &self.name
    }

    pub fn info(&self) -> Option<&ToolInfo> {
        self.info.as_ref()
    }

    pub fn is_found(&self) -> bool {
        self.info.is_some()
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolchainReport {
    c_compiler: Tool,
    make: Tool,
    cmake: Tool,
    cargo: Tool,
    pkg_config: Tool,
}

impl ToolchainReport {
    pub fn c_compiler(&self) -> &Tool {
        &self.c_compiler
    }
    pub fn make(&self) -> &Tool {
        &self.make
    }
    pub fn cmake(&self) -> &Tool {
        &self.cmake
    }
    pub fn cargo(&self) -> &Tool {
        &self.cargo
    }
    pub fn pkg_config(&self) -> &Tool {
        &self.pkg_config
    }

    pub fn generate() -> Self {
        Self {
            c_compiler: check_c_compiler(),
            make: check_executable("make"),
            cmake: check_executable("cmake"),
            cargo: check_executable("cargo"),
            pkg_config: check_executable("pkg-config"),
        }
    }
}

fn check_executable(name: &str) -> Tool {
    match which(name) {
        Ok(path) => {
            let version = try_get_version(Command::new(&path));

            Tool {
                name: name.to_string(),
                info: Some(ToolInfo { path, version }),
            }
        }
        Err(_) => Tool {
            name: name.to_string(),
            info: None,
        },
    }
}

fn check_c_compiler() -> Tool {
    let host = Triple::host();
    match cc::Build::new()
        .host(&host.to_string())
        .target(&host.to_string())
        .opt_level(2)
        .try_get_compiler()
    {
        Ok(compiler) => {
            let path = compiler.path().to_path_buf();

            let binary = path
                .file_name()
                .map(|n| n.to_string_lossy().to_string())
                .unwrap_or_else(|| "unknown".to_string());

            let name = format!("C compiler ({})", binary);
            let version = try_get_version(Command::new(&path));

            Tool {
                name,
                info: Some(ToolInfo { path, version }),
            }
        }

        Err(_) => Tool {
            name: "C compiler".to_string(),
            info: None,
        },
    }
}

fn try_get_version(mut cmd: Command) -> Option<String> {
    cmd.arg("--version")
        .output()
        .ok()
        .map(|output| String::from_utf8_lossy(&output.stdout).to_string())
        .and_then(|stdout| try_parse_version(&stdout))
}

fn try_parse_version(stdout: &str) -> Option<String> {
    stdout
        .lines()
        .find(|line| !line.trim().is_empty())
        .map(|line| line.trim().to_string())
}

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

    #[test]
    fn test_try_parse_version() {
        let gcc_output = "gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0\nCopyright (C) 2021 Free Software Foundation, Inc.\nThis is free software...";
        assert_eq!(
            try_parse_version(gcc_output),
            Some("gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0".to_string())
        );

        let loose_output = "\n\n   cmake version 3.22.1   \nConfigured safely";
        assert_eq!(
            try_parse_version(loose_output),
            Some("cmake version 3.22.1".to_string())
        );

        assert_eq!(try_parse_version("   \n\n  "), None);
    }

    #[test]
    fn test_live_environment_smoke() {
        let report = ToolchainReport::generate();

        let tools = [
            report.c_compiler(),
            report.make(),
            report.cmake(),
            report.cargo(),
            report.pkg_config(),
        ];

        for tool in tools {
            if let Some(info) = tool.info() {
                assert!(!info.path().as_os_str().is_empty());
            }
        }
    }
}
