//! JSON Canvas 1.0 data model.
//!
//! Spec: <https://jsoncanvas.org/spec/1.0/>

use serde::Deserialize;

/// Root document. Contains the full set of nodes and edges in a `.canvas` file.
#[derive(Debug, Clone, Deserialize, Default)]
pub struct Canvas {
    #[serde(default)]
    pub nodes: Vec<Node>,
    #[serde(default)]
    pub edges: Vec<Edge>,
}

/// A node, tagged by its `type` field per JSON Canvas 1.0.
#[derive(Debug, Clone, Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum Node {
    Text(TextNode),
    File(FileNode),
    Link(LinkNode),
    Group(GroupNode),
}

impl Node {
    pub fn id(&self) -> &str {
        match self {
            Node::Text(n) => &n.id,
            Node::File(n) => &n.id,
            Node::Link(n) => &n.id,
            Node::Group(n) => &n.id,
        }
    }

    pub fn position(&self) -> (i32, i32) {
        match self {
            Node::Text(n) => (n.x, n.y),
            Node::File(n) => (n.x, n.y),
            Node::Link(n) => (n.x, n.y),
            Node::Group(n) => (n.x, n.y),
        }
    }

    pub fn size(&self) -> (u32, u32) {
        match self {
            Node::Text(n) => (n.width, n.height),
            Node::File(n) => (n.width, n.height),
            Node::Link(n) => (n.width, n.height),
            Node::Group(n) => (n.width, n.height),
        }
    }
}

#[derive(Debug, Clone, Deserialize)]
pub struct TextNode {
    pub id: String,
    pub x: i32,
    pub y: i32,
    pub width: u32,
    pub height: u32,
    pub text: String,
    #[serde(default)]
    pub color: Option<String>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct FileNode {
    pub id: String,
    pub x: i32,
    pub y: i32,
    pub width: u32,
    pub height: u32,
    pub file: String,
    #[serde(default)]
    pub subpath: Option<String>,
    #[serde(default)]
    pub color: Option<String>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct LinkNode {
    pub id: String,
    pub x: i32,
    pub y: i32,
    pub width: u32,
    pub height: u32,
    pub url: String,
    #[serde(default)]
    pub color: Option<String>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct GroupNode {
    pub id: String,
    pub x: i32,
    pub y: i32,
    pub width: u32,
    pub height: u32,
    #[serde(default)]
    pub label: Option<String>,
    #[serde(default)]
    pub background: Option<String>,
    #[serde(default, rename = "backgroundStyle")]
    pub background_style: Option<String>,
    #[serde(default)]
    pub color: Option<String>,
}

#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum Side {
    Top,
    Right,
    Bottom,
    Left,
}

#[derive(Debug, Clone, Deserialize)]
pub struct Edge {
    pub id: String,
    #[serde(rename = "fromNode")]
    pub from_node: String,
    #[serde(default, rename = "fromSide")]
    pub from_side: Option<Side>,
    #[serde(default, rename = "fromEnd")]
    pub from_end: Option<EdgeEnd>,
    #[serde(rename = "toNode")]
    pub to_node: String,
    #[serde(default, rename = "toSide")]
    pub to_side: Option<Side>,
    #[serde(default, rename = "toEnd")]
    pub to_end: Option<EdgeEnd>,
    #[serde(default)]
    pub label: Option<String>,
    #[serde(default)]
    pub color: Option<String>,
}

#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum EdgeEnd {
    None,
    Arrow,
}

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

    #[test]
    fn text_node_round_trip() {
        let json = r#"{"type":"text","id":"a","x":1,"y":2,"width":3,"height":4,"text":"hi"}"#;
        let n: Node = serde_json::from_str(json).unwrap();
        assert_eq!(n.id(), "a");
        assert!(matches!(n, Node::Text(_)));
        assert_eq!(n.position(), (1, 2));
        assert_eq!(n.size(), (3, 4));
    }

    #[test]
    fn file_node_round_trip() {
        let json = r#"{"type":"file","id":"f","x":0,"y":0,"width":1,"height":1,"file":"x.md"}"#;
        let n: Node = serde_json::from_str(json).unwrap();
        match n {
            Node::File(f) => assert_eq!(f.file, "x.md"),
            _ => panic!("expected file"),
        }
    }

    #[test]
    fn link_node_round_trip() {
        let json = r#"{"type":"link","id":"l","x":0,"y":0,"width":1,"height":1,"url":"https://x"}"#;
        let n: Node = serde_json::from_str(json).unwrap();
        match n {
            Node::Link(l) => assert_eq!(l.url, "https://x"),
            _ => panic!("expected link"),
        }
    }

    #[test]
    fn group_node_round_trip() {
        let json = r#"{"type":"group","id":"g","x":0,"y":0,"width":1,"height":1,"label":"L"}"#;
        let n: Node = serde_json::from_str(json).unwrap();
        match n {
            Node::Group(g) => assert_eq!(g.label.as_deref(), Some("L")),
            _ => panic!("expected group"),
        }
    }

    #[test]
    fn edge_with_optional_fields() {
        let json = r#"{"id":"e","fromNode":"a","toNode":"b"}"#;
        let e: Edge = serde_json::from_str(json).unwrap();
        assert_eq!(e.from_node, "a");
        assert!(e.from_side.is_none());
        assert!(e.label.is_none());
    }
}
