use crate::{interpreter::DependencyGraph, utils::Ptr};
use std::{
    cell::RefCell,
    collections::HashMap,
    fmt::{Debug, Display, Write},
};

#[derive(Debug)]
pub struct Dependency(pub String, pub LibraryId);

// Was useful for debugging.
impl Display for Dependency {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
        Display::fmt(&self.1 .0, f)?;
        f.write_str(" as ")?;
        f.write_str(&self.0)?;
        f.write_char('\n')
    }
}

#[derive(Debug, Clone, Copy)]
pub struct LibraryId(usize);

#[derive(Debug)]
pub struct Library {
    file: syn::File,
    dependencies: Vec<Dependency>,
}

impl Library {
    pub fn get_file(&self) -> &syn::File {
        &self.file
    }
    pub fn get_dependencies(&self) -> &[Dependency] {
        &self.dependencies
    }
}

#[derive(Debug, Default)]
pub struct Libraries {
    libraries: Vec<Library>,
    root_deps: Vec<Dependency>,
}

impl Libraries {
    fn push(&mut self, library: Library) -> LibraryId {
        let id = self.libraries.len();
        self.libraries.push(library);
        LibraryId(id)
    }
    pub fn get(&self, id: LibraryId) -> &Library {
        &self.libraries[id.0]
    }
    pub fn file_iter(&self) -> impl Iterator<Item = &syn::File> {
        self.libraries.iter().map(Library::get_file)
    }
}

// Was useful for debugging.
impl Display for Libraries {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str("root deps:\n")?;
        for dep in &self.root_deps {
            Display::fmt(&dep, f)?;
        }
        for (i, lib) in self.libraries.iter().enumerate() {
            f.write_str("deps of ")?;
            Display::fmt(&i, f)?;
            f.write_str(":\n")?;
            for dep in lib.get_dependencies() {
                Display::fmt(&dep, f)?;
            }
        }
        Ok(())
    }
}

#[derive(Debug)]
struct Metadata {
    nodes: Vec<cargo_metadata::Node>,
    packages: Vec<cargo_metadata::Package>,
    arcis: cargo_metadata::PackageId,
    name_to_package_id: HashMap<String, cargo_metadata::PackageId>,
    package_id_to_package_idx: HashMap<cargo_metadata::PackageId, usize>,
    package_id_to_node_idx: HashMap<cargo_metadata::PackageId, usize>,
}

impl Metadata {
    fn get_package(&self, package_id: &cargo_metadata::PackageId) -> &cargo_metadata::Package {
        &self.packages[self.package_id_to_package_idx[package_id]]
    }
    fn get_node(&self, package_id: &cargo_metadata::PackageId) -> &cargo_metadata::Node {
        &self.nodes[self.package_id_to_node_idx[package_id]]
    }
    fn is_arcis_library(&self, package_id: &cargo_metadata::PackageId) -> bool {
        self.get_package(package_id)
            .keywords
            .iter()
            .any(|x| x == "arcis-library")
    }
    fn get_root(&self) -> &cargo_metadata::PackageId {
        let package_name = std::env::var("CARGO_PKG_NAME").expect("get_root");
        self.name_to_package_id
            .get(&package_name)
            .expect("Cannot find local package in dependencies.")
    }
    fn check_root(&self) -> bool {
        let package_name = std::env::var("CARGO_PKG_NAME").expect("check_root");
        self.name_to_package_id.contains_key(&package_name)
    }
}

fn build_metadata(metadata: cargo_metadata::Metadata) -> Metadata {
    let Some(resolve) = metadata.resolve else {
        panic!("No resolve found");
    };
    let packages = metadata.packages;
    let nodes = resolve.nodes;
    let package_id_to_node_idx = nodes
        .iter()
        .map(|x| x.id.clone())
        .enumerate()
        .map(|(a, b)| (b, a))
        .collect();
    let package_id_to_package_idx = packages
        .iter()
        .map(|x| x.id.clone())
        .enumerate()
        .map(|(a, b)| (b, a))
        .collect();
    let name_to_package_id: HashMap<String, cargo_metadata::PackageId> = packages
        .iter()
        .map(|x| (x.name.clone(), x.id.clone()))
        .collect();
    let arcis = name_to_package_id
        .get("arcis")
        .cloned()
        .expect("Dependency arcis was not found.");

    Metadata {
        nodes,
        packages,
        arcis,
        name_to_package_id,
        package_id_to_node_idx,
        package_id_to_package_idx,
    }
}

fn compute_dependencies(metadata: &Metadata) -> Libraries {
    fn add_dependency(
        metadata: &Metadata,
        map: &mut HashMap<cargo_metadata::PackageId, LibraryId>,
        libraries: &mut Libraries,
        dep: &cargo_metadata::NodeDep,
    ) -> Option<Dependency> {
        if !metadata.is_arcis_library(&dep.pkg) {
            return None;
        }
        if !dep
            .dep_kinds
            .iter()
            .any(|info| matches!(info.kind, cargo_metadata::DependencyKind::Normal))
        {
            return None;
        }
        match map.get(&dep.pkg) {
            None => {
                let dependencies = metadata
                    .get_node(&dep.pkg)
                    .deps
                    .iter()
                    .flat_map(|x| add_dependency(metadata, map, libraries, x))
                    .collect::<Vec<_>>();
                let package_folder = metadata
                    .get_package(&dep.pkg)
                    .manifest_path
                    .as_str()
                    .strip_suffix("/Cargo.toml")
                    .expect("Package manifest not 'Cargo.toml'");
                let file_path = format!("{}/src/lib.rs", package_folder);
                let file = syn::parse_file(&std::fs::read_to_string(file_path).unwrap()).unwrap();
                let res = libraries.push(Library { file, dependencies });
                map.insert(dep.pkg.clone(), res);
                Some(res)
            }
            Some(v) => Some(*v),
        }
        .map(|l| Dependency(dep.name.clone(), l))
    }
    let mut libraries = Libraries::default();
    let mut map: HashMap<cargo_metadata::PackageId, LibraryId> = HashMap::new();
    let root_node = metadata.get_node(metadata.get_root());
    for dep in root_node.deps.iter() {
        let dep = add_dependency(metadata, &mut map, &mut libraries, dep);
        let Some(dep) = dep else {
            continue;
        };
        libraries.root_deps.push(dep);
    }
    libraries
}

/// All the libraries in Cargo.toml that are arcis libraries.
pub fn get_dependencies() -> Libraries {
    check_metadata();
    get_metadata(compute_dependencies)
}

pub fn libraries_to_dependency_graph(libraries: &Libraries) -> DependencyGraph<'_> {
    let mut graph = HashMap::new();
    for lib in &libraries.libraries {
        graph.insert(Ptr(&lib.file), Vec::new());
    }
    for parent in &libraries.libraries {
        for dep in &parent.dependencies {
            let child = libraries.get(dep.1);
            graph
                .get_mut(&Ptr(&child.file))
                .expect("Dependency graph does not contain a file that it should contain.")
                .push((dep.0.as_str(), Some(Ptr(&parent.file))));
        }
    }
    for dep in &libraries.root_deps {
        let child = libraries.get(dep.1);
        graph
            .get_mut(&Ptr(&child.file))
            .expect("Dependency graph does not contain a file but it should.")
            .push((dep.0.as_str(), None));
    }

    graph
}

fn compute_metadata() -> Metadata {
    let mut metadata = cargo_metadata::MetadataCommand::new();
    if let Ok(manifest_path) = std::env::var("CARGO_MANIFEST_PATH") {
        metadata.manifest_path(manifest_path);
    }
    build_metadata(metadata.exec().unwrap())
}

fn get_metadata<T>(f: impl Fn(&Metadata) -> T) -> T {
    METADATA.with_borrow(f)
}
fn check_metadata() {
    let is_good = get_metadata(|metadata| metadata.check_root());
    if !is_good {
        METADATA.set(compute_metadata());
    }
}
thread_local! {
    static METADATA: RefCell<Metadata> = RefCell::new(compute_metadata());
    static MOCK_LIBRARY: &'static [syn::File] = compute_mock_library().leak();
    static STANDARD_LIBRARY: &'static [syn::File] = compute_standard_library().leak();
}

pub fn is_root_arcis_library() -> bool {
    check_metadata();
    get_metadata(|metadata| metadata.is_arcis_library(metadata.get_root()))
}

pub fn is_root_arcis() -> bool {
    std::env::var("CARGO_PKG_NAME").is_ok_and(|t| t == "arcis")
}

fn compute_standard_library() -> Vec<syn::File> {
    check_metadata();
    get_metadata(|metadata| {
        let package_folder = metadata
            .get_package(&metadata.arcis)
            .manifest_path
            .as_str()
            .strip_suffix("/Cargo.toml")
            .expect("arcis manifest is not 'Cargo.toml'");
        let file_path = format!("{}/src/standard_library", package_folder);
        let files = [
            "rng.rs",
            "math.rs",
            "arcis_ed25519.rs",
            "crypto.rs",
            "publickey.rs",
            "ml.rs",
            "sha3.rs",
        ];
        files
            .into_iter()
            .map(|f| {
                let path = format!("{}/{}", file_path, f);
                let content = std::fs::read_to_string(&path).unwrap();
                syn::parse_file(&content).unwrap()
            })
            .collect()
    })
}
fn compute_mock_library() -> Vec<syn::File> {
    let files = [
        include_str!("mock-library/arcis.rs"),
        include_str!("mock-library/cipher.rs"),
        include_str!("mock-library/float.rs"),
        include_str!("mock-library/integer.rs"),
        include_str!("mock-library/iterator.rs"),
        include_str!("mock-library/pack.rs"),
        include_str!("mock-library/slice.rs"),
        include_str!("mock-library/std.rs"),
    ];
    files
        .into_iter()
        .map(|file| syn::parse_file(file).expect("A mock-library file could not be parsed."))
        .collect()
}

/// The standard library, from /arcis/src/standard_library
/// The standard library will be read both by the interpreter and by rust.
pub fn get_standard_library() -> std::borrow::Cow<'static, [syn::File]> {
    extern crate proc_macro;
    if proc_macro::is_available() {
        std::borrow::Cow::Owned(compute_standard_library())
    } else {
        std::borrow::Cow::Borrowed(STANDARD_LIBRARY.with(|x| *x))
    }
}
/// The mock library, from /arcis-interpreter/src/mock-library
/// The mock library will only be read by the interpreter.
pub fn get_mock_library() -> std::borrow::Cow<'static, [syn::File]> {
    extern crate proc_macro;
    if proc_macro::is_available() {
        std::borrow::Cow::Owned(compute_mock_library())
    } else {
        std::borrow::Cow::Borrowed(MOCK_LIBRARY.with(|x| *x))
    }
}
