#
# This file is part of pysnmp software.
#
# Copyright (c) 2005-2019, Ilya Etingof <etingof@gmail.com>
# License: http://snmplabs.com/pysnmp/license.html
#
import sys

from pysnmp.carrier import error


class TimerCallable(object):
    def __init__(self, cbFun, callInterval):
        self.__cbFun = cbFun
        self.__nextCall = 0

        if sys.version_info > (2, 6):
            self.__callInterval = callInterval
        else:
            self.interval = callInterval

    def __call__(self, timeNow):
        if self.__nextCall <= timeNow:
            self.__cbFun(timeNow)
            self.__nextCall = timeNow + self.interval

    def __eq__(self, cbFun):
        return self.__cbFun == cbFun

    def __ne__(self, cbFun):
        return self.__cbFun != cbFun

    def __lt__(self, cbFun):
        return self.__cbFun < cbFun

    def __le__(self, cbFun):
        return self.__cbFun <= cbFun

    def __gt__(self, cbFun):
        return self.__cbFun > cbFun

    def __ge__(self, cbFun):
        return self.__cbFun >= cbFun

    if sys.version_info > (2, 6):
        @property
        def interval(self):
            return self.__callInterval

        @interval.setter
        def interval(self, callInterval):
            self.__callInterval = callInterval


class AbstractTransportDispatcher(object):
    def __init__(self):
        self.__transports = {}
        self.__transportDomainMap = {}
        self.__jobs = {}
        self.__recvCallables = {}
        self.__timerCallables = []
        self.__ticks = 0
        self.__timerResolution = 0.5
        self.__timerDelta = self.__timerResolution * 0.05
        self.__nextTime = 0
        self.__routingCbFun = None

    def _cbFun(self, incomingTransport, transportAddress, incomingMessage):
        if incomingTransport in self.__transportDomainMap:
            transportDomain = self.__transportDomainMap[incomingTransport]
        else:
            raise error.CarrierError(
                'Unregistered transport %s' % (incomingTransport,)
            )

        if self.__routingCbFun:
            recvId = self.__routingCbFun(
                transportDomain, transportAddress, incomingMessage
            )
        else:
            recvId = None

        if recvId in self.__recvCallables:
            self.__recvCallables[recvId](
                self, transportDomain, transportAddress, incomingMessage
            )
        else:
            raise error.CarrierError(
                'No callback for "%r" found - loosing incoming event' % (recvId,)
            )

    # Dispatcher API

    def registerRoutingCbFun(self, routingCbFun):
        if self.__routingCbFun:
            raise error.CarrierError(
                'Data routing callback already registered'
            )
        self.__routingCbFun = routingCbFun

    def unregisterRoutingCbFun(self):
        if self.__routingCbFun:
            self.__routingCbFun = None

    def registerRecvCbFun(self, recvCb, recvId=None):
        if recvId in self.__recvCallables:
            raise error.CarrierError(
                'Receive callback %r already registered' % (recvId is None and '<default>' or recvId,)
            )
        self.__recvCallables[recvId] = recvCb

    def unregisterRecvCbFun(self, recvId=None):
        if recvId in self.__recvCallables:
            del self.__recvCallables[recvId]

    def registerTimerCbFun(self, timerCbFun, tickInterval=None):
        if not tickInterval:
            tickInterval = self.__timerResolution
        self.__timerCallables.append(TimerCallable(timerCbFun, tickInterval))

    def unregisterTimerCbFun(self, timerCbFun=None):
        if timerCbFun:
            self.__timerCallables.remove(timerCbFun)
        else:
            self.__timerCallables = []

    def registerTransport(self, tDomain, transport):
        if tDomain in self.__transports:
            raise error.CarrierError(
                'Transport %s already registered' % (tDomain,)
            )
        transport.registerCbFun(self._cbFun)
        self.__transports[tDomain] = transport
        self.__transportDomainMap[transport] = tDomain

    def unregisterTransport(self, tDomain):
        if tDomain not in self.__transports:
            raise error.CarrierError(
                'Transport %s not registered' % (tDomain,)
            )
        self.__transports[tDomain].unregisterCbFun()
        del self.__transportDomainMap[self.__transports[tDomain]]
        del self.__transports[tDomain]

    def getTransport(self, transportDomain):
        if transportDomain in self.__transports:
            return self.__transports[transportDomain]
        raise error.CarrierError(
            'Transport %s not registered' % (transportDomain,)
        )

    def sendMessage(self, outgoingMessage, transportDomain,
                    transportAddress):
        if transportDomain in self.__transports:
            self.__transports[transportDomain].sendMessage(
                outgoingMessage, transportAddress
            )
        else:
            raise error.CarrierError(
                'No suitable transport domain for %s' % (transportDomain,)
            )

    def getTimerResolution(self):
        return self.__timerResolution

    def setTimerResolution(self, timerResolution):
        if timerResolution < 0.01 or timerResolution > 10:
            raise error.CarrierError('Impossible timer resolution')

        for timerCallable in self.__timerCallables:
            if timerCallable.interval == self.__timerResolution:
                # Update periodics for default resolutions
                timerCallable.interval = timerResolution

        self.__timerResolution = timerResolution
        self.__timerDelta = timerResolution * 0.05

    def getTimerTicks(self):
        return self.__ticks

    def handleTimerTick(self, timeNow):
        if self.__nextTime == 0:  # initial initialization
            self.__nextTime = timeNow + self.__timerResolution - self.__timerDelta

        if self.__nextTime >= timeNow:
            return

        self.__ticks += 1
        self.__nextTime = timeNow + self.__timerResolution - self.__timerDelta

        for timerCallable in self.__timerCallables:
            timerCallable(timeNow)

    def jobStarted(self, jobId, count=1):
        if jobId in self.__jobs:
            self.__jobs[jobId] += count
        else:
            self.__jobs[jobId] = count

    def jobFinished(self, jobId, count=1):
        self.__jobs[jobId] -= count
        if self.__jobs[jobId] == 0:
            del self.__jobs[jobId]

    def jobsArePending(self):
        return bool(self.__jobs)

    def runDispatcher(self, timeout=0.0):
        raise error.CarrierError('Method not implemented')

    def closeDispatcher(self):
        for tDomain in list(self.__transports):
            self.__transports[tDomain].closeTransport()
            self.unregisterTransport(tDomain)
        self.__transports.clear()
        self.unregisterRecvCbFun()
        self.unregisterTimerCbFun()


class AbstractTransportAddress(object):
    _localAddress = None

    def setLocalAddress(self, s):
        self._localAddress = s
        return self

    def getLocalAddress(self):
        return self._localAddress

    def clone(self, localAddress=None):
        return self.__class__(self).setLocalAddress(localAddress is None and self.getLocalAddress() or localAddress)


class AbstractTransport(object):
    protoTransportDispatcher = None
    addressType = AbstractTransportAddress
    _cbFun = None

    @classmethod
    def isCompatibleWithDispatcher(cls, transportDispatcher):
        return isinstance(transportDispatcher, cls.protoTransportDispatcher)

    def registerCbFun(self, cbFun):
        if self._cbFun:
            raise error.CarrierError(
                'Callback function %s already registered at %s' % (self._cbFun, self)
            )
        self._cbFun = cbFun

    def unregisterCbFun(self):
        self._cbFun = None

    def closeTransport(self):
        self.unregisterCbFun()

    # Public API

    def openClientMode(self, iface=None):
        raise error.CarrierError('Method not implemented')

    def openServerMode(self, iface):
        raise error.CarrierError('Method not implemented')

    def sendMessage(self, outgoingMessage, transportAddress):
        raise error.CarrierError('Method not implemented')
