//! Just for fun I thought I would add a Discord RPC client to the emulator.
//!
//! This will display information like the current value of PCX, architecture name and
//! GitHub repo links to show off the ISA. Perhaps in the future if we cross-compile to
//! WASM we could include a link to run this software in the browser.
//!
//!
//! # Configuration
//!
//! This may be disabled like so in your `.dsa.emulator.toml` file:
//!
//! ```toml
//! [misc]
//! use_discord_rpc = false
//! ```
//!
//! Alternatively, you can hide this in your Discord settings.

#[cfg(feature = "discord-rpc")]
use std::{path::PathBuf, sync::Arc, time::Duration};

use std::sync::mpsc::{Receiver, Sender};

#[cfg(feature = "discord-rpc")]
use discord_presence::{Client, DiscordError, models::ActivityTimestamps};

use crate::emulator::config::Config;

#[derive(Debug)]
#[cfg(feature = "discord-rpc")]
pub enum RpcClientError {
    Client(DiscordError),
}

#[cfg(feature = "discord-rpc")]
impl std::fmt::Display for RpcClientError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Client(why) => write!(f, "discord RPC error: {why}"),
        }
    }
}

#[cfg(feature = "discord-rpc")]
impl std::error::Error for RpcClientError {}

#[cfg(feature = "discord-rpc")]
impl From<DiscordError> for RpcClientError {
    fn from(err: DiscordError) -> Self {
        Self::Client(err)
    }
}

/// The type of activity the user is currently doing.
#[derive(Debug, Clone)]
#[cfg(feature = "discord-rpc")]
pub enum Activity {
    Idle,
    EditingFile(PathBuf),
}

/// Messages to send over the wire.
#[derive(Debug)]
#[cfg(feature = "discord-rpc")]
pub enum Message {
    /// Sent when we want to update the [`Context`].
    Update(Activity),
    /// Sent when the main program wants to exit.
    Stop,
}

#[cfg(feature = "discord-rpc")]
unsafe impl Send for Message {}

#[derive(Debug, Clone)]
#[cfg(feature = "discord-rpc")]
pub struct RpcClient {
    /// Sends updates to [`Context`] (our state).
    sender: Sender<Message>,
    /// Stored for later cleanup on Drop.
    thread_handle: Option<Arc<std::thread::JoinHandle<()>>>,
}

#[cfg(feature = "discord-rpc")]
impl RpcClient {
    #[expect(clippy::unreadable_literal)]
    /// Sets up the [`RpcClient`].
    pub fn new(
        sender: Sender<Message>,
        reciever: Receiver<Message>,
    ) -> Result<Self, RpcClientError> {
        // TODO: Put client id into a .env file.
        let mut client = discord_presence::Client::new(1384303074088190042);

        let thread_handle = std::thread::spawn(move || {
            client.start();

            eprintln!("INFO: Started Discord RPC client.");

            std::thread::sleep(Duration::from_millis(1000));

            // Recieve updates and do shit.
            for message in &reciever {
                match message {
                    Message::Update(activity) => {
                        Self::handle_activity(&mut client, &activity);
                    }
                    Message::Stop => {
                        eprintln!("INFO: Stopping discord RPC client.");
                        if let Err(why) = client.shutdown() {
                            eprintln!("ERROR: Stopping discord RPC client failed: {why}");
                        }

                        break;
                    }
                }
            }
        });

        Ok(Self {
            sender,
            thread_handle: Some(Arc::new(thread_handle)),
        })
    }

    fn handle_activity(client: &mut Client, activity: &Activity) {
        let current_time = std::time::SystemTime::now();
        let timestamps = ActivityTimestamps::new().start(
            current_time
                .duration_since(std::time::UNIX_EPOCH)
                .expect("Failed to get UNIX timestamp for activity.")
                .as_secs(),
        );

        match activity {
            Activity::Idle => {
                client
                    .set_activity(|act| act.details("Idle").timestamps(|_| timestamps))
                    .expect("TODO: Exponential backoff.");
            }
            Activity::EditingFile(file_path) => {
                client
                    .set_activity(|act| {
                        act.details(format!("Editing file: {}", file_path.display()))
                            .timestamps(|_| timestamps)
                    })
                    .expect("TODO: Exponential backoff.");
            }
        }

        eprintln!("INFO: RPC sent: {activity:?}");
    }

    /// Stops the [`RpcClient`].
    ///
    /// # Panics
    ///
    /// May panic if the reciever was deallocated. This should not happen.
    fn stop(&self) {
        self.sender
            .send(Message::Stop)
            .expect("Failed to send stop message to RPC client.");
    }

    /// Send an update with a given [`Activity`] to the [`RpcClient`].
    ///
    /// # Panics
    ///
    /// May panic if the reciever was deallocated. This should not happen.
    pub fn update(&self, activity: Activity) {
        self.sender
            .send(Message::Update(activity))
            .expect("Failed to send update to RPC client. This should not happen.");
    }
}

// Possibly unneeded but good practice.
#[cfg(feature = "discord-rpc")]
impl Drop for RpcClient {
    fn drop(&mut self) {
        self.stop();

        if let Some(handle) = self.thread_handle.take() {
            if let Some(handle) = Arc::into_inner(handle) {
                let _ = handle.join();
            }
        }
    }
}

/// Stub for when the feature is disabled.
#[cfg(not(feature = "discord-rpc"))]
pub struct RpcClient {}

/// Stub for when the feature is disabled.
#[cfg(not(feature = "discord-rpc"))]
pub enum Message {}

/// Stub for when the feature is disabled.
#[cfg(not(feature = "discord-rpc"))]
pub enum Activity {}

/// Gets the discord [`RpcClient`] or returns None if this has been disabled in the config
/// options.
#[cfg(feature = "config")]
#[allow(clippy::needless_pass_by_value, unused_variables)]
pub fn get_rpc_client_or_none(
    config: &Config,
    rpc_sender: Sender<Message>,
    rpc_reciever: Receiver<Message>,
) -> Result<Option<RpcClient>, Box<dyn std::error::Error + 'static>> {
    #[cfg(not(feature = "discord-rpc"))]
    return Ok(None);

    #[cfg(feature = "discord-rpc")]
    if config.misc.use_discord_rpc {
        Ok(Some(RpcClient::new(rpc_sender, rpc_reciever)?))
    } else {
        Ok(None)
    }
}
