use core::panic;
use graph::input::dotgraph::DotGraph;
use graph::prelude::{Graph as OtherGraph, *};
use graph::UndirectedNodeLabeledCsrGraph;
use std::path::Path;
use std::{
    collections::HashMap, convert::TryFrom, fmt::Display, ops::Deref, str::FromStr, time::Instant,
};

use crate::{Config, Error, Filter};

use linereader::LineReader;

type CsrGraph = UndirectedNodeLabeledCsrGraph<usize, usize>;

pub struct Graph {
    graph: CsrGraph,
    neighbor_label_frequencies: Option<Box<[HashMap<usize, usize>]>>,
}

impl Graph {
    delegate::delegate! {
        to self.graph {
            pub fn node_count(&self) -> usize;
            pub fn edge_count(&self) -> usize;
            pub fn degree(&self, node: usize) -> usize;
            pub fn max_degree(&self) -> usize;
            pub fn label(&self, node: usize) -> usize;
            pub fn neighbors(&self, node: usize) -> &[usize];
            pub fn nodes_by_label(&self, label: usize) -> &[usize];
            pub fn label_count(&self) -> usize;
            pub fn max_label(&self) -> usize;
            pub fn max_label_frequency(&self) -> usize;
        }
    }

    pub fn exists(&self, source: usize, target: usize) -> bool {
        self.neighbors(source).binary_search(&target).is_ok()
    }

    pub fn neighbor_label_frequency(&self, node: usize) -> &HashMap<usize, usize> {
        match &self.neighbor_label_frequencies {
            Some(nlfs) => &nlfs[node],
            None => panic!("Neighbor label frequencies have not been loaded."),
        }
    }
}

impl Display for Graph {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "|V|: {}, |E|: {}, |Σ|: {}\nMax Degree: {}, Max Label Frequency: {}",
            self.node_count(),
            self.edge_count(),
            self.label_count(),
            self.max_degree(),
            self.max_label_frequency()
        )
    }
}

impl FromStr for Graph {
    type Err = Error;

    fn from_str(input: &str) -> Result<Self, Error> {
        let reader = LineReader::new(input.as_bytes());
        let dot_graph: DotGraph<usize, usize> = DotGraph::try_from(reader)?;
        let csr_graph: CsrGraph = CsrGraph::from((dot_graph, CsrLayout::Sorted));

        Ok(Graph::from((
            csr_graph,
            LoadConfig::with_neighbor_label_frequency(),
        )))
    }
}

impl From<(CsrGraph, LoadConfig)> for Graph {
    fn from((graph, load_config): (CsrGraph, LoadConfig)) -> Self {
        let neighbor_label_frequencies = if load_config.neighbor_label_frequency {
            Some(neighbor_label_frequencies(&graph).into_boxed_slice())
        } else {
            None
        };

        Self {
            graph,
            neighbor_label_frequencies,
        }
    }
}

fn neighbor_label_frequencies(graph: &CsrGraph) -> Vec<HashMap<usize, usize>> {
    let mut nlfs = Vec::with_capacity(graph.node_count());

    for node in 0..graph.node_count() {
        let mut nlf = HashMap::<usize, usize>::new();

        for &target in graph.neighbors(node) {
            let target_label = graph.label(target);
            let count = nlf.entry(target_label).or_insert(0);
            *count += 1;
        }

        nlfs.push(nlf);
    }

    nlfs
}

pub struct GdlGraph(Graph);

impl Deref for GdlGraph {
    type Target = Graph;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl FromStr for GdlGraph {
    type Err = Error;

    fn from_str(gdl: &str) -> Result<Self, Error> {
        let csr_graph: CsrGraph = GraphBuilder::new().gdl_str::<usize, _>(gdl).build()?;
        let graph = Graph::from((csr_graph, LoadConfig::with_neighbor_label_frequency()));
        Ok(GdlGraph(graph))
    }
}

#[derive(Clone, Copy, Default)]
pub struct LoadConfig {
    neighbor_label_frequency: bool,
}

impl LoadConfig {
    pub fn with_neighbor_label_frequency() -> Self {
        Self {
            neighbor_label_frequency: true,
        }
    }
}

impl From<Config> for LoadConfig {
    fn from(config: Config) -> Self {
        let neighbor_label_frequency = config.filter == Filter::Nlf;

        LoadConfig {
            neighbor_label_frequency,
        }
    }
}

pub fn load(path: &Path, load_config: LoadConfig) -> Result<Graph, Error> {
    println!("Reading from: {:?}", path);
    let start = Instant::now();
    println!("Preparing input: {:?}", start.elapsed());

    let start = Instant::now();
    let csr_graph: CsrGraph = GraphBuilder::new()
        .csr_layout(CsrLayout::Sorted)
        .file_format(graph::input::dotgraph::DotGraphInput::default())
        .path(path)
        .build()?;
    println!("Parsing graph: {:?}", start.elapsed());

    let start = Instant::now();
    let graph = Graph::from((csr_graph, load_config));
    println!("Building graph: {:?}", start.elapsed());

    Ok(graph)
}

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

    #[test]
    fn read_from_slice() {
        let graph = "
        |t 5 6
        |v 0 0 2
        |v 1 1 3
        |v 2 2 3
        |v 3 1 2
        |v 4 2 2
        |e 0 1
        |e 0 2
        |e 1 2
        |e 1 3
        |e 2 4
        |e 3 4
        |"
        .trim_margin()
        .unwrap();

        let graph = graph.parse::<Graph>().unwrap();

        assert_eq!(graph.node_count(), 5);
        assert_eq!(graph.edge_count(), 6);
        assert_eq!(graph.label_count(), 3);

        assert_eq!(graph.max_label(), 2);
        assert_eq!(graph.max_degree(), 3);
        assert_eq!(graph.max_label_frequency(), 2);

        assert_eq!(graph.label(0), 0);
        assert_eq!(graph.label(1), 1);
        assert_eq!(graph.label(2), 2);
        assert_eq!(graph.label(3), 1);
        assert_eq!(graph.label(4), 2);

        assert_eq!(graph.degree(0), 2);
        assert_eq!(graph.degree(1), 3);
        assert_eq!(graph.degree(2), 3);
        assert_eq!(graph.degree(3), 2);
        assert_eq!(graph.degree(4), 2);

        assert_eq!(graph.neighbors(0), &[1, 2]);
        assert_eq!(graph.neighbors(1), &[0, 2, 3]);
        assert_eq!(graph.neighbors(2), &[0, 1, 4]);
        assert_eq!(graph.neighbors(3), &[1, 4]);
        assert_eq!(graph.neighbors(4), &[2, 3]);

        assert!(graph.exists(0, 1));
        assert!(graph.exists(0, 2));
        assert!(!graph.exists(0, 3));
        assert!(graph.exists(3, 4));
        assert!(!graph.exists(3, 2));

        assert_eq!(graph.nodes_by_label(0), &[0]);
        assert_eq!(graph.nodes_by_label(1), &[1, 3]);
        assert_eq!(graph.nodes_by_label(2), &[2, 4]);
    }

    #[test]
    fn read_from_gdl() {
        let graph = "
        |(n0:L0),
        |(n1:L1),
        |(n2:L2),
        |(n3:L1),
        |(n4:L2),
        |(n0)-->(n1),
        |(n0)-->(n2),
        |(n1)-->(n2),
        |(n1)-->(n3),
        |(n2)-->(n4),
        |(n3)-->(n4)
        |"
        .trim_margin()
        .unwrap()
        .parse::<GdlGraph>()
        .unwrap();

        assert_eq!(graph.node_count(), 5);
        assert_eq!(graph.edge_count(), 6);
        assert_eq!(graph.label_count(), 3);

        assert_eq!(graph.max_label(), 2);
        assert_eq!(graph.max_degree(), 3);
        assert_eq!(graph.max_label_frequency(), 2);

        assert_eq!(graph.label(0), 0);
        assert_eq!(graph.label(1), 1);
        assert_eq!(graph.label(2), 2);
        assert_eq!(graph.label(3), 1);
        assert_eq!(graph.label(4), 2);

        assert_eq!(graph.degree(0), 2);
        assert_eq!(graph.degree(1), 3);
        assert_eq!(graph.degree(2), 3);
        assert_eq!(graph.degree(3), 2);
        assert_eq!(graph.degree(4), 2);

        assert_eq!(graph.neighbors(0), &[1, 2]);
        assert_eq!(graph.neighbors(1), &[0, 2, 3]);
        assert_eq!(graph.neighbors(2), &[0, 1, 4]);
        assert_eq!(graph.neighbors(3), &[1, 4]);
        assert_eq!(graph.neighbors(4), &[2, 3]);

        assert!(graph.exists(0, 1));
        assert!(graph.exists(0, 2));
        assert!(!graph.exists(0, 3));
        assert!(graph.exists(3, 4));
        assert!(!graph.exists(3, 2));

        assert_eq!(graph.nodes_by_label(0), &[0]);
        assert_eq!(graph.nodes_by_label(1), &[1, 3]);
        assert_eq!(graph.nodes_by_label(2), &[2, 4]);
    }

    #[test]
    fn neighbor_label_frequencies() {
        let graph = "
        |(n0:L0),
        |(n1:L1),
        |(n2:L2),
        |(n3:L1),
        |(n4:L2),
        |(n0)-->(n1),
        |(n0)-->(n2),
        |(n0)-->(n4),
        |(n1)-->(n2),
        |(n1)-->(n3),
        |(n2)-->(n4),
        |(n3)-->(n4)
        |"
        .trim_margin()
        .unwrap()
        .parse::<GdlGraph>()
        .unwrap();

        assert_eq!(graph.neighbor_label_frequency(0).get(&0), None);
        assert_eq!(graph.neighbor_label_frequency(0).get(&1), Some(&1));
        assert_eq!(graph.neighbor_label_frequency(0).get(&2), Some(&2));
        assert_eq!(graph.neighbor_label_frequency(4).get(&2), Some(&1));
        assert_eq!(graph.neighbor_label_frequency(4).get(&1), Some(&1));
        assert_eq!(graph.neighbor_label_frequency(4).get(&4), None);
    }
}
