import hal
import _hal
import traceback
from qtpy.QtCore import QObject, QTimer, Signal

# Set up logging
from common import logger
log = logger.getLogger(__name__)
# log.setLevel(logger.INFO) # One of DEBUG, INFO, WARNING, ERROR, CRITICAL, VERBOSE

class QPin(hal.Pin, QObject):

    value_changed = Signal('PyQt_PyObject')# depreciated
    pinValueChanged = Signal('PyQt_PyObject','PyQt_PyObject')
    isDrivenChanged = Signal('PyQt_PyObject','PyQt_PyObject')
    REGISTRY = []
    UPDATE = False

    def __init__(self, *a, **kw):
        super(QPin, self).__init__(*a, **kw)
        QObject.__init__(self, None)
        self.hal = hal
        self._item_wrap(self._item)
        self._prev = None
        self._prevDriven = None
        self.REGISTRY.append(self)
        self.prefix = None

    def update(self):
        tmp = self.get()
        if tmp != self._prev:
            self.value_changed.emit(tmp)
            self.pinValueChanged.emit(self, tmp)
        self._prev = tmp

    def isDriven(self):
        return hal.pin_has_writer('{}.{}'.format(self.prefix, self.get_name()))

    def updateDriven(self):
        tmp = self.isDriven()
        if tmp != self._prevDriven:
            self.isDrivenChanged.emit(self, tmp)
        self._prevDriven = tmp

    def text(self):
        return self.get_name()

    # always returns False because
    # there was no error when making pin
    # see class DUMMY
    def error(self):
        return False

    @classmethod
    def update_all(cls):
        if not cls.UPDATE:
            return
        kill = []
        for p in cls.REGISTRY:
            try:
                p.update()
            except Exception as e:
                kill.append(p)
                log.error("Error updating pin {}; Removing".format(p))
                log.exception(e)
            if p.receivers(p.isDrivenChanged) > 0:
                p.updateDriven()

        for p in kill:
            cls.REGISTRY.remove(p)
        return cls.UPDATE

    @classmethod
    def update_start(cls, cycletime=None):
        if QPin.UPDATE:
            return
        QPin.UPDATE = True
        cls.timer = QTimer()
        cls.timer.timeout.connect(cls.update_all)
        cls.timer.start(cycletime)

    @classmethod
    def update_stop(cls):
        QPin.UPDATE = False


# so errors when making QPins aren't fatal
class DummyPin(QObject):
    value_changed = Signal('PyQt_PyObject')

    def __init__(self, *a, **kw):
        super(DummyPin, self).__init__(None)
        self._a = a
        self._kw = kw

    # always returns True because
    # there was an error when making HAL pin
    # see class QPin
    def error(self):
        return True

    def getError(self):
        print('{}'.format(self._kw.get('ERROR')))

    # send a safe value
    def get(self):
        return 0

    # accept but don't set anything
    def set(self, *a, **kw):
        pass

    def get_name(self):
        return self._a[0]

class _QHal(object):
    HAL_BIT = hal.HAL_BIT
    HAL_FLOAT = hal.HAL_FLOAT
    HAL_S32 = hal.HAL_S32
    HAL_U32 = hal.HAL_U32

    HAL_IN = hal.HAL_IN
    HAL_OUT = hal.HAL_OUT
    HAL_IO = hal.HAL_IO
    HAL_RO = hal.HAL_RO
    HAL_RW = hal.HAL_RW

    def __new__(cls, *a, **kw):
        instance = super(_QHal, cls).__new__(cls)
        instance.__init__(*a, **kw)
        return instance

    def __init__(self, comp=None, hal=None):
        # only initialize once for all instances
        if not self.__class__._instance is None:
            return

        if isinstance(comp, _QHal):
            comp = comp.comp
        self.comp = comp
        self.hal = hal

    def setUpdateRate(self, cyclerate):
        QPin.update_start(cyclerate)

    def newpin(self, *a, **kw):
        return self.newPin(*a,**kw)
    def newPin(self, *a, **kw):
        try:
            p = QPin(_hal.component.newpin(self.comp, *a, **kw))
        except ValueError as e:
            # if pin is already made, find a new name
            if 'Duplicate pin name' in '{}'.format(e):
                try:
                    # tuples are immutable, convert to list
                    y = list(a)
                    y[0] = self.makeUniqueName(y[0])
                    a = tuple(y)
                    # this late in the game, component is probably already 'ready'
                    if self.hal.component_is_ready(self.comp.getprefix()):
                        self.comp.unready()
                        p = QPin(_hal.component.newpin(self.comp, *a, **kw))
                        self.comp.ready()
                    else:
                        p = QPin(_hal.component.newpin(self.comp, *a, **kw))
                except Exception as e:
                    raise
        except Exception as e:
            if log.getEffectiveLevel() == logger.VERBOSE:
                raise
            t = inspect.getframeinfo(inspect.currentframe().f_back)
            log.error("QHal: Error making new HAL pin: {}\n    {}\n    Line {}\n    Function: {}".
                format(e, t[0], t[1], t[2]))
            log.error("QHal: {}".format(traceback.format_exc()))
            p = DummyPin(*a, ERROR=e)
        p.prefix = self.comp.getprefix()
        return p

    # accept either, first is depreciated
    def getpin(self, *a, **kw):
        return self.getPinObject(*a, **kw)
    def getPinObject(self, *a, **kw):
        return QPin(_hal.component.getpin(self.comp, *a, **kw))

    # accept either, first is depreciated
    def getvalue(self, name):
        return self.getValue(name)
    def getValue(self, name):
        try:
            return hal.get_value(name)
        except Exception as e:
            raise("QHal: Error getting value of {}\n {}".format(name, e))

    # accept either, first is depreciated
    def setp(self,name, value):
        return self.setPin(name, value)
    def setPin(self,name, value):
        try:
            return hal.set_p(name,value)
        except Exception as e:
            raise("QHal: Error setting pin {} to {}\n {}".format(name,value, e))

    # accept either, first is depreciated
    def sets(self,name, value):
        return self.setSignal(name, value)
    def setSignal(self,name, value):
        try:
            return hal.set_s(name,value)
        except Exception as e:
            raise("QHal: Error setting signal {} to {}\n {}".format(name,value, e))

    def exit(self, *a, **kw):
        return self.comp.exit(*a, **kw)

    # find a unique HAL pin name by adding '-x' to the base name
    # x being an ever increasing number till name is unique
    def makeUniqueName(self, name):
        num = 2
        base = self.comp.getprefix()
        while True:
            trial = ('{}.{}-{}').format(base,name,num)
            for i in hal.get_info_pins():
                if i['NAME'] == trial:
                    num +=1
                    trial = ('{}.{}-{}').format(base,name,num)
                    break
            else:
                break
        return ('{}-{}').format(name,num)

    def __getitem__(self, k): return self.comp[k]
    def __setitem__(self, k, v): self.comp[k] = v

class Qhal(_QHal):
    _instance = None

    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = _QHal.__new__(cls, *args, **kwargs)
        return cls._instance
