from . import BoolParameterItem, SimpleParameter
from .basetypes import GroupParameterItem, GroupParameter, WidgetParameterItem
from .list import ListParameter
from .slider import Emitter
from ..ParameterItem import ParameterItem
from ... import functions as fn
from ...Qt import QtWidgets

class ChecklistParameterItem(GroupParameterItem):
    """
    Wraps a :class:`GroupParameterItem` to manage ``bool`` parameter children. Also provides convenience buttons to
    select or clear all values at once. Note these conveniences are disabled when ``exclusive`` is *True*.
    """
    def __init__(self, param, depth):
        self.btnGrp = QtWidgets.QButtonGroup()
        self.btnGrp.setExclusive(False)
        self._constructMetaBtns()

        super().__init__(param, depth)

    def _constructMetaBtns(self):
        self.metaBtnWidget = QtWidgets.QWidget()
        self.metaBtnLayout = lay = QtWidgets.QHBoxLayout(self.metaBtnWidget)
        lay.setContentsMargins(0, 0, 0, 0)
        lay.setSpacing(2)
        self.metaBtns = {}
        lay.addStretch(0)
        for title in 'Clear', 'Select':
            self.metaBtns[title] = btn = QtWidgets.QPushButton(f'{title} All')
            self.metaBtnLayout.addWidget(btn)
            btn.clicked.connect(getattr(self, f'{title.lower()}AllClicked'))

        self.metaBtns['default'] = WidgetParameterItem.makeDefaultButton(self)
        self.metaBtnLayout.addWidget(self.metaBtns['default'])

    def defaultClicked(self):
        self.param.setToDefault()

    def treeWidgetChanged(self):
        ParameterItem.treeWidgetChanged(self)
        tw = self.treeWidget()
        if tw is None:
            return
        tw.setItemWidget(self, 1, self.metaBtnWidget)

    def selectAllClicked(self):
        self.param.setValue(self.param.reverse[0])

    def clearAllClicked(self):
        self.param.setValue([])

    def insertChild(self, pos, item):
        ret = super().insertChild(pos, item)
        self.btnGrp.addButton(item.widget)
        return ret

    def addChild(self, item):
        ret = super().addChild(item)
        self.btnGrp.addButton(item.widget)
        return ret

    def takeChild(self, i):
        child = super().takeChild(i)
        self.btnGrp.removeButton(child.widget)

    def optsChanged(self, param, opts):
        if 'expanded' in opts:
            for btn in self.metaBtns.values():
                btn.setVisible(opts['expanded'])
        exclusive = opts.get('exclusive', param.opts['exclusive'])
        enabled = opts.get('enabled', param.opts['enabled'])
        for btn in self.metaBtns.values():
            btn.setDisabled(exclusive or (not enabled))
        self.btnGrp.setExclusive(exclusive)

    def expandedChangedEvent(self, expanded):
        for btn in self.metaBtns.values():
            btn.setVisible(expanded)

class RadioParameterItem(BoolParameterItem):
    """
    Allows radio buttons to function as booleans when `exclusive` is *True*
    """

    def __init__(self, param, depth):
        self.emitter = Emitter()
        super().__init__(param, depth)

    def makeWidget(self):
        w = QtWidgets.QRadioButton()
        w.value = w.isChecked
        # Since these are only used during exclusive operations, only fire a signal when "True"
        # to avoid a double-fire
        w.setValue = w.setChecked
        w.sigChanged = self.emitter.sigChanged
        w.toggled.connect(self.maybeSigChanged)
        self.hideWidget = False
        return w

    def maybeSigChanged(self, val):
        """
        Make sure to only activate on a "true" value, since an exclusive button group fires once to deactivate
        the old option and once to activate the new selection
        """
        if not val:
            return
        self.emitter.sigChanged.emit(self, val)


# Proxy around radio/bool type so the correct item class gets instantiated
class BoolOrRadioParameter(SimpleParameter):

    def __init__(self, **kargs):
        if kargs.get('type') == 'bool':
            self.itemClass = BoolParameterItem
        else:
            self.itemClass = RadioParameterItem
        super().__init__(**kargs)

class ChecklistParameter(GroupParameter):
    """
    Can be set just like a :class:`ListParameter`, but allows for multiple values to be selected simultaneously.

    ============== ========================================================
    **Options**
    exclusive      When *False*, any number of options can be selected. The resulting ``value()`` is a list of
                   all checked values. When *True*, it behaves like a ``list`` type -- only one value can be selected.
                   If no values are selected and ``exclusive`` is set to *True*, the first available limit is selected.
                   The return value of an ``exclusive`` checklist is a single value rather than a list with one element.
    ============== ========================================================
    """
    itemClass = ChecklistParameterItem

    def __init__(self, **opts):
        self.targetValue = None
        limits = opts.setdefault('limits', [])
        self.forward, self.reverse = ListParameter.mapping(limits)
        value = opts.setdefault('value', limits)
        opts.setdefault('exclusive', False)
        super().__init__(**opts)
        # Force 'exclusive' to trigger by making sure value is not the same
        self.sigLimitsChanged.connect(self.updateLimits)
        self.sigOptionsChanged.connect(self.optsChanged)
        if len(limits):
            # Since update signal wasn't hooked up until after parameter construction, need to fire manually
            self.updateLimits(self, limits)
            # Also, value calculation will be incorrect until children are added, so make sure to recompute
            self.setValue(value)

    def updateLimits(self, _param, limits):
        oldOpts = self.names
        val = self.opts['value']
        # Make sure adding and removing children don't cause tree state changes
        self.blockTreeChangeSignal()
        self.clearChildren()
        self.forward, self.reverse = ListParameter.mapping(limits)
        if self.opts.get('exclusive'):
            typ = 'radio'
        else:
            typ = 'bool'
        for chName in self.forward:
            # Recycle old values if they match the new limits
            newVal = bool(oldOpts.get(chName, False))
            child = BoolOrRadioParameter(type=typ, name=chName, value=newVal, default=None)
            self.addChild(child)
            # Prevent child from broadcasting tree state changes, since this is handled by self
            child.blockTreeChangeSignal()
            child.sigValueChanged.connect(self._onSubParamChange)
        # Purge child changes before unblocking
        self.treeStateChanges.clear()
        self.unblockTreeChangeSignal()
        self.setValue(val)

    def _onSubParamChange(self, param, value):
        if self.opts['exclusive']:
            val = self.reverse[0][self.reverse[1].index(param.name())]
            return self.setValue(val)
        # Interpret value, fire sigValueChanged
        return self.setValue(self.value())

    def optsChanged(self, param, opts):
        if 'exclusive' in opts:
            # Force set value to ensure updates
            # self.opts['value'] = self._VALUE_UNSET
            self.updateLimits(None, self.opts.get('limits', []))
    
    def value(self):
        vals = [self.forward[p.name()] for p in self.children() if p.value()]
        exclusive = self.opts['exclusive']
        if not vals and exclusive:
            return None
        elif exclusive:
            return vals[0]
        else:
            return vals

    def setValue(self, value, blockSignal=None):
        self.targetValue = value
        exclusive = self.opts['exclusive']
        # Will emit at the end, so no problem discarding existing changes
        cmpVals = value if isinstance(value, list) else [value]
        for ii in range(len(cmpVals)-1, -1, -1):
            exists = any(fn.eq(cmpVals[ii], lim) for lim in self.reverse[0])
            if not exists:
                del cmpVals[ii]
        names = [self.reverse[1][self.reverse[0].index(val)] for val in cmpVals]
        if exclusive and len(names) > 1:
            names = [names[0]]
        elif exclusive and not len(names) and len(self.forward):
            # An option is required during exclusivity
            names = [self.reverse[1][0]]
        for chParam in self:
            checked = chParam.name() in names
            chParam.setValue(checked, self._onSubParamChange)
        super().setValue(self.value(), blockSignal)
