from __future__ import annotations

import param
from panel.util import edit_readonly, isIn
from panel.util.parameters import get_params_to_inherit
from panel.widgets.base import Widget
from panel.widgets.select import NestedSelect as _PnNestedSelect
from panel.widgets.select import Select as _PnSelect
from panel.widgets.select import SingleSelectBase as _PnSingleSelectBase
from panel.widgets.select import _MultiSelectBase as _PnMultiSelectBase
from typing_extensions import Self

from ..base import COLORS, LoadingTransform, ThemedTransform
from .base import MaterialWidget
from .button import _ButtonLike


class MaterialSingleSelectBase(MaterialWidget, _PnSingleSelectBase):
    """
    Base class for Material UI single-select widgets.

    This class combines Material UI styling with Panel's single select functionality.
    It provides the foundation for widgets that allow selecting a single value from
    a list of options, such as dropdown selects and autocomplete inputs.

    This is an abstract base class and should not be used directly.
    """

    value = param.Parameter(default=None, allow_None=True, doc="The selected value.")

    _allows_values = False
    _constants = {"loading_inset": -6}

    __abstract = True


class MaterialMultiSelectBase(MaterialWidget, _PnMultiSelectBase):
    """
    Base class for Material UI multi-select widgets.

    This class combines Material UI styling with Panel's multi-select functionality.
    It provides the foundation for widgets that allow selecting multiple values from
    a list of options, such as multi-select dropdowns and autocomplete inputs.

    This is an abstract base class and should not be used directly.
    """

    value = param.List(default=[], allow_None=True, doc="The selected values.")

    _constants = {"loading_inset": -6}

    _rename = {"name": "name"}

    _allows_values = False

    __abstract = True

    def __init__(self, **params):
        if params.get('value') is None:
            params['value'] = []
        super().__init__(**params)


class AutocompleteInput(MaterialSingleSelectBase):
    """
    The `AutocompleteInput` widget allows searching and selecting a single value
    from a list of `options`.

    It falls into the broad category of single-value, option-selection widgets
    that provide a compatible API and include the  `Select`,
    `RadioBoxGroup` and `RadioButtonGroup` widgets.

    :References:

    - https://panel-material-ui.holoviz.org/reference/widgets/AutocompleteInput.html
    - https://panel.holoviz.org/reference/widgets/AutocompleteInput.html
    - https://mui.com/material-ui/react-autocomplete/

    :Example:

    >>> AutocompleteInput(
    ...     label='Study', options=['Biology', 'Chemistry', 'Physics'],
    ... )
    """

    case_sensitive = param.Boolean(default=True, doc="""
        Enable or disable case sensitivity.""")

    color = param.Selector(objects=COLORS, default="primary", doc="The color of the autocomplete input.")

    lazy_search = param.Boolean(default=False, doc="""
        If True, search queries are sent to the backend for processing.
        This is useful when options are large or need server-side filtering.""")

    min_characters = param.Integer(default=2, doc="""
        The number of characters a user must type before
        completions are presented.""")

    placeholder = param.String(default="", doc="""
        Placeholder for empty input field.""")

    restrict = param.Boolean(default=True, doc="""
        Set to False in order to allow users to enter text that is not
        present in the list of completion strings.""")

    search_strategy = param.Selector(default='starts_with',
        objects=['starts_with', 'includes'], doc="""
        Define how to search the list of completion strings. The default option
        `"starts_with"` means that the user's text must match the start of a
        completion string. Using `"includes"` means that the user's text can
        match any substring of a completion string.""")

    size = param.Selector(objects=["small", "medium", "large"], default="medium", doc="""
        Size of the input field. Options:
        - 'small': Compact size for dense layouts
        - 'medium': Standard size (default  for most use cases)
        - 'large': Larger size for more visibility""")

    value_input = param.Parameter(default="", readonly=True, doc="""
        Initial or entered text value updated on every key press.""")

    variant = param.Selector(objects=["filled", "outlined", "standard"], default="outlined", doc="Variant style of the autocomplete input.")

    _allows_none = True

    _esm_base = "Autocomplete.jsx"

    _rename = {"name": "name"}

    def _process_property_change(self, msg):
        is_none = 'value' in msg and not msg['value']
        try:
            params = super()._process_property_change(msg)
        except Exception:
            params = Widget._process_property_change(self, msg)
        if is_none:
            params['value'] = None if self.restrict else ''
        return params

    def _process_param_change(self, msg):
        props = super()._process_param_change(msg)
        if 'value' in msg and not self.restrict and not isIn(msg['value'], self.values):
            with param.parameterized.discard_events(self):
                self.value = props['value'] = msg['value']
        elif self.lazy_search and "options" in props:
            del props['options']
        return props

    @param.depends('value', watch=True, on_init=True)
    def _sync_value_input(self):
        with edit_readonly(self):
            self.value_input = '' if self.value is None else self.value

    def _handle_msg(self, msg: dict) -> None:
        """
        Process messages from the frontend.

        Parameters
        ----------
        msg : dict
            Message from the frontend. Expected keys:
            - type: "search" for search queries
            - id: unique message ID for response matching
            - query: search query string
            - case_sensitive: whether search should be case sensitive
            - search_strategy: "starts_with" or "includes"
        """
        if msg.get('type') != 'search':
            return
        query_id = msg.get('id')
        query = msg.get('query', '')
        case_sensitive = msg.get('case_sensitive', self.case_sensitive)
        search_strategy = msg.get('search_strategy', self.search_strategy)

        filtered = self._filter_options(query, case_sensitive, search_strategy)
        self._send_msg({
            'type': 'search_response',
            'id': query_id,
            'options': filtered
        })

    def _filter_options(self, query: str, case_sensitive: bool = None, search_strategy: str = None) -> list:
        """
        Filter options based on query string.

        Parameters
        ----------
        query : str
            Search query string
        case_sensitive : bool, optional
            Whether search should be case sensitive. Defaults to self.case_sensitive
        search_strategy : str, optional
            Search strategy: "starts_with" or "includes". Defaults to self.search_strategy

        Returns
        -------
        list
            Filtered list of options
        """
        if case_sensitive is None:
            case_sensitive = self.case_sensitive
        if search_strategy is None:
            search_strategy = self.search_strategy

        if not query or len(query) < self.min_characters:
            return []

        options = self.values
        if not case_sensitive:
            query = query.lower()

        filtered = []
        for opt in options:
            opt_str = str(opt)
            if not case_sensitive:
                opt_str = opt_str.lower()

            if search_strategy == 'includes':
                if query in opt_str:
                    filtered.append(opt)
            else:  # starts_with
                if opt_str.startswith(query):
                    filtered.append(opt)

        return filtered

    def clone(self, **params) -> Self:
        """
        Makes a copy of the object sharing the same parameters.

        Parameters
        ----------
        params: Keyword arguments override the parameters on the clone.

        Returns
        -------
        Cloned Viewable object
        """
        inherited = get_params_to_inherit(self)
        if 'value_input' in inherited:
            del inherited['value_input']
        return type(self)(**dict(inherited, **params))


class _SelectDropdownBase(MaterialWidget):
    """
    Base class for Material UI dropdown based select widgets.

    This is an abstract base class and should not be used directly.
    """

    bookmarks = param.List(default=[], doc="List of bookmarked options")

    disabled_options = param.List(default=[], nested_refs=True, doc="""
        Optional list of ``options`` that are disabled, i.e. unusable and
        un-clickable. If ``options`` is a dictionary the list items have to
        correspond to the values in the options dictionary..""")

    filter_str = param.String(default="", doc="Filter string for the dropdown")

    filter_on_search = param.Boolean(default=True, doc="""
        Whether options are filtered or merely highlighted on search.""")

    dropdown_height = param.Integer(default=500, doc="Height of the dropdown menu")

    dropdown_open = param.Boolean(default=False, doc="Whether the dropdown is open")

    searchable = param.Boolean(default=False, doc="Whether the dropdown is searchable")

    value_label = param.String(doc="Custom label to describe the current option(s).")

    __abstract = True


class Select(MaterialSingleSelectBase, _PnSelect, _SelectDropdownBase):
    """
    The `Select` widget allows selecting a value from a list.

    It falls into the broad category of single-value, option-selection widgets
    that provide a compatible API and include the  `AutocompleteInput`,
    `RadioBoxGroup` and `RadioButtonGroup` widgets.

    :References:

    - https://panel-material-ui.holoviz.org/reference/widgets/Select.html
    - https://panel.holoviz.org/reference/widgets/Select.html
    - https://mui.com/material-ui/react-select/

    :Example:

    >>> Select(label='Study', options=['Biology', 'Chemistry', 'Physics'])
    """

    color = param.Selector(objects=COLORS, default="primary", doc="The color of the select widget.")

    groups = param.Dict(default=None, nested_refs=True, doc="""
        Dictionary whose keys are used to visually group the options
        and whose values are either a list or a dictionary of options
        to select from. Mutually exclusive with ``options``  and valid only
        if ``size`` is 1.""")

    size = param.Selector(objects=["small", "medium", "large"], default="medium")

    variant = param.Selector(objects=["filled", "outlined", "standard"], default="outlined", doc="The variant style of the select widget.")

    _constants = {"multi": False, "loading_inset": -6}
    _esm_base = "Select.jsx"
    _rename = {"name": "name", "groups": None}

    def _validate_options_groups(self, *events):
        if self.options and self.groups:
            raise ValueError(
                f'{type(self).__name__} options and groups parameters '
                'are mutually exclusive.'
            )

class _RadioGroup(MaterialWidget):
    """
    Base class for Material UI radio groups.

    This class combines Material UI styling with Panel's radio group functionality.
    It provides the foundation for widgets that allow selecting a single value from
    a list of options, such as radio buttons and checkboxes.
    """

    color = param.Selector(default="primary", objects=COLORS, doc="""
        The color of the widget.""")

    label_placement = param.Selector(default="end", objects=["bottom", "start", "top", "end"], doc="""
        Placement of the option labels.""")

    inline = param.Boolean(default=False, doc="""
        Whether the items be arrange vertically (``False``) or
        horizontally in-line (``True``).""")

    width = param.Integer(default=None)

    _esm_base = "RadioGroup.jsx"

    _rename = {"name": "name"}

    __abstract = True


class RadioBoxGroup(_RadioGroup, MaterialSingleSelectBase):
    """
    The `RadioBoxGroup` widget allows selecting a value from a list of options.

    It falls into the broad category of single-value, option-selection widgets
    that provide a compatible API and include the  `AutocompleteInput`,
    `Select` and `RadioButtonGroup` widgets.

    :References:

    - https://panel-material-ui.holoviz.org/reference/widgets/RadioBoxGroup.html
    - https://panel.holoviz.org/reference/widgets/RadioBoxGroup.html
    - https://mui.com/material-ui/react-radio-button/

    :Example:

    >>> RadioBoxGroup(
    ...     label='Study', options=['Biology', 'Chemistry', 'Physics'],
    ... )
    """

    value = param.Parameter(default=None, allow_None=True)

    _constants = {"exclusive": True, "loading_inset": -6}


class CheckBoxGroup(_RadioGroup, MaterialMultiSelectBase):
    """
    The `CheckBoxGroup` widget allows selecting between a list of options by
    ticking the corresponding checkboxes.

    It falls into the broad category of multi-option selection widgets that
    provide a compatible API that also include the `CheckButtonGroup` widget.

    :References:

    - https://panel-material-ui.holoviz.org/reference/widgets/CheckBoxGroup.html
    - https://panel.holoviz.org/reference/widgets/CheckBoxGroup.html
    - https://mui.com/material-ui/react-checkbox/#formgroup

    :Example:

    >>> CheckBoxGroup(
    ...     name='Fruits', value=['Apple', 'Pear'], options=['Apple', 'Banana', 'Pear', 'Strawberry'],
    ... )
    """

    value = param.List(default=None, allow_None=True)

    _constants = {"exclusive": False, "loading_inset": -6}


class _ButtonGroup(_ButtonLike):
    """
    Base class for Material UI button groups.

    This class combines Material UI styling with Panel's button group functionality.
    It provides the foundation for widgets that allow selecting a single or multiple
    values from a list of options, such as toggle buttons and checkboxes.

    This is an abstract base class and should not be used directly.
    """

    orientation = param.Selector(default="horizontal", objects=["horizontal", "vertical"], doc="""
        Button group orientation, either 'horizontal' (default) or 'vertical'.""")

    size = param.Selector(objects=["small", "medium", "large"], default="medium", doc="The size of the button group.")

    width = param.Integer(default=None)

    _esm_base = "ButtonGroup.jsx"

    _esm_transforms = [LoadingTransform, ThemedTransform]

    _rename = {"name": "name"}

    __abstract = True


class RadioButtonGroup(_ButtonGroup, MaterialSingleSelectBase):
    """
    The `RadioButtonGroup` widget allows selecting from a list or dictionary
    of values using a set of toggle buttons.

    It falls into the broad category of single-value, option-selection widgets
    that provide a compatible API and include the `AutocompleteInput`, `Select`,
    and `RadioBoxGroup` widgets.

    :References:

    - https://panel-material-ui.holoviz.org/reference/widgets/RadioButtonGroup.html
    - https://panel.holoviz.org/reference/widgets/RadioButtonGroup.html
    - https://mui.com/material-ui/react-toggle-button/

    :Example:

    >>> RadioButtonGroup(
    ...     label='Plotting library', options=['Matplotlib', 'Bokeh', 'Plotly'],
    ... )
    """

    value = param.Parameter()

    _constants = {"exclusive": True, "loading_inset": -6}


class CheckButtonGroup(_ButtonGroup, MaterialMultiSelectBase):
    """
    The `CheckButtonGroup` widget allows selecting from a list or dictionary
    of values using a set of toggle buttons.

    It falls into the broad category of multi-option selection widgets that
    provide a compatible API that also include the `CheckBoxGroup` widget.

    :References:

    - https://panel-material-ui.holoviz.org/reference/widgets/CheckButtonGroup.html
    - https://panel.holoviz.org/reference/widgets/CheckButtonGroup.html
    - https://mui.com/material-ui/react-toggle-button/

    :Example:

    >>> CheckButtonGroup(
    ...     label='Regression Models', value=['Lasso', 'Ridge'],
    ...     options=['Lasso', 'Linear', 'Ridge', 'Polynomial']
    ... )

    """

    _constants = {"exclusive": False, "loading_inset": -6}


class MultiSelect(MaterialMultiSelectBase):
    """
    The `MultiSelect` widget allows selecting multiple values from a list of
    `options`.

    It falls into the broad category of multi-value, option-selection widgets
    that provide a compatible API and include the `MultiSelect`,
    `CrossSelector`, `CheckBoxGroup` and `CheckButtonGroup` widgets.

    :References:

    - https://panel-material-ui.holoviz.org/reference/widgets/MultiSelect.html
    - https://panel.holoviz.org/reference/widgets/MultiSelect.html
    - https://mui.com/material-ui/react-select/#multiple-select

    >>> pmui.MultiSelect(label='MultiSelect', value=['Apple', 'Pear'],
    ...     options=['Apple', 'Banana', 'Pear', 'Strawberry'], size=8)
    """

    color = param.Selector(objects=COLORS, default="primary", doc="Color of the multi-select component.")

    max_items = param.Integer(default=None, bounds=(1, None), doc="""
        Maximum number of options that can be selected.""")

    size = param.Integer(default=None, doc="""
        The number of options to display at once (not currently supported).""")

    value = param.List(default=[], allow_None=True)

    variant = param.Selector(objects=["filled", "outlined", "standard"], default="outlined", doc="Variant style of the multi-select component.")

    _esm_base = "MultiSelect.jsx"


class MultiChoice(_SelectDropdownBase, MultiSelect):
    """
    The `MultiChoice` widget allows selecting multiple values from a list of
    `options`.

    It falls into the broad category of multi-value, option-selection widgets
    that provide a compatible API and include the `MultiSelect`,
    `CrossSelector`, `CheckBoxGroup` and `CheckButtonGroup` widgets.

    The `MultiChoice` widget provides a much more compact UI than
    `MultiSelect`.

    :References:

    - https://panel-material-ui.holoviz.org/reference/widgets/MultiChoice.html
    - https://panel.holoviz.org/reference/widgets/MultiChoice.html
    - https://mui.com/material-ui/react-select/#multiple-select

    :Example:

    >>> MultiChoice(
    ...     label='Favourites', value=['Panel', 'hvPlot'],
    ...     options=['Panel', 'hvPlot', 'HoloViews', 'GeoViews', 'Datashader', 'Param', 'Colorcet'],
    ...     max_items=2
    ... )
    """

    chip = param.Boolean(default=True, doc="Whether to display a chip for each selected option")

    delete_button = param.Boolean(default=True, doc="""
        Whether to display a button to delete a selected option.""")

    option_limit = param.Integer(default=None, bounds=(1, None), doc="""
        Maximum number of options to display at once.""")

    search_option_limit = param.Integer(default=None, bounds=(1, None), doc="""
        Maximum number of options to display at once if search string is entered.""")

    placeholder = param.String(default='', doc="""
        String displayed when no selection has been made.""")

    solid = param.Boolean(default=True, doc="""
        Whether to display chips with solid or outlined style.""")

    _constants = {"multi": True, "loading_inset": -6}
    _esm_base = "Select.jsx"
    _rename = {"name": None}


class CrossSelector(MaterialMultiSelectBase):
    """
    The `CrossSelector` widget allows selecting multiple values from a list of
    `options`.

    It falls into the broad category of multi-value, option-selection widgets
    that provide a compatible API and include the `MultiSelect`,
    `CrossSelector`, `CheckBoxGroup` and `CheckButtonGroup` widgets.

    :References:

    - https://panel-material-ui.holoviz.org/reference/widgets/CrossSelector.html
    - https://panel.holoviz.org/reference/widgets/CrossSelector.html
    - https://mui.com/material-ui/react-select/#multiple-select

    :Example:

    >>> CrossSelector(
    ...     label='Favourites', value=['Panel', 'hvPlot'],
    ...     options=['Panel', 'hvPlot', 'HoloViews', 'GeoViews', 'Datashader', 'Param', 'Colorcet'],
    ...     max_items=2
    ... )
    """

    color = param.Selector(objects=COLORS, default="primary", doc="""The color of the cross selector widget.""")

    searchable = param.Boolean(default=True, doc="Whether the dropdown is searchable")

    width = param.Integer(default=None, doc="Width of the widget")

    size = param.Integer(default=10, doc="""
        The number of options shown at once (note this is the only way
        to control the height of this widget)""")

    _esm_base = "CrossSelector.jsx"


class NestedSelect(_PnNestedSelect):
    """
    The `NestedSelect` widget is composed of multiple widgets, where subsequent select options
    depend on the parent's value.

    :References:

    - https://panel-material-ui.holoviz.org/reference/widgets/NestedSelect.html
    - https://panel.holoviz.org/reference/widgets/NestedSelect.html
    - https://mui.com/material-ui/react-select/

    :Example:

    >>> NestedSelect(
    ...     options={
    ...         "gfs": {"tmp": [1000, 500], "pcp": [1000]},
    ...         "name": {"tmp": [1000, 925, 850, 700, 500], "pcp": [1000]},
    ...     },
    ...     levels=["model", "var", "level"],
    ... )
    """

    def _extract_level_metadata(self, i):
        """
        Extract the widget type and keyword arguments from the level metadata.
        """
        level = self._levels[i]
        if isinstance(level, int):
            return Select, {}
        elif isinstance(level, str):
            return Select, {"name": level}
        widget_type = level.get("type", Select)
        widget_kwargs = {k: v for k, v in level.items() if k != "type"}
        return widget_type, widget_kwargs


__all__ = [
    "AutocompleteInput",
    "CrossSelector",
    "Select",
    "RadioBoxGroup",
    "CheckBoxGroup",
    "RadioButtonGroup",
    "CheckButtonGroup",
    "MultiSelect",
    "MultiChoice",
    "NestedSelect",
]
