# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2026)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Handle App logos."""

from __future__ import annotations

from typing import Literal

from streamlit import url_util
from streamlit.elements.lib.image_utils import AtomicImage, image_to_url
from streamlit.elements.lib.layout_utils import LayoutConfig
from streamlit.errors import StreamlitAPIException, StreamlitValueError
from streamlit.proto.ForwardMsg_pb2 import ForwardMsg
from streamlit.proto.Logo_pb2 import Logo as LogoProto
from streamlit.runtime.metrics_util import gather_metrics
from streamlit.runtime.scriptrunner_utils.script_run_context import get_script_run_ctx
from streamlit.string_util import is_emoji, validate_material_icon


def _invalid_logo_text(field_name: str) -> str:
    return (
        f"The {field_name} passed to st.logo is invalid - See "
        "[documentation](https://docs.streamlit.io/develop/api-reference/media/st.logo) "
        "for more information on valid types"
    )


def _process_logo_image(
    image: AtomicImage | str, image_id: str
) -> tuple[LogoProto.ImageType.ValueType, str]:
    """Detects the image type and prepares the image data for the frontend.

    Parameters
    ----------
    image :
        The image that was provided by the user. Can be an image file,
        emoji, or material icon string.
    image_id : str
        The image ID used when serving local images via the media file manager.

    Returns
    -------
    tuple[ImageType, str]
        The detected image type and the prepared image data.

    Raises
    ------
    StreamlitAPIException
        If the image is an empty string or a plain text string that is not
        a valid file path, URL, emoji, or material icon.
    """
    ImageType = LogoProto.ImageType  # noqa: N806

    # Check if it's a material icon
    if isinstance(image, str) and image.startswith(":material"):
        return ImageType.ICON, validate_material_icon(image)

    # Check if it's an emoji
    if isinstance(image, str) and is_emoji(image):
        return ImageType.EMOJI, image

    # Otherwise, treat it as an image file
    image_url = image_to_url(
        image,
        layout_config=LayoutConfig(width="content"),
        clamp=False,
        channels="RGB",
        output_format="auto",
        image_id=image_id,
    )
    return ImageType.IMAGE, image_url


@gather_metrics("logo")
def logo(
    image: AtomicImage | str,
    *,  # keyword-only args:
    size: Literal["small", "medium", "large"] = "medium",
    link: str | None = None,
    icon_image: AtomicImage | str | None = None,
) -> None:
    r"""
    Renders a logo in the upper-left corner of your app and its sidebar.

    If ``st.logo`` is called multiple times within a page, Streamlit will
    render the image passed in the last call. For the most consistent results,
    call ``st.logo`` early in your page script and choose an image that works
    well in both light and dark mode. Avoid empty margins around your image.

    If your logo does not work well for both light and dark mode, consider
    setting the theme and hiding the settings menu from users with the
    `configuration option <https://docs.streamlit.io/develop/api-reference/configuration/config.toml>`_
    ``client.toolbarMode="minimal"``.

    If a user clicks the logo, they are redirected to your app's home page.

    Parameters
    ----------
    image : Anything supported by st.image (except list) or str
        The image to display in the upper-left corner of your app and its
        sidebar. If ``icon_image`` is also provided, then Streamlit will only
        display ``image`` in the sidebar.

        ``image`` can be any of the types supported by |st.image|_ except
        a list. Additionally, the following strings are valid:

        - A single-character emoji. For example, you can set ``image="🏠"``
          or ``image="🚀"``. Emoji short codes are not supported.

        - An icon from the Material Symbols library (rounded style) in the
          format ``":material/icon_name:"`` where "icon_name" is the name
          of the icon in snake case.

          For example, ``image=":material/home:"`` will display the
          Home icon. Find additional icons in the `Material Symbols \
          <https://fonts.google.com/icons?icon.set=Material+Symbols&icon.style=Rounded>`_
          font library.

        Streamlit scales the image to a max height set by ``size`` and a max
        width to fit within the sidebar.

        .. |st.image| replace:: ``st.image``
        .. _st.image: https://docs.streamlit.io/develop/api-reference/media/st.image

    size : "small", "medium", or "large"
        The size of the image displayed in the upper-left corner of the app and its
        sidebar. The possible values are as follows:

        - ``"small"``: 20px max height
        - ``"medium"`` (default): 24px max height
        - ``"large"``: 32px max height

    link : str or None
        The external URL to open when a user clicks on the logo. The URL must
        start with "\http://" or "\https://". If ``link`` is ``None`` (default),
        the logo will not include a hyperlink.

    icon_image : Anything supported by st.image (except list), str, or None
        An optional, typically smaller image to replace ``image`` in the
        upper-left corner when the sidebar is closed. This can be any of the
        types allowed for the ``image`` parameter. If ``icon_image`` is
        ``None`` (default), Streamlit will always display ``image`` in the
        upper-left corner, regardless of whether the sidebar is open or closed.
        Otherwise, Streamlit will render ``icon_image`` in the upper-left
        corner of the app when the sidebar is closed.

        Streamlit scales the image to a max height set by ``size`` and a max
        width to fit within the sidebar. If the sidebar is closed, the max
        width is retained from when it was last open.

        For best results, pass a wide or horizontal image to ``image`` and a
        square image to ``icon_image``. Or, pass a square image to ``image``
        and leave ``icon_image=None``.

    Examples
    --------
    A common design practice is to use a wider logo in the sidebar, and a
    smaller, icon-styled logo in your app's main body.

    >>> import streamlit as st
    >>>
    >>> st.logo(
    ...     LOGO_URL_LARGE,
    ...     link="https://streamlit.io/gallery",
    ...     icon_image=LOGO_URL_SMALL,
    ... )

    Try switching logos around in the following example:

    >>> import streamlit as st
    >>>
    >>> HORIZONTAL_RED = "images/horizontal_red.png"
    >>> ICON_RED = "images/icon_red.png"
    >>> HORIZONTAL_BLUE = "images/horizontal_blue.png"
    >>> ICON_BLUE = "images/icon_blue.png"
    >>>
    >>> options = [HORIZONTAL_RED, ICON_RED, HORIZONTAL_BLUE, ICON_BLUE]
    >>> sidebar_logo = st.selectbox("Sidebar logo", options, 0)
    >>> main_body_logo = st.selectbox("Main body logo", options, 1)
    >>>
    >>> st.logo(sidebar_logo, icon_image=main_body_logo)
    >>> st.sidebar.markdown("Hi!")

    .. output::
        https://doc-logo.streamlit.app/
        height: 300px

    """

    ctx = get_script_run_ctx()
    if ctx is None:
        return

    fwd_msg = ForwardMsg()

    try:
        image_type, image_value = _process_logo_image(image, "logo")
        fwd_msg.logo.image = image_value
        fwd_msg.logo.image_type = image_type
    except Exception as ex:
        raise StreamlitAPIException(_invalid_logo_text("image")) from ex

    if link:
        # Handle external links:
        if url_util.is_url(link, ("http", "https")):
            fwd_msg.logo.link = link
        else:
            raise StreamlitAPIException(
                f"Invalid link: {link} - the link param supports external links only and must "
                f"start with either http:// or https://."
            )

    if icon_image:
        try:
            icon_image_type, icon_image_value = _process_logo_image(
                icon_image, "icon-image"
            )
            fwd_msg.logo.icon_image = icon_image_value
            fwd_msg.logo.icon_image_type = icon_image_type
        except Exception as ex:
            raise StreamlitAPIException(_invalid_logo_text("icon_image")) from ex

    def validate_size(size: str) -> str:
        if isinstance(size, str):
            image_size = size.lower()
            valid_sizes = ["small", "medium", "large"]

            if image_size in valid_sizes:
                return image_size

        raise StreamlitValueError("size", ["'small'", "'medium'", "'large'"])

    fwd_msg.logo.size = validate_size(size)

    ctx.enqueue(fwd_msg)
