use core::fmt;

use accesskit::{Node, TreeUpdate};
use bone_app::WindowPoint;
use thiserror::Error;

use crate::scenario::{NodeRef, PointerTarget};

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Candidate {
    pub node: NodeRef,
    pub label: String,
}

impl fmt::Display for Candidate {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{} {:?}", self.node, self.label)
    }
}

fn list(candidates: &[Candidate]) -> String {
    if candidates.is_empty() {
        return "none".to_owned();
    }
    candidates
        .iter()
        .map(ToString::to_string)
        .collect::<Vec<_>>()
        .join(", ")
}

#[derive(Clone, Debug, PartialEq, Eq, Error)]
pub enum ResolveError {
    #[error("node {node} not in tree; labeled nodes: {}", list(.candidates))]
    NodeNotFound {
        node: NodeRef,
        candidates: Vec<Candidate>,
    },
    #[error("node {node} has no bounds")]
    NodeWithoutBounds { node: NodeRef },
    #[error("no node label contains {label:?}; labeled nodes: {}", list(.candidates))]
    LabelNotFound {
        label: String,
        candidates: Vec<Candidate>,
    },
    #[error("label {label:?} is ambiguous; matches: {}", list(.matches))]
    LabelAmbiguous {
        label: String,
        matches: Vec<Candidate>,
    },
}

pub fn resolve_target(
    target: &PointerTarget,
    tree: &TreeUpdate,
) -> Result<WindowPoint, ResolveError> {
    match target {
        PointerTarget::At { x, y } => Ok(WindowPoint::new(*x, *y)),
        PointerTarget::Node(node) => resolve_node(*node, tree),
        PointerTarget::Label(label) => resolve_label(label, tree),
    }
}

fn resolve_node(node: NodeRef, tree: &TreeUpdate) -> Result<WindowPoint, ResolveError> {
    tree.nodes
        .iter()
        .find(|(id, _)| *id == node.node_id())
        .ok_or_else(|| ResolveError::NodeNotFound {
            node,
            candidates: labeled(tree),
        })
        .and_then(|(_, n)| center(node, n))
}

fn resolve_label(needle: &str, tree: &TreeUpdate) -> Result<WindowPoint, ResolveError> {
    let needle_fold = needle.to_lowercase();
    let matches: Vec<(NodeRef, &Node, String)> = tree
        .nodes
        .iter()
        .filter_map(|(id, n)| {
            NodeRef::from_node_id(*id)
                .zip(n.label())
                .filter(|(_, label)| label.to_lowercase().contains(&needle_fold))
                .map(|(node, label)| (node, n, label.to_owned()))
        })
        .collect();
    let exact: Vec<&(NodeRef, &Node, String)> = matches
        .iter()
        .filter(|(_, _, label)| label.to_lowercase() == needle_fold)
        .collect();
    match (matches.as_slice(), exact.as_slice()) {
        ([], _) => Err(ResolveError::LabelNotFound {
            label: needle.to_owned(),
            candidates: labeled(tree),
        }),
        ([(node, n, _)], _) | (_, [(node, n, _)]) => center(*node, n),
        _ => Err(ResolveError::LabelAmbiguous {
            label: needle.to_owned(),
            matches: matches
                .iter()
                .map(|(node, _, label)| Candidate {
                    node: *node,
                    label: label.clone(),
                })
                .collect(),
        }),
    }
}

fn center(node: NodeRef, n: &Node) -> Result<WindowPoint, ResolveError> {
    n.bounds()
        .map(|r| WindowPoint::new((r.x0 + r.x1) * 0.5, (r.y0 + r.y1) * 0.5))
        .ok_or(ResolveError::NodeWithoutBounds { node })
}

fn labeled(tree: &TreeUpdate) -> Vec<Candidate> {
    tree.nodes
        .iter()
        .filter_map(|(id, n)| {
            NodeRef::from_node_id(*id)
                .zip(n.label())
                .map(|(node, label)| Candidate {
                    node,
                    label: label.to_owned(),
                })
        })
        .collect()
}

#[cfg(test)]
mod tests {
    use accesskit::{NodeId, Rect, Role, Tree, TreeId};

    use super::*;

    fn button(label: &str, bounds: Option<Rect>) -> Node {
        let mut node = Node::new(Role::Button);
        node.set_label(label);
        if let Some(rect) = bounds {
            node.set_bounds(rect);
        }
        node
    }

    fn rect(x0: f64, y0: f64, x1: f64, y1: f64) -> Rect {
        Rect { x0, y0, x1, y1 }
    }

    fn tree(nodes: Vec<(NodeId, Node)>) -> TreeUpdate {
        TreeUpdate {
            nodes,
            tree: Some(Tree::new(NodeId(1))),
            tree_id: TreeId::ROOT,
            focus: NodeId(1),
        }
    }

    fn node_ref(raw: u64) -> NodeRef {
        match NodeRef::from_node_id(NodeId(raw)) {
            Some(node) => node,
            None => panic!("raw must be non-zero"),
        }
    }

    fn shell() -> TreeUpdate {
        tree(vec![
            (NodeId(1), Node::new(Role::Window)),
            (
                NodeId(2),
                button("Extrude", Some(rect(10.0, 20.0, 30.0, 40.0))),
            ),
            (
                NodeId(3),
                button("Extrude Cut", Some(rect(40.0, 20.0, 60.0, 40.0))),
            ),
            (
                NodeId(4),
                button("Fillet", Some(rect(70.0, 20.0, 90.0, 40.0))),
            ),
        ])
    }

    fn resolved(target: &PointerTarget, tree: &TreeUpdate) -> WindowPoint {
        match resolve_target(target, tree) {
            Ok(point) => point,
            Err(e) => panic!("resolve failed: {e}"),
        }
    }

    #[test]
    fn coordinates_pass_through() {
        let point = resolved(&PointerTarget::At { x: 12.5, y: 7.0 }, &shell());
        assert_eq!(point, WindowPoint::new(12.5, 7.0));
    }

    #[test]
    fn unique_label_substring_hits_bounds_center() {
        let point = resolved(&PointerTarget::Label("fill".to_owned()), &shell());
        assert_eq!(point, WindowPoint::new(80.0, 30.0));
    }

    #[test]
    fn exact_label_wins_over_substring_ambiguity() {
        let point = resolved(&PointerTarget::Label("Extrude".to_owned()), &shell());
        assert_eq!(point, WindowPoint::new(20.0, 30.0));
    }

    #[test]
    fn ambiguous_label_names_the_matches() {
        let needle = PointerTarget::Label("trude".to_owned());
        match resolve_target(&needle, &shell()) {
            Err(ResolveError::LabelAmbiguous { matches, .. }) => {
                let labels: Vec<&str> = matches.iter().map(|c| c.label.as_str()).collect();
                assert_eq!(labels, vec!["Extrude", "Extrude Cut"]);
            }
            other => panic!("expected ambiguity, got {other:?}"),
        }
    }

    #[test]
    fn missing_label_names_the_candidates() {
        let needle = PointerTarget::Label("Revolve".to_owned());
        match resolve_target(&needle, &shell()) {
            Err(ref err @ ResolveError::LabelNotFound { ref candidates, .. }) => {
                assert_eq!(candidates.len(), 3);
                let message = err.to_string();
                assert!(message.contains("Extrude Cut"), "message was: {message}");
                assert!(message.contains("Fillet"), "message was: {message}");
            }
            other => panic!("expected not-found, got {other:?}"),
        }
    }

    #[test]
    fn node_ref_resolves_to_bounds_center() {
        let point = resolved(&PointerTarget::Node(node_ref(3)), &shell());
        assert_eq!(point, WindowPoint::new(50.0, 30.0));
    }

    #[test]
    fn missing_node_ref_names_the_candidates() {
        match resolve_target(&PointerTarget::Node(node_ref(99)), &shell()) {
            Err(ResolveError::NodeNotFound { candidates, .. }) => {
                assert_eq!(candidates.len(), 3);
            }
            other => panic!("expected not-found, got {other:?}"),
        }
    }

    #[test]
    fn node_without_bounds_is_an_error() {
        let update = tree(vec![
            (NodeId(1), Node::new(Role::Window)),
            (NodeId(2), button("Ghost", None)),
        ]);
        match resolve_target(&PointerTarget::Label("Ghost".to_owned()), &update) {
            Err(ResolveError::NodeWithoutBounds { node }) => assert_eq!(node, node_ref(2)),
            other => panic!("expected missing bounds, got {other:?}"),
        }
    }
}
