use std::{collections::VecDeque, fmt::Display, io};

use blake3::Hash;
#[cfg(feature = "mpris")]
use mpris_server::TrackId as ConnectorId;
use serde::{Deserialize, Serialize};
use thiserror::Error;

use crate::playlist::{LoopMode, Playable, PlayableTrack, ShuffleMode, TracklistTrack};

#[derive(Debug, Serialize, Deserialize, Clone)]
pub enum IpcCommand {
    // Generic
    Flush,

    // Playback
    SetPlaying(bool),
    TogglePlaying,
    Seek {
        seconds: f64,
        increment: bool,
    },
    GetTime,

    GetVolume,
    SetVolume {
        volume: f32,
        increment: bool,
    },

    // The override queue
    QueueGet,
    QueueSet {
        tracks: VecDeque<PlayableTrack>,
        expected_state: Hash,
    },
    QueueExtend(Vec<Playable>),
    QueueShuffle,
    QueueClear,

    // Playlist
    PlaylistGet,
    PlaylistSet {
        playables: Vec<Playable>,
        expected_state: Hash,
    },
    PlaylistExtend(Vec<Playable>),
    PlaylistClear,

    // Playlist // Shuffle mode
    PlaylistSetShuffleMode {
        shuffle_mode: ShuffleMode,
    },
    PlaylistGetShuffleMode,
    TracklistRebuild,

    // Playlist // Loop mode
    PlaylistSetLoopMode {
        loop_mode: LoopMode,
    },
    PlaylistGetLoopMode,

    // Tracklist
    TracklistSeek {
        index: isize,
        increment: bool,
    },

    // Queries
    GetState,
    GetPlaying,
    Disconnect,
    TracklistGet,
    QueueGetState,
    PlaylistGetState,
    Stop,
    Play {
        playable: Playable,
    },
}

#[repr(u8)]
pub enum PacketType {
    Unknown,
    PlayerResponse,
    PlayerEvent,
}

impl From<u8> for PacketType {
    fn from(value: u8) -> Self {
        match value {
            1 => Self::PlayerResponse,
            2 => Self::PlayerEvent,
            _ => Self::Unknown,
        }
    }
}

#[derive(Serialize, Deserialize, Clone)]
pub enum PlayerEvent {
    CurrentlyPlayingChanged {
        currently_playing: Option<TracklistTrack>,
    },

    IsPlayingChanged {
        is_playing: bool,
    },

    ShuffleModeChanged {
        shuffle_mode: ShuffleMode,
    },
    LoopModeChanged {
        loop_mode: LoopMode,
    },

    VolumeChanged {
        volume: f32,
    },
    SeekOccured {
        time: f64,
    },

    QueueChanged,
    PlaylistChanged,
    TracklistChanged,

    Shutdown,
}

impl Display for PlayerEvent {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            PlayerEvent::CurrentlyPlayingChanged { .. } => f.write_str("CurrentlyPlayingChanged"),
            PlayerEvent::IsPlayingChanged { .. } => f.write_str("IsPlayingChanged"),
            PlayerEvent::ShuffleModeChanged { .. } => f.write_str("ShuffleModeChanged"),
            PlayerEvent::LoopModeChanged { .. } => f.write_str("LoopModeChanged"),
            PlayerEvent::VolumeChanged { .. } => f.write_str("VolumeChanged"),
            PlayerEvent::SeekOccured { .. } => f.write_str("SeekOccured"),
            PlayerEvent::QueueChanged => f.write_str("QueueChanged"),
            PlayerEvent::PlaylistChanged => f.write_str("PlaylistChanged"),
            PlayerEvent::TracklistChanged => f.write_str("TracklistChanged"),
            PlayerEvent::Shutdown => f.write_str("Shutdown"),
        }
    }
}

#[derive(Debug, Serialize, Deserialize)]
pub enum PlayerResponse {
    // Generic
    Done,

    // Responses
    IsPlaying(bool),
    Volume(f32),
    Time(Option<f64>),
    TracklistPosition {
        index: usize,
        max: usize,
    },
    ShuffleMode(crate::playlist::ShuffleMode),
    Playlist(Vec<Playable>),
    PlaylistState(Hash),
    Tracklist(Vec<TracklistTrack>),
    Queue(Vec<PlayableTrack>),
    QueueState(Hash),
    LoopMode(LoopMode),
    Playing(Option<Box<TracklistTrack>>),
    #[cfg(feature = "mpris")]
    PlayingConnectorId(Option<ConnectorId>),
}

impl Display for PlayerResponse {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            PlayerResponse::Done => f.write_str("Done"),
            PlayerResponse::IsPlaying(_) => f.write_str("IsPlaying"),
            PlayerResponse::Volume(_) => f.write_str("Volume"),
            PlayerResponse::Time(_) => f.write_str("Time"),
            PlayerResponse::TracklistPosition { .. } => f.write_str("TracklistPosition"),
            PlayerResponse::ShuffleMode(_) => f.write_str("ShuffleMode"),
            PlayerResponse::Playlist(_) => f.write_str("Playlist"),
            PlayerResponse::PlaylistState(_) => f.write_str("PlaylistState"),
            PlayerResponse::Tracklist(_) => f.write_str("Tracklist"),
            PlayerResponse::Queue(_) => f.write_str("Queue"),
            PlayerResponse::QueueState(_) => f.write_str("QueueState"),
            PlayerResponse::LoopMode(_) => f.write_str("LoopMode"),
            PlayerResponse::Playing(_) => f.write_str("Playing"),
            #[cfg(feature = "mpris")]
            PlayerResponse::PlayingConnectorId(_) => f.write_str("PlayingConnectorId"),
        }
    }
}

#[derive(Debug)]
pub enum ConnectErrorKind {
    DaemonNotRunning,
    ConnectionRefused,
    Other(io::Error),
}

impl Display for ConnectErrorKind {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ConnectErrorKind::DaemonNotRunning => f.write_str("The daemon is not running"),
            ConnectErrorKind::ConnectionRefused => {
                f.write_str("The daemon listener thread halted and must be restarted")
            }
            ConnectErrorKind::Other(error) => error.fmt(f),
        }
    }
}

#[derive(Debug, Error)]
pub enum IpcHandleError {
    #[error("Failed to connect: {0}")]
    FailedToConnect(ConnectErrorKind),

    #[error("The handling thread cannot be communicated with")]
    HandleDied,

    #[error("The current platform is not supported")]
    UnsupportedPlatform,
}

#[derive(Debug, Serialize, Deserialize, Error)]
pub enum IpcActionError {
    #[error("Nothing playing")]
    NothingPlaying,

    #[error("Index {0} was out of bounds")]
    IndexOutOfBounds(usize),

    #[error("State mismatch")]
    StateMismatch,

    #[error("The client is disconnected from the daemon")]
    Disconnected,

    #[error("Packet too large: Recieved {packet} bytes of {max} max bytes")]
    PacketTooLarge { packet: usize, max: usize },
}
