import pcaspy
import threading

from . import const
from .db import guarddb


# add the REQUEST_ENUM interface channel
guarddb['REQUEST_ENUM'] = {
    'type': 'enum',
    'enums': (),
    'writable': True,
    }


def request_enum_index(request):
    """Returns index of request string in REQUEST_ENUM"""
    return guarddb['REQUEST_ENUM']['enums'].index(str(request))


class CAServerError(Exception):
    pass

class CADriver(pcaspy.Driver):
    def __init__(self, request_event):
        super(CADriver, self).__init__()
        self._state_index = {}
        self._request_event = request_event

    ##########
    # these methods are for the external interface
    # NOTE: caller must updatePV after set

    def __getitem__(self, channel):
        value = self.getParam(channel)
        if guarddb[channel]['type'] == 'enum':
            if guarddb[channel].get('guardbool', False) and value == 0:
                value = False
            else:
                value = guarddb[channel]['enums'][value]
        return value

    def __setitem__(self, channel, value):
        # handle state readbacks
        if guarddb[channel].get('guardstate', False):
            nvalue = self._state_index[value]
            self.setParam(channel+'_S', value)
            self.setParam(channel+'_N', nvalue)
        # resolve enums, since setParam expects numeric value
        if guarddb[channel]['type'] == 'enum':
            value = guarddb[channel]['enums'].index(str(value))
        # update REQUEST_ENUM
        if channel == 'REQUEST' and value in guarddb['REQUEST_ENUM']['enums']:
            self.setParam('REQUEST_ENUM', request_enum_index(value))
        self.setParam(channel, value)

    ##########
    # these methods are for the internal interface, e.g. CA clients

    def write(self, channel, value):
        # NOTE: for enum records the value here is the numeric value,
        # not the string, and setParam() expects the numeric value.

        # reject writes to non-writable channels
        if not guarddb[channel].get('writable', False):
            return False

        # reject values that don't correspond to an actual index of
        # the enum
        # FIXME: this is apparently a feature? of cas that allows for
        # setting numeric values higher than the enum?
        if guarddb[channel]['type'] == 'enum' \
           and value >= len(guarddb[channel]['enums']):
            return False

        if channel in ['REQUEST', 'REQUEST_ENUM']:
            if channel == 'REQUEST':
                # reject invalid state requests
                if value not in self._state_index.keys():
                    return False
                if value in guarddb['REQUEST_ENUM']['enums']:
                    self.setParam('REQUEST_ENUM', request_enum_index(value))
                svalue = value
            elif channel == 'REQUEST_ENUM':
                self.setParam('REQUEST_ENUM', value)
                svalue = guarddb['REQUEST_ENUM']['enums'][value]

            self.setParam('REQUEST', svalue)
            self.setParam('REQUEST_S', svalue)
            self.setParam('REQUEST_N', self._state_index[svalue])
            self._request_event.set()

        elif channel == 'MODE':
            # value as string
            mode = guarddb['MODE']['enums'][value]
            # reject attempts to set MANAGED mode directly without
            # specifying manager
            if mode == 'MANAGED':
                return False
            self.setParam('MODE', value)
            # make sure manager channel is unset if switching away
            # from managed mode
            if mode in ['AUTO', 'MANUAL']:
                self.setParam('MANAGER', '')

        elif channel == 'MANAGER':
            # reject setting manager to empty string
            if value == '':
                return False
            self.setParam('MANAGER', value)
            self.setParam('MODE', guarddb['MODE']['enums'].index('MANAGED'))

        else:
            self.setParam(channel, value)

        self.updatePVs()

        return True


class CAServer(object):
    def __init__(self, system_name):
        self.prefix = const.CAS_PREFIX_FMT.format(IFO=const.IFO, SYSTEM=system_name)
        self._request_event = threading.Event()
        self._server = pcaspy.SimpleServer()
        #self._server.setDebugLevel(4)
        self._loaddb()
        self._driver = CADriver(self._request_event)
        # clear undefined alarms at startup
        for chan in guarddb:
            self._driver.setParamStatus(chan, pcaspy.Severity.NO_ALARM, pcaspy.Severity.NO_ALARM)
        self._driver.updatePVs()
        self._running = True
        self.daemon = True


    def _loaddb(self):
        self._server.createPV(self.prefix, guarddb)


    def process(self, timeout):
        """Server process select loop

        Returns in max `timeout` seconds, but usually much faster
        (this is presumably the timeout of the select itself).

        """
        self._server.process(timeout)

    ########################################

    def __getitem__(self, channel):
        """get PV value

         """
        return self._driver[channel]


    def __setitem__(self, channel, value):
        """set PV value

        ** Caller required to run updatePVs **

         """
        self._driver[channel] = value


    def updatePVs(self):
        """Push updates to driver

         """
        # The driver.updatePVs() call is the heaviest interaction with
        # the cas, consuming more processing time than anything else.
        # Calling it after every setitem call causes the load on the
        # system to be unacceptably high.
        #
        # FIXME: The placement of this also seems to have a strong
        # affect on the behavior of the EPICS interaction with
        # external clients.  One would assume that it only needs to be
        # called once per cycle of the main loop, to push all param
        # changes that happened in the loop.  But if that's done, the
        # EPICS interaction with the clients seems to become very odd,
        # and client puts don't necessarily show up as expected
        # (they're there in EPICS but not in pcaspy???  Why does this
        # seem to affect client writes??).  Needs more investigation.
        #
        # Anyway...  PLACEMENT OF THIS CALL NEEDS TO BE CAREFULLY
        # CONSIDERED AND TESTED.
        self._driver.updatePVs()


    def update_state_index(self, index, init=False):
        """Update state index dictionary

        """
        self._driver._state_index = index
        if init:
            return
        # update all guardstate channels, in case the state:index
        # mapping has changed
        for channel, entry in guarddb.items():
            if entry.get('guardstate', False):
                self._driver[channel] = self._driver[channel]
        self._driver.updatePVs()


    def update_request_enum(self, enum, init=False):
        """Update request enum tuple

        Returns True if the enum was changed.

        """
        if enum == guarddb['REQUEST_ENUM']['enums']:
            return False
        guarddb['REQUEST_ENUM']['enums'] = enum
        self._driver.setParamEnums('REQUEST_ENUM', enum)
        if not init:
            # update request enum, in case it's changed
            request = self._driver.getParam('REQUEST')
            if request in guarddb['REQUEST_ENUM']['enums']:
                self._driver.setParam('REQUEST_ENUM', request_enum_index(request))
        self._driver.updatePVs()
        return True
