use std::collections::{HashMap, HashSet};

use petgraph::{
    dot::Dot,
    stable_graph::{NodeIndex, StableGraph, EdgeIndex},
};

use crate::{breakpoint::BreakPoint, Genome};

#[derive(Debug, Clone)]
struct GenomicGraphParams {
    sens_symb: String,
    rc_symb: String,
    thousand_sep: String,
}

impl Default for GenomicGraphParams {
    fn default() -> Self {
        Self {
            sens_symb: "►".to_string(),
            rc_symb: "◄".to_string(),
            thousand_sep: ",".to_string(),
        }
    }
}

#[derive(Debug, Clone)]
pub struct GenomicGraph {
    g: StableGraph<BreakPoint, String>,
    params: GenomicGraphParams,
}

impl Default for GenomicGraph {
    fn default() -> Self {
        Self {
            g: StableGraph::new(),
            params: GenomicGraphParams::default(),
        }
    }
}

impl GenomicGraph {
    pub fn from_genome(genome: &Genome) -> Self {
        let mut graph = GenomicGraph::default();

        // Load unique breakpoints as graph nodes.
        let mut breakpoints_id = HashMap::new();
        let mut dedup_hash = HashSet::new();
        for contig in genome.contigs() {
            if let Some(mappings) = contig.contig_ref.breakpoints() {
                let bp = BreakPoint::new(mappings);
                if !dedup_hash
                    .contains(&bp.str_sens(&graph.params.sens_symb, &graph.params.rc_symb))
                {
                    let id = graph.g.add_node(bp.clone());
                    breakpoints_id.insert(bp.id.clone(), id);
                    dedup_hash.insert(bp.str_sens(&graph.params.sens_symb, &graph.params.rc_symb));
                    dedup_hash.insert(bp.str_rc(&graph.params.sens_symb, &graph.params.rc_symb));
                }
            }
        }
        graph.draw_edges();
        graph
    }

    pub fn draw_edges(&mut self) {
        // Draw edges between nodes.
        let g_iter = self.g.clone();
        for (prev_bp, a) in g_iter.node_weights().zip(g_iter.node_indices()) {
            for (next, b) in g_iter.node_weights().zip(g_iter.node_indices()) {
                if prev_bp == next {
                    continue;
                }

                // Don't add edge between start and end nodes.
                // if a == start_id && b == end_id || a == end_id && b == start_id {
                //     continue;
                // }

                // A->B.
                if prev_bp.is_next(next) || prev_bp.is_next(&next.rc()) {
                    if self.g.find_edge(a, b).is_none() {
                        self.g.add_edge(a, b, "".to_string());
                    }
                }

                // If the rc of A can follow B, B->A.
                if prev_bp.rc().is_next(&next) || prev_bp.rc().is_next(&next.rc()) {
                    if self.g.find_edge(b, a).is_none() {
                        self.g.add_edge(b, a, "".to_string());
                    }
                }
            }
        }
    }

    pub fn dot_graph(&self) -> String {
        let g = self.g.map(
            |ni, nv| {
                format!(
                    "{} ({})",
                    nv.str_sens(&self.params.sens_symb, &self.params.rc_symb)
                        .to_string(),
                    ni.index() + 1
                )
            },
            |_, v| v.to_string(),
        );
        Dot::new(&g).to_string().replace("\\\"", "")
    }
    pub fn ways(
        &mut self,
        start: (bool, &str, i32),
        end: (bool, &str, i32),
    ) -> (
        GenomicGraph,
        StableGraph<String, String>,
        Vec<Vec<(NodeIndex, bool, BreakPoint, String)>>,
    ) {
        // Create a new working graph.
        let mut gg = self.clone();
        // Add start and end nodes.
        // start node
        let start_bp = BreakPoint::from(start.0, start.1, start.2, "start");
        let start_id = gg.g.add_node(start_bp.clone());

        // end node
        let end_bp = BreakPoint::from(end.0, end.1, end.2, "end");
        let end_id = gg.g.add_node(end_bp.clone());

        // Redraw edges and remove direct edge from start to end.
        gg.draw_edges();
        if let Some(e) = gg.g.find_edge(start_id, end_id) {
            gg.g.remove_edge(e);
        }
        if let Some(e) = gg.g.find_edge(end_id, start_id) {
            gg.g.remove_edge(e);
        }

        // Find ways between start and end nodes.
        let ways = find_ways(&gg.g, start_id, start.0, end_id);

        // Add corresponding Breakpoints and their representating String to ways steps.
        let mut ways: Vec<Vec<(NodeIndex, bool, BreakPoint, String)>> = ways
            .into_iter()
            .map(|way| {
                way.iter()
                    .map(|(node_id, sens)| {
                        let bp = gg.g.node_weight(*node_id).unwrap().to_owned();
                        (*node_id, *sens, bp)
                    })
                    .map(|(id, sens, bp)| {
                        let s = if sens {
                            bp.str_sens(&gg.params.sens_symb, &gg.params.rc_symb)
                        } else {
                            bp.str_rc(&gg.params.sens_symb, &gg.params.rc_symb)
                        };
                        (id, sens, bp, s)
                    })
                    .collect()
            })
            .collect();

        // Sort ways.
        ways.sort_by_key(|way| {
            let s: Vec<String> = way.iter().map(|(_, _, _, s)| s.to_string()).collect();
            s.join("")
        });

        // Dedup ways.
        ways.dedup_by_key(|way| {
            let s: Vec<String> = way.iter().map(|(_, _, _, s)| s.to_string()).collect();
            s.join("")
        });

        // Draw a presentable graph that integrate ways.
        let mut integrated_graph = gg.g.map(
            |node_id, node_val| Vec::new(),
            |edge_id, edge_val| Vec::new(),
        );
        ways.iter().enumerate().for_each(|(i, way)| {
            way.iter().for_each(|(node_id, sens, bp, s)| {
                let v = integrated_graph.node_weight_mut(*node_id).unwrap();
                v.push((s.to_string(), i));
            });
            way.windows(2).for_each(|w| {
                let (left_node_id, left_sens, left_bp, left_s) = &w[0];
                let (right_node_id, right_sens, right_bp, right_s) = &w[1];
                let edge_id = integrated_graph.find_edge(*left_node_id, *right_node_id).unwrap();
                let v = integrated_graph.edge_weight_mut(edge_id).unwrap();
                v.push(i);
            });
        });

        let mut change_edges: Vec<(EdgeIndex, Vec<usize>)> = Vec::new();
        let mut integrated_graph = integrated_graph.map(
            |node_id, node_val| {
                if node_val.len() == 0 {
                    return gg.g.node_weight(node_id).unwrap().str_sens(&gg.params.sens_symb, &gg.params.rc_symb);
                }
                let mut h: HashMap<String, Vec<usize>> = HashMap::new();
                node_val.iter().for_each(|(k, v)| {
                    if let Some(val) = h.get_mut(k) {
                        val.push(*v)
                    } else {
                        h.insert(k.to_string(), vec![*v]);
                    }
                });
                let s: Vec<String> = h.iter()
                    .map(|(k, v)| {
                        let mut v = v.clone();
                        v.sort();
                        let v = v.iter().map(|u| (u + 1).to_string()).collect::<Vec<String>>().join(", ");
                        format!("{k} ({v})")
                }).collect();
                s.join("\n")
            },
            |edge_id, edge_val| {
                change_edges.push((edge_id, edge_val.to_vec()));
                "".to_string()
            },
        );

        for (edge_id, new_edges_val) in change_edges {
            let (left_id, right_id) = integrated_graph.edge_endpoints(edge_id).unwrap();
            integrated_graph.remove_edge(edge_id);
            new_edges_val.iter().for_each(|v| {
                integrated_graph.add_edge(left_id,right_id, (v + 1).to_string());
            })
        }

        // let dot = integrated_graph.dot_graph();
        // println!("{dot}");

        (gg, integrated_graph, ways)
    }
}

fn get_next(
    graph: &StableGraph<BreakPoint, String>,
    ways: Vec<Vec<(NodeIndex, bool)>>,
) -> Vec<Vec<(NodeIndex, bool)>> {
    let mut visited = Vec::new();
    for way in ways.into_iter() {
        let mut possible_next: Vec<(NodeIndex, bool)> = Vec::new();
        let (last_id, last_sens) = way.get(way.len() - 1).unwrap().to_owned();
        let a = graph.node_weight(last_id).unwrap().to_owned();
        let last = if last_sens { a.clone() } else { a.rc() };

        // Check for every neighbors if they can be visited
        for neighbor_id in graph.neighbors(last_id) {
            let b = graph.node_weight(neighbor_id).unwrap().to_owned();
            if last.is_next(&b) {
                possible_next.push((neighbor_id, true));
            }
            if last.is_next(&b.rc()) {
                possible_next.push((neighbor_id, false));
            }
        }

        // If no visitable neighbors continue to the next way
        if possible_next.len() == 0 {
            continue;
        }
        for pn in possible_next {
            // If the possible next node in the way isn't already visited add it as visited
            let mut new_way = way.clone();
            if !new_way
                .iter()
                .map(|(id, _)| id.clone())
                .collect::<Vec<NodeIndex>>()
                .contains(&pn.0)
            {
                new_way.push(pn);
                visited.push(new_way);
            } else {
                // println!("dropping way: {way:?} next : {:?}", pn);
            }
        }
    }
    visited
}

pub fn find_ways(
    graph: &StableGraph<BreakPoint, String>,
    start_id: NodeIndex,
    start_sens: bool,
    end_id: NodeIndex,
) -> Vec<Vec<(NodeIndex, bool)>> {
    let mut reached_end = Vec::new();
    let mut ways = vec![vec![(start_id, start_sens)]];
    loop {
        let new_ways = get_next(graph, ways.clone());
        if new_ways.len() == 0 {
            break;
        }

        ways.clear();
        for new_way in new_ways.iter() {
            let last_id = new_way.last().unwrap().to_owned().0;
            if last_id == end_id {
                reached_end.push(new_way.clone());
            } else {
                ways.push(new_way.clone());
            }
        }
    }
    reached_end
}
