use std::{collections::HashMap, fmt::Write as _, hash::Hasher};

use fjadra::{
    Link, Node, PositionX, PositionY, Simulation,
    force::{Collide, SimulationBuilder},
};
use sable_vault::{Note, NoteContextOwned, SharedVault};
use svg::{
    Document, Node as _,
    node::element::{Anchor, Circle, Group, Line, Text},
};
use tera::{Function, Value};

pub(super) struct LinkGraph {
    vault: SharedVault,
}

impl LinkGraph {
    pub(super) const fn new(vault: SharedVault) -> Self {
        Self { vault }
    }
}

impl Function for LinkGraph {
    #[allow(clippy::significant_drop_tightening)]
    fn call(&self, args: &HashMap<String, Value>) -> tera::Result<Value> {
        let Some(note) = args.get("note") else {
            return Err(tera::Error::msg(
                "Function `link_graph` was called without a `note` argument",
            ));
        };

        // TODO: maybe use deserialize the index directly
        let note = match serde_json::from_value::<NoteContextOwned>(note.clone()) {
            Ok(note) => note,
            Err(err) => {
                return Err(tera::Error::msg(format!(
                    "Function `link_graph` received an invalid `note` argument: {err}"
                )));
            }
        };

        // NOTE: clippy::significant_drop_tightening
        let read = self.vault.read();

        // TODO
        let note = read.notes.get(&note.path).unwrap();

        let references = read.get_note_references(note.index).collect::<Vec<_>>();

        Ok(render_node_graph(&note, references.as_slice()).into())
    }
}

trait GraphNode {
    fn name(&self) -> String;
    fn link(&self) -> String;
}

impl GraphNode for NoteContextOwned {
    fn name(&self) -> String {
        self.title.clone()
    }

    fn link(&self) -> String {
        format!("/{}.html", self.path.slug)
    }
}

impl GraphNode for &Note {
    fn name(&self) -> String {
        self.title().to_string()
    }

    fn link(&self) -> String {
        format!("/{}.html", self.path().slug)
    }
}

#[allow(clippy::cast_precision_loss)]
fn render_node_graph<N: GraphNode>(center: &N, nodes: &[N]) -> String {
    let (smallest, largest, simulation) = simulate(nodes);

    let elements = build_svg_elements(center, nodes, &simulation);

    let mut svg = Document::new().set("class", "sable-link-graph").set(
        "viewBox",
        (
            smallest[0] - 10.0,
            smallest[1] - 10.0,
            (largest[0].abs() + smallest[0].abs()).abs() + 20.0,
            (largest[1].abs() + smallest[1].abs()).abs() + 20.0,
        ),
    );

    // for node in elements.connections {
    //     svg.append(node);
    // }
    // for node in elements.nodes {
    //     svg.append(node);
    // }
    // for node in elements.titles {
    //     svg.append(node);
    // }

    // svg.append(svg::node::element::Style::new(elements.style));

    for (i, pos) in simulation.positions().enumerate() {
        if pos != [0.0, 0.0] {
            svg.append(
                Line::new()
                    .set("id", hash(format!("{}-{}-{}-{}", 0.0, 0.0, pos[0], pos[1])))
                    .set("class", "note-connection")
                    .set("x1", 0.0)
                    .set("y1", 0.0)
                    .set("x2", pos[0])
                    .set("y2", pos[1])
                    .set("stroke", "gray"),
            );
        }

        let note = if i == 0 { center } else { &nodes[i - 1] };

        svg.append(
            Group::new()
                .set("id", hash(format!("{}-{}", pos[0], pos[1])))
                .set("class", "note-container")
                .add(
                    Anchor::new()
                        .set("class", "note-link")
                        .set("href", note.link())
                        .add(
                            Circle::new()
                                .set("class", "note-node")
                                .set("r", 1.5)
                                .set("cx", pos[0])
                                .set("cy", pos[1])
                                .set("fill", "black"),
                        ),
                )
                .add(
                    Text::new(note.name())
                        .set("class", "note-text")
                        .set("x", pos[0] + 2.0)
                        .set("y", pos[1] + 0.65),
                ),
        );
    }

    svg.to_string()
}

fn simulate<N: GraphNode>(nodes: &[N]) -> ([f64; 2], [f64; 2], Simulation) {
    let angle = nodes.len() as f64 / 360.0;

    let links = nodes.iter().enumerate().map(|(i, _)| (0, i + 1));

    let mut simulation = SimulationBuilder::default()
        .with_velocity_decay(0.1)
        .build(
            std::iter::once(Node::from([0.0, 0.0]).fixed_position(0.0, 0.0)).chain(
                (0..nodes.len()).enumerate().map(|(i, _)| {
                    Node::from([
                        1.0 * (angle * i as f64).cos(),
                        1.0 * (angle * i as f64).sin(),
                    ])
                }),
            ),
        )
        .add_force("collide", Collide::new().radius(|_| 5.0).iterations(3))
        .add_force(
            "link",
            Link::new(links).strength(1.0).distance(10.0).iterations(10),
        )
        .add_force("x", PositionX::new())
        .add_force("y", PositionY::new());

    simulation.step();

    let mut smallest = [f64::INFINITY, f64::INFINITY];
    let mut largest = [f64::NEG_INFINITY, f64::NEG_INFINITY];

    for node in simulation.positions() {
        if node[0] < smallest[0] {
            smallest[0] = node[0];
        }
        if node[1] < smallest[1] {
            smallest[1] = node[1];
        }
        if node[0] > largest[0] {
            largest[0] = node[0];
        }
        if node[1] > largest[1] {
            largest[1] = node[1];
        }
    }

    (smallest, largest, simulation)
}

#[derive(Default)]
struct Elements {
    ids: Vec<String>,

    connections: Vec<Line>,
    nodes: Vec<Anchor>,
    titles: Vec<Text>,

    style: String,
}

fn build_svg_elements<N: GraphNode>(center: &N, nodes: &[N], simulation: &Simulation) -> Elements {
    let mut elements = Elements::default();

    for (i, pos) in simulation.positions().enumerate() {
        let note_id = hash(format!("{}-{}", pos[0], pos[1]));

        if pos != [0.0, 0.0] {
            let id = hash(format!("{}-{}-{}-{}", 0.0, 0.0, pos[0], pos[1]));

            write!(
                &mut elements.style,
                ".note--{note_id}:hover ~ .note-connection--{note_id} {{}}"
            )
            .expect("failed to write to string");

            elements.connections.push(
                Line::new()
                    .set("id", id)
                    .set(
                        "class",
                        format!("note-connection note-connection--{note_id}"),
                    )
                    .set("x1", 0.0)
                    .set("y1", 0.0)
                    .set("x2", pos[0])
                    .set("y2", pos[1])
                    .set("stroke", "gray"),
            );

            elements.ids.push(note_id.clone());
        }

        let note = if i == 0 { center } else { &nodes[i - 1] };

        elements.nodes.push(
            Anchor::new()
                .set("class", format!("note note--{note_id}"))
                .set("href", note.link())
                .add(
                    Circle::new()
                        .set("class", "note-node")
                        .set("r", 1.5)
                        .set("cx", pos[0])
                        .set("cy", pos[1])
                        .set("fill", "black"),
                ),
        );

        write!(
            &mut elements.style,
            ".note--{note_id}:hover ~ .note-text--{note_id} {{opacity:100}}"
        )
        .expect("failed to write to string");

        elements.titles.push(
            Text::new(note.name())
                .set("class", format!("note-text note-text--{note_id}"))
                .set("x", pos[0] + 2.0)
                .set("y", pos[1] + 0.65),
        );
    }

    elements
}

#[allow(clippy::needless_pass_by_value)]
fn hash(s: String) -> String {
    let mut hasher = rustc_hash::FxHasher::default();

    hasher.write(s.as_bytes());
    hasher.write_usize(s.len());

    data_encoding::BASE64URL_NOPAD.encode(&hasher.finish().to_le_bytes())
}

#[cfg(test)]
mod test_render_node_graph {
    use super::*;

    struct NodeImpl;

    impl GraphNode for NodeImpl {
        fn name(&self) -> String {
            "test".to_string()
        }

        fn link(&self) -> String {
            String::new()
        }
    }

    #[test]
    #[ignore]
    fn simple() {
        let rendered = render_node_graph(&NodeImpl, &[NodeImpl, NodeImpl, NodeImpl]);

        panic!("{rendered}");
    }
}
