use std::cmp::Ordering;

use serde::{Deserialize, Serialize};

pub(crate) type NetworkId = u32;

/// Serde Serializable V2 presence payload. `V2Presence` is the root
/// presence object
///
/// The `V2Presence` payload models an undirected graph.
/// - `peers` = graph vertices
/// - `connections` = graph edges.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct V2Presence {
    /// The local peer's network Id. This can be used to identify the
    /// peer in `peers`.
    pub local_peer: NetworkId,

    /// A set of _all_ peers currently reported by presence (including the
    /// local peer). These form the presence graph vertices.
    ///
    /// 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 peers: Vec<V2Peer>,

    /// A set of all known connections between the `peers`. These
    /// form the presence graph edges.
    pub connections: Vec<V2UndirectedConnection>,
}

/// Serde Serializable V2 presence peer.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct V2Peer {
    /// The `network_id` as found in a peer's announce string. This is not
    /// the same as the `site_id`. Site_ids are unique but network_ids may
    /// have rare collisions and may even be empty for transports which don't
    /// exchange announce data.
    pub id: NetworkId,

    /// The peer's site-id (if known). Note that for local presence
    /// we key by NetworkId rather than SiteId - as we're also interested in
    /// nearby disconnected (and potentially incompatible) peers so we
    /// might not know the SiteId.
    pub site_id: Option<String>,

    /// The peer's public key (if known).
    pubkey: Option<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<V2Os>,

    /// This field is no longer in use and will always be `0`.
    #[serde(default, skip_deserializing)]
    #[deprecated(
        note = "This feature has been removed, this value will always be 0 from SDK release 4.8.0"
    )]
    pub query_overlap_group: u8,

    #[deprecated(
        note = "This feature has been removed, this value will always be 0 from SDK release 4.8.0"
    )]
    #[serde(default, skip_deserializing)]
    pub mesh_role: 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_hydra_connected: 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>,
}

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

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

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

/// 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 V2UndirectedConnection {
    /// 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 network Id. Note that for local presence
    /// we key by NetworkId rather than SiteId - as we're also interested in
    /// nearby disconnected (and potentially incompatible) peers so we
    /// might not know the SiteId.
    pub from: NetworkId,

    /// The peer with the higher network Id. Note that for local presence
    /// we key by NetworkId rather than SiteId - as we're also interested in
    /// nearby disconnected (and potentially incompatible) peers so we
    /// might not know the SiteId.
    pub to: NetworkId,

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

    /// 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 V2UndirectedConnection {}
impl PartialEq<Self> for V2UndirectedConnection {
    fn eq(&self, other: &Self) -> bool {
        self.id == other.id
    }
}

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

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

/// Serde Serializable V2 operating system.
#[derive(PartialEq, Eq, Hash, Clone, Debug, Serialize, Deserialize)]
pub enum V2Os {
    #[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 V2ConnectionType {
    Bluetooth,
    AccessPoint,
    P2PWiFi,
    WebSocket,
}

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

    const V2_PEER_JSON: &str = r#"
            {
                "localPeer": 1,
                "peers": [
                    {
                        "id": 1,
                        "siteId": "1",
                        "deviceName": "local-peer",
                        "os": "macOS",
                        "isHydraConnected": false,
                        "isCompatible": true,
                        "dittoSdkVersion": "1.0.0",
                        "meshRole": 0,
                        "queryOverlapGroup": 0
                    },
                    {
                        "id": 2,
                        "siteId": "2",
                        "deviceName": "device-2",
                        "os": "iOS",
                        "isHydraConnected": false,
                        "isCompatible": true,
                        "dittoSdkVersion": null,
                        "meshRole": 0,
                        "queryOverlapGroup": 0
                    },
                    {
                        "id": 3,
                        "siteId": "3",
                        "deviceName": "device-3",
                        "os": "Android",
                        "isHydraConnected": false,
                        "isCompatible": true,
                        "dittoSdkVersion": "1.0.3",
                        "meshRole": 32,
                        "queryOverlapGroup": 32
                    },
                    {
                        "id": 4,
                        "siteId": "4",
                        "deviceName": "device-4",
                        "os": "Linux",
                        "isHydraConnected": false,
                        "isCompatible": true,
                        "dittoSdkVersion": null
                    }
                ],
                "connections": [
                    {
                        "id": "1<->2:Bluetooth",
                        "from": 1,
                        "to": 2,
                        "connectionType": "Bluetooth",
                        "approximateDistanceInMeters": null
                    },
                    {
                        "id": "1<->3:AccessPoint",
                        "from": 1,
                        "to": 3,
                        "connectionType": "AccessPoint",
                        "approximateDistanceInMeters": null
                    },
                    {
                        "id": "1<->4:WebSocket",
                        "from": 1,
                        "to": 4,
                        "connectionType": "WebSocket",
                        "approximateDistanceInMeters": null
                    }
                ]
            }
        "#;

    #[test]
    fn test_json_parsing() {
        let presence: V2Presence = serde_json::from_str(V2_PEER_JSON).unwrap();
        let peers = presence.peers;

        assert_eq!(peers.len(), 4);

        // Local Peer
        assert_eq!(peers[0].id, 1);
        assert_eq!(peers[0].device_name, "local-peer");
        assert_eq!(peers[0].os, Some(V2Os::MacOS));

        // Remote Peers
        assert_eq!(peers[1].id, 2);
        assert_eq!(peers[1].device_name, "device-2");
        assert_eq!(peers[1].os, Some(V2Os::Ios));

        assert_eq!(peers[2].id, 3);
        assert_eq!(peers[2].device_name, "device-3");
        assert_eq!(peers[2].os, Some(V2Os::Android));

        assert_eq!(peers[3].id, 4);
        assert_eq!(peers[3].device_name, "device-4");
        assert_eq!(peers[3].os, Some(V2Os::Linux));

        #[allow(deprecated)]
        {
            assert_eq!(peers[0].query_overlap_group, 0);
            assert_eq!(peers[1].query_overlap_group, 0);
            assert_eq!(peers[2].query_overlap_group, 0);
            assert_eq!(peers[3].query_overlap_group, 0);
            assert_eq!(peers[0].mesh_role, 0); // 0 in JSON
            assert_eq!(peers[1].mesh_role, 0); // 0 in JSON
            assert_eq!(peers[2].mesh_role, 0); // 32 in JSON
            assert_eq!(peers[3].mesh_role, 0); // Missing in JSON
        }
    }
}
