# SPDX-License-Identifier: MIT

from __future__ import annotations

import datetime
from typing import TYPE_CHECKING, Any, Dict, Optional, Union

from .asset import Asset, AssetMixin
from .mixins import Hashable
from .partial_emoji import PartialEmoji
from .utils import MISSING, _get_as_snowflake, snowflake_time

if TYPE_CHECKING:
    from .emoji import Emoji
    from .guild import Guild
    from .state import ConnectionState
    from .types.soundboard import (
        GuildSoundboardSound as GuildSoundboardSoundPayload,
        PartialSoundboardSound as PartialSoundboardSoundPayload,
        SoundboardSound as SoundboardSoundPayload,
    )
    from .user import User


__all__ = (
    "PartialSoundboardSound",
    "SoundboardSound",
    "GuildSoundboardSound",
)


class PartialSoundboardSound(Hashable, AssetMixin):
    """Represents a partial soundboard sound.

    Used for sounds in :class:`VoiceChannelEffect`\\s,
    and as the base for full :class:`SoundboardSound`/:class:`GuildSoundboardSound` objects.

    .. versionadded:: 2.10

    .. collapse:: operations

        .. describe:: x == y

            Checks if two soundboard sounds are equal.

        .. describe:: x != y

            Checks if two soundboard sounds are not equal.

        .. describe:: hash(x)

            Returns the soundboard sounds' hash.

    Attributes
    ----------
    id: :class:`int`
        The sound's ID.
    volume: :class:`float`
        The sound's volume (from ``0.0`` to ``1.0``).
    """

    __slots__ = (
        "id",
        "volume",
    )

    def __init__(
        self,
        *,
        data: PartialSoundboardSoundPayload,
        state: Optional[ConnectionState] = None,
    ) -> None:
        self._state = state
        self.id: int = int(data["sound_id"])
        self.volume: float = data["volume"]

    def __repr__(self) -> str:
        return f"<{self.__class__.__name__} id={self.id!r}>"

    @property
    def created_at(self) -> Optional[datetime.datetime]:
        """Optional[:class:`datetime.datetime`]: Returns the sound's creation time in UTC.
        Can be ``None`` if this is a default sound.
        """
        if self.is_default():
            return None
        return snowflake_time(self.id)

    @property
    def url(self) -> str:
        """:class:`str`: The url for the sound file."""
        return f"{Asset.BASE}/soundboard-sounds/{self.id}"

    def is_default(self) -> bool:
        """Whether the sound is a default sound provided by Discord.

        :return type: :class:`bool`
        """
        # default sounds have IDs starting from 1, i.e. tiny numbers compared to real snowflakes.
        # assume any "snowflake" with a zero timestamp is for a default sound
        return (self.id >> 22) == 0


class SoundboardSound(PartialSoundboardSound):
    """Represents a soundboard sound.

    .. versionadded:: 2.10

    .. collapse:: operations

        .. describe:: x == y

            Checks if two soundboard sounds are equal.

        .. describe:: x != y

            Checks if two soundboard sounds are not equal.

        .. describe:: hash(x)

            Returns the soundboard sounds' hash.

    Attributes
    ----------
    id: :class:`int`
        The sound's ID.
    volume: :class:`float`
        The sound's volume (from ``0.0`` to ``1.0``).
    name: :class:`str`
        The sound's name.
    emoji: Optional[Union[:class:`Emoji`, :class:`PartialEmoji`]]
        The sound's emoji, if any.
        Due to a Discord limitation, this will have an empty
        :attr:`~PartialEmoji.name` if it is a custom :class:`PartialEmoji`.
    available: :class:`bool`
        Whether this sound is available for use.
    """

    __slots__ = ("name", "emoji", "available")

    _state: ConnectionState

    def __init__(
        self,
        *,
        data: SoundboardSoundPayload,
        state: ConnectionState,
    ) -> None:
        super().__init__(data=data, state=state)

        self.name: str = data["name"]
        self.emoji: Optional[Union[Emoji, PartialEmoji]] = self._state._get_emoji_from_fields(
            name=data.get("emoji_name"),
            id=_get_as_snowflake(data, "emoji_id"),
        )
        self.available: bool = data.get("available", True)

    def __repr__(self) -> str:
        return f"<SoundboardSound id={self.id!r} name={self.name!r}>"


class GuildSoundboardSound(SoundboardSound):
    """Represents a soundboard sound that belongs to a guild.

    .. versionadded:: 2.10

    .. collapse:: operations

        .. describe:: x == y

            Checks if two soundboard sounds are equal.

        .. describe:: x != y

            Checks if two soundboard sounds are not equal.

        .. describe:: hash(x)

            Returns the soundboard sounds' hash.

    Attributes
    ----------
    id: :class:`int`
        The sound's ID.
    volume: :class:`float`
        The sound's volume (from ``0.0`` to ``1.0``).
    name: :class:`str`
        The sound's name.
    emoji: Optional[Union[:class:`Emoji`, :class:`PartialEmoji`]]
        The sound's emoji, if any.
        Due to a Discord limitation, this will have an empty
        :attr:`~PartialEmoji.name` if it is a custom :class:`PartialEmoji`.
    guild_id: :class:`int`
        The ID of the guild this sound belongs to.
    available: :class:`bool`
        Whether this sound is available for use.
    user: Optional[:class:`User`]
        The user that created this sound. This can only be retrieved using
        :meth:`Guild.fetch_soundboard_sound`/:meth:`Guild.fetch_soundboard_sounds` while
        having the :attr:`~Permissions.create_guild_expressions` or
        :attr:`~Permissions.manage_guild_expressions` permission.
    """

    __slots__ = ("guild_id", "user")

    def __init__(
        self,
        *,
        data: GuildSoundboardSoundPayload,
        state: ConnectionState,
        # `guild_id` isn't sent over REST, so we manually keep track of it
        guild_id: int,
    ) -> None:
        super().__init__(data=data, state=state)

        self.guild_id: int = guild_id
        self.user: Optional[User] = (
            state.store_user(user_data) if (user_data := data.get("user")) is not None else None
        )

    def __repr__(self) -> str:
        return (
            f"<GuildSoundboardSound id={self.id!r} name={self.name!r}"
            f" guild_id={self.guild_id!r} user={self.user!r}>"
        )

    @property
    def guild(self) -> Guild:
        """:class:`Guild`: The guild that this sound is from."""
        # this will most likely never return None
        return self._state._get_guild(self.guild_id)  # type: ignore

    async def edit(
        self,
        *,
        name: str = MISSING,
        volume: float = MISSING,
        emoji: Optional[Union[str, Emoji, PartialEmoji]] = MISSING,
        reason: Optional[str] = None,
    ) -> GuildSoundboardSound:
        """|coro|

        Edits a :class:`GuildSoundboardSound` for the guild.

        You must have :attr:`~Permissions.manage_guild_expressions` permission to
        do this.
        If this sound was created by you, :attr:`~Permissions.create_guild_expressions`
        permission is also sufficient.

        All fields are optional.

        Parameters
        ----------
        name: :class:`str`
            The sounds's new name. Must be at least 2 characters.
        volume: :class:`float`
            The sound's new volume (from ``0.0`` to ``1.0``).
        emoji: Optional[Union[:class:`str`, :class:`Emoji`, :class:`PartialEmoji`]]
            The sound's new emoji. Can be ``None``.
        reason: Optional[:class:`str`]
            The reason for editing this sound. Shows up on the audit log.

        Raises
        ------
        Forbidden
            You are not allowed to edit this soundboard sound.
        HTTPException
            An error occurred editing the soundboard sound.

        Returns
        -------
        :class:`GuildSoundboardSound`
            The newly modified soundboard sound.
        """
        payload: Dict[str, Any] = {}

        if name is not MISSING:
            payload["name"] = name
        if volume is not MISSING:
            payload["volume"] = volume
        if emoji is not MISSING:
            emoji_name, emoji_id = PartialEmoji._emoji_to_name_id(emoji)
            payload["emoji_name"] = emoji_name
            payload["emoji_id"] = emoji_id

        data = await self._state.http.edit_guild_soundboard_sound(
            self.guild_id, self.id, reason=reason, **payload
        )
        return GuildSoundboardSound(data=data, state=self._state, guild_id=self.guild_id)

    async def delete(self, *, reason: Optional[str] = None) -> None:
        """|coro|

        Deletes the :class:`GuildSoundboardSound` from the guild.

        You must have :attr:`~Permissions.manage_guild_expressions` permission to
        do this.
        If this sound was created by you, :attr:`~Permissions.create_guild_expressions`
        permission is also sufficient.

        Parameters
        ----------
        reason: Optional[:class:`str`]
            The reason for deleting this sound. Shows up on the audit log.

        Raises
        ------
        Forbidden
            You are not allowed to delete this soundboard sound.
        HTTPException
            An error occurred deleting the soundboard sound.
        """
        await self._state.http.delete_guild_soundboard_sound(self.guild_id, self.id, reason=reason)
