"""
The input widgets generally allow entering arbitrary information into
a text field or similar.
"""
import ast
import json

from base64 import b64decode
from datetime import datetime, date

import numpy as np
import param

from bokeh.models.formatters import TickFormatter
from bokeh.models.widgets import (
    CheckboxGroup as _BkCheckboxGroup, ColorPicker as _BkColorPicker,
    DatePicker as _BkDatePicker, Div as _BkDiv, TextInput as _BkTextInput,
    PasswordInput as _BkPasswordInput, Spinner as _BkSpinner,
    FileInput as _BkFileInput, TextAreaInput as _BkTextAreaInput,
    NumericInput as _BkNumericInput
)

from ..config import config
from ..layout import Column
from ..util import param_reprs
from .base import Widget, CompositeWidget
from ..models import DatetimePicker as _bkDatetimePicker


class TextInput(Widget):
    """
    The `TextInput` widget allows entering any string using a text input box.

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

    :Example:

    >>> TextInput(name='Name', placeholder='Enter your name here ...')
    """

    max_length = param.Integer(default=5000, doc="""
      Max count of characters in the input field.""")

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

    value = param.String(default='', allow_None=True, doc="""
      Initial or entered text value updated when <enter> key is pressed.""")

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

    _widget_type = _BkTextInput


class PasswordInput(TextInput):
    """
    The `PasswordInput` allows entering any string using an obfuscated text
    input box.

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

    :Example:

    >>> PasswordInput(
    ...     name='Password', placeholder='Enter your password here...'
    ... )
    """

    _widget_type = _BkPasswordInput


class TextAreaInput(TextInput):
    """
    The `TextAreaInput` allows entering any multiline string using a text input
    box.
    
    Lines are joined with the newline character `\n`.

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

    :Example:

    >>> TextAreaInput(
    ...     name='Description', placeholder='Enter your description here...'
    ... )
    """

    _widget_type = _BkTextAreaInput


class FileInput(Widget):
    """
    The `FileInput` allows the user to upload one or more files to the server.
    
    It makes the filename, MIME type and (bytes) content available in Python.
    
    Please note 
    
    - you can in fact *drag and drop* files onto the `FileInput`.
    - you easily save the files using the `save` method.
    
    Reference: https://panel.holoviz.org/reference/widgets/FileInput.html

    :Example:

    >>> FileInput(accept='.png,.jpeg', multiple=True)
    """

    accept = param.String(default=None)

    filename = param.ClassSelector(default=None, class_=(str, list),
                               is_instance=True)

    mime_type = param.ClassSelector(default=None, class_=(str, list),
                               is_instance=True)

    multiple = param.Boolean(default=False)

    value = param.Parameter(default=None)

    _widget_type = _BkFileInput

    _source_transforms = {'value': "'data:' + source.mime_type + ';base64,' + value"}

    _rename = {'name': None, 'filename': None}

    def _process_param_change(self, msg):
        msg = super()._process_param_change(msg)
        if 'value' in msg:
            msg.pop('value')
        if 'mime_type' in msg:
            msg.pop('mime_type')
        return msg

    def _filter_properties(self, properties):
        properties = super()._filter_properties(properties)
        return properties + ['value', 'mime_type', 'filename']

    def _process_property_change(self, msg):
        msg = super()._process_property_change(msg)
        if 'value' in msg:
            if isinstance(msg['value'], str):
                msg['value'] = b64decode(msg['value'])
            else:
                msg['value'] = [b64decode(content) for content in msg['value']]
        return msg

    def save(self, filename):
        """
        Saves the uploaded FileInput data object(s) to file(s) or
        BytesIO object(s).

        Arguments
        ---------
        filename (str or list[str]): File path or file-like object
        """
        value = self.value
        if isinstance(filename, list) and not isinstance(value, list):
            raise TypeError(
                "FileInput contains a list of files but only a single "
                "filename was given. Please provide a list of filenames or "
                "file-like objects."
            )
        elif not isinstance(filename, list) and isinstance(value, list):
            raise TypeError(
                "FileInput contains a single files but a list of "
                "filenames was given. Please provide a single filename "
                "or file-like object."
            )
        elif not isinstance(value, list):
            value = [self.value]
        elif not isinstance(filename, list):
            filename = [filename]

        for val, fn in zip(value, filename):
            if isinstance(fn, str):
                with open(fn, 'wb') as f:
                    f.write(val)
            else:
                fn.write(val)


class StaticText(Widget):
    """
    The `StaticText` widget displays a text value, but does not allow editing
    it.
    
    Reference: https://panel.holoviz.org/reference/widgets/StaticText.html

    :Example:

    >>> StaticText(name='Model', value='animagen2')
    """

    style = param.Dict(default=None, doc="""
        Dictionary of CSS property:value pairs to apply to this Div.""")

    value = param.Parameter(default=None, doc="""
        The current value""")

    _format = '<b>{title}</b>: {value}'

    _rename = {'name': None, 'value': 'text'}

    _target_transforms = {'value': 'target.text.split(": ")[0]+": "+value'}

    _source_transforms = {'value': 'value.split(": ")[1]'}

    _widget_type = _BkDiv

    def _process_param_change(self, msg):
        msg = super()._process_property_change(msg)
        if 'value' in msg:
            text = str(msg.pop('value'))
            partial = self._format.replace('{value}', '').format(title=self.name)
            if self.name:
                text = self._format.format(title=self.name, value=text.replace(partial, ''))
            msg['text'] = text
        return msg


class DatePicker(Widget):
    """
    The `DatePicker` allows selecting selecting a `date` value using a text box
    and a date-picking utility.
    
    Reference: https://panel.holoviz.org/reference/widgets/DatePicker.html

    :Example:

    >>> DatePicker(
    ...     value=date(2025,1,1),
    ...     start=date(2025,1,1), end=date(2025,12,31),
    ...     name='Date'
    ... )
    """

    value = param.CalendarDate(default=None, doc="""
        The current value""")

    start = param.CalendarDate(default=None, doc="""
        Inclusive lower bound of the allowed date selection""")

    end = param.CalendarDate(default=None, doc="""
        Inclusive upper bound of the allowed date selection""")

    disabled_dates = param.List(default=None, class_=(date, str))

    enabled_dates = param.List(default=None, class_=(date, str))

    _source_transforms = {}

    _rename = {'start': 'min_date', 'end': 'max_date', 'name': 'title'}

    _widget_type = _BkDatePicker

    def _process_property_change(self, msg):
        msg = super()._process_property_change(msg)
        if 'value' in msg:
            if isinstance(msg['value'], str):
                msg['value'] = datetime.date(datetime.strptime(msg['value'], '%Y-%m-%d'))
        return msg


class _DatetimePickerBase(Widget):

    disabled_dates = param.List(default=None, class_=(date, str), doc="""
      Dates to make unavailable for selection.""")

    enabled_dates = param.List(default=None, class_=(date, str), doc="""
      Dates to make available for selection.""")

    enable_time = param.Boolean(default=True, doc="""
      Enable editing of the time in the widget.""")

    enable_seconds = param.Boolean(default=True, doc="""
      Enable editing of the seconds in the widget.""")

    end = param.Date(default=None, doc="""
      Inclusive upper bound of the allowed date selection. Note that while
      the parameter accepts datetimes the time portion of the range
      is ignored.""")

    military_time = param.Boolean(default=True, doc="""
      Whether to display time in 24 hour format.""")

    start = param.Date(default=None, doc="""
      Inclusive lower bound of the allowed date selection. Note that while
      the parameter accepts datetimes the time portion of the range
      is ignored.""")

    _source_transforms = {'value': None, 'start': None, 'end': None, 'mode': None}

    _rename = {'start': 'min_date', 'end': 'max_date', 'name': 'title'}

    _widget_type = _bkDatetimePicker

    __abstract = True

    def __init__(self, **params):
        super().__init__(**params)
        self._update_value_bounds()

    @staticmethod
    def _date_to_datetime(x):
        if isinstance(x, date):
            return datetime(x.year, x.month, x.day)

    @param.depends('start', 'end', watch=True)
    def _update_value_bounds(self):
        self.param.value.bounds = (
            self._date_to_datetime(self.start),
            self._date_to_datetime(self.end),
        )
        self.param.value._validate(self.value)

    def _process_property_change(self, msg):
        msg = super()._process_property_change(msg)
        if 'value' in msg:
            msg['value'] = self._serialize_value(msg['value'])
        return msg

    def _process_param_change(self, msg):
        msg = super()._process_param_change(msg)
        if 'value' in msg:
            msg['value'] = self._deserialize_value(msg['value'])
        return msg


class DatetimePicker(_DatetimePickerBase):
    """
    The `DatetimePicker` allows selecting selecting a `datetime` value using a
    textbox and a datetime-picking utility.
    
    Reference: https://panel.holoviz.org/reference/widgets/DatetimePicker.html

    :Example:

    >>> DatetimePicker(
    ...    value=datetime(2025,1,1,22,0),
    ...    start=date(2025,1,1), end=date(2025,12,31),
    ...    military_time=True, name='Date and time'
    ... )
    """


    value = param.Date(default=None)

    mode = param.String('single', constant=True)

    def _serialize_value(self, value):
        if isinstance(value, str) and value:
            value = datetime.strptime(value, r'%Y-%m-%d %H:%M:%S')

            # Hour, minute and seconds can be increased after end is reached.
            # This forces the hours, minute and second to be 0.
            end = self._date_to_datetime(self.end)
            if end is not None and value > end:
                value = end

        return value

    def _deserialize_value(self, value):
        if isinstance(value, (datetime, date)):
            value = value.strftime(r'%Y-%m-%d %H:%M:%S')

        return value


class DatetimeRangePicker(_DatetimePickerBase):
    """
    The `DatetimeRangePicker` allows selecting selecting a `datetime` range
    using a text box and a datetime-range-picking utility.
    
    Reference: https://panel.holoviz.org/reference/widgets/DatetimeRangePicker.html

    :Example:

    >>> DatetimeRangePicker(
    ...    value=(datetime(2025,1,1,22,0), datetime(2025,1,2,22,0)),
    ...    start=date(2025,1,1), end=date(2025,12,31),
    ...    military_time=True, name='Datetime Range'
    ... )
    """

    value = param.DateRange(default=None, doc="""
        The current value""")

    mode = param.String('range', constant=True)

    def _serialize_value(self, value):
        if isinstance(value, str) and value:
            value = [
                datetime.strptime(value, r'%Y-%m-%d %H:%M:%S')
                for value in value.split(' to ')
            ]

            # Hour, minute and seconds can be increased after end is reached.
            # This forces the hours, minute and second to be 0.
            end = self._date_to_datetime(self.end)
            if end is not None and value[0] > end:
                value[0] = end
            if end is not None and value[1] > end:
                value[1] = end

            value = tuple(value)

        return value

    def _deserialize_value(self, value):
        if isinstance(value, tuple):
            value = " to ".join(v.strftime(r'%Y-%m-%d %H:%M:%S') for v in value)
        if value is None:
            value = ""

        return value


class ColorPicker(Widget):
    """
    The `ColorPicker` widget allows selecting a hexidecimal RGB color value
    using the browser’s color-picking widget.
    
    Reference: https://panel.holoviz.org/reference/widgets/ColorPicker.html

    :Example:

    >>> ColorPicker(name='Color', value='#99ef78')
    """

    value = param.Color(default=None, doc="""
        The selected color""")

    _widget_type = _BkColorPicker

    _rename = {'value': 'color', 'name': 'title'}


class _NumericInputBase(Widget):

    value = param.Number(default=0, allow_None=True, doc="""
        The current value of the spinner.""")

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

    format = param.ClassSelector(default=None, class_=(str, TickFormatter,), doc="""
        Allows defining a custom format string or bokeh TickFormatter.""")

    start = param.Parameter(default=None, allow_None=True, doc="""
        Optional minimum allowable value.""")

    end = param.Parameter(default=None, allow_None=True, doc="""
        Optional maximum allowable value.""")

    _rename = {'name': 'title', 'start': 'low', 'end': 'high'}

    _widget_type = _BkNumericInput

    __abstract = True


class _IntInputBase(_NumericInputBase):

    value = param.Integer(default=0, allow_None=True, doc="""
        The current value of the spinner.""")

    start = param.Integer(default=None, allow_None=True, doc="""
        Optional minimum allowable value.""")

    end = param.Integer(default=None, allow_None=True, doc="""
        Optional maximum allowable value.""")

    mode = param.String(default='int', constant=True, doc="""
        Define the type of number which can be enter in the input""")

    __abstract = True


class _FloatInputBase(_NumericInputBase):

    value = param.Number(default=0, allow_None=True, doc="""
        The current value of the spinner.""")

    start = param.Number(default=None, allow_None=True, doc="""
        Optional minimum allowable value.""")

    end = param.Number(default=None, allow_None=True, doc="""
        Optional maximum allowable value.""")

    mode = param.String(default='float', constant=True, doc="""
        Define the type of number which can be enter in the input""")

    __abstract = True


class _SpinnerBase(_NumericInputBase):

    page_step_multiplier = param.Integer(default=10, bounds=(0, None), doc="""
        Defines the multiplication factor applied to step when the page up
        and page down keys are pressed.""")

    wheel_wait = param.Integer(default=100, doc="""
        Defines the debounce time in ms before updating `value_throttled` when
        the mouse wheel is used to change the input.""")

    _widget_type = _BkSpinner

    __abstract = True

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

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

    def _update_model(self, events, msg, root, model, doc, comm):
        if 'value_throttled' in msg:
            del msg['value_throttled']

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

    def _process_param_change(self, msg):
        # Workaround for -inf serialization errors
        if 'value' in msg and msg['value'] == float('-inf'):
            msg['value'] = None
            msg['value_throttled'] = None
        return super()._process_param_change(msg)

    def _process_property_change(self, msg):
        if config.throttled:
            if "value" in msg:
                del msg["value"]
            if "value_throttled" in msg:
                msg["value"] = msg["value_throttled"]
        return super()._process_property_change(msg)


class IntInput(_SpinnerBase, _IntInputBase):
    """
    The `IntInput` allows selecting an integer value using a spinbox.
    
    It behaves like a slider except that lower and upper bounds are optional
    and a specific value can be entered. The value can be changed using the
    keyboard (up, down, page up, page down), mouse wheel and arrow buttons.
    
    Reference: https://panel.holoviz.org/reference/widgets/IntInput.html

    :Example:

    >>> IntInput(name='Value', value=100, start=0, end=1000, step=10)
    """

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

    value_throttled = param.Integer(default=None, constant=True, doc="""
        The current value. Updates only on `<enter>` or when the widget looses focus.""")


class FloatInput(_SpinnerBase, _FloatInputBase):
    """
    The `FloatInput` allows selecting a floating point value using a spinbox.
    
    It behaves like a slider except that the lower and upper bounds are
    optional and a specific value can be entered. The value can be changed
    using the keyboard (up, down, page up, page down), mouse wheel and arrow
    buttons.
    
    Reference: https://panel.holoviz.org/reference/widgets/FloatInput.html

    :Example:

    >>> FloatInput(name='Value', value=5., step=1e-1, start=0, end=10)
    """

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

    value_throttled = param.Number(default=None, constant=True, doc="""
        The current value. Updates only on `<enter>` or when the widget looses focus.""")


class NumberInput(_SpinnerBase):

    def __new__(self, **params):
        param_list = ["value", "start", "stop", "step"]
        if all(isinstance(params.get(p, 0), int) for p in param_list):
            return IntInput(**params)
        else:
            return FloatInput(**params)


# Backward compatibility
Spinner = NumberInput


class LiteralInput(Widget):
    """
    The `LiteralInput` allows declaring Python literals using a text
    input widget. 
    
    A *literal* is some specific primitive value of type `str`
    , `int`, `float`, `bool` etc or a `dict`, `list`, `tuple`, `set` etc of
    primitive values.
    
    Optionally the literal `type` may be declared.

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

    :Example:

    >>> LiteralInput(name='Dictionary', value={'key': [1, 2, 3]}, type=dict)
    """

    serializer = param.ObjectSelector(default='ast', objects=['ast', 'json'], doc="""
       The serialization (and deserialization) method to use. 'ast'
       uses ast.literal_eval and 'json' uses json.loads and json.dumps.
    """)

    type = param.ClassSelector(default=None, class_=(type, tuple),
                               is_instance=True)

    value = param.Parameter(default=None)

    _rename = {'name': 'title', 'type': None, 'serializer': None}

    _source_transforms = {'value': """JSON.parse(value.replace(/'/g, '"'))"""}

    _target_transforms = {'value': """JSON.stringify(value).replace(/,/g, ",").replace(/:/g, ": ")"""}

    _widget_type = _BkTextInput

    def __init__(self, **params):
        super().__init__(**params)
        self._state = ''
        self._validate(None)
        self._callbacks.append(self.param.watch(self._validate, 'value'))

    def _validate(self, event):
        if self.type is None: return
        new = self.value
        if not isinstance(new, self.type) and new is not None:
            if event:
                self.value = event.old
            types = repr(self.type) if isinstance(self.type, tuple) else self.type.__name__
            raise ValueError('LiteralInput expected %s type but value %s '
                             'is of type %s.' %
                             (types, new, type(new).__name__))

    def _process_property_change(self, msg):
        msg = super()._process_property_change(msg)
        new_state = ''
        if 'value' in msg:
            value = msg.pop('value')
            try:
                if self.serializer == 'json':
                    value = json.loads(value)
                else:
                    value = ast.literal_eval(value)
            except Exception:
                new_state = ' (invalid)'
                value = self.value
            else:
                if self.type and not isinstance(value, self.type):
                    vtypes = self.type if isinstance(self.type, tuple) else (self.type,)
                    typed_value = None
                    for vtype in vtypes:
                        try:
                            typed_value = vtype(value)
                        except Exception:
                            pass
                        else:
                            break
                    if typed_value is None and value is not None:
                        new_state = ' (wrong type)'
                        value = self.value
                    else:
                        value = typed_value
            msg['value'] = value
        msg['name'] = msg.get('title', self.name).replace(self._state, '') + new_state
        self._state = new_state
        self.param.trigger('name')
        return msg

    def _process_param_change(self, msg):
        msg = super()._process_param_change(msg)
        if 'value' in msg:
            value = msg['value']
            if isinstance(value, str):
                value = repr(value)
            elif self.serializer == 'json':
                value = json.dumps(value)
            else:
                value = '' if value is None else str(value)
            msg['value'] = value
        msg['title'] = self.name
        return msg


class ArrayInput(LiteralInput):
    """
    The `ArrayInput` allows rendering and editing NumPy arrays in a text
    input widget.
    
    Arrays larger than the `max_array_size` will be summarized and editing
    will be disabled.

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

    :Example:

    >>> To be determined ...
    """

    max_array_size = param.Number(default=1000, doc="""
        Arrays larger than this limit will be allowed in Python but
        will not be serialized into JavaScript. Although such large
        arrays will thus not be editable in the widget, such a
        restriction helps avoid overwhelming the browser and lets
        other widgets remain usable.""")

    _rename = dict(LiteralInput._rename, max_array_size=None)

    _source_transforms = {'value': None}

    def __init__(self, **params):
        super().__init__(**params)
        self._auto_disabled = False

    def _process_property_change(self, msg):
        msg = super()._process_property_change(msg)
        if 'value' in msg and isinstance(msg['value'], list):
            msg['value'] = np.asarray(msg['value'])
        return msg

    def _process_param_change(self, msg):
        if msg.get('disabled', False):
            self._auto_disabled = False
        value = msg.get('value')
        if value is None:
            return super()._process_param_change(msg)
        if value.size <= self.max_array_size:
            msg['value'] = value.tolist()
            # If array is no longer larger than max_array_size
            # unset disabled
            if self.disabled and self._auto_disabled:
                self.disabled = False
                msg['disabled'] = False
                self._auto_disabled = False
        else:
            msg['value'] = np.array2string(
                msg['value'], separator=',',
                threshold=self.max_array_size
            )
            if not self.disabled:
                self.param.warning(
                    f"Number of array elements ({value.size}) exceeds "
                    f"`max_array_size` ({self.max_array_size}), editing "
                    "will be disabled."
                )
                self.disabled = True
                msg['disabled'] = True
                self._auto_disabled = True
        return super()._process_param_change(msg)


class DatetimeInput(LiteralInput):
    """
    The `DatetimeInput` allows specifying Python `datetime` like values using
    a text input widget.
    
    An optional `type` may be declared.
    
    Reference: https://panel.holoviz.org/reference/widgets/DatetimeInput.html

    :Example:

    >>> DatetimeInput(name='Datetime', value=datetime(2019, 2, 8))
    """

    value = param.Date(default=None, doc="""
        The current value""")

    start = param.Date(default=None, doc="""
        Inclusive lower bound of the allowed date selection""")

    end = param.Date(default=None, doc="""
        Inclusive upper bound of the allowed date selection""")

    format = param.String(default='%Y-%m-%d %H:%M:%S', doc="""
        Datetime format used for parsing and formatting the datetime.""")

    type = datetime

    _source_transforms = {'value': None, 'start': None, 'end': None}

    _rename = {'format': None, 'type': None, 'name': 'title',
               'start': None, 'end': None, 'serializer': None}

    def __init__(self, **params):
        super().__init__(**params)
        self.param.watch(self._validate, 'value')
        self._validate(None)

    def _validate(self, event):
        new = self.value
        if new is not None and ((self.start is not None and self.start > new) or
                                (self.end is not None and self.end < new)):
            value = datetime.strftime(new, self.format)
            start = datetime.strftime(self.start, self.format)
            end = datetime.strftime(self.end, self.format)
            if event:
                self.value = event.old
            raise ValueError('DatetimeInput value must be between {start} and {end}, '
                             'supplied value is {value}'.format(start=start, end=end,
                                                                value=value))

    def _process_property_change(self, msg):
        msg = Widget._process_property_change(self, msg)
        new_state = ''
        if 'value' in msg:
            value = msg.pop('value')
            try:
                value = datetime.strptime(value, self.format)
            except Exception:
                new_state = ' (invalid)'
                value = self.value
            else:
                if value is not None and ((self.start is not None and self.start > value) or
                                          (self.end is not None and self.end < value)):
                    new_state = ' (out of bounds)'
                    value = self.value
            msg['value'] = value
        msg['name'] = msg.get('title', self.name).replace(self._state, '') + new_state
        self._state = new_state
        return msg

    def _process_param_change(self, msg):
        msg = Widget._process_param_change(self, msg)
        if 'value' in msg:
            value = msg['value']
            if value is None:
                value = ''
            else:
                value = datetime.strftime(msg['value'], self.format)
            msg['value'] = value
        msg['title'] = self.name
        return msg


class DatetimeRangeInput(CompositeWidget):
    """
    The `DatetimeRangeInput` widget allows selecting a `datetime` range using
    two `DatetimeInput` widgets, which return a `tuple` range.
    
    Reference: https://panel.holoviz.org/reference/widgets/DatetimeRangeInput.html

    :Example:

    >>> DatetimeRangeInput(
    ...     name='Datetime Range',
    ...     value=(datetime(2017, 1, 1), datetime(2018, 1, 10)),
    ...     start=datetime(2017, 1, 1), end=datetime(2019, 1, 1),
    ... )
    """

    value = param.Tuple(default=(None, None), length=2, doc="""
        The current value""")

    start = param.Date(default=None, doc="""
        Inclusive lower bound of the allowed date selection""")

    end = param.Date(default=None, doc="""
        Inclusive upper bound of the allowed date selection""")

    format = param.String(default='%Y-%m-%d %H:%M:%S', doc="""
        Datetime format used for parsing and formatting the datetime.""")

    _composite_type = Column

    def __init__(self, **params):
        self._text = StaticText(margin=(5, 0, 0, 0), style={'white-space': 'nowrap'})
        self._start = DatetimeInput(sizing_mode='stretch_width', margin=(5, 0, 0, 0))
        self._end = DatetimeInput(sizing_mode='stretch_width', margin=(5, 0, 0, 0))
        if 'value' not in params:
            params['value'] = (params['start'], params['end'])
        super().__init__(**params)
        self._msg = ''
        self._composite.extend([self._text, self._start, self._end])
        self._updating = False
        self.param.watch(self._update_widgets, [p for p in self.param if p != 'name'])
        self._update_widgets()
        self._update_label()

    @param.depends('name', '_start.name', '_end.name', watch=True)
    def _update_label(self):
        self._text.value = f'{self.name}{self._start.name}{self._end.name}{self._msg}'

    @param.depends('_start.value', '_end.value', watch=True)
    def _update(self):
        if self._updating:
            return
        if (self._start.value is not None and
            self._end.value is not None and
            self._start.value > self._end.value):
            self._msg = ' (start of range must be <= end)'
            self._update_label()
            return
        elif self._msg:
            self._msg = ''
            self._update_label()
        try:
            self._updating = True
            self.value = (self._start.value, self._end.value)
        finally:
            self._updating = False

    def _update_widgets(self, *events):
        filters = [event.name for event in events] if events else list(self.param)
        if 'name' in filters:
            filters.remove('name')
        if self._updating:
            return
        try:
            self._updating = True
            params = {k: v for k, v in self.param.values().items() if k in filters}
            start_params = dict(params, value=self.value[0])
            end_params = dict(params, value=self.value[1])
            self._start.param.update(**start_params)
            self._end.param.update(**end_params)
        finally:
            self._updating = False


class Checkbox(Widget):
    """
    The `Checkbox` allows toggling a single condition between `True`/`False`
    states by ticking a checkbox.
    
    This widget is interchangeable with the `Toggle` widget.
    
    Reference: https://panel.holoviz.org/reference/widgets/Checkbox.html

    :Example:

    >>> Checkbox(name='Works with the tools you know and love', value=True)
    """

    value = param.Boolean(default=False, doc="""
        The current value""")

    _supports_embed = True

    _rename = {'value': 'active', 'name': 'labels'}

    _source_transforms = {'value': "value.indexOf(0) >= 0", 'name': "value[0]"}

    _target_transforms = {'value': "value ? [0] : []", 'name': "[value]"}

    _widget_type = _BkCheckboxGroup

    def _process_property_change(self, msg):
        msg = super()._process_property_change(msg)
        if 'value' in msg:
            msg['value'] = 0 in msg.pop('value')
        if 'name' in msg:
            msg['name'] = [msg['name']]
        return msg

    def _process_param_change(self, msg):
        msg = super()._process_param_change(msg)
        if 'active' in msg:
            msg['active'] = [0] if msg.pop('active', None) else []
        if 'labels' in msg:
            msg['labels'] = [msg.pop('labels')]
        return msg

    def _get_embed_state(self, root, values=None, max_opts=3):
        return (self, self._models[root.ref['id']][0], [False, True],
                lambda x: str(0 in x.active).lower(), 'active',
                "String(cb_obj.active.indexOf(0) >= 0)")
