"""
The MIT License (MIT)

Copyright (c) 2015-2021 Rapptz
Copyright (c) 2021-present Pycord Development

Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""

from __future__ import annotations

import datetime
from typing import TYPE_CHECKING, Any, Mapping, TypeVar

from . import utils
from .colour import Colour

__all__ = (
    "Embed",
    "EmbedField",
    "EmbedAuthor",
    "EmbedFooter",
    "EmbedMedia",
    "EmbedProvider",
)


E = TypeVar("E", bound="Embed")

if TYPE_CHECKING:
    from discord.types.embed import Embed as EmbedData
    from discord.types.embed import EmbedType


class EmbedAuthor:
    """Represents the author on the :class:`Embed` object.

    .. versionadded:: 2.5

    Attributes
    ----------
    name: :class:`str`
        The name of the author.
    url: :class:`str`
        The URL of the hyperlink created in the author's name.
    icon_url: :class:`str`
        The URL of the author icon image.
    proxy_icon_url: :class:`str`
        The proxied URL of the author icon image. This can't be set directly, it is set by Discord.
    """

    def __init__(
        self,
        name: str,
        url: str | None = None,
        icon_url: str | None = None,
    ) -> None:
        self.name: str = name
        self.url: str | None = url
        self.icon_url: str | None = icon_url
        self.proxy_icon_url: str | None = None

    @classmethod
    def from_dict(cls, data: dict[str, str | None]) -> EmbedAuthor:
        self = cls.__new__(cls)
        name = data.get("name")
        if not name:
            raise ValueError("name field is required")
        self.name = name
        self.url = data.get("url")
        self.icon_url = data.get("icon_url")
        self.proxy_icon_url = data.get("proxy_icon_url")
        return self

    def to_dict(self) -> dict[str, str]:
        d = {"name": str(self.name)}
        if self.url:
            d["url"] = str(self.url)
        if self.icon_url:
            d["icon_url"] = str(self.icon_url)
        return d

    def __len__(self) -> int:
        """Returns the total number of characters in the author name."""
        return len(self.name)

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


class EmbedFooter:
    """Represents the footer on the :class:`Embed` object.

    .. versionadded:: 2.5

    Attributes
    ----------
    text: :class:`str`
       The text inside the footer.
    icon_url: :class:`str`
        The URL of the footer icon image.
    proxy_icon_url: :class:`str`
        The proxied URL of the footer icon image. This can't be set directly, it is set by Discord.
    """

    def __init__(
        self,
        text: str,
        icon_url: str | None = None,
    ) -> None:
        self.text: str = text
        self.icon_url: str | None = icon_url
        self.proxy_icon_url: str | None = None

    @classmethod
    def from_dict(cls, data: dict[str, str | None]) -> EmbedFooter:
        self = cls.__new__(cls)
        text = data.get("text")
        if not text:
            raise ValueError("text field is required")
        self.text = text
        self.icon_url = data.get("icon_url")
        self.proxy_icon_url = data.get("proxy_icon_url")
        return self

    def to_dict(self) -> dict[str, Any]:
        d = {"text": str(self.text)}
        if self.icon_url:
            d["icon_url"] = str(self.icon_url)
        return d

    def __len__(self) -> int:
        """Returns the total number of characters in the footer text."""
        return len(self.text)

    def __repr__(self) -> str:
        return f"<EmbedFooter text={self.text!r} icon_url={self.icon_url!r}>"


class EmbedMedia:  # Thumbnail, Image, Video
    """Represents a media on the :class:`Embed` object.
    This includes thumbnails, images, and videos.

    .. versionadded:: 2.5

    Attributes
    ----------
    url: :class:`str`
        The source URL of the media.
    proxy_url: :class:`str`
        The proxied URL of the media.
    height: :class:`int`
        The height of the media.
    width: :class:`int`
        The width of the media.
    """

    url: str
    proxy_url: str | None
    height: int | None
    width: int | None

    def __init__(self, url: str):
        self.url = url
        self.proxy_url = None
        self.height = None
        self.width = None

    @classmethod
    def from_dict(cls, data: dict[str, str | int]) -> EmbedMedia:
        self = cls.__new__(cls)
        self.url = str(data.get("url"))
        self.proxy_url = (
            str(proxy_url) if (proxy_url := data.get("proxy_url")) else None
        )
        self.height = int(height) if (height := data.get("height")) else None
        self.width = int(width) if (width := data.get("width")) else None
        return self

    def __repr__(self) -> str:
        return f"<EmbedMedia url={self.url!r} proxy_url={self.proxy_url!r}> height={self.height!r} width={self.width!r}>"


class EmbedProvider:
    """Represents a provider on the :class:`Embed` object.

    .. versionadded:: 2.5

    Attributes
    ----------
    name: :class:`str`
        The name of the provider.
    url: :class:`str`
        The URL of the provider.
    """

    name: str | None
    url: str | None

    @classmethod
    def from_dict(cls, data: dict[str, str | None]) -> EmbedProvider:
        self = cls.__new__(cls)
        self.name = data.get("name")
        self.url = data.get("url")
        return self

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


class EmbedField:
    """Represents a field on the :class:`Embed` object.

    .. versionadded:: 2.0

    Attributes
    ----------
    name: :class:`str`
        The name of the field.
    value: :class:`str`
        The value of the field.
    inline: :class:`bool`
        Whether the field should be displayed inline.
    """

    def __init__(self, name: str, value: str, inline: bool | None = False):
        self.name = name
        self.value = value
        self.inline = inline

    @classmethod
    def from_dict(cls, data: dict[str, str | bool]) -> EmbedField:
        """Converts a :class:`dict` to a :class:`EmbedField` provided it is in the
        format that Discord expects it to be in.

        You can find out about this format in the `official Discord documentation`__.

        .. _DiscordDocsEF: https://docs.discord.com/developers/resources/message#embed-object-embed-field-structure

        __ DiscordDocsEF_

        Parameters
        ----------
        data: :class:`dict`
            The dictionary to convert into an EmbedField object.
        """
        self = cls.__new__(cls)

        self.name = data["name"]
        self.value = data["value"]
        self.inline = data.get("inline", False)

        return self

    def to_dict(self) -> dict[str, str | bool | None]:
        """Converts this EmbedField object into a dict.

        Returns
        -------
        Dict[:class:`str`, Union[:class:`str`, :class:`bool`]]
            A dictionary of :class:`str` embed field keys bound to the respective value.
        """
        return {
            "name": self.name,
            "value": self.value,
            "inline": self.inline,
        }

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


class Embed:
    """Represents a Discord embed.

    .. container:: operations

        .. describe:: len(x)

            Returns the total size of the embed.
            Useful for checking if it's within the 6000 character limit.

        .. describe:: bool(b)

            Returns whether the embed has any data set.

            .. versionadded:: 2.0

    For ease of use, all parameters that expect a :class:`str` are implicitly
    cast to :class:`str` for you.

    Attributes
    ----------
    title: :class:`str`
        The title of the embed.
        This can be set during initialisation.
        Must be 256 characters or fewer.
    type: :class:`str`
        The type of embed. Usually "rich".
        This can be set during initialisation.
        Possible strings for embed types can be found on discord's
        `api docs <https://docs.discord.com/developers/resources/message#embed-object-embed-types>`_
    description: :class:`str`
        The description of the embed.
        This can be set during initialisation.
        Must be 4096 characters or fewer.
    url: :class:`str`
        The URL of the embed.
        This can be set during initialisation.
    timestamp: :class:`datetime.datetime`
        The timestamp of the embed content. This is an aware datetime.
        If a naive datetime is passed, it is converted to an aware
        datetime with the local timezone.
    colour: Union[:class:`Colour`, :class:`int`]
        The colour code of the embed. Aliased to ``color`` as well.
        This can be set during initialisation.
    """

    __slots__ = (
        "title",
        "url",
        "type",
        "_timestamp",
        "_colour",
        "_footer",
        "_image",
        "_thumbnail",
        "_video",
        "_provider",
        "_author",
        "_fields",
        "description",
    )

    def __init__(
        self,
        *,
        colour: int | Colour | None = None,
        color: int | Colour | None = None,
        title: Any | None = None,
        type: EmbedType = "rich",
        url: Any | None = None,
        description: Any | None = None,
        timestamp: datetime.datetime | None = None,
        fields: list[EmbedField] | None = None,
        author: EmbedAuthor | None = None,
        footer: EmbedFooter | None = None,
        image: str | EmbedMedia | None = None,
        thumbnail: str | EmbedMedia | None = None,
    ):
        self.colour = colour if colour else color
        self.title = title
        self.type = type
        self.url = url
        self.description = description

        if self.title:
            self.title = str(self.title)

        if self.description:
            self.description = str(self.description)

        if self.url:
            self.url = str(self.url)

        if timestamp:
            self.timestamp = timestamp

        self._fields: list[EmbedField] = fields if fields is not None else []

        self.author = author
        self.footer = footer
        self.image = image
        self.thumbnail = thumbnail

    @classmethod
    def from_dict(cls: type[E], data: Mapping[str, Any]) -> E:
        """Converts a :class:`dict` to a :class:`Embed` provided it is in the
        format that Discord expects it to be in.

        You can find out about this format in the `official Discord documentation`__.

        .. _DiscordDocs: https://docs.discord.com/developers/resources/message#embed-object

        __ DiscordDocs_

        Parameters
        ----------
        data: :class:`dict`
            The dictionary to convert into an embed.

        Returns
        -------
        :class:`Embed`
            The converted embed object.
        """
        # we are bypassing __init__ here since it doesn't apply here
        self: E = cls.__new__(cls)

        # fill in the basic fields

        self.title = data.get("title", None)
        self.type = data.get("type", None)
        self.description = data.get("description", None)
        self.url = data.get("url", None)

        if self.title:
            self.title = str(self.title)

        if self.description:
            self.description = str(self.description)

        if self.url:
            self.url = str(self.url)

        # try to fill in the more rich fields

        try:
            self._colour = Colour(value=data["color"])
        except KeyError:
            pass

        try:
            self._timestamp = utils.parse_time(data["timestamp"])
        except KeyError:
            pass

        for attr in (
            "thumbnail",
            "video",
            "provider",
            "author",
            "fields",
            "image",
            "footer",
        ):
            if attr == "fields":
                value = data.get(attr, [])
                self._fields = [EmbedField.from_dict(d) for d in value] if value else []
            else:
                try:
                    value = data[attr]
                except KeyError:
                    continue
                else:
                    setattr(self, f"_{attr}", value)

        return self

    def copy(self: E) -> E:
        """Creates a shallow copy of the :class:`Embed` object.

        Returns
        -------
        :class:`Embed`
            The copied embed object.
        """
        return self.__class__.from_dict(self.to_dict())

    def __len__(self) -> int:
        total = 0
        if self.title:
            total += len(self.title)
        if self.description:
            total += len(self.description)
        for field in getattr(self, "_fields", []):
            total += len(field.name) + len(field.value)

        try:
            footer_text = self._footer["text"]
        except (AttributeError, KeyError):
            pass
        else:
            total += len(footer_text)

        try:
            author = self._author
        except AttributeError:
            pass
        else:
            total += len(author["name"])

        return total

    def __bool__(self) -> bool:
        return any(
            (
                self.title,
                self.url,
                self.description,
                self.colour,
                self.fields,
                self.timestamp,
                self.author,
                self.thumbnail,
                self.footer,
                self.image,
                self.provider,
                self.video,
            )
        )

    @property
    def colour(self) -> Colour | None:
        return getattr(self, "_colour", None)

    @colour.setter
    def colour(self, value: int | Colour | None):  # type: ignore
        self._colour = Colour.resolve_value(value)

    color = colour

    @property
    def timestamp(self) -> datetime.datetime | None:
        return getattr(self, "_timestamp", None)

    @timestamp.setter
    def timestamp(self, value: datetime.datetime | None):
        if isinstance(value, datetime.datetime):
            if value.tzinfo is None:
                value = value.astimezone()
            self._timestamp = value
        elif value is None:
            self._timestamp = value
        else:
            raise TypeError(
                "Expected datetime.datetime or None. Received"
                f" {value.__class__.__name__} instead"
            )

    @property
    def footer(self) -> EmbedFooter | None:
        """Returns an :class:`EmbedFooter` denoting the footer contents.

        See :meth:`set_footer` for possible values you can access.

        If the footer is not set then `None` is returned.
        """
        foot = getattr(self, "_footer", None)
        if not foot:
            return None
        return EmbedFooter.from_dict(foot)

    @footer.setter
    def footer(self, value: EmbedFooter | None):
        if value is None:
            self.remove_footer()
        elif isinstance(value, EmbedFooter):
            self._footer = value.to_dict()
        else:
            raise TypeError(
                "Expected EmbedFooter or None. Received"
                f" {value.__class__.__name__} instead"
            )

    def set_footer(
        self: E,
        *,
        text: Any | None = None,
        icon_url: Any | None = None,
    ) -> E:
        """Sets the footer for the embed content.

        This function returns the class instance to allow for fluent-style
        chaining.

        Parameters
        ----------
        text: :class:`str`
            The footer text.
            Must be 2048 characters or fewer.
        icon_url: :class:`str`
            The URL of the footer icon. Only HTTP(S) is supported.
        """

        self._footer = {}
        if text:
            self._footer["text"] = str(text)

        if icon_url:
            self._footer["icon_url"] = str(icon_url)

        return self

    def remove_footer(self: E) -> E:
        """Clears embed's footer information.

        This function returns the class instance to allow for fluent-style
        chaining.

        .. versionadded:: 2.0
        """
        try:
            del self._footer
        except AttributeError:
            pass

        return self

    @property
    def image(self) -> EmbedMedia | None:
        """Returns an :class:`EmbedMedia` denoting the image contents.

        Attributes you can access are:

        - ``url``
        - ``proxy_url``
        - ``width``
        - ``height``

        If the image is not set then `None` is returned.
        """
        img = getattr(self, "_image", None)
        if not img:
            return None
        return EmbedMedia.from_dict(img)

    @image.setter
    def image(self, value: str | EmbedMedia | None):
        if value is None:
            self.remove_image()
        elif isinstance(value, str):
            self.set_image(url=value)
        elif isinstance(value, EmbedMedia):
            self.set_image(url=value.url)
        else:
            raise TypeError(
                "Expected discord.EmbedMedia, or None but received"
                f" {value.__class__.__name__} instead."
            )

    def set_image(self: E, *, url: Any | None) -> E:
        """Sets the image for the embed content.

        This function returns the class instance to allow for fluent-style
        chaining.

        .. versionchanged:: 1.4
            Passing `None` removes the image.

        Parameters
        ----------
        url: :class:`str`
            The source URL for the image. Only HTTP(S) is supported.
        """

        if url is None:
            try:
                del self._image
            except AttributeError:
                pass
        else:
            self._image = {
                "url": str(url),
            }

        return self

    def remove_image(self: E) -> E:
        """Removes the embed's image.

        This function returns the class instance to allow for fluent-style
        chaining.

        .. versionadded:: 2.0
        """
        try:
            del self._image
        except AttributeError:
            pass

        return self

    @property
    def thumbnail(self) -> EmbedMedia | None:
        """Returns an :class:`EmbedMedia` denoting the thumbnail contents.

        Attributes you can access are:

        - ``url``
        - ``proxy_url``
        - ``width``
        - ``height``

        If the thumbnail is not set then `None` is returned.
        """
        thumb = getattr(self, "_thumbnail", None)
        if not thumb:
            return None
        return EmbedMedia.from_dict(thumb)

    @thumbnail.setter
    def thumbnail(self, value: str | EmbedMedia | None):
        if value is None:
            self.remove_thumbnail()
        elif isinstance(value, str):
            self.set_thumbnail(url=value)
        elif isinstance(value, EmbedMedia):
            self.set_thumbnail(url=value.url)
        else:
            raise TypeError(
                "Expected discord.EmbedMedia, or None but received"
                f" {value.__class__.__name__} instead."
            )

    def set_thumbnail(self: E, *, url: Any | None) -> E:
        """Sets the thumbnail for the embed content.

        This function returns the class instance to allow for fluent-style
        chaining.

        .. versionchanged:: 1.4
            Passing `None` removes the thumbnail.

        Parameters
        ----------
        url: :class:`str`
            The source URL for the thumbnail. Only HTTP(S) is supported.
        """

        if url is None:
            try:
                del self._thumbnail
            except AttributeError:
                pass
        else:
            self._thumbnail = {
                "url": str(url),
            }

        return self

    def remove_thumbnail(self: E) -> E:
        """Removes the embed's thumbnail.

        This function returns the class instance to allow for fluent-style
        chaining.

        .. versionadded:: 2.0
        """
        try:
            del self._thumbnail
        except AttributeError:
            pass

        return self

    @property
    def video(self) -> EmbedMedia | None:
        """Returns an :class:`EmbedMedia` denoting the video contents.

        Attributes include:

        - ``url`` for the video URL.
        - ``height`` for the video height.
        - ``width`` for the video width.

        If the video is not set then `None` is returned.
        """
        vid = getattr(self, "_video", None)
        if not vid:
            return None
        return EmbedMedia.from_dict(vid)

    @property
    def provider(self) -> EmbedProvider | None:
        """Returns an :class:`EmbedProvider` denoting the provider contents.

        The only attributes that might be accessed are ``name`` and ``url``.

        If the provider is not set then `None` is returned.
        """
        prov = getattr(self, "_provider", None)
        if not prov:
            return None
        return EmbedProvider.from_dict(prov)

    @property
    def author(self) -> EmbedAuthor | None:
        """Returns an :class:`EmbedAuthor` denoting the author contents.

        See :meth:`set_author` for possible values you can access.

        If the author is not set then `None` is returned.
        """
        auth = getattr(self, "_author", None)
        if not auth:
            return None
        return EmbedAuthor.from_dict(auth)

    @author.setter
    def author(self, value: EmbedAuthor | None):
        if value is None:
            self.remove_author()
        elif isinstance(value, EmbedAuthor):
            self._author = value.to_dict()
        else:
            raise TypeError(
                "Expected discord.EmbedAuthor, or None but received"
                f" {value.__class__.__name__} instead."
            )

    def set_author(
        self: E,
        *,
        name: Any,
        url: Any | None = None,
        icon_url: Any | None = None,
    ) -> E:
        """Sets the author for the embed content.

        This function returns the class instance to allow for fluent-style
        chaining.

        Parameters
        ----------
        name: :class:`str`
            The name of the author.
            Must be 256 characters or fewer.
        url: :class:`str`
            The URL for the author.
        icon_url: :class:`str`
            The URL of the author icon. Only HTTP(S) is supported.
        """

        self._author = {
            "name": str(name),
        }

        if url:
            self._author["url"] = str(url)

        if icon_url:
            self._author["icon_url"] = str(icon_url)

        return self

    def remove_author(self: E) -> E:
        """Clears embed's author information.

        This function returns the class instance to allow for fluent-style
        chaining.

        .. versionadded:: 1.4
        """
        try:
            del self._author
        except AttributeError:
            pass

        return self

    @property
    def fields(self) -> list[EmbedField]:
        """Returns a :class:`list` of :class:`EmbedField` objects denoting the field contents.

        See :meth:`add_field` for possible values you can access.

        If the attribute has no value then ``None`` is returned.
        """
        return self._fields

    @fields.setter
    def fields(self, value: list[EmbedField]) -> None:
        """Sets the fields for the embed. This overwrites any existing fields.

        Parameters
        ----------
        value: List[:class:`EmbedField`]
            The list of :class:`EmbedField` objects to include in the embed.
        """
        if not all(isinstance(x, EmbedField) for x in value):
            raise TypeError("Expected a list of EmbedField objects.")

        self._fields = value

    def append_field(self, field: EmbedField) -> None:
        """Appends an :class:`EmbedField` object to the embed.

        .. versionadded:: 2.0

        Parameters
        ----------
        field: :class:`EmbedField`
            The field to add.
        """
        if not isinstance(field, EmbedField):
            raise TypeError("Expected an EmbedField object.")

        self._fields.append(field)

    def add_field(self: E, *, name: str, value: str, inline: bool = True) -> E:
        """Adds a field to the embed object.

        This function returns the class instance to allow for fluent-style
        chaining. There must be 25 fields or fewer.

        Parameters
        ----------
        name: :class:`str`
            The name of the field.
            Must be 256 characters or fewer.
        value: :class:`str`
            The value of the field.
            Must be 1024 characters or fewer.
        inline: :class:`bool`
            Whether the field should be displayed inline.
        """
        self._fields.append(EmbedField(name=str(name), value=str(value), inline=inline))

        return self

    def insert_field_at(
        self: E, index: int, *, name: Any, value: Any, inline: bool = True
    ) -> E:
        """Inserts a field before a specified index to the embed.

        This function returns the class instance to allow for fluent-style
        chaining. There must be 25 fields or fewer.

        .. versionadded:: 1.2

        Parameters
        ----------
        index: :class:`int`
            The index of where to insert the field.
        name: :class:`str`
            The name of the field.
            Must be 256 characters or fewer.
        value: :class:`str`
            The value of the field.
            Must be 1024 characters or fewer.
        inline: :class:`bool`
            Whether the field should be displayed inline.
        """

        field = EmbedField(name=str(name), value=str(value), inline=inline)

        self._fields.insert(index, field)

        return self

    def clear_fields(self) -> None:
        """Removes all fields from this embed."""
        self._fields.clear()

    def remove_field(self, index: int) -> None:
        """Removes a field at a specified index.

        If the index is invalid or out of bounds then the error is
        silently swallowed.

        .. note::

            When deleting a field by index, the index of the other fields
            shift to fill the gap just like a regular list.

        Parameters
        ----------
        index: :class:`int`
            The index of the field to remove.
        """
        try:
            del self._fields[index]
        except IndexError:
            pass

    def set_field_at(
        self: E, index: int, *, name: Any, value: Any, inline: bool = True
    ) -> E:
        """Modifies a field to the embed object.

        The index must point to a valid pre-existing field. There must be 25 fields or fewer.

        This function returns the class instance to allow for fluent-style
        chaining.

        Parameters
        ----------
        index: :class:`int`
            The index of the field to modify.
        name: :class:`str`
            The name of the field.
            Must be 256 characters or fewer.
        value: :class:`str`
            The value of the field.
            Must be 1024 characters or fewer.
        inline: :class:`bool`
            Whether the field should be displayed inline.

        Raises
        ------
        IndexError
            An invalid index was provided.
        """

        try:
            field = self._fields[index]
        except (TypeError, IndexError):
            raise IndexError("field index out of range")

        field.name = str(name)
        field.value = str(value)
        field.inline = inline
        return self

    def to_dict(self) -> EmbedData:
        """Converts this embed object into a dict.

        Returns
        -------
        Dict[:class:`str`, Union[:class:`str`, :class:`int`, :class:`bool`]]
            A dictionary of :class:`str` embed keys bound to the respective value.
        """

        # add in the raw data into the dict
        result = {
            key[1:]: getattr(self, key)
            for key in self.__slots__
            if key != "_fields" and key[0] == "_" and hasattr(self, key)
        }

        # add in the fields
        result["fields"] = [field.to_dict() for field in self._fields]

        # deal with basic convenience wrappers

        try:
            colour = result.pop("colour")
        except KeyError:
            pass
        else:
            if colour:
                result["color"] = colour.value

        try:
            timestamp = result.pop("timestamp")
        except KeyError:
            pass
        else:
            if timestamp:
                if timestamp.tzinfo:
                    result["timestamp"] = timestamp.astimezone(
                        tz=datetime.timezone.utc
                    ).isoformat()
                else:
                    result["timestamp"] = timestamp.replace(
                        tzinfo=datetime.timezone.utc
                    ).isoformat()

        # add in the non-raw attribute ones
        if self.type:
            result["type"] = self.type

        if self.description:
            result["description"] = self.description

        if self.url:
            result["url"] = self.url

        if self.title:
            result["title"] = self.title

        return result  # type: ignore
