use std::cmp::Ordering;

use serde::{Deserialize, Serialize};

use super::SiteId;

#[derive(Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Debug, Clone, Hash)]
/// DittoPeer identifier
#[serde(rename_all = "camelCase")]
pub struct DittoAddress {
    pub(crate) pubkey: Vec<u8>,
    pub(crate) site_id: Option<SiteId>,
}

/// Graph of all known peers generated by [`Presence`](crate::transport::Presence) methods.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PresenceGraph {
    /// The local peer.
    pub local_peer: Peer,
    /// Note that the peers in this set might not be directly connected to
    /// the local peer. Some peers might be be connected indirectly via a
    /// another peer or even form part of an isolated graph.
    ///
    /// Others yet might be discovered but disconnected due to version
    /// incompatibilities.
    pub remote_peers: Vec<Peer>,
}

/// Peer in the Ditto mesh.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Peer {
    /// An address uniquely representing another device on the Ditto network.
    pub address: DittoAddress,

    /// The peer key is a unique identifier for a given peer, equal to or
    /// derived from the cryptographic public key used to authenticate it.
    ///
    /// NOTE: This will be be empty when a peer is not updated to the latest
    /// version of the SDK.
    pub peer_key: Vec<u8>,

    /// The human-readable device name for a peer. This defaults to the
    /// the hostname but can be manually set by the application developer.
    pub device_name: String,

    /// The operating system of a peer (if known).
    pub os: Option<PresenceOs>,

    /// An optional query overlap group which an app developer might provide
    /// to influence connection priorities. Values can range between
    /// 0-63 (inclusive). Defaults to `0` if not set.
    #[serde(default)]
    pub query_overlap_group: u8,

    /// Flag which indicates if this peer is connected to HyDRA. This is
    /// represented as a simple flag since attempting add HyDRA as a node
    /// to a graph would be extremely convoluted. The presence viewer
    /// depicts the HyDRA connection with a simple cloud icon above a peer.
    pub is_connected_to_ditto_cloud: bool,

    /// A simplified boolean flag indicating whether the is peer is
    /// compatible with our own peer (if known). Note that there _might_
    /// be connections to this peer even if incompatible with our own
    /// peer, provided that some other peers are able to interoperate.
    pub is_compatible: Option<bool>,

    /// The marketing version of the SDK (if known). For instance: `"1.0.3"`.
    pub ditto_sdk_version: Option<String>,

    /// List of current connections between this peer and other peers.
    pub connections: Vec<UndirectedConnection>,
}

impl Eq for Peer {}
impl PartialEq<Self> for Peer {
    fn eq(&self, other: &Self) -> bool {
        self.address.eq(&other.address)
    }
}

impl PartialOrd for Peer {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

impl Ord for Peer {
    fn cmp(&self, other: &Self) -> Ordering {
        self.address.cmp(&other.address)
    }
}

/// Serde Serializable V2 presence undirected connection. These connections
/// indicate P2P connections _only_. A connection to HyDRA is recorded by
/// a simple boolean flag on the peer.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UndirectedConnection {
    /// An string ID in the form `"from<->to:connection_type"`. These ids
    /// are stable: the lower site Id will always be placed first.
    ///
    /// ## Example
    /// "1<->2:Bluetooth"
    pub id: String,

    /// The peer with the lower Peer Key.
    pub peer1: Vec<u8>,

    /// The peer with the higher Peer Key.
    pub peer2: Vec<u8>,

    /// The type of connection. One `UndirectedConnection` will exist
    /// for each connection type that might be active.
    pub connection_type: ConnectionType,

    /// A made-up figure that changes based on RSSI. This may become meaningful
    /// if more accurate range-finding transports are added in future.
    pub approximate_distance_in_meters: Option<f32>,
}

impl Eq for UndirectedConnection {}
impl PartialEq<Self> for UndirectedConnection {
    fn eq(&self, other: &Self) -> bool {
        self.id == other.id
    }
}

impl PartialOrd for UndirectedConnection {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

impl Ord for UndirectedConnection {
    fn cmp(&self, other: &Self) -> Ordering {
        self.id.cmp(&other.id)
    }
}

/// Operating system used by a given peer.
#[derive(PartialEq, Eq, Hash, Clone, Debug, Serialize, Deserialize)]
pub enum PresenceOs {
    #[serde(rename = "Generic")]
    Generic,
    #[serde(rename = "iOS")]
    Ios,
    #[serde(rename = "Android")]
    Android,
    #[serde(rename = "Linux")]
    Linux,
    #[serde(rename = "Windows")]
    Windows,
    #[serde(rename = "macOS")]
    MacOS,
}

/// Defines a simplified connection type between peers for reporting presence
/// info.
///
/// These connections indicate P2P connections _only_. A connection to HyDRA
/// is recorded by a simple boolean flag on the peer.
#[derive(PartialEq, Eq, Hash, Ord, PartialOrd, Clone, Copy, Debug, Serialize, Deserialize)]
pub enum ConnectionType {
    Bluetooth,
    AccessPoint,
    P2PWiFi,
    WebSocket,
}

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

    const V3_PEER_JSON: &str = r#"
            {
                "localPeer": {
                    "address": {"siteId":1, "pubkey":[1]},
                    "deviceName": "local-peer",
                    "peerKey": [1],
                    "os": "macOS",
                    "isConnectedToDittoCloud": false,
                    "isCompatible": true,
                    "dittoSdkVersion": "1.0.0",
                    "meshRole": 0,
                    "queryOverlapGroup": 0,
                    "connections" : [
                        {
                            "id": "01<->02:Bluetooth",
                            "peer1": [1],
                            "peer2": [2],
                            "connectionType": "Bluetooth",
                            "approximateDistanceInMeters": 2.2963063716888428
                        },
                        {
                            "id": "01<->03:AccessPoint",
                            "peer1": [1],
                            "peer2": [3],
                            "connectionType": "AccessPoint",
                            "approximateDistanceInMeters": null
                        },
                        {
                            "id": "01<->04:WebSocket",
                            "peer1": [1],
                            "peer2": [4],
                            "connectionType": "WebSocket",
                            "approximateDistanceInMeters": null
                        }
                    ]
                },
                "remotePeers": [
                    {
                        "address": {"siteId":2, "pubkey":[2]},
                        "peerKey": [2],
                        "deviceName": "device-2",
                        "os": "iOS",
                        "isConnectedToDittoCloud": false,
                        "isCompatible": true,
                        "dittoSdkVersion": null,
                        "meshRole": 0,
                        "queryOverlapGroup": 0,
                        "connections" : [
                            {
                                "id": "01<->02:Bluetooth",
                                "peer1": [1],
                                "peer2": [2],
                                "connectionType": "Bluetooth",
                                "approximateDistanceInMeters": 2.2963063716888428
                            }
                        ]
                    },
                    {
                        "address": {"siteId":3, "pubkey":[3]},
                        "peerKey": [3],
                        "deviceName": "device-3",
                        "os": "Android",
                        "isConnectedToDittoCloud": false,
                        "isCompatible": true,
                        "dittoSdkVersion": "1.0.3",
                        "meshRole": 32,
                        "queryOverlapGroup": 32,
                        "connections" : [
                            {
                                "id": "01<->03:AccessPoint",
                                "peer1": [1],
                                "peer2": [3],
                                "connectionType": "AccessPoint",
                                "approximateDistanceInMeters": null
                            }
                        ]
                    },
                    {
                        "address": {"siteId":4, "pubkey":[4]},
                        "peerKey": [4],
                        "deviceName": "device-4",
                        "os": "Linux",
                        "isConnectedToDittoCloud": false,
                        "isCompatible": true,
                        "dittoSdkVersion": null,
                        "connections" : [
                            {
                                "id": "01<->04:WebSocket",
                                "peer1": [1],
                                "peer2": [4],
                                "connectionType": "WebSocket",
                                "approximateDistanceInMeters": null
                            }
                        ]
                    }
                ]
            }
        "#;

    #[test]
    fn test_json_parsing() {
        let graph: PresenceGraph = serde_json::from_str(V3_PEER_JSON).unwrap();

        // Local Peer
        assert_eq!(graph.local_peer.address.pubkey, vec![1]);
        assert_eq!(graph.local_peer.device_name, "local-peer");
        assert_eq!(graph.local_peer.os, Some(PresenceOs::MacOS));
        assert_eq!(graph.local_peer.query_overlap_group, 0);
        assert_eq!(graph.local_peer.query_overlap_group, 0);
        assert_eq!(graph.local_peer.connections.len(), 3);
        assert_eq!(graph.local_peer.peer_key, vec![1]);

        // Remote Peers

        assert_eq!(graph.remote_peers[0].address.pubkey, vec![2]);
        assert_eq!(graph.remote_peers[0].peer_key, vec![2]);
        assert_eq!(graph.remote_peers[0].device_name, "device-2");
        assert_eq!(graph.remote_peers[0].os, Some(PresenceOs::Ios));
        assert_eq!(graph.remote_peers[0].query_overlap_group, 0);

        assert_eq!(graph.remote_peers[1].address.pubkey, vec![3]);
        assert_eq!(graph.remote_peers[1].peer_key, vec![3]);
        assert_eq!(graph.remote_peers[1].device_name, "device-3");
        assert_eq!(graph.remote_peers[1].os, Some(PresenceOs::Android));
        assert_eq!(graph.remote_peers[1].query_overlap_group, 32);

        assert_eq!(graph.remote_peers[2].address.pubkey, vec![4]);
        assert_eq!(graph.remote_peers[2].peer_key, vec![4]);
        assert_eq!(graph.remote_peers[2].device_name, "device-4");
        assert_eq!(graph.remote_peers[2].os, Some(PresenceOs::Linux));
        assert_eq!(graph.remote_peers[2].query_overlap_group, 0);
    }
}
