use std::{
    fs::{self, FileType},
    path::{Path, PathBuf},
};

use dependency_graph::{DependencyGraph, Node, Step};

use crate::{
    build::cargo::{CargoBuilder, ModuleManifest},
    env::BuildEnv,
    error::Error,
};

use super::cargo::{ModuleDependency, ModuleInfo};

pub fn build_module(env: &BuildEnv, info: &ModuleInfo) -> Result<PathBuf, Error> {
    log::info!("Building module: {:?}", info.manifest.name);

    CargoBuilder::Module(env, info).build(&info.manifest_dir)?;

    Ok(info.manifest_dir.join(format!(
        "target/{}/{}/lib{}.so",
        env.arch.kernel_triple(),
        env.profile.name(),
        info.module_dir_name.display()
    )))
}

impl Node for ModuleInfo {
    type DependencyType = (String, String, ModuleDependency);

    fn matches(&self, dependency: &Self::DependencyType) -> bool {
        let (_, dep_name, dep_info) = dependency;

        let version_match = dep_info
            .version
            .as_ref()
            .map(|v| v.matches(&self.manifest.version))
            .unwrap_or(true);
        let name_match = dep_name == &self.manifest.name;

        name_match && version_match
    }

    fn dependencies(&self) -> &[Self::DependencyType] {
        &self.dependencies
    }
}

pub fn collect_module_list<P: AsRef<Path>>(dir: P) -> Result<Vec<ModuleInfo>, Error> {
    let mut module_list = vec![];
    for dir in fs::read_dir(dir)? {
        let dir = dir?;
        let manifest = dir.path().join("module.toml");

        if !dir
            .file_type()
            .as_ref()
            .map(FileType::is_dir)
            .unwrap_or(false)
            || !manifest.exists()
        {
            continue;
        }

        let manifest: ModuleManifest = toml::from_str(&fs::read_to_string(&manifest)?)
            .map_err(|e| Error::TomlParseError(manifest, e))?;

        module_list.push(ModuleInfo {
            module_dir_name: dir.file_name().to_str().unwrap().into(),
            dependencies: manifest
                .dependencies
                .iter()
                .map(|(k, v)| (manifest.name.clone(), k.clone(), v.clone()))
                .collect(),
            manifest,
            manifest_dir: dir.path(),
        });
    }
    Ok(module_list)
}

pub fn build_modules<P: AsRef<Path>>(env: &BuildEnv, dir: P) -> Result<Vec<PathBuf>, Error> {
    let modules = collect_module_list(dir)?;
    let graph = DependencyGraph::from(&modules[..]);
    let mut any_unresolved = false;
    let mut build_steps = vec![];
    for entry in graph {
        match entry {
            Step::Unresolved((name, dep_name, dep)) => {
                log::error!("{:?}: unresolved dependency: {:?}", name, dep_name);
                if let Some(version) = dep.version.as_ref() {
                    log::error!("\tCould not satisfy required version: {}", version);
                }
                any_unresolved = true;
            }
            Step::Resolved(package) => {
                build_steps.push(package);
            }
        }
    }
    if any_unresolved {
        return Err(Error::UnresolvedModuleDependencies);
    }

    let built = build_steps
        .into_iter()
        .map(|entry| build_module(env, entry))
        .collect::<Result<Vec<_>, _>>()?;

    Ok(built)
}
