use std::collections::BTreeMap;
use std::path::{Path, PathBuf};

use accesskit::{Node, NodeId, Rect, Role, Toggled, TreeUpdate};
use serde::{Deserialize, Serialize};
use thiserror::Error;

use crate::scenario::NodeRef;

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct TreeDump {
    pub root: DumpNode,
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct DumpNode {
    #[serde(rename = "ref")]
    pub node: NodeRef,
    pub role: Role,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub label: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub bounds: Option<Rect>,
    pub states: DumpStates,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub children: Vec<DumpNode>,
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct DumpStates {
    pub enabled: bool,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub selected: Option<bool>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub expanded: Option<bool>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub toggled: Option<Toggled>,
}

#[derive(Clone, Debug, PartialEq, Eq, Error)]
pub enum TreeDumpError {
    #[error("tree update carries no root")]
    MissingTree,
    #[error("node {0:?} referenced but absent from update")]
    MissingNode(NodeId),
    #[error("node id zero cannot become a ref")]
    ZeroNodeId,
}

#[derive(Debug, Error)]
pub enum TreeLoadError {
    #[error("read {path}: {error}")]
    Read {
        path: PathBuf,
        error: std::io::Error,
    },
    #[error("parse {path}: {error}")]
    Parse {
        path: PathBuf,
        error: serde_json::Error,
    },
}

impl TreeDump {
    pub fn from_update(update: &TreeUpdate) -> Result<Self, TreeDumpError> {
        let nodes: BTreeMap<NodeId, &Node> =
            update.nodes.iter().map(|(id, node)| (*id, node)).collect();
        let root = update.tree.as_ref().ok_or(TreeDumpError::MissingTree)?.root;
        Ok(Self {
            root: dump_node(root, &nodes)?,
        })
    }

    pub fn load(path: &Path) -> Result<Self, TreeLoadError> {
        let text = std::fs::read_to_string(path).map_err(|error| TreeLoadError::Read {
            path: path.to_path_buf(),
            error,
        })?;
        serde_json::from_str(&text).map_err(|error| TreeLoadError::Parse {
            path: path.to_path_buf(),
            error,
        })
    }

    #[must_use]
    pub fn nodes(&self) -> Vec<&DumpNode> {
        self.root.subtree()
    }

    #[must_use]
    pub fn matching(&self, role: Role, label: &str) -> Vec<&DumpNode> {
        self.nodes()
            .into_iter()
            .filter(|n| n.role == role && n.label.as_deref() == Some(label))
            .collect()
    }

    #[must_use]
    pub fn to_text(&self) -> String {
        lines(&self.root, 0).into_iter().map(|l| l + "\n").collect()
    }
}

impl DumpNode {
    #[must_use]
    pub fn subtree(&self) -> Vec<&Self> {
        core::iter::once(self)
            .chain(self.children.iter().flat_map(Self::subtree))
            .collect()
    }
}

fn dump_node(id: NodeId, nodes: &BTreeMap<NodeId, &Node>) -> Result<DumpNode, TreeDumpError> {
    let node = nodes.get(&id).ok_or(TreeDumpError::MissingNode(id))?;
    let node_ref = NodeRef::from_node_id(id).ok_or(TreeDumpError::ZeroNodeId)?;
    let children = node
        .children()
        .iter()
        .map(|child| dump_node(*child, nodes))
        .collect::<Result<Vec<_>, _>>()?;
    Ok(DumpNode {
        node: node_ref,
        role: node.role(),
        label: node.label().map(str::to_owned),
        bounds: node.bounds(),
        states: DumpStates {
            enabled: !node.is_disabled(),
            selected: node.is_selected(),
            expanded: node.is_expanded(),
            toggled: node.toggled(),
        },
        children,
    })
}

fn lines(node: &DumpNode, depth: usize) -> Vec<String> {
    std::iter::once(line(node, depth))
        .chain(
            node.children
                .iter()
                .flat_map(|child| lines(child, depth + 1)),
        )
        .collect()
}

fn line(node: &DumpNode, depth: usize) -> String {
    [
        Some(format!(
            "{}{} {}",
            "  ".repeat(depth),
            node.node,
            role_token(node.role),
        )),
        node.label.as_ref().map(|label| format!("{label:?}")),
        node.bounds.map(bounds_token),
        (!node.states.enabled).then(|| "disabled".to_owned()),
        node.states.selected.map(|v| format!("selected={v}")),
        node.states.expanded.map(|v| format!("expanded={v}")),
        node.states
            .toggled
            .map(|t| format!("toggled={}", toggled_token(t))),
    ]
    .into_iter()
    .flatten()
    .collect::<Vec<_>>()
    .join(" ")
}

fn role_token(role: Role) -> String {
    serde_json::to_string(&role).map_or_else(
        |_| format!("{role:?}"),
        |quoted| quoted.trim_matches('"').to_owned(),
    )
}

fn bounds_token(rect: Rect) -> String {
    format!("({},{})-({},{})", rect.x0, rect.y0, rect.x1, rect.y1)
}

const fn toggled_token(toggled: Toggled) -> &'static str {
    match toggled {
        Toggled::False => "false",
        Toggled::True => "true",
        Toggled::Mixed => "mixed",
    }
}

#[cfg(test)]
mod tests {
    use accesskit::Toggled;
    use bone_ui::a11y::{AccessNode, AccessTreeBuilder};
    use bone_ui::layout::{LayoutPos, LayoutPx, LayoutRect, LayoutSize};
    use bone_ui::strings::StringTable;
    use bone_ui::widgets::LabelText;
    use bone_ui::{Role, WidgetId, WidgetKey};

    use super::*;

    const RIBBON: WidgetKey = WidgetKey::new("ribbon");
    const BUTTON: WidgetKey = WidgetKey::new("button");

    fn rect(x: f32, y: f32, w: f32, h: f32) -> LayoutRect {
        LayoutRect::new(
            LayoutPos::new(LayoutPx::new(x), LayoutPx::new(y)),
            LayoutSize::new(LayoutPx::new(w), LayoutPx::new(h)),
        )
    }

    fn labeled_button(label: &str) -> AccessNode {
        AccessNode::new(Role::Button).with_label_text(LabelText::Owned(label.to_owned()))
    }

    fn dumped(builder: &AccessTreeBuilder) -> TreeDump {
        let update = builder.build(&StringTable::new(), None);
        match TreeDump::from_update(&update) {
            Ok(dump) => dump,
            Err(e) => panic!("dump failed: {e}"),
        }
    }

    #[test]
    fn dump_nests_children_under_the_window_root() {
        let mut builder = AccessTreeBuilder::new();
        builder.push(
            WidgetId::ROOT.child(RIBBON),
            rect(10.0, 20.0, 20.0, 20.0),
            labeled_button("Extrude").with_toggled(Toggled::True),
        );
        builder.push(
            WidgetId::ROOT.child(BUTTON),
            rect(40.0, 20.0, 20.0, 20.0),
            labeled_button("Fillet").with_disabled(true),
        );
        let dump = dumped(&builder);
        assert_eq!(dump.root.role, Role::Window);
        assert_eq!(dump.root.node, NodeRef::from_widget(WidgetId::ROOT));
        let labels: Vec<Option<&str>> = dump
            .root
            .children
            .iter()
            .map(|c| c.label.as_deref())
            .collect();
        assert_eq!(labels, vec![Some("Extrude"), Some("Fillet")]);
        assert_eq!(dump.root.children[0].states.toggled, Some(Toggled::True));
        assert!(!dump.root.children[1].states.enabled);
    }

    #[test]
    fn refs_stay_stable_across_frames() {
        let extrude = WidgetId::ROOT.child(RIBBON).child_named(BUTTON, "extrude");
        let mut builder = AccessTreeBuilder::new();
        builder.push(
            extrude,
            rect(10.0, 20.0, 20.0, 20.0),
            labeled_button("Extrude"),
        );
        let first = dumped(&builder);
        builder.begin_frame();
        builder.push(
            WidgetId::ROOT.child(BUTTON),
            rect(0.0, 0.0, 5.0, 5.0),
            labeled_button("New"),
        );
        builder.push(
            extrude,
            rect(10.0, 60.0, 20.0, 20.0),
            labeled_button("Extrude"),
        );
        let second = dumped(&builder);
        let ref_of = |dump: &TreeDump, label: &str| {
            dump.root
                .children
                .iter()
                .find(|c| c.label.as_deref() == Some(label))
                .map(|c| c.node)
        };
        assert_eq!(ref_of(&first, "Extrude"), ref_of(&second, "Extrude"));
        assert_eq!(
            ref_of(&first, "Extrude"),
            Some(NodeRef::from_widget(extrude))
        );
    }

    #[test]
    fn text_form_is_one_indented_line_per_node() {
        let mut builder = AccessTreeBuilder::new();
        builder.push(
            WidgetId::ROOT.child(RIBBON),
            rect(10.0, 20.0, 20.0, 20.0),
            labeled_button("Extrude").with_selected(true),
        );
        let dump = dumped(&builder);
        let child = NodeRef::from_widget(WidgetId::ROOT.child(RIBBON));
        let expected = format!(
            "b0feb0feb0feb0fe window\n  {child} button \"Extrude\" (10,20)-(30,40) selected=true\n",
        );
        assert_eq!(dump.to_text(), expected);
    }

    #[test]
    fn json_form_carries_refs_roles_bounds_and_states() {
        let mut builder = AccessTreeBuilder::new();
        builder.push(
            WidgetId::ROOT.child(RIBBON),
            rect(10.0, 20.0, 20.0, 20.0),
            labeled_button("Extrude").with_expanded(false),
        );
        let dump = dumped(&builder);
        let child = NodeRef::from_widget(WidgetId::ROOT.child(RIBBON));
        let value = match serde_json::to_value(&dump) {
            Ok(v) => v,
            Err(e) => panic!("serialize failed: {e}"),
        };
        assert_eq!(
            value,
            serde_json::json!({
                "root": {
                    "ref": "b0feb0feb0feb0fe",
                    "role": "window",
                    "states": { "enabled": true },
                    "children": [{
                        "ref": child.to_string(),
                        "role": "button",
                        "label": "Extrude",
                        "bounds": { "x0": 10.0, "y0": 20.0, "x1": 30.0, "y1": 40.0 },
                        "states": { "enabled": true, "expanded": false },
                    }],
                },
            }),
        );
    }

    #[test]
    fn missing_root_is_a_typed_error() {
        let update = TreeUpdate {
            nodes: vec![],
            tree: None,
            tree_id: accesskit::TreeId::ROOT,
            focus: NodeId(1),
        };
        assert_eq!(
            TreeDump::from_update(&update),
            Err(TreeDumpError::MissingTree),
        );
    }
}
