use core::ops::Index;
use std::collections::HashMap;
use std::collections::HashSet;

use petgraph::graph::NodeIndex;
use petgraph::prelude::DiGraph;

use crate::types::cooked::Definition;
use alloc::collections::VecDeque;
use alloc::vec;
use petgraph::Direction;

#[derive(Debug, Default)]
pub struct DependencyGraph<'a> {
    graph: DiGraph<&'a str, ()>,
    ids: HashMap<&'a str, NodeIndex>,
}

impl<'a> DependencyGraph<'a> {
    pub fn construct_for(definitions: &'a HashSet<Definition>, roots: &[&'a str]) -> Self {
        let mut graph = Self::default();

        let mut node_queue = VecDeque::from(roots.to_vec());
        let mut edge_queue = vec![];
        while let Some(id) = node_queue.pop_front() {
            if graph.ids.contains_key(id) {
                continue;
            }

            let from = graph.lookup_node(id);
            for dependency in definitions
                .get(id)
                .unwrap_or_else(|| panic!("unknown definition for {id:?}"))
                .dependencies()
            {
                node_queue.push_back(dependency);
                edge_queue.push((from, dependency));
            }
        }

        for (from, to_id) in edge_queue {
            let to = graph.ids.get(to_id).unwrap_or_else(|| {
                todo!("handle component {to_id}: {:?}", &definitions.get(to_id))
            });
            graph.graph.add_edge(from, *to, ());
        }

        graph
    }
}

impl<'a> DependencyGraph<'a> {
    pub fn bottom_up(&self) -> impl Iterator<Item = &'a str> + '_ {
        self.iter().rev()
    }

    pub fn roots(&self) -> impl Iterator<Item = &'a str> + '_ {
        self.graph
            .externals(Direction::Incoming)
            .map(|index| self[index])
    }

    pub fn top_down(&self) -> impl Iterator<Item = &'a str> + '_ {
        self.iter()
    }
}

impl<'a> DependencyGraph<'a> {
    fn iter(&self) -> impl DoubleEndedIterator<Item = &'a str> + '_ {
        let order = petgraph::algo::toposort(&self.graph, None).unwrap();

        order.into_iter().map(move |index| self[index])
    }
}

impl<'a> DependencyGraph<'a> {
    fn lookup_node(&mut self, id: &'a str) -> NodeIndex {
        *self
            .ids
            .entry(id)
            .or_insert_with(|| self.graph.add_node(id))
    }
}

impl<'a> Index<NodeIndex> for DependencyGraph<'a> {
    type Output = &'a str;

    fn index(&self, index: NodeIndex) -> &Self::Output {
        &self.graph[index]
    }
}