use std::collections::BTreeSet;

use accesskit::{Node, NodeId, Rect, Tree, TreeId, TreeUpdate};

pub use accesskit::{Role, Toggled};

use crate::layout::LayoutRect;
use crate::strings::{StringKey, StringTable};
use crate::widget_id::WidgetId;
use crate::widgets::LabelText;

#[derive(Copy, Clone, Debug, Default, PartialEq)]
pub struct AccessState {
    pub disabled: bool,
    pub selected: Option<bool>,
    pub expanded: Option<bool>,
    pub toggled: Option<Toggled>,
}

#[derive(Copy, Clone, Debug, PartialEq)]
pub struct AccessRange {
    pub value: f64,
    pub min: f64,
    pub max: f64,
    pub step: f64,
}

#[derive(Clone, Debug, PartialEq)]
pub struct AccessNode {
    pub role: Role,
    pub label: Option<LabelText>,
    pub description: Option<StringKey>,
    pub state: AccessState,
    pub range: Option<AccessRange>,
}

impl AccessNode {
    #[must_use]
    pub const fn new(role: Role) -> Self {
        Self {
            role,
            label: None,
            description: None,
            state: AccessState {
                disabled: false,
                selected: None,
                expanded: None,
                toggled: None,
            },
            range: None,
        }
    }

    #[must_use]
    pub fn with_label(self, key: StringKey) -> Self {
        self.with_label_text(LabelText::Key(key))
    }

    #[must_use]
    pub fn with_label_text(self, label: LabelText) -> Self {
        Self {
            label: Some(label),
            ..self
        }
    }

    #[must_use]
    pub fn with_description(self, key: StringKey) -> Self {
        Self {
            description: Some(key),
            ..self
        }
    }

    #[must_use]
    pub fn with_disabled(self, disabled: bool) -> Self {
        Self {
            state: AccessState {
                disabled,
                ..self.state
            },
            ..self
        }
    }

    #[must_use]
    pub fn with_selected(self, selected: bool) -> Self {
        Self {
            state: AccessState {
                selected: Some(selected),
                ..self.state
            },
            ..self
        }
    }

    #[must_use]
    pub fn with_expanded(self, expanded: bool) -> Self {
        Self {
            state: AccessState {
                expanded: Some(expanded),
                ..self.state
            },
            ..self
        }
    }

    #[must_use]
    pub fn with_toggled(self, toggled: Toggled) -> Self {
        Self {
            state: AccessState {
                toggled: Some(toggled),
                ..self.state
            },
            ..self
        }
    }

    #[must_use]
    pub fn with_range(self, range: AccessRange) -> Self {
        Self {
            range: Some(range),
            ..self
        }
    }
}

#[derive(Clone, Debug, PartialEq)]
struct AccessEntry {
    id: WidgetId,
    rect: LayoutRect,
    node: AccessNode,
}

#[derive(Clone, Debug, Default)]
pub struct AccessTreeBuilder {
    entries: Vec<AccessEntry>,
    seen: BTreeSet<WidgetId>,
}

impl AccessTreeBuilder {
    #[must_use]
    pub fn new() -> Self {
        Self::default()
    }

    pub fn begin_frame(&mut self) {
        self.entries.clear();
        self.seen.clear();
    }

    pub fn push(&mut self, id: WidgetId, rect: LayoutRect, node: AccessNode) {
        assert!(
            self.seen.insert(id),
            "AccessTreeBuilder::push duplicate widget id {id:?}; pushes must be unique per frame",
        );
        self.entries.push(AccessEntry { id, rect, node });
    }

    #[must_use]
    pub fn contains(&self, id: WidgetId) -> bool {
        self.seen.contains(&id)
    }

    #[must_use]
    pub fn len(&self) -> usize {
        self.entries.len()
    }

    #[must_use]
    pub fn is_empty(&self) -> bool {
        self.entries.is_empty()
    }

    pub fn ids(&self) -> impl Iterator<Item = WidgetId> + '_ {
        self.entries.iter().map(|e| e.id)
    }

    #[must_use]
    pub fn build(&self, strings: &StringTable, focused: Option<WidgetId>) -> TreeUpdate {
        let root_id = node_id(WidgetId::ROOT);
        let mut root = Node::new(Role::Window);
        let children: Vec<NodeId> = self.entries.iter().map(|e| node_id(e.id)).collect();
        if !children.is_empty() {
            root.set_children(children);
        }
        let nodes = std::iter::once((root_id, root))
            .chain(
                self.entries
                    .iter()
                    .map(|entry| (node_id(entry.id), build_node(strings, entry))),
            )
            .collect();
        TreeUpdate {
            nodes,
            tree: Some(Tree::new(root_id)),
            tree_id: TreeId::ROOT,
            focus: focused.map_or(root_id, node_id),
        }
    }
}

fn node_id(id: WidgetId) -> NodeId {
    NodeId::from(id.raw().get())
}

fn build_node(strings: &StringTable, entry: &AccessEntry) -> Node {
    let mut node = Node::new(entry.node.role);
    node.set_bounds(rect_to_accesskit(entry.rect));
    if let Some(label) = &entry.node.label {
        node.set_label(label.resolve(strings));
    }
    if let Some(key) = entry.node.description {
        node.set_description(strings.resolve(key));
    }
    if entry.node.state.disabled {
        node.set_disabled();
    }
    if let Some(selected) = entry.node.state.selected {
        node.set_selected(selected);
    }
    if let Some(expanded) = entry.node.state.expanded {
        node.set_expanded(expanded);
    }
    if let Some(toggled) = entry.node.state.toggled {
        node.set_toggled(toggled);
    }
    if let Some(range) = entry.node.range {
        node.set_numeric_value(range.value);
        node.set_min_numeric_value(range.min);
        node.set_max_numeric_value(range.max);
        node.set_numeric_value_step(range.step);
    }
    node
}

fn rect_to_accesskit(rect: LayoutRect) -> Rect {
    Rect {
        x0: f64::from(rect.min_x().value()),
        y0: f64::from(rect.min_y().value()),
        x1: f64::from(rect.max_x().value()),
        y1: f64::from(rect.max_y().value()),
    }
}

#[must_use]
pub fn root_node_id() -> NodeId {
    node_id(WidgetId::ROOT)
}

#[must_use]
pub fn widget_node_id(id: WidgetId) -> NodeId {
    node_id(id)
}

#[cfg(test)]
mod tests {
    use accesskit::{Role, Toggled};

    use super::{AccessNode, AccessRange, AccessTreeBuilder};
    use crate::layout::{LayoutPos, LayoutPx, LayoutRect, LayoutSize};
    use crate::strings::{StringKey, StringTable};
    use crate::widget_id::{WidgetId, WidgetKey};

    const LABEL: StringKey = StringKey::new("smoke.label");
    const DESC: StringKey = StringKey::new("smoke.desc");

    fn id(name: &'static str) -> WidgetId {
        WidgetId::ROOT.child(WidgetKey::new(name))
    }

    fn rect() -> LayoutRect {
        LayoutRect::new(
            LayoutPos::new(LayoutPx::new(0.0), LayoutPx::new(0.0)),
            LayoutSize::new(LayoutPx::new(40.0), LayoutPx::new(20.0)),
        )
    }

    #[test]
    #[should_panic(expected = "AccessTreeBuilder::push duplicate widget id")]
    fn push_panics_on_duplicate_id_in_debug() {
        let mut builder = AccessTreeBuilder::new();
        let node = AccessNode::new(Role::Button).with_label(LABEL);
        builder.push(id("a"), rect(), node.clone());
        builder.push(id("a"), rect(), node);
    }

    #[test]
    fn begin_frame_clears() {
        let mut builder = AccessTreeBuilder::new();
        builder.push(id("a"), rect(), AccessNode::new(Role::Button));
        builder.begin_frame();
        assert!(builder.is_empty());
        assert!(!builder.contains(id("a")));
    }

    #[test]
    fn build_emits_root_with_children() {
        let mut builder = AccessTreeBuilder::new();
        builder.push(
            id("a"),
            rect(),
            AccessNode::new(Role::Button).with_label(LABEL),
        );
        builder.push(
            id("b"),
            rect(),
            AccessNode::new(Role::CheckBox)
                .with_label(LABEL)
                .with_toggled(Toggled::True),
        );
        let strings = StringTable::from_entries([(LABEL, "Save".to_owned())]);
        let update = builder.build(&strings, Some(id("b")));
        assert_eq!(update.nodes.len(), 3);
        assert!(update.tree.is_some());
        let (root_id, root_node) = &update.nodes[0];
        assert_eq!(*root_id, super::root_node_id());
        assert_eq!(root_node.children().len(), 2);
        assert_eq!(update.focus, super::widget_node_id(id("b")));
    }

    #[test]
    fn build_resolves_label_through_string_table() {
        let mut builder = AccessTreeBuilder::new();
        builder.push(
            id("a"),
            rect(),
            AccessNode::new(Role::Button)
                .with_label(LABEL)
                .with_description(DESC),
        );
        let strings = StringTable::from_entries([
            (LABEL, "Save".to_owned()),
            (DESC, "Persist current part".to_owned()),
        ]);
        let update = builder.build(&strings, None);
        let (_, button) = &update.nodes[1];
        assert_eq!(button.label(), Some("Save"));
        assert_eq!(button.description(), Some("Persist current part"));
    }

    #[test]
    fn build_emits_numeric_value_for_ranges() {
        let mut builder = AccessTreeBuilder::new();
        builder.push(
            id("s"),
            rect(),
            AccessNode::new(Role::Slider)
                .with_label(LABEL)
                .with_range(AccessRange {
                    value: 5.0,
                    min: 0.0,
                    max: 10.0,
                    step: 1.0,
                }),
        );
        let update = builder.build(StringTable::empty(), None);
        let (_, slider) = &update.nodes[1];
        assert_eq!(slider.numeric_value(), Some(5.0));
        assert_eq!(slider.min_numeric_value(), Some(0.0));
        assert_eq!(slider.max_numeric_value(), Some(10.0));
        assert_eq!(slider.numeric_value_step(), Some(1.0));
    }

    #[test]
    fn build_marks_disabled_state() {
        let mut builder = AccessTreeBuilder::new();
        builder.push(
            id("a"),
            rect(),
            AccessNode::new(Role::Button)
                .with_label(LABEL)
                .with_disabled(true),
        );
        let update = builder.build(StringTable::empty(), None);
        let (_, node) = &update.nodes[1];
        assert!(node.is_disabled());
    }

    #[test]
    fn empty_builder_emits_root_only_focus_falls_back() {
        let builder = AccessTreeBuilder::new();
        let update = builder.build(StringTable::empty(), None);
        assert_eq!(update.nodes.len(), 1);
        assert_eq!(update.focus, super::root_node_id());
    }
}
