# 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.

from __future__ import annotations

from typing import TYPE_CHECKING

from streamlit.errors import (
    StreamlitAPIException,
    StreamlitInvalidBindValueError,
    StreamlitInvalidPersistStateError,
)
from streamlit.runtime.scriptrunner_utils.script_run_context import (
    ThreadState,
)
from streamlit.runtime.state.common import (
    BindOption,
    PersistStateOption,
    RegisterWidgetResult,
    T,
    ValueFieldName,
    WidgetArgs,
    WidgetCallback,
    WidgetDeserializer,
    WidgetKwargs,
    WidgetMetadata,
    WidgetSerializer,
    WidgetValuePresenter,
    user_key_from_element_id,
)

if TYPE_CHECKING:
    from streamlit.runtime.scriptrunner import ScriptRunContext


def register_widget(
    element_id: str,
    *,
    deserializer: WidgetDeserializer[T],
    serializer: WidgetSerializer[T],
    ctx: ScriptRunContext | None,
    callbacks: dict[str, WidgetCallback] | None = None,
    on_change_handler: WidgetCallback | None = None,
    args: WidgetArgs | None = None,
    kwargs: WidgetKwargs | None = None,
    value_type: ValueFieldName,
    presenter: WidgetValuePresenter | None = None,
    bind: BindOption = None,
    persist_state: PersistStateOption = None,
    # For selection widgets with bind="query-params": the valid option strings
    # used to validate and filter URL values during seeding.
    formatted_options: list[str] | None = None,
    clearable: bool | None = None,
    max_array_length: int | None = None,
    allow_url_duplicates: bool = False,
    disabled: bool = False,
) -> RegisterWidgetResult[T]:
    """Register a widget with Streamlit, and return its current value.
    NOTE: This function should be called after the proto has been filled.

    Parameters
    ----------
    element_id : str
        The id of the element. Must be unique.
    deserializer : WidgetDeserializer[T]
        Called to convert a widget's protobuf value to the value returned by
        its st.<widget_name> function.
    serializer : WidgetSerializer[T]
        Called to convert a widget's value to its protobuf representation.
    ctx : ScriptRunContext or None
        Used to ensure uniqueness of widget IDs, and to look up widget values.
    callbacks : dict[str, WidgetCallback] or None
        A dictionary of callbacks for multi-callback support.
    on_change_handler : WidgetCallback or None
        An optional callback invoked when the widget's value changes.
    args : WidgetArgs or None
        Positional arguments to pass to the `on_change_handler` or `callbacks`.
    kwargs : WidgetKwargs or None
        Keyword arguments to pass to the `on_change_handler` or `callbacks`.
    value_type: ValueFieldName
        The value_type the widget is going to use.
        We use this information to start with a best-effort guess for the value_type
        of each widget. Once we actually receive a proto for a widget from the
        frontend, the guess is updated to be the correct type. Unfortunately, we're
        not able to always rely on the proto as the type may be needed earlier.
        Thankfully, in these cases (when value_type == "trigger_value"), the static
        table here being slightly inaccurate should never pose a problem.
    presenter : WidgetValuePresenter or None
        An optional hook that allows a widget to customize how its value should be
        presented.
    bind : BindOption
        Optional binding for the widget's value to external state.
        Currently only "query-params" is supported, which binds the widget
        value to a URL query parameter. Requires a user-provided key.
    persist_state : "page", "session", or None
        How long to preserve the widget's value when it isn't rendered. If
        "page", the value is preserved only while the user stays on the page
        where the widget is defined (for example, while it is conditionally
        hidden); it is discarded on a page switch and not restored on return.
        If "session", it is preserved for the whole session, including across
        page switches, so it returns when the user navigates back. If None, it
        is not preserved. Requires a user-provided key. If bind="query-params"
        is also set, the binding takes precedence: the value is stored in the
        URL and therefore persists across page switches regardless of the
        persist_state scope.
    formatted_options : list[str] or None
        **Temporary** - will be removed once all selection widgets use string-based
        wire formats. Currently used for index-based widgets (pills, segmented_control,
        select_slider) to convert indices back to human-readable option strings
        in URLs when auto-correcting filtered values.
    clearable : bool or None
        Whether the widget can be cleared to an empty state (reflects widget's UI
        behavior). When True, an empty URL param (e.g., ?foo=) will seed the widget
        with an empty value. When False, an empty URL param will be ignored.
        **Required when bind='query-params'**, otherwise defaults to False.
    disabled : bool
        Whether the widget is disabled. A disabled widget cannot be interacted
        with in the browser, so Streamlit enforces this server-side: any incoming
        value from the frontend is discarded (the widget keeps its default/previous
        value) and its on-change callback is not invoked. This guards against
        forged widget values from a manipulated BackMsg. Defaults to False.

    Returns
    -------
    register_widget_result : RegisterWidgetResult[T]
        Provides information on which value to return to the widget caller,
        and whether the UI needs updating.

        - Unhappy path:
            - Our ScriptRunContext doesn't exist (meaning that we're running
            as a "bare script" outside streamlit).
            - We are disconnected from the SessionState instance.
            In both cases we'll return a fallback RegisterWidgetResult[T].
        - Happy path:
            - The widget has already been registered on a previous run but the
            user hasn't interacted with it on the client. The widget will have
            the default value it was first created with. We then return a
            RegisterWidgetResult[T], containing this value.
            - The widget has already been registered and the user *has*
            interacted with it. The widget will have that most recent
            user-specified value. We then return a RegisterWidgetResult[T],
            containing this value.

        For both paths a widget return value is provided, allowing the widgets
        to be used in a non-streamlit setting.
    """
    if on_change_handler is not None and callbacks is not None:
        raise StreamlitAPIException(
            "Cannot provide both `on_change` and `callbacks` to a widget."
        )

    # Validate bind parameter value
    if bind is not None and bind != "query-params":
        raise StreamlitInvalidBindValueError("bind", ["'query-params'", "None"])

    # Validate that widget with bind="query-params" has a provided key
    if bind == "query-params":
        user_key = user_key_from_element_id(element_id)
        if user_key is None:
            raise StreamlitAPIException(
                "When using bind='query-params', the widget must have a unique 'key' "
                "parameter specified. This 'key' will be used as the name of the "
                "query parameter."
            )
        # Internal API check: clearable must be set for query param binding
        if clearable is None:
            raise ValueError(
                "clearable must be explicitly set when bind='query-params'. "
                "This is required for correct empty value handling."
            )

    # Validate persist_state value and key requirement.
    if persist_state is not None:
        if persist_state not in {"page", "session"}:
            raise StreamlitInvalidPersistStateError(
                "persist_state", ["'page'", "'session'", "None"]
            )
        if user_key_from_element_id(element_id) is None:
            raise StreamlitAPIException(
                "When using persist_state, the widget must have a unique 'key' "
                "parameter specified so its value can be preserved across reruns "
                "and page switches."
            )

    # Create the widget's updated metadata, and register it with session_state.
    metadata = WidgetMetadata(
        element_id,
        deserializer,
        serializer,
        value_type=value_type,
        callback=on_change_handler,
        callbacks=callbacks,
        callback_args=args,
        callback_kwargs=kwargs,
        fragment_id=ThreadState.get().fragment_id if ctx else None,
        presenter=presenter,
        bind=bind,
        persist_state=persist_state,
        formatted_options=formatted_options,
        clearable=clearable if clearable is not None else False,
        max_array_length=max_array_length,
        allow_url_duplicates=allow_url_duplicates,
        disabled=disabled,
    )
    return register_widget_from_metadata(metadata, ctx)


def register_widget_from_metadata(
    metadata: WidgetMetadata[T],
    ctx: ScriptRunContext | None,
) -> RegisterWidgetResult[T]:
    """Register a widget and return its value, using an already constructed
    `WidgetMetadata`.

    This is split out from `register_widget` to allow caching code to replay
    widgets by saving and reusing the completed metadata.

    See `register_widget` for details on what this returns.
    """
    if ctx is None:
        # Early-out if we don't have a script run context (which probably means
        # we're running as a "bare" Python script, and not via `streamlit run`).
        return RegisterWidgetResult.failure(deserializer=metadata.deserializer)

    widget_id = metadata.id
    user_key = user_key_from_element_id(widget_id)

    return ctx.session_state.register_widget(metadata, user_key)
