"""
Sliders allow you to select a value from a defined range of values by
moving one or more handle(s).

- The `value` will update when a handle is dragged.
- The `value_throttled`will update when a handle is released.
"""
from __future__ import annotations

import datetime as dt
import typing as t

import numpy as np
import param

from bokeh.models import CustomJS
from bokeh.models.formatters import TickFormatter
from bokeh.models.widgets import (
    DateRangeSlider as _BkDateRangeSlider, DateSlider as _BkDateSlider,
    RangeSlider as _BkRangeSlider, Slider as _BkSlider,
)
from bokeh.models.widgets.sliders import NumericalSlider as _BkNumericalSlider
from param.parameterized import resolve_value

from ..config import config
from ..io import state
from ..io.resources import CDN_DIST
from ..layout import Column, Row
from ..models.datetime_slider import DatetimeSlider as _BkDatetimeSlider
from ..util import (
    datetime_as_utctimestamp, edit_readonly, param_reprs, value_as_date,
    value_as_datetime,
)
from ..viewable import Layoutable
from ..widgets import FloatInput, IntInput
from .base import CompositeWidget, Widget, WidgetBase
from .input import StaticText

if t.TYPE_CHECKING:
    from collections.abc import Mapping

    from bokeh.document import Document
    from bokeh.model import Model
    from pyviz_comms import Comm

    from ..layout.base import ListLike, NamedListLike


class _SliderBase(Widget):

    bar_color = param.Color(default="#e6e6e6", doc="""
        The color of the slider bar. Accepts any valid CSS color string.""")

    direction: t.Literal['ltr', 'rtl'] = param.Selector(
        default='ltr', objects=['ltr', 'rtl'], doc="""
        Whether the slider should go from left-to-right ('ltr') or
        right-to-left ('rtl').""")  # type: ignore[assignment, ty:invalid-assignment]

    orientation: t.Literal['horizontal', 'vertical'] = param.Selector(
        default='horizontal', objects=['horizontal', 'vertical'],
        doc="""
        Whether the slider should be oriented horizontally or
        vertically.""")  # type: ignore[assignment, ty:invalid-assignment]

    show_value = param.Boolean(default=True, doc="""
        Whether to show the widget value as a label or not.""")

    tooltips = param.Boolean(default=True, doc="""
        Whether the slider handle should display tooltips.""")

    _widget_type: t.ClassVar[type[Model]] = _BkSlider

    __abstract = True

    def __init__(self, **params):
        if 'value' in params and 'value_throttled' in self.param:
            params['value_throttled'] = params['value']
        if 'orientation' == 'vertical':
            params['height'] = self.param.width.default
        super().__init__(**params)

    def __repr__(self, depth=0):
        return '{cls}({params})'.format(cls=type(self).__name__,
                                        params=', '.join(param_reprs(self, ['value_throttled'])))

    @property
    def _linked_properties(self) -> tuple[str, ...]:
        return super()._linked_properties + ('value_throttled',)

    def _process_property_change(self, props: dict[str, t.Any]) -> dict[str, t.Any]:
        if config.throttled:
            if "value" in props:
                del props["value"]
            if "value_throttled" in props:
                props["value"] = props["value_throttled"]
        return super()._process_property_change(props)

    def _update_model(
        self, events: dict[str, param.parameterized.Event], msg: dict[str, t.Any],
        root: Model, model: Model, doc: Document, comm: Comm | None
    ) -> None:
        if 'value_throttled' in msg:
            del msg['value_throttled']

        return super()._update_model(events, msg, root, model, doc, comm)

    def _process_events(self, events: dict[str, t.Any]) -> None:
        if config.throttled:
            events.pop("value", None)
        super()._process_events(events)


class ContinuousSlider(_SliderBase):

    format = param.ClassSelector(class_=(str, TickFormatter,), doc="""
        A custom format string or Bokeh TickFormatter.""")

    _supports_embed: bool = True

    __abstract = True

    def __init__(self, **params):
        if 'value' not in params:
            params['value'] = params.get('start', self.start)
        super().__init__(**params)

    def _get_embed_state(self, root, values=None, max_opts=3):
        ref = root.ref['id']
        w_model, parent = self._models[ref]
        if not isinstance(w_model, _BkNumericalSlider):
            is_composite = True
            parent = w_model
            w_model = w_model.select_one({'type': _BkNumericalSlider})
        else:
            is_composite = False
        _, _, doc, comm = state._views[ref]

        # Compute sampling
        start, end, step = w_model.start, w_model.end, w_model.step
        if values is None:
            span = end-start
            dtype = int if isinstance(step, int) else float
            if (span/step) > (max_opts-1):
                step = dtype(span/(max_opts-1))
            values = [dtype(v) for v in np.arange(start, end+step, step)]
        elif any(v < start or v > end for v in values):
            raise ValueError(f'Supplied embed states for {type(self).__name__} widget outside '
                             'of valid range.')

        # Replace model
        layout_opts = {k: v for k, v in self.param.values().items()
                       if k in Layoutable.param and k != 'name'}

        if is_composite:
            layout_opts['show_value'] = False
        else:
            layout_opts['label'] = self.label

        value = values[np.argmin(np.abs(np.array(values)-self.value))]
        dw = DiscreteSlider(options=values, value=value,  **layout_opts)
        dw.link(self, value='value')
        self._models.pop(ref)
        index = parent.children.index(w_model)
        with config.set(embed=True):
            w_model = dw._get_model(doc, root, parent, comm)
        link = CustomJS(code=dw._jslink.code['value'], args={
            'source': w_model.children[1], 'target': w_model.children[0]})
        parent.children[index] = w_model
        w_model = w_model.children[1]
        w_model.js_on_change('value', link)

        return (dw, w_model, values, lambda x: x.value, 'value', 'cb_obj.value')


class FloatSlider(ContinuousSlider):
    """
    The FloatSlider widget allows selecting a floating-point value
    within a set of bounds using a slider.

    Reference: https://panel.holoviz.org/reference/widgets/FloatSlider.html

    :Example:

    >>> FloatSlider(value=0.5, start=0.0, end=1.0, step=0.1, name="Float value")
    """

    start = param.Number(default=0.0, doc="The lower bound.")

    end = param.Number(default=1.0, doc="The upper bound.")

    step = param.Number(default=0.1, doc="The step size.")

    value = param.Number(default=0.0, allow_None=True, doc="""
        The selected floating-point value of the slider. Updated when
        the handle is dragged."""
    )

    value_throttled = param.Number(default=None, constant=True, doc="""
         The value of the slider. Updated when the handle is released.""")

    _rename: t.ClassVar[Mapping[str, str | None]] = {
        **Widget._rename,
        'value_throttled': None,
    }


class IntSlider(ContinuousSlider):
    """
    The IntSlider widget allows selecting an integer value within a
    set of bounds using a slider.

    Reference: https://panel.holoviz.org/reference/widgets/IntSlider.html

    :Example:

    >>> IntSlider(value=5, start=0, end=10, step=1, name="Integer Value")
    """

    start = param.Integer(default=0, doc="""
        The lower bound.""")

    end = param.Integer(default=1, doc="""
        The upper bound.""")

    step = param.Integer(default=1, doc="""
        The step size.""")

    value = param.Integer(default=0, allow_None=True, doc="""
        The selected integer value of the slider. Updated when the handle is dragged.""")

    value_throttled = param.Integer(default=None, constant=True, doc="""
        The value of the slider. Updated when the handle is released""")

    _rename: t.ClassVar[Mapping[str, str | None]] = {
        **Widget._rename,
        'value_throttled': None,
    }

    def _process_property_change(self, props: dict[str, t.Any]) -> dict[str, t.Any]:
        params = super()._process_property_change(props)
        if 'value' in params:
            params['value'] = params['value'] if params['value'] is None else int(params['value'])
        if 'value_throttled' in params:
            throttled = params['value_throttled']
            params['value_throttled'] = throttled if throttled is None else int(throttled)
        return params


class DateSlider(_SliderBase):
    """
    The DateSlider widget allows selecting a value within a set of
    bounds using a slider.  Supports datetime.datetime, datetime.date
    and np.datetime64 values. The step size is fixed at 1 day.

    Reference: https://panel.holoviz.org/reference/widgets/DateSlider.html

    :Example:

    >>> import datetime as dt
    >>> DateSlider(
    ...     value=dt.datetime(2025, 1, 1),
    ...     start=dt.datetime(2025, 1, 1),
    ...     end=dt.datetime(2025, 1, 7),
    ...     name="A datetime value"
    ... )
    """

    value = param.Date(default=None, doc="""
        The selected date value of the slider. Updated when the slider
        handle is dragged. Supports datetime.datetime, datetime.date
        or np.datetime64 types.""")

    value_throttled = param.Date(default=None, constant=True, doc="""
        The value of the slider. Updated when the slider handle is released.""")

    start = param.Date(default=None, doc="""
        The lower bound.""")

    end = param.Date(default=None, doc="""
        The upper bound.""")

    as_datetime = param.Boolean(default=False, doc="""
        Whether to store the date as a datetime.""")

    step = param.Integer(default=1, bounds=(1, None), doc="""
        The step parameter in days.""")

    format = param.String(default=None, doc="""
        Datetime format used for parsing and formatting the date.""")

    _rename: t.ClassVar[Mapping[str, str | None]] = {
        **Widget._rename,
        'as_datetime': None, 'value_throttled': None
    }

    _source_transforms: t.ClassVar[Mapping[str, str | None]] = {
        'value': None, 'value_throttled': None, 'start': None, 'end': None
    }

    _widget_type: t.ClassVar[type[Model]] = _BkDateSlider

    def __init__(self, **params):
        if 'value' not in params:
            params['value'] = params.get('start', self.start)
        super().__init__(**params)

    def _process_param_change(self, params: dict[str, t.Any]) -> dict[str, t.Any]:
        props = super()._process_param_change(params)
        if 'value' in props:
            value = props['value']
            if isinstance(value, dt.datetime):
                value = datetime_as_utctimestamp(value)
            props['value'] = value
        return props

    def _process_property_change(self, props: dict[str, t.Any]) -> dict[str, t.Any]:
        params = super()._process_property_change(props)
        transform = value_as_datetime if self.as_datetime else value_as_date
        if 'value' in params:
            params['value'] = transform(params['value'])
        if 'value_throttled' in params:
            params['value_throttled'] = transform(params['value_throttled'])
        return params


class DatetimeSlider(DateSlider):
    """
    The DatetimeSlider widget allows selecting a value within a set of
    bounds using a slider. Supports datetime.date, datetime.datetime
    and np.datetime64 values. The step size is fixed at 1 minute.

    Reference: https://panel.holoviz.org/reference/widgets/DatetimeSlider.html

    :Example:

    >>> import datetime as dt
    >>> DatetimeSlider(
    ...     value=dt.datetime(2025, 1, 1),
    ...     start=dt.datetime(2025, 1, 1),
    ...     end=dt.datetime(2025, 1, 7),
    ...     name="A datetime value"
    ... )
    """

    as_datetime = param.Boolean(default=True, readonly=True, doc="""
        Whether to store the date as a datetime.""")

    step = param.Number(default=60, bounds=(1, None), doc="""
        The step size in seconds. Default is 1 minute, i.e 60 seconds.""")  # type: ignore[assignment, ty:invalid-assignment]

    _property_conversion = staticmethod(value_as_datetime)

    _widget_type: t.ClassVar[type[Model]] = _BkDatetimeSlider


class DiscreteSlider(CompositeWidget, _SliderBase):
    """
    The DiscreteSlider widget allows selecting a value from a discrete
    list or dictionary of values using a slider.

    Reference: https://panel.holoviz.org/reference/widgets/DiscreteSlider.html

    :Example:

    >>> DiscreteSlider(
    ...     value=0,
    ...     options=list([0, 1, 2, 4, 8, 16, 32, 64]),
    ...     name="A discrete value",
    ... )
    """

    value = param.Parameter(doc="""
        The selected value of the slider. Updated when the handle is
        dragged. Must be one of the options.""")

    value_throttled: t.Any = param.Parameter(constant=True, doc="""
        The value of the slider. Updated when the handle is released.""")  # type: ignore[assignment, ty:invalid-assignment]

    options = param.ClassSelector(default=[], class_=(dict, list), doc="""
        A list or dictionary of valid options.""")

    formatter = param.String(default='%.3g', doc="""
        A custom format string. Separate from format parameter since
        formatting is applied in Python, not via the bokeh TickFormatter.""")

    _rename: t.ClassVar[Mapping[str, str | None]] = {'formatter': None}

    _source_transforms: t.ClassVar[Mapping[str, str | None]] = {
        'value': None, 'value_throttled': None, 'options': None
    }

    _supports_embed: bool = True

    _style_params: t.ClassVar[list[str]] = [
        p for p in list(Layoutable.param) if p != 'name'
    ] + ['orientation']

    _slider_style_params: t.ClassVar[list[str]] = [
        'bar_color', 'direction', 'disabled', 'orientation'
    ]

    _text_link = """
    var labels = {labels}
    target.text = labels[source.value]
    """

    def __init__(self, **params):
        self._syncing = False
        super().__init__(**params)
        if 'formatter' not in params and all(isinstance(v, (int, np.int_)) for v in self.values):
            self.formatter = '%d'
        if self.value is None and None not in self.values and self.options:
            self.value = self.values[0]
        elif self.value not in self.values and not (self.value is None or self.options):
            raise ValueError(f'Value {self.value} not a valid option, '
                             'ensure that the supplied value '
                             'is one of the declared options.')

        self._text = StaticText(
            margin=(5, 0, 0, 5), styles={'white-space': 'nowrap'}
        )
        self._slider = None
        self._composite = Column(self._text, self._slider)
        self._update_options()
        self.param.watch(self._update_options, ['options', 'formatter', 'label'])
        self.param.watch(self._update_value, 'value')
        self.param.watch(self._update_value, 'value_throttled')
        self.param.watch(self._update_style, self._style_params)

    def _update_options(self, *events):
        values, labels = self.values, self.labels
        if not self.options and self.value is None:
            value = 0
            label = (f'{self.label}: ' if self.label else '') + '<b>-</b>'
        elif self.value not in values:
            value = 0
            self.value = values[0]
            label = labels[value]
        else:
            value = values.index(self.value)
            label = labels[value]
        disabled = True if len(values) in (0, 1) else self.disabled
        end = 1 if disabled else len(self.options)-1

        self._slider = IntSlider(
            start=0, end=end, value=value, tooltips=False,
            show_value=False, margin=(0, 5, 5, 5),
            _supports_embed=False, disabled=disabled,
            **{p: getattr(self, p) for p in self._slider_style_params if p != 'disabled'}
        )
        self._update_style()
        js_code = self._text_link.format(
            labels='['+', '.join([repr(l) for l in labels])+']'
        )
        self._jslink = self._slider.jslink(self._text, code={'value': js_code})
        self._slider.param.watch(self._sync_value, 'value')
        self._slider.param.watch(self._sync_value, 'value_throttled')
        self.param.watch(self._update_slider_params, self._slider_style_params)
        self._text.value = label
        self._composite[1] = self._slider

    def _update_value(self, event):
        """
        This will update the IntSlider (behind the scene)
        based on changes to the DiscreteSlider (front).

        _syncing options is to avoid infinite loop.

        event.name is either value or value_throttled.
        """

        values = self.values
        if getattr(self, event.name) not in values:
            with param.edit_constant(self):
                setattr(self, event.name, values[0])
            return
        index = self.values.index(getattr(self, event.name))
        if event.name == 'value':
            self._text.value = self.labels[index]
        if self._syncing:
            return
        try:
            self._syncing = True
            with param.edit_constant(self._slider):
                setattr(self._slider, event.name, index)
        finally:
            self._syncing = False

    def _update_style(self, *events):
        style = {p: getattr(self, p) for p in self._style_params}
        margin = style.pop('margin')
        if isinstance(margin, tuple):
            if len(margin) == 2:
                t = b = margin[0]
                r = l = margin[1]
            else:
                t, r, b, l = margin
        else:
            t = r = b = l = margin
        text_margin = (t, 0, 0, l)
        slider_margin = (0, r, b, l)
        text_style = {k: v for k, v in style.items()
                      if k not in ('style', 'orientation')}
        text_style['visible'] = self.show_value and text_style['visible']
        self._text.param.update(margin=text_margin, **text_style)
        self._slider.param.update(margin=slider_margin, **style)
        if self.width:
            style['width'] = self.width + l + r
        col_style = {k: v for k, v in style.items()
                     if k != 'orientation'}
        self._composite.param.update(**col_style)

    def _update_slider_params(self, *events):
        style = {e.name: e.new for e in events}
        disabled = style.get('disabled', None)
        if disabled is False:
            if len(self.values) in (0, 1):
                self.param.warning(
                    'A DiscreteSlider can only be disabled if it has more than 1 option.'
                )
                end = 1
                del style['disabled']
            else:
                end = len(self.options) - 1
            style['end'] = end
        self._slider.param.update(**style)

    def _sync_value(self, event):
        """
        This will update the DiscreteSlider (front)
        based on changes to the IntSlider (behind the scene).

        _syncing options is to avoid infinite loop.

        event.name is either value or value_throttled.
        """
        if event.name not in ('value', 'value_throttled'):
            return
        if self._syncing:
            return
        try:
            self._syncing = True
            with param.edit_constant(self):
                setattr(self, event.name, self.values[event.new])
        finally:
            self._syncing = False

    def _get_embed_state(self, root, values=None, max_opts=3):
        model = self._composite[1]._models[root.ref['id']][0]
        if values is None:
            values = self.values
        elif any(v not in self.values for v in values):
            raise ValueError("Supplieed embed states were not found "
                             f"in the {type(self).__name__} widgets' values list.")
        return self, model, values, lambda x: x.value, 'value', 'cb_obj.value'

    @property
    def labels(self):
        """The list of labels to display"""
        title = (self.label + ': ' if self.label else '')
        if isinstance(self.options, dict):
            return [title + (f'<b>{o}</b>') for o in self.options]
        else:
            return [title + ('<b>%s</b>' % (o if isinstance(o, str) else (self.formatter % o)))
                    for o in self.options]
    @property
    def values(self):
        """The list of option values"""
        return list(self.options.values()) if isinstance(self.options, dict) else self.options


class _RangeSliderBase(_SliderBase):

    value = param.Tuple(default=(None, None), length=2, allow_None=False, nested_refs=True, doc="""
        The selected range of the slider. Updated when a handle is dragged.""")

    value_start: t.Any = param.Parameter(readonly=True, doc="""The lower value of the selected range.""")  # type: ignore[assignment, ty:invalid-assignment]

    value_end: t.Any = param.Parameter(readonly=True, doc="""The upper value of the selected range.""")  # type: ignore[assignment, ty:invalid-assignment]

    __abstract = True

    def __init__(self, **params):
        if 'value' not in params:
            params['value'] = (
                params.get('start', self.start), params.get('end', self.end)
            )
        if params['value'] is not None:
            v1, v2 = params['value']
            params['value_start'], params['value_end'] = resolve_value(v1), resolve_value(v2)
        with edit_readonly(self):
            super().__init__(**params)

    @param.depends('value', watch=True)
    def _sync_values(self):
        vs, ve = self.value
        with edit_readonly(self):
            self.param.update(value_start=vs, value_end=ve)

    def _process_property_change(self, props: dict[str, t.Any]) -> dict[str, t.Any]:
        params = super()._process_property_change(props)
        if 'value' in params:
            params['value'] = tuple(params['value'])
        if 'value_throttled' in params:
            params['value_throttled'] = tuple(params['value_throttled'])
        return params


class RangeSlider(_RangeSliderBase):
    """
    The RangeSlider widget allows selecting a floating-point range
    using a slider with two handles.

    Reference: https://panel.holoviz.org/reference/widgets/RangeSlider.html

    :Example:

    >>> RangeSlider(
    ...     value=(1.0, 1.5), start=0.0, end=2.0, step=0.25, name="A tuple of floats"
    ... )
    """

    value = param.Range(default=(0, 1), allow_None=False, nested_refs=True, doc=
        """The selected range as a tuple of values. Updated when a handle is
        dragged.""")

    value_throttled = param.Range(default=None, constant=True, nested_refs=True, doc="""
        The selected range as a tuple of floating point values. Updated when a handle is
        released""")

    value_start = param.Number(default=0, readonly=True, doc="""
        The lower value of the selected range.""")

    value_end = param.Number(default=1, readonly=True, doc="""
        The upper value of the selected range.""")

    start = param.Number(default=0, doc="""
        The lower bound.""")

    end = param.Number(default=1, doc="""
        The upper bound.""")

    step = param.Number(default=0.1, doc="""
        The step size.""")

    format = param.ClassSelector(class_=(str, TickFormatter,), doc="""
        A format string or bokeh TickFormatter.""")

    _rename: t.ClassVar[Mapping[str, str | None]] = {
        **Widget._rename, 'value_start': None, 'value_end': None, 'value_throttled': None}

    _widget_type: t.ClassVar[type[Model]] = _BkRangeSlider

    def __init__(self, **params):
        super().__init__(**params)
        values = [self.value[0], self.value[1], self.start, self.end]
        if (all(v is None or isinstance(v, int) for v in values) and
            'step' not in params):
            self.step = 1


class IntRangeSlider(RangeSlider):
    """
    The IntRangeSlider widget allows selecting an integer range using
    a slider with two handles.

    Reference: https://panel.holoviz.org/reference/widgets/IntRangeSlider.html

    :Example:

    >>> IntRangeSlider(
    ...     value=(2, 4), start=0, end=10, step=2, name="A tuple of integers"
    ... )
    """

    start = param.Integer(default=0, doc="The lower bound.")  # type: ignore[assignment]

    end = param.Integer(default=1, doc="The upper bound.")  # type: ignore[assignment]

    step = param.Integer(default=1, doc="The step size.")  # type: ignore[assignment]

    def _process_property_change(self, props: dict[str, t.Any]) -> dict[str, t.Any]:
        params = super()._process_property_change(props)
        if 'value' in params:
            params['value'] = tuple([
                v if v is None else round(v)
                for v in params['value']
            ])
        if 'value_throttled' in params:
            params['value_throttled'] = tuple([v if v is None else round(v)
                                            for v in params['value_throttled']])
        return params


class DateRangeSlider(_SliderBase):
    """
    The DateRangeSlider widget allows selecting a date range using a
    slider with two handles. Supports datetime.datetime, datetime.date
    and np.datetime64 ranges.

    Reference: https://panel.holoviz.org/reference/widgets/DateRangeSlider.html

    :Example:

    >>> import datetime as dt
    >>> DateRangeSlider(
    ...     value=(dt.datetime(2025, 1, 9), dt.datetime(2025, 1, 16)),
    ...     start=dt.datetime(2025, 1, 1),
    ...     end=dt.datetime(2025, 1, 31),
    ...     step=2,
    ...     name="A tuple of datetimes"
    ... )
    """

    value = param.DateRange(default=None, allow_None=False,  # type: ignore[call-overload]
        doc="""
        The selected range as a tuple of values. Updated when one of the handles is
        dragged. Supports datetime.datetime, datetime.date, and np.datetime64 ranges.""")

    value_start = param.Date(default=None, readonly=True, doc="""
        The lower value of the selected range.""")

    value_end = param.Date(default=None, readonly=True, doc="""
        The upper value of the selected range.""")

    value_throttled = param.DateRange(default=None, constant=True, nested_refs=True, doc="""
        The selected range as a tuple of values. Updated one of the handles is released. Supports
        datetime.datetime, datetime.date and np.datetime64 ranges""")

    start = param.Date(default=None, doc="""
        The lower bound.""")

    end = param.Date(default=None, doc="""
        The upper bound.""")

    step = param.Number(default=1, doc="""
        The step size in days. Default is 1 day.""")

    format = param.String(default=None, doc="""
        Datetime format used for parsing and formatting the date.""")

    _source_transforms: t.ClassVar[Mapping[str, str | None]] = {
        'value': None, 'value_throttled': None, 'start': None, 'end': None,
        'step': None
    }

    _rename: t.ClassVar[Mapping[str, str | None]] = {
        **Widget._rename, 'value_start': None, 'value_end': None,
        'value_throttled': None
    }

    _widget_type: t.ClassVar[type[Model]] = _BkDateRangeSlider

    _property_conversion = staticmethod(value_as_date)

    def __init__(self, **params):
        if 'value' not in params:
            value_to_None = False
            for attr in ['start', 'end']:
                if params.get(attr, getattr(self, attr)):
                    continue
                self.param.warning(
                    f'Parameter {attr!r} must be set for the widget to be rendered.'
                )
                value_to_None = True
            if value_to_None:
                params['value'] = None
            else:
                params['value'] = (params.get('start', self.start),
                                params.get('end', self.end))
        if params['value'] is not None:
            v1, v2 = params['value']
            params['value_start'], params['value_end'] = resolve_value(v1), resolve_value(v2)
        with edit_readonly(self):
            super().__init__(**params)

    @param.depends('value', watch=True)
    def _sync_values(self):
        vs, ve = self.value
        with edit_readonly(self):
            self.param.update(value_start=vs, value_end=ve)

    def _process_param_change(self, params: dict[str, t.Any]) -> dict[str, t.Any]:
        props = super()._process_param_change(params)
        if props.get('value', 'unchanged') is None:
            del props['value']
        elif 'value' in props:
            v1, v2 = props['value']
            if isinstance(v1, dt.datetime):
                v1 = datetime_as_utctimestamp(v1)
            if isinstance(v2, dt.datetime):
                v2 = datetime_as_utctimestamp(v2)
            props['value'] = (v1, v2)
        if props.get('value_throttled', 'unchanged') is None:
            del props['value_throttled']
        for key in ('start', 'end'):
            if key in props and isinstance(props[key], dt.datetime):
                props[key] = datetime_as_utctimestamp(props[key])
        return props

    def _process_property_change(self, props: dict[str, t.Any]) -> dict[str, t.Any]:
        params = super()._process_property_change(props)
        if 'value' in params:
            v1, v2 = params['value']
            params['value'] = (self._property_conversion(v1), self._property_conversion(v2))
        if 'value_throttled' in params:
            v1, v2 = params['value_throttled']
            params['value_throttled'] = (self._property_conversion(v1), self._property_conversion(v2))
        return params



class DatetimeRangeSlider(DateRangeSlider):

    """
    The DatetimeRangeSlider widget allows selecting a datetime range
    using a slider with two handles. Supports datetime.datetime and
    np.datetime64 ranges.

    Reference: https://panel.holoviz.org/reference/widgets/DatetimeRangeSlider.html

    :Example:

    >>> import datetime as dt
    >>> DatetimeRangeSlider(
    ...     value=(dt.datetime(2025, 1, 9), dt.datetime(2025, 1, 16)),
    ...     start=dt.datetime(2025, 1, 1),
    ...     end=dt.datetime(2025, 1, 31),
    ...     step=10000,
    ...     name="A tuple of datetimes"
    ... )
    """

    step = param.Number(default=60_000, doc="""
        The step size in ms. Default is 1 min.""")

    _property_conversion = staticmethod(value_as_datetime)

    @property
    def _widget_type(self):
        try:
            from bokeh.models import DatetimeRangeSlider
        except ImportError:
            raise ValueError("DatetimeRangeSlider requires bokeh >= 2.4.3") from None
        return DatetimeRangeSlider


class _EditableContinuousSlider(CompositeWidget):
    """
    The EditableFloatSlider extends the FloatSlider by adding a text
    input field to manually edit the value and potentially override
    the bounds.
    """

    editable = param.Boolean(default=True, doc="""
        Whether the value is editable via the text input.""")

    show_value = param.Boolean(default=False, readonly=True, precedence=-1, doc="""
        Whether to show the widget value.""")

    _composite_type: t.ClassVar[type[ListLike] | type[NamedListLike]] = Column
    _slider_widget: t.ClassVar[type[WidgetBase]]
    _input_widget: t.ClassVar[type[WidgetBase]]
    __abstract = True

    def __init__(self, **params):
        if 'width' not in params and 'sizing_mode' not in params:
            params['width'] = 300
        self._validate_init_bounds(params)
        super().__init__(**params)
        self._label = StaticText(margin=0, align='end')
        self._slider = self._slider_widget(
            value=self.value, margin=(0, 0, 5, 0), sizing_mode='stretch_width',
            tags=['composite']
        )
        self._slider.param.watch(self._sync_value, 'value')
        self._slider.param.watch(self._sync_value, 'value_throttled')

        self._value_edit = self._input_widget(
            margin=0, align='end', css_classes=['slider-edit'],
            stylesheets=[f'{CDN_DIST}css/editable_slider.css'],
            format=self.format,
        )
        self._value_edit.param.watch(self._sync_value, 'value')
        self._value_edit.param.watch(self._sync_value, 'value_throttled')
        self._value_edit.jscallback(args={'slider': self._slider}, value="""
        if (cb_obj.value < slider.start)
          slider.start = cb_obj.value
        else if (cb_obj.value > slider.end)
          slider.end = cb_obj.value
        """)

        label = Row(self._label, self._value_edit)
        self._composite.extend([label, self._slider])
        self._update_disabled()
        self._update_editable()
        self._update_layout()
        self._update_name()
        self._update_slider()
        self._update_value()
        self._update_bounds()

    def _validate_init_bounds(self, params):
        """
        This updates the default value, start and end
        if outside the fixed_start and fixed_end
        """
        start, end = None, None
        if "start" not in params:
            if "fixed_start" in params:
                start = params["fixed_start"]
            elif "end" in params:
                start = params.get("end") - params.get("step", 1)
            elif "fixed_end" in params:
                start = params.get("fixed_end") - params.get("step", 1)

        if "end" not in params:
            if "fixed_end" in params:
                end = params["fixed_end"]
            elif "start" in params:
                end = params["start"] + params.get("step", 1)
            elif "fixed_start" in params:
                end = params["fixed_start"] + params.get("step", 1)

        if start is not None:
            params["start"] = start
        if end is not None:
            params["end"] = end

        if "value" not in params and "start" in params:
            params["value"] = params["start"]
        if "value" not in params and "end" in params:
            params["value"] = params["end"]

    @param.depends('width', 'height', 'sizing_mode', watch=True)
    def _update_layout(self):
        self._value_edit.sizing_mode = self.sizing_mode
        if self.sizing_mode not in ('stretch_width', 'stretch_both'):
            w = (self.width or 300)//4
            self._value_edit.width = w

    @param.depends('disabled', 'editable', watch=True)
    def _update_editable(self):
        self._value_edit.disabled = (not self.editable) or self.disabled

    @param.depends('disabled', watch=True)
    def _update_disabled(self):
        self._slider.disabled = self.disabled

    @param.depends('label', watch=True)
    def _update_name(self):
        if self.label:
            label = f'{self.label}:'
            margin = (0, 10, 0, 0)
        else:
            label = ''
            margin = (0, 0, 0, 0)
        self._label.param.update(margin=margin, value=label)

    @param.depends('start', 'end', 'step', 'bar_color', 'direction',
                   'show_value', 'tooltips', 'format', watch=True)
    def _update_slider(self):
        self._slider.param.update(
            format=self.format,
            start=self.start,
            end=self.end,
            step=self.step,
            bar_color=self.bar_color,
            direction=self.direction,
            show_value=self.show_value,
            tooltips=self.tooltips
        )
        self._value_edit.step = self.step

    @param.depends('value', watch=True)
    def _update_value(self):
        if self.value is not None:
            self._slider.value = self.value
        self._value_edit.value = self.value

    def _sync_value(self, event):
        if event.name not in ('value', 'value_throttled'):
            return
        with param.edit_constant(self):
            self.param.update(**{event.name: event.new})

    @param.depends("start", "end", "fixed_start", "fixed_end", watch=True)
    def _update_bounds(self):
        self.param.value.softbounds = (self.start, self.end)
        self.param.value_throttled.softbounds = (self.start, self.end)
        self.param.value.bounds = (self.fixed_start, self.fixed_end)
        self.param.value_throttled.bounds = (self.fixed_start, self.fixed_end)

        # First changing _slider and then _value_edit,
        # because else _value_edit will change the _slider
        # with the jscallback.
        if self.fixed_start is not None:
            self._slider.start = max(self.fixed_start, self.start)
        if self.fixed_end is not None:
            self._slider.end = min(self.fixed_end, self.end)

        self._value_edit.start = self.fixed_start
        self._value_edit.end = self.fixed_end


class EditableFloatSlider(_EditableContinuousSlider, FloatSlider):
    """
    The EditableFloatSlider widget allows selecting selecting a
    numeric floating-point value within a set of bounds using a slider
    and for more precise control offers an editable number input box.

    Reference: https://panel.holoviz.org/reference/widgets/EditableFloatSlider.html

    :Example:

    >>> EditableFloatSlider(
    ...     value=1.0, start=0.0, end=2.0, step=0.25, name="A float value"
    ... )
    """

    fixed_start = param.Number(default=None, doc="""
        A fixed lower bound for the slider and input.""")

    fixed_end = param.Number(default=None, doc="""
        A fixed upper bound for the slider and input.""")

    _slider_widget: t.ClassVar[type[WidgetBase]] = FloatSlider
    _input_widget: t.ClassVar[type[WidgetBase]] = FloatInput


class EditableIntSlider(_EditableContinuousSlider, IntSlider):
    """
    The EditableIntSlider widget allows selecting selecting an integer
    value within a set of bounds using a slider and for more precise
    control offers an editable integer input box.

    Reference: https://panel.holoviz.org/reference/widgets/EditableIntSlider.html

    :Example:

    >>> EditableIntSlider(
    ...     value=2, start=0, end=5, step=1, name="An integer value"
    ... )
    """

    fixed_start = param.Integer(default=None, doc="""
        A fixed lower bound for the slider and input.""")

    fixed_end = param.Integer(default=None, doc="""
       A fixed upper bound for the slider and input.""")

    _slider_widget: t.ClassVar[type[WidgetBase]] = IntSlider
    _input_widget: t.ClassVar[type[WidgetBase]] = IntInput


class EditableRangeSlider(CompositeWidget, _SliderBase):
    """
    The EditableRangeSlider widget allows selecting a floating-point
    range using a slider with two handles and for more precise control
    also offers a set of number input boxes.

    Reference: https://panel.holoviz.org/reference/widgets/EditableRangeSlider.html

    :Example:

    >>> EditableRangeSlider(
    ...      value=(1.0, 1.5), start=0.0, end=2.0, step=0.25, name="A tuple of floats"
    ... )
    """

    value = param.Range(default=(0, 1), allow_None=False, doc="""
        Current range value. Updated when a handle is dragged.""")

    value_throttled = param.Range(default=None, constant=True, doc="""
        The value of the slider. Updated when the handle is released.""")

    start = param.Number(default=0., doc="Lower bound of the range.")

    end = param.Number(default=1., doc="Upper bound of the range.")

    fixed_start = param.Number(default=None, doc="""
        A fixed lower bound for the slider and input.""")

    fixed_end = param.Number(default=None, doc="""
        A fixed upper bound for the slider and input.""")

    step = param.Number(default=0.1, doc="Slider and number input step.")

    editable = param.Tuple(default=(True, True), doc="""
        Whether the lower and upper values are editable.""")

    format = param.ClassSelector(default='0.0[0000]', class_=(str, TickFormatter,), doc="""
        Allows defining a custom format string or bokeh TickFormatter.""")

    show_value = param.Boolean(default=False, readonly=True, precedence=-1, doc="""
        Whether to show the widget value.""")

    _composite_type: t.ClassVar[type[ListLike] | type[NamedListLike]] = Column

    def __init__(self, **params):
        if 'width' not in params and 'sizing_mode' not in params:
            params['width'] = 300
        self._validate_init_bounds(params)
        super().__init__(**params)
        self._label = StaticText(margin=0, align='end')
        self._slider = RangeSlider(margin=(0, 0, 5, 0), show_value=False)
        self._slider.param.watch(self._sync_value, 'value')
        self._slider.param.watch(self._sync_value, 'value_throttled')
        self._start_edit = FloatInput(
            css_classes=['slider-edit'], stylesheets=[f'{CDN_DIST}css/editable_slider.css'],
            min_width=50, margin=0, format=self.format
        )
        self._end_edit = FloatInput(
            css_classes=['slider-edit'], stylesheets=[f'{CDN_DIST}css/editable_slider.css'],
            min_width=50, margin=(0, 0, 0, 10), format=self.format
        )
        self._start_edit.param.watch(self._sync_start_value, 'value')
        self._start_edit.param.watch(self._sync_start_value, 'value_throttled')
        self._end_edit.param.watch(self._sync_end_value, 'value')
        self._end_edit.param.watch(self._sync_end_value, 'value_throttled')

        sep = StaticText(value='...', margin=(0, 5, 0, 5), align='end')
        edit = Row(self._label, self._start_edit, sep, self._end_edit,
                   sizing_mode='stretch_width', margin=0)
        self._composite.extend([edit, self._slider])
        self._start_edit.jscallback(args={'slider': self._slider, 'end': self._end_edit}, value="""
        // start value always smaller than the end value
        if (cb_obj.value >= end.value) {
          cb_obj.value = end.value
          return
        }
        if (cb_obj.value < slider.start) {
          slider.start = cb_obj.value
        } else if (cb_obj.value > slider.end) {
          slider.end = cb_obj.value
        }
        """)
        self._end_edit.jscallback(args={'slider': self._slider ,'start': self._start_edit}, value="""
        // end value always larger than the start value
        if (cb_obj.value <= start.value) {
          cb_obj.value = start.value
          return
        }
        if (cb_obj.value < slider.start) {
          slider.start = cb_obj.value
        } else if (cb_obj.value > slider.end) {
          slider.end = cb_obj.value
        }
        """)
        self._update_editable()
        self._update_disabled()
        self._update_layout()
        self._update_name()
        self._update_slider()
        self._update_value()
        self._update_bounds()

    def _validate_init_bounds(self, params):
        """
        This updates the default value, start and end
        if outside the fixed_start and fixed_end
        """
        start, end = None, None
        if "start" not in params:
            if "fixed_start" in params:
                start = params["fixed_start"]
            elif "end" in params:
                start = params.get("end") - params.get("step", 1)
            elif "fixed_end" in params:
                start = params.get("fixed_end") - params.get("step", 1)

        if "end" not in params:
            if "fixed_end" in params:
                end = params["fixed_end"]
            elif "start" in params:
                end = params["start"] + params.get("step", 1)
            elif "fixed_start" in params:
                end = params["fixed_start"] + params.get("step", 1)

        if start is not None:
            params["start"] = start
        if end is not None:
            params["end"] = end

        if "value" not in params and "start" in params:
            start = params["start"]
            end = params.get("end", start + params.get("step", 1))
            params["value"] = (start, end)
        if "value" not in params and "end" in params:
            end = params["end"]
            start = params.get("start", end - params.get("step", 1))
            params["value"] = (start, end)

    @param.depends('disabled', watch=True)
    def _update_disabled(self):
        self._slider.disabled = self.disabled

    @param.depends('disabled', 'editable', watch=True)
    def _update_editable(self):
        self._start_edit.disabled = (not self.editable[0]) or self.disabled
        self._end_edit.disabled = (not self.editable[1]) or self.disabled

    @param.depends('label', watch=True)
    def _update_name(self):
        if self.label:
            label = f'{self.label}:'
            margin = (0, 10, 0, 0)
        else:
            label = ''
            margin = (0, 0, 0, 0)
        self._label.param.update(margin=margin, value=label)

    @param.depends('width', 'height', 'sizing_mode', watch=True)
    def _update_layout(self):
        self._start_edit.sizing_mode = self.sizing_mode
        self._end_edit.sizing_mode = self.sizing_mode
        if self.sizing_mode not in ('stretch_width', 'stretch_both'):
            w = (self.width or 300)//4
            self._start_edit.width = w
            self._end_edit.width = w

    @param.depends('start', 'end', 'step', 'bar_color', 'direction',
                   'show_value', 'tooltips', 'label', 'format', watch=True)
    def _update_slider(self):
        self._slider.param.update(
            format=self.format,
            start=self.start,
            end=self.end,
            step=self.step,
            bar_color=self.bar_color,
            direction=self.direction,
            show_value=self.show_value,
            tooltips=self.tooltips
        )
        self._start_edit.step = self.step
        self._end_edit.step = self.step

    @param.depends('value', watch=True)
    def _update_value(self):
        self._slider.value = self.value
        self._start_edit.value = self.value[0]
        self._end_edit.value = self.value[1]

    def _sync_value(self, event):
        if event.name not in ('value', 'value_throttled'):
            return
        with param.edit_constant(self):
            self.param.update(**{event.name: event.new})

    def _sync_start_value(self, event):
        if event.name not in ('value', 'value_throttled'):
            return
        if event.name == 'value':
            end = self.value[1] if self.value else self.end
        else:
            end = self.value_throttled[1] if self.value_throttled else self.end
        with param.edit_constant(self):
            self.param.update(
                **{event.name: (event.new, end)}
            )

    def _sync_end_value(self, event):
        if event.name not in ('value', 'value_throttled'):
            return
        if event.name == 'value':
            start = self.value[0] if self.value else self.start
        else:
            start = self.value_throttled[0] if self.value_throttled else self.start
        with param.edit_constant(self):
            self.param.update(
                **{event.name: (start, event.new)}
            )

    @param.depends("start", "end", "fixed_start", "fixed_end", watch=True)
    def _update_bounds(self):
        self.param.value.softbounds = (self.start, self.end)
        self.param.value_throttled.softbounds = (self.start, self.end)
        self.param.value.bounds = (self.fixed_start, self.fixed_end)
        self.param.value_throttled.bounds = (self.fixed_start, self.fixed_end)

        # First changing _slider and then _value_edit,
        # because else _value_edit will change the _slider
        # with the jscallback.
        if self.fixed_start is not None:
            self._slider.start = max(self.fixed_start, self.start)
        if self.fixed_end is not None:
            self._slider.end = min(self.fixed_end, self.end)

        self._start_edit.start = self.fixed_start
        self._start_edit.end = self.fixed_end
        self._end_edit.start = self.fixed_start
        self._end_edit.end = self.fixed_end
