# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2024)
#
# 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.

from __future__ import annotations

import types
from pathlib import Path
from typing import Callable

from streamlit.errors import StreamlitAPIException
from streamlit.runtime.metrics_util import gather_metrics
from streamlit.runtime.scriptrunner.script_run_context import get_script_run_ctx
from streamlit.source_util import page_icon_and_name
from streamlit.string_util import validate_icon_or_emoji
from streamlit.util import calc_md5


@gather_metrics("Page")
def Page(
    page: str | Path | Callable[[], None],
    *,
    title: str | None = None,
    icon: str | None = None,
    url_path: str | None = None,
    default: bool = False,
):
    """Configure a page for ``st.navigation`` in a multipage app.

    Call ``st.Page`` to initialize a ``StreamlitPage`` object, and pass it to
    ``st.navigation`` to declare a page in your app.

    When a user navigates to a page, ``st.navigation`` returns the selected
    ``StreamlitPage`` object. Call ``.run()`` on the returned ``StreamlitPage``
    object to execute the page. You can only run the page returned by
    ``st.navigation``, and you can only run it once per app rerun.

    A page can be defined by a Python file or ``Callable``. Python files used
    as a ``StreamlitPage`` source will have ``__name__ == "__page__"``.
    Functions used as a ``StreamlitPage`` source will have ``__name__``
    corresponding to the module they were imported from. Only the entrypoint
    file and functions defined within the entrypoint file have
    ``__name__ == "__main__"`` to adhere to Python convention.

    Parameters
    ----------

    page: str, Path, or callable
        The page source as a ``Callable`` or path to a Python file. If the page
        source is defined by a Python file, the path can be a string or
        ``pathlib.Path`` object, but must be declared relative to the
        entrypoint file. If the page source is defined by a ``Callable``, the
        ``Callable`` can't accept arguments.

    title: str or None
        The title of the page. If this is ``None`` (default), the page title
        (in the browser tab) and label (in the navigation menu) will be
        inferred from the filename or callable name in ``page``. For more
        information, see `Overview of multipage apps
        <https://docs.streamlit.io/st.page.automatic-page-labels>`_.

    icon: str or None
        An optional emoji or icon to display next to the page title and label.
        If ``icon`` is ``None`` (default), no icon is displayed next to the
        page label in the navigation menu, and a Streamlit icon is displayed
        next to the title (in the browser tab). If ``icon`` is a string, the
        following options are valid:

        * A single-character emoji. For example, you can set ``icon="🚨"``
            or ``icon="🔥"``. 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, ``icon=":material/thumb_up:"`` will display the
            Thumb Up icon. Find additional icons in the `Material Symbols \
            <https://fonts.google.com/icons?icon.set=Material+Symbols&icon.style=Rounded>`_
            font library.

    url_path: str or None
        The page's URL pathname, which is the path relative to the app's root
        URL. If this is ``None`` (default), the URL pathname will be inferred
        from the filename or callable name in ``page``. For more information,
        see `Overview of multipage apps
        <https://docs.streamlit.io/st.page.automatic-page-urls>`_.

        The default page will have a pathname of ``""``, indicating the root
        URL of the app. If you set ``default=True``, ``url_path`` is ignored.

    default: bool
        Whether this page is the default page to be shown when the app is
        loaded. If ``default`` is ``False`` (default), the page will have a
        nonempty URL pathname. However, if no default page is passed to
        ``st.navigation`` and this is the first page, this page will become the
        default page. If ``default`` is ``True``, then the page will have
        an empty pathname and ``url_path`` will be ignored.

    Returns
    -------
    StreamlitPage
        The page object associated to the given script.

    Example
    -------
    >>> import streamlit as st
    >>>
    >>> def page2():
    >>>     st.title("Second page")
    >>>
    >>> pg = st.navigation([
    >>>	    st.Page("page1.py", title="First page", icon="🔥"),
    >>>	    st.Page(page2, title="Second page", icon=":material/favorite:"),
    >>> ])
    >>> pg.run()
    """
    return StreamlitPage(
        page, title=title, icon=icon, url_path=url_path, default=default
    )


class StreamlitPage:
    """A page within a multipage Streamlit app.

    Use ``st.Page`` to initialize a ``StreamlitPage`` object.

    Attributes
    ----------
    icon : str
        The icon of the page.

        If no icon was declared in ``st.Page``, this property returns ``""``.

    title : str
        The title of the page.

        Unless declared otherwise in ``st.Page``, the page title is inferred
        from the filename or callable name. For more information, see
        `Overview of multipage apps
        <https://docs.streamlit.io/st.page.automatic-page-labels>`_.

    url_path : str
        The page's URL pathname, which is the path relative to the app's root
        URL.

        Unless declared otherwise in ``st.Page``, the URL pathname is inferred
        from the filename or callable name. For more information, see
        `Overview of multipage apps
        <https://docs.streamlit.io/st.page.automatic-page-urls>`_.

        The default page will always have a ``url_path`` of ``""`` to indicate
        the root URL (e.g. homepage).

    """

    def __init__(
        self,
        page: str | Path | Callable[[], None],
        *,
        title: str | None = None,
        icon: str | None = None,
        url_path: str | None = None,
        default: bool = False,
    ):
        ctx = get_script_run_ctx()
        if not ctx:
            return

        main_path = Path(ctx.pages_manager.main_script_path).parent
        if isinstance(page, str):
            page = Path(page)
        if isinstance(page, Path):
            page = (main_path / page).resolve()

            if not page.is_file():
                raise StreamlitAPIException(
                    f"Unable to create Page. The file `{page.name}` could not be found."
                )

        inferred_name = ""
        inferred_icon = ""
        if isinstance(page, Path):
            inferred_icon, inferred_name = page_icon_and_name(page)
        elif hasattr(page, "__name__"):
            inferred_name = str(page.__name__)
        elif title is None:
            # At this point, we know the page is not a string or a path, so it
            # must be a callable. We expect it to have a __name__ attribute,
            # but in special cases (e.g. a callable class instance), one may
            # not exist. In that case, we should inform the user the title is
            # mandatory.
            raise StreamlitAPIException(
                "Cannot infer page title for Callable. Set the `title=` keyword argument."
            )

        self._page: Path | Callable[[], None] = page
        self._title: str = title or inferred_name.replace("_", " ")
        self._icon: str = icon or inferred_icon
        if url_path is not None and url_path.strip() == "" and not default:
            raise StreamlitAPIException(
                "The URL path cannot be an empty string unless the page is the default page."
            )

        self._url_path: str = inferred_name
        if url_path is not None:
            self._url_path = url_path.lstrip("/")

        if self._icon:
            validate_icon_or_emoji(self._icon)

        self._default: bool = default
        # used by st.navigation to ordain a page as runnable
        self._can_be_called: bool = False

    @property
    def title(self) -> str:
        """The title of the page.

        Unless declared otherwise in ``st.Page``, the page title is inferred
        from the filename or callable name. For more information, see
        `Overview of multipage apps
        <https://docs.streamlit.io/st.page.automatic-page-labels>`_.
        """
        return self._title

    @property
    def icon(self) -> str:
        """The icon of the page.

        If no icon was declared in ``st.Page``, this property returns ``""``.
        """
        return self._icon

    @property
    def url_path(self) -> str:
        """The page's URL pathname, which is the path relative to the app's \
        root URL.

        Unless declared otherwise in ``st.Page``, the URL pathname is inferred
        from the filename or callable name. For more information, see
        `Overview of multipage apps
        <https://docs.streamlit.io/st.page.automatic-page-urls>`_.

        The default page will always have a ``url_path`` of ``""`` to indicate
        the root URL (e.g. homepage).
        """
        return "" if self._default else self._url_path

    def run(self) -> None:
        """Execute the page.

        When a page is returned by ``st.navigation``, use the ``.run()`` method
        within your entrypoint file to render the page. You can only call this
        method on the page returned by ``st.navigation``. You can only call
        this method once per run of your entrypoint file.

        """
        if not self._can_be_called:
            raise StreamlitAPIException(
                "This page cannot be called directly. Only the page returned from st.navigation can be called once."
            )

        self._can_be_called = False

        ctx = get_script_run_ctx()
        if not ctx:
            return

        with ctx.pages_manager.run_with_active_hash(self._script_hash):
            if callable(self._page):
                self._page()
                return
            else:
                code = ctx.pages_manager.get_page_script_byte_code(str(self._page))

                # We create a module named __page__ for this specific
                # script. This is differentiate it from the `__main__` module
                module = types.ModuleType("__page__")
                # We want __file__ to be the path to the script
                module.__dict__["__file__"] = self._page
                exec(code, module.__dict__)

    @property
    def _script_hash(self) -> str:
        return calc_md5(self._url_path)
