//! Provides an interface for building activities to send
//! to Discord via [`DiscordIpc::set_activity`](crate::DiscordIpc::set_activity).
use serde_derive::Serialize;
use serde_repr::Serialize_repr;
use std::borrow::Cow;

/// A struct representing a Discord rich presence activity
///
/// Note that all methods return `Self`, and can be chained
/// for fluency
#[derive(Serialize, Clone)]
pub struct Activity<'a> {
    #[serde(skip_serializing_if = "Option::is_none")]
    name: Option<Cow<'a, str>>,

    #[serde(skip_serializing_if = "Option::is_none")]
    state: Option<Cow<'a, str>>,

    #[serde(skip_serializing_if = "Option::is_none")]
    state_url: Option<Cow<'a, str>>,

    #[serde(skip_serializing_if = "Option::is_none")]
    details: Option<Cow<'a, str>>,

    #[serde(skip_serializing_if = "Option::is_none")]
    details_url: Option<Cow<'a, str>>,

    #[serde(skip_serializing_if = "Option::is_none")]
    timestamps: Option<Timestamps>,

    #[serde(skip_serializing_if = "Option::is_none")]
    party: Option<Party<'a>>,

    #[serde(skip_serializing_if = "Option::is_none")]
    assets: Option<Assets<'a>>,

    #[serde(skip_serializing_if = "Option::is_none")]
    secrets: Option<Secrets<'a>>,

    #[serde(skip_serializing_if = "Option::is_none")]
    buttons: Option<Vec<Button<'a>>>,

    #[serde(skip_serializing_if = "Option::is_none", rename = "type")]
    activity_type: Option<ActivityType>,

    #[serde(skip_serializing_if = "Option::is_none")]
    status_display_type: Option<StatusDisplayType>,
}

/// A struct representing an `Activity`'s timestamps
///
/// For `ActivityType::Listening` and `ActivityType::Watching`,
/// including both `start` and `end` timestamps will display
/// a time bar
///
/// Note that all methods return `Self`, and can be chained
/// for fluency
#[derive(Serialize, Clone)]
pub struct Timestamps {
    /// Unix time (in milliseconds) of when the activity started
    #[serde(skip_serializing_if = "Option::is_none")]
    start: Option<i64>,

    /// Unix time (in milliseconds) of when the activity ends
    #[serde(skip_serializing_if = "Option::is_none")]
    end: Option<i64>,
}

/// A struct representing an `Activity`'s game party
///
/// Note that all methods return `Self`, and can be chained
/// for fluency
#[derive(Serialize, Clone)]
pub struct Party<'a> {
    #[serde(skip_serializing_if = "Option::is_none")]
    id: Option<Cow<'a, str>>,

    #[serde(skip_serializing_if = "Option::is_none")]
    size: Option<[i32; 2]>,
}

/// A struct representing the art assets and hover text
/// used by an `Activity`
///
/// Note that all methods return `Self`, and can be chained
/// for fluency
#[derive(Serialize, Clone)]
pub struct Assets<'a> {
    #[serde(skip_serializing_if = "Option::is_none")]
    large_image: Option<Cow<'a, str>>,

    #[serde(skip_serializing_if = "Option::is_none")]
    large_text: Option<Cow<'a, str>>,

    #[serde(skip_serializing_if = "Option::is_none")]
    large_url: Option<Cow<'a, str>>,

    #[serde(skip_serializing_if = "Option::is_none")]
    small_image: Option<Cow<'a, str>>,

    #[serde(skip_serializing_if = "Option::is_none")]
    small_text: Option<Cow<'a, str>>,

    #[serde(skip_serializing_if = "Option::is_none")]
    small_url: Option<Cow<'a, str>>,
}

/// A struct representing the secrets used by an
/// `Activity`
///
/// Note that all methods return `Self`, and can be chained
/// for fluency
#[derive(Serialize, Clone)]
pub struct Secrets<'a> {
    #[serde(skip_serializing_if = "Option::is_none")]
    join: Option<Cow<'a, str>>,

    #[serde(skip_serializing_if = "Option::is_none")]
    spectate: Option<Cow<'a, str>>,

    #[serde(skip_serializing_if = "Option::is_none")]
    r#match: Option<Cow<'a, str>>,
}

/// A struct representing the buttons that are
/// attached to an `Activity`
///
/// An activity may have a maximum of 2 buttons
#[derive(Serialize, Clone)]
pub struct Button<'a> {
    label: Cow<'a, str>,
    url: Cow<'a, str>,
}

/// A struct to set the Activity Type of the `Activity`
#[derive(Serialize_repr, Clone)]
#[repr(u8)]
pub enum ActivityType {
    /// Activity type "Playing X"
    Playing = 0,
    /// Activity type "Listening to X"
    Listening = 2,
    /// Activity type "Watching X"
    Watching = 3,
    /// Activity type "Competing in X"
    Competing = 5,
}

/// A struct to set the Status Display Type of the `Activity`
/// Controls which field is displayed in the user's status text in the member list
#[derive(Serialize_repr, Clone)]
#[repr(u8)]
pub enum StatusDisplayType {
    /// "Listening to Spotify"
    Name = 0,
    /// "Listening to Rick Astley"
    State = 1,
    /// "Listening to Never Gonna Give You Up"
    Details = 2,
}

impl<'a> Activity<'a> {
    /// Creates a new `Activity`
    #[must_use]
    pub const fn new() -> Self {
        Activity {
            name: None,
            state: None,
            state_url: None,
            details: None,
            details_url: None,
            assets: None,
            buttons: None,
            party: None,
            secrets: None,
            timestamps: None,
            activity_type: None,
            status_display_type: None,
        }
    }

    /// Sets the name of the activity (overrides default App name)
    #[must_use]
    pub fn name<S: Into<Cow<'a, str>>>(mut self, name: S) -> Self {
        self.name = Some(name.into());
        self
    }

    /// Sets the state of the activity
    #[must_use]
    pub fn state<S: Into<Cow<'a, str>>>(mut self, state: S) -> Self {
        self.state = Some(state.into());
        self
    }

    /// Sets the state URL of the activity
    #[must_use]
    pub fn state_url<S: Into<Cow<'a, str>>>(mut self, state_url: S) -> Self {
        self.state_url = Some(state_url.into());
        self
    }

    /// Sets the details of the activity
    #[must_use]
    pub fn details<S: Into<Cow<'a, str>>>(mut self, details: S) -> Self {
        self.details = Some(details.into());
        self
    }

    /// Sets the details URL of the activity
    #[must_use]
    pub fn details_url<S: Into<Cow<'a, str>>>(mut self, details_url: S) -> Self {
        self.details_url = Some(details_url.into());
        self
    }

    /// Add a `Timestamps` to this activity
    #[must_use]
    pub const fn timestamps(mut self, timestamps: Timestamps) -> Self {
        self.timestamps = Some(timestamps);
        self
    }

    /// Add a `Party` to this activity
    #[must_use]
    pub fn party(mut self, party: Party<'a>) -> Self {
        self.party = Some(party);
        self
    }

    /// Add an `Assets` to this activity
    #[must_use]
    pub fn assets(mut self, assets: Assets<'a>) -> Self {
        self.assets = Some(assets);
        self
    }

    /// Add a `Secrets` to this activity
    #[must_use]
    pub fn secrets(mut self, secrets: Secrets<'a>) -> Self {
        self.secrets = Some(secrets);
        self
    }

    /// Add a `Vec` of `Button`s to this activity
    ///
    /// An activity may contain no more than 2 buttons
    #[must_use]
    pub fn buttons(mut self, buttons: Vec<Button<'a>>) -> Self {
        // API call fails if the array is empty, so we skip serialization
        // entirely if this is the case
        if buttons.is_empty() {
            return self;
        }

        self.buttons = Some(buttons);
        self
    }

    /// Add an `ActivityType` to this activity
    #[must_use]
    pub const fn activity_type(mut self, activity_type: ActivityType) -> Self {
        self.activity_type = Some(activity_type);
        self
    }

    /// Add a `StatusDisplayType` to this activity
    #[must_use]
    pub const fn status_display_type(mut self, status_display_type: StatusDisplayType) -> Self {
        self.status_display_type = Some(status_display_type);
        self
    }
}

impl Default for Activity<'_> {
    fn default() -> Self {
        Self::new()
    }
}

impl Timestamps {
    /// Creates a new `Timestamps`
    #[must_use]
    pub const fn new() -> Self {
        Self {
            start: None,
            end: None,
        }
    }

    /// Sets the start time
    #[must_use]
    pub const fn start(mut self, start: i64) -> Self {
        self.start = Some(start);
        self
    }

    /// Sets the end time
    #[must_use]
    pub const fn end(mut self, end: i64) -> Self {
        self.end = Some(end);
        self
    }
}

impl Default for Timestamps {
    fn default() -> Self {
        Self::new()
    }
}

impl<'a> Party<'a> {
    /// Creates a new `Party`
    #[must_use]
    pub const fn new() -> Self {
        Self {
            id: None,
            size: None,
        }
    }

    /// Sets the ID of the party
    #[must_use]
    pub fn id<S: Into<Cow<'a, str>>>(mut self, id: S) -> Self {
        self.id = Some(id.into());
        self
    }

    /// Sets the size of the party (current and maximum)
    ///
    /// # Example
    /// ```
    /// // Creates a party with a current size
    /// // of 1, and a max size of 3
    /// let party = Party::new().size([1, 3])
    /// ```
    #[must_use]
    pub const fn size(mut self, size: [i32; 2]) -> Self {
        self.size = Some(size);
        self
    }
}

impl Default for Party<'_> {
    fn default() -> Self {
        Self::new()
    }
}

impl<'a> Assets<'a> {
    /// Creates a new `Assets`
    #[must_use]
    pub const fn new() -> Self {
        Self {
            large_image: None,
            large_text: None,
            large_url: None,
            small_image: None,
            small_text: None,
            small_url: None,
        }
    }

    /// Sets the asset name or URL to be used as the large image
    #[must_use]
    pub fn large_image<S: Into<Cow<'a, str>>>(mut self, large_image: S) -> Self {
        self.large_image = Some(large_image.into());
        self
    }

    /// Sets the text to be shown when hovering over the large
    /// image
    #[must_use]
    pub fn large_text<S: Into<Cow<'a, str>>>(mut self, large_text: S) -> Self {
        self.large_text = Some(large_text.into());
        self
    }

    /// Sets the url to be shown when clicking the large image
    #[must_use]
    pub fn large_url<S: Into<Cow<'a, str>>>(mut self, large_url: S) -> Self {
        self.large_url = Some(large_url.into());
        self
    }

    /// Sets the asset name or URL to be used as the small image
    #[must_use]
    pub fn small_image<S: Into<Cow<'a, str>>>(mut self, small_image: S) -> Self {
        self.small_image = Some(small_image.into());
        self
    }

    /// Sets the text that is shown when hovering over the small
    /// image
    #[must_use]
    pub fn small_text<S: Into<Cow<'a, str>>>(mut self, small_text: S) -> Self {
        self.small_text = Some(small_text.into());
        self
    }

    /// Sets the url to be shown when clicking the small image
    #[must_use]
    pub fn small_url<S: Into<Cow<'a, str>>>(mut self, small_url: S) -> Self {
        self.small_url = Some(small_url.into());
        self
    }
}

impl Default for Assets<'_> {
    fn default() -> Self {
        Self::new()
    }
}

impl<'a> Secrets<'a> {
    /// Creates a new `Secrets`
    #[must_use]
    pub const fn new() -> Self {
        Self {
            join: None,
            spectate: None,
            r#match: None,
        }
    }

    /// Sets the secret for joining a game party
    #[must_use]
    pub fn join<S: Into<Cow<'a, str>>>(mut self, join: S) -> Self {
        self.join = Some(join.into());
        self
    }

    /// Sets the secret for spectating a match
    #[must_use]
    pub fn spectate<S: Into<Cow<'a, str>>>(mut self, spectate: S) -> Self {
        self.spectate = Some(spectate.into());
        self
    }

    /// Sets the secret for a specific, instanced match
    #[must_use]
    pub fn r#match<S: Into<Cow<'a, str>>>(mut self, r#match: S) -> Self {
        self.r#match = Some(r#match.into());
        self
    }
}

impl Default for Secrets<'_> {
    fn default() -> Self {
        Self::new()
    }
}

impl<'a> Button<'a> {
    /// Creates a new `Button` with the given label and
    /// URL
    ///
    /// The label must be 1-32 characters long
    ///
    /// The URL must be 1-512 characters long
    pub fn new<L: Into<Cow<'a, str>>, U: Into<Cow<'a, str>>>(label: L, url: U) -> Self {
        Self {
            label: label.into(),
            url: url.into(),
        }
    }
}
