use serde::de::DeserializeOwned;
use std::{fmt, sync::Arc};

use crate::protocol::{NetworkItemFlags, NetworkPrint, NetworkText};
use crate::{Client, Error, Item, LocatedItem, Location, Player};

pub use crate::protocol::TextColor;

/// A rich-text message sent by the server, with annotations indicating how the
/// text should be formatted and what individual components refer to.
#[derive(Debug, Clone)]
pub enum Print {
    /// A player received an item.
    ItemSend {
        data: Vec<RichText>,
        item: LocatedItem,
    },

    /// A player used the `!getitem` command.
    ItemCheat {
        data: Vec<RichText>,
        item: LocatedItem,
    },

    /// A player hinted.
    Hint {
        data: Vec<RichText>,
        item: LocatedItem,
        found: bool,
    },

    /// A player connected.
    Join {
        data: Vec<RichText>,
        player: Arc<Player>,
        tags: Vec<String>,
    },

    /// A player disconnected.
    Part {
        data: Vec<RichText>,
        player: Arc<Player>,
    },

    /// A player sent a chat message.
    Chat {
        data: Vec<RichText>,
        player: Arc<Player>,
        message: String,
    },

    /// The server broadcasted a message.
    ServerChat {
        data: Vec<RichText>,
        message: String,
    },

    /// The client has triggered a tutorial message, such as when first
    /// connecting.
    Tutorial { data: Vec<RichText> },

    /// A player changed their tags.
    TagsChanged {
        data: Vec<RichText>,
        player: Arc<Player>,
        tags: Vec<String>,
    },

    /// Someone (usually the client) entered an `!` command.
    CommandResult { data: Vec<RichText> },

    /// The client entered an `!admin` command.
    AdminCommandResult { data: Vec<RichText> },

    /// A player reached their goal.
    Goal {
        data: Vec<RichText>,
        player: Arc<Player>,
    },

    /// A player released the remaining items in their world.
    Release {
        data: Vec<RichText>,
        player: Arc<Player>,
    },

    /// A player collected the remaining items for their world.
    Collect {
        data: Vec<RichText>,
        player: Arc<Player>,
    },

    /// The current server countdown has progressed.
    Countdown { data: Vec<RichText>, countdown: u64 },

    /// A catch-call for unrecognized message types, plain-text messages, and
    /// synthetic messages generated by [Print::message].
    Unknown { data: Vec<RichText> },
}

impl Print {
    /// Create this from a [NetworkPrint] using information from the client.
    pub(crate) fn hydrate<S: DeserializeOwned>(
        network: NetworkPrint,
        client: &Client<S>,
    ) -> Result<Self, Error> {
        Ok(match network {
            NetworkPrint::ItemSend {
                data,
                receiving,
                item,
            } => {
                let sender = client.teammate_arc(item.player)?;
                Print::ItemSend {
                    data: RichText::hydrate_vec(data, client)?,
                    item: LocatedItem::hydrate(
                        item,
                        sender,
                        client.teammate_arc(receiving)?,
                        client,
                    )?,
                }
            }
            NetworkPrint::ItemCheat {
                data,
                receiving,
                item,
                team,
            } => {
                let sender = client.player_arc(team, item.player)?;
                Print::ItemCheat {
                    data: RichText::hydrate_vec(data, client)?,
                    item: LocatedItem::hydrate(
                        item,
                        sender,
                        client.player_arc(team, receiving)?,
                        client,
                    )?,
                }
            }
            NetworkPrint::Hint {
                data,
                receiving,
                item,
                found,
            } => {
                let sender = client.teammate_arc(item.player)?;
                Print::Hint {
                    data: RichText::hydrate_vec(data, client)?,
                    item: LocatedItem::hydrate(
                        item,
                        sender,
                        client.teammate_arc(receiving)?,
                        client,
                    )?,
                    found,
                }
            }
            NetworkPrint::Join {
                data,
                team,
                slot,
                tags,
            } => Print::Join {
                data: RichText::hydrate_vec(data, client)?,
                player: client.player_arc(team, slot)?,
                tags,
            },
            NetworkPrint::Part { data, team, slot } => Print::Part {
                data: RichText::hydrate_vec(data, client)?,
                player: client.player_arc(team, slot)?,
            },
            NetworkPrint::Chat {
                data,
                team,
                slot,
                message,
            } => Print::Chat {
                data: RichText::hydrate_vec(data, client)?,
                player: client.player_arc(team, slot)?,
                message,
            },
            NetworkPrint::ServerChat { data, message } => Print::ServerChat {
                data: RichText::hydrate_vec(data, client)?,
                message,
            },
            NetworkPrint::Tutorial { data } => Print::Tutorial {
                data: RichText::hydrate_vec(data, client)?,
            },
            NetworkPrint::TagsChanged {
                data,
                team,
                slot,
                tags,
            } => Print::TagsChanged {
                data: RichText::hydrate_vec(data, client)?,
                player: client.player_arc(team, slot)?,
                tags,
            },
            NetworkPrint::CommandResult { data } => Print::CommandResult {
                data: RichText::hydrate_vec(data, client)?,
            },
            NetworkPrint::AdminCommandResult { data } => Print::AdminCommandResult {
                data: RichText::hydrate_vec(data, client)?,
            },
            NetworkPrint::Goal { data, team, slot } => Print::Goal {
                data: RichText::hydrate_vec(data, client)?,
                player: client.player_arc(team, slot)?,
            },
            NetworkPrint::Release { data, team, slot } => Print::Release {
                data: RichText::hydrate_vec(data, client)?,
                player: client.player_arc(team, slot)?,
            },
            NetworkPrint::Collect { data, team, slot } => Print::Collect {
                data: RichText::hydrate_vec(data, client)?,
                player: client.player_arc(team, slot)?,
            },
            NetworkPrint::Countdown { data, countdown } => Print::Countdown {
                data: RichText::hydrate_vec(data, client)?,
                countdown,
            },
            NetworkPrint::Unknown { data } => Print::Unknown {
                data: RichText::hydrate_vec(data, client)?,
            },
        })
    }

    /// A utility method that returns a message of an unknown type that just
    /// contains the given unformatted `text`.
    pub fn message(text: String) -> Print {
        text.into()
    }

    /// Returns the data field for any Print.
    pub fn data(&self) -> &[RichText] {
        use Print::*;
        match self {
            ItemSend { data, .. } => data,
            ItemCheat { data, .. } => data,
            Hint { data, .. } => data,
            Join { data, .. } => data,
            Part { data, .. } => data,
            Chat { data, .. } => data,
            ServerChat { data, .. } => data,
            Tutorial { data, .. } => data,
            TagsChanged { data, .. } => data,
            CommandResult { data, .. } => data,
            AdminCommandResult { data, .. } => data,
            Goal { data, .. } => data,
            Release { data, .. } => data,
            Collect { data, .. } => data,
            Countdown { data, .. } => data,
            Unknown { data, .. } => data,
        }
    }
}

impl From<String> for Print {
    fn from(value: String) -> Print {
        vec![value.into()].into()
    }
}

impl From<&str> for Print {
    fn from(value: &str) -> Print {
        value.to_string().into()
    }
}

impl From<RichText> for Print {
    fn from(value: RichText) -> Print {
        vec![value].into()
    }
}

impl From<Vec<RichText>> for Print {
    fn from(data: Vec<RichText>) -> Print {
        Print::Unknown { data }
    }
}

impl fmt::Display for Print {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
        for part in self.data() {
            part.fmt(f)?;
        }
        Ok(())
    }
}

/// A single text component of a [Print], with additional metadata indicating
/// its formatting and semantics.
///
/// Unlike [RichText], this has not yet been hydrated with additional metadata
/// known by the client.
#[derive(Debug, Clone)]
pub enum RichText {
    /// A reference to a player.
    Player(Arc<Player>),

    /// A reference to a player's name. This is used for messages that should be
    /// rendered like player names but which don't correspond to any actual
    /// [Player] objects in the game.
    PlayerName(String),

    /// A reference to an item for a particular player.
    Item {
        item: Item,
        player: Arc<Player>,
        progression: bool,
        useful: bool,
        trap: bool,
    },

    /// A reference to a location in a particular player's game.
    Location {
        location: Location,
        player: Arc<Player>,
    },

    /// A reference to an entrance in a game.
    EntranceName(String),

    /// Text with a particular specified color.
    Color { text: String, color: TextColor },

    // We don't explicitly provide variants for `ItemName` or `LocationName`
    // because they aren't ever actually sent by the server. If that changes,
    // this will fall back to using [Text] for them until we add explicit
    // support.
    /// Plain text. This is also used as the fallback if the server sends an
    /// unrecognized message type. (If that happens, please file an issue so we
    /// can add support!)
    Text(String),
}

impl RichText {
    /// Converts [NetworkText]s in [vec] to [RichText]s.
    fn hydrate_vec<S: DeserializeOwned>(
        vec: Vec<NetworkText>,
        client: &Client<S>,
    ) -> Result<Vec<RichText>, Error> {
        vec.into_iter()
            .map(|rt| RichText::hydrate(rt, client))
            .collect()
    }

    /// Creates this from a [NetworkText] using information from the client.
    fn hydrate<S: DeserializeOwned>(
        network: NetworkText,
        client: &Client<S>,
    ) -> Result<RichText, Error> {
        Ok(match network {
            NetworkText::PlayerId { id } => RichText::Player(client.teammate_arc(id)?),
            NetworkText::PlayerName { text } => RichText::PlayerName(text),
            NetworkText::ItemId { id, player, flags } => {
                let player = client.teammate_arc(player)?;
                RichText::Item {
                    item: client.game_or_err(player.game())?.item_or_err(id)?,
                    player,
                    progression: flags.contains(NetworkItemFlags::PROGRESSION),
                    useful: flags.contains(NetworkItemFlags::USEFUL),
                    trap: flags.contains(NetworkItemFlags::TRAP),
                }
            }
            NetworkText::LocationId { id, player } => {
                let player = client.teammate_arc(player)?;
                RichText::Location {
                    location: client.game_or_err(player.game())?.location_or_err(id)?,
                    player,
                }
            }
            NetworkText::EntranceName { text } => RichText::EntranceName(text),
            NetworkText::Color { text, color } => RichText::Color { text, color },
            NetworkText::Text { text } => RichText::Text(text),
        })
    }
}

impl From<String> for RichText {
    fn from(value: String) -> RichText {
        RichText::Text(value)
    }
}

impl From<&str> for RichText {
    fn from(value: &str) -> RichText {
        value.to_string().into()
    }
}

impl fmt::Display for RichText {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
        use RichText::*;
        match self {
            Player(player) => player.fmt(f),
            Item { item, .. } => item.fmt(f),
            Location { location, .. } => location.fmt(f),
            PlayerName(text) | EntranceName(text) | Color { text, .. } | Text(text) => text.fmt(f),
        }
    }
}
