import os
from typing import Any, Callable, Mapping, Optional


_component_func = None


def viewer(
    dataset: Mapping[str, Any],
    *,
    settings: Optional[dict] = None,
    mode: str = "default",
    no_info_panel: bool = False,
    width: str | int = "stretch",
    height: int = 550,
    key: str = "chemiscope_viewer",
    selected_index: Optional[int] = None,
    on_select: Optional[Callable[[Optional[int]], Any]] = None,
    on_settings_change: Optional[Callable[[dict], Any]] = None,
) -> Any:
    """
    Render a Chemiscope viewer inside a Streamlit app.

    :param dict dataset: Chemiscope dataset (same dict generated by
        :py:func:`chemiscope.create_input`).
    :param dict settings: Optional settings dictionary to configure the viewer
        appearance and behavior, as generated e.g. by
        :py:func:`chemiscope.quick_settings`.
    :param str mode: Visualization mode: "default", "structure", or "map".
    :param bool no_info_panel: Hide the info panel, disabling in-widget selection
        for ``structure`` mode
    :param str|int width: Width in pixels or "stretch" for full width (default).
    :param int height: Height in pixels for the viewer (or None to auto-size).
    :param str key: Streamlit widget key.
    :param int selected_index: Structure index to select. Can also be used
        to update the selection from outside the component if set to a streamlit
        state variable.
    :param callable on_select: Optional callback function called when the
        structure selection changes. Receives the new selected index or None
        if unselected.
    :param callable on_settings_change: Called when settings change, settings
        dict as argument.
    """
    global _component_func

    try:
        import streamlit as st
        import streamlit.components.v1 as components
    except ImportError:
        raise ImportError(
            "Streamlit is required to use chemiscope.streamlit.viewer. "
            "Install it with: pip install 'chemiscope[streamlit]'"
        )

    if _component_func is None:
        _component_func = components.declare_component(
            "chemiscope_viewer",
            path=os.path.relpath(os.path.dirname(__file__)),
        )

    if mode not in ("default", "structure", "map"):
        raise ValueError(
            f"Invalid mode '{mode}', expected 'default', 'structure', or 'map'"
        )

    has_structures = "structures" in dataset and len(dataset["structures"]) > 0
    has_properties = "properties" in dataset and len(dataset["properties"]) > 0
    if mode == "structure" and not has_structures:
        raise ValueError("Cannot show structure: dataset has no structures")
    if mode == "map" and (not has_properties or len(dataset["properties"]) < 2):
        raise ValueError("Map mode requires at least two properties")

    state_key = f"{key}_state"
    if state_key not in st.session_state:
        st.session_state[state_key] = {
            "selected_index": selected_index,
            "settings": settings or {},
            "last_update": None,
        }

    def on_change():
        current_state = st.session_state[state_key]
        component_value = st.session_state[key]

        if component_value is None:
            return

        new_selection = component_value.get("selected_id")
        new_settings = component_value.get("settings", {})

        selection_changed = new_selection != current_state["selected_index"]
        settings_changed = new_settings != current_state["settings"]

        if selection_changed:
            current_state["selected_index"] = new_selection
            if on_select:
                on_select(new_selection)

        if settings_changed:
            current_state["settings"] = new_settings
            if on_settings_change:
                on_settings_change(new_settings)

        if selection_changed or settings_changed:
            current_state["last_update"] = "component"

    current_state = st.session_state[state_key]

    external_selection_changed = (
        selected_index is not None
        and selected_index != current_state["selected_index"]
        and current_state["last_update"] != "streamlit"
    )

    external_settings_changed = (
        settings is not None
        and settings != current_state["settings"]
        and current_state["last_update"] != "streamlit"
    )

    if external_selection_changed:
        current_state["selected_index"] = selected_index
        current_state["last_update"] = "streamlit"

    if external_settings_changed:
        current_state["settings"] = settings
        current_state["last_update"] = "streamlit"

    if current_state["last_update"] == "streamlit":
        current_state["last_update"] = None

    return _component_func(
        dataset=dataset,
        settings=current_state["settings"],
        mode=mode,
        no_info_panel=no_info_panel,
        key=key,
        width=width,
        height=height,
        selected_index=current_state["selected_index"],
        default=None,
        on_change=on_change,
    )
