use crate::{
    activity::Activity,
    pack_unpack::{pack, unpack},
};
use serde_json::{json, Value};
use std::error::Error;
use uuid::Uuid;

type Result<T> = std::result::Result<T, Box<dyn Error>>;

/// A client that connects to and communicates with the Discord IPC.
pub trait DiscordIpc {
    /// Connects the client to the Discord IPC.
    /// 
    /// This method attempts to first establish a connection,
    /// and then sends a handshake.
    /// 
    /// # Errors
    /// 
    /// Returns an `Err` variant if the client
    /// failed to connect to the socket, or if it failed to
    /// send a handshake.
    /// 
    /// # Examples
    /// ```
    /// let mut client = discord_rich_presence::new_client("<some client id>")?;
    /// client.connect()?;
    /// ```
    fn connect(&mut self) -> Result<()> {
        self.connect_ipc()?;
        self.send_handshake()?;

        Ok(())
    }

    /// Reconnects to the Discord IPC.
    ///
    /// This method closes the client's active connection,
    /// then re-connects it and re-sends a handshake.
    /// 
    /// # Errors
    /// 
    /// Returns an `Err` variant if the client
    /// failed to connect to the socket, or if it failed to
    /// send a handshake.
    /// 
    /// # Examples
    /// ```
    /// let mut client = discord_rich_presence::new_client("<some client id>")?;
    /// client.connect()?;
    /// 
    /// client.close()?;
    /// client.reconnect()?;
    /// ```
    fn reconnect(&mut self) -> Result<()> {
        self.close()?;
        self.connect_ipc()?;
        self.send_handshake()?;

        Ok(())
    }

    #[doc(hidden)]
    fn get_client_id(&self) -> &String;

    #[doc(hidden)]
    fn connect_ipc(&mut self) -> Result<()>;

    /// Handshakes the Discord IPC.
    /// 
    /// This method sends the handshake signal to the IPC.
    /// It is usually not called manually, as it is automatically
    /// called by [`connect`] and/or [`reconnect`].
    /// 
    /// [`connect`]: #method.connect
    /// [`reconnect`]: #method.reconnect
    /// 
    /// # Errors
    /// 
    /// Returns an `Err` variant if sending the handshake failed.
    fn send_handshake(&mut self) -> Result<()> {
        self.send(
            json!({
                "v": 1,
                "client_id": self.get_client_id()
            }),
            0,
        )?;
        // TODO: Return an Err if the handshake is rejected
        self.recv()?;

        Ok(())
    }

    /// Sends JSON data to the Discord IPC.
    /// 
    /// This method takes data (`serde_json::Value`) and
    /// an opcode as its parameters.
    /// 
    /// # Errors
    /// Returns an `Err` variant if writing to the socket failed
    /// 
    /// # Examples
    /// ```
    /// let payload = serde_json::json!({ "field": "value" });
    /// client.send(payload, 0)?;
    /// ```
    fn send(&mut self, data: Value, opcode: u8) -> Result<()> {
        let data_string = data.to_string();
        let header = pack(opcode.into(), data_string.len() as u32)?;

        self.write(&header)?;
        self.write(data_string.as_bytes())?;

        Ok(())
    }

    #[doc(hidden)]
    fn write(&mut self, data: &[u8]) -> Result<()>;

    /// Receives an opcode and JSON data from the Discord IPC.
    /// 
    /// This method returns any data received from the IPC.
    /// It returns a tuple containing the opcode, and the JSON data.
    /// 
    /// # Errors
    /// Returns an `Err` variant if reading the socket was
    /// unsuccessful.
    /// 
    /// # Examples
    /// ```
    /// client.connect_ipc()?;
    /// client.send_handshake()?;
    /// 
    /// println!("{:?}", client.recv()?);
    /// ```
    fn recv(&mut self) -> Result<(u32, Value)> {
        let mut header = [0; 8];

        self.read(&mut header)?;
        let (op, length) = unpack(header.to_vec())?;

        let mut data = vec![0u8; length as usize];
        self.read(&mut data)?;

        let response = String::from_utf8(data.to_vec())?;
        let json_data = serde_json::from_str::<Value>(&response)?;

        Ok((op, json_data))
    }

    #[doc(hidden)]
    fn read(&mut self, buffer: &mut [u8]) -> Result<()>;

    /// Sets a Discord activity.
    /// 
    /// This method is an abstraction of [`send`],
    /// wrapping it such that only an activity payload
    /// is required.
    /// 
    /// [`send`]: #method.send
    /// 
    /// # Errors
    /// Returns an `Err` variant if sending the payload failed.
    fn set_activity(&mut self, activity_payload: Activity) -> Result<()> {
        let data = json!({
            "cmd": "SET_ACTIVITY",
            "args": {
                "pid": std::process::id(),
                "activity": activity_payload
            },
            "nonce": Uuid::new_v4().to_string()
        });
        self.send(data, 1)?;

        Ok(())
    }

    /// Closes the Discord IPC connection. Implementation is dependent on platform.
    fn close(&mut self) -> Result<()>;
}
