
###############################################################################
#
# Copyright 2019-present GEANT RARE project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed On an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
###############################################################################

import os, io, mmap, struct, time

## Apparently, this is needed to make super() work with python 2.7
__metaclass__ = type

class SMIv2:
    def __init__(self, mib, offset):
        self.mib = mib
        self.offset = offset

class int_t(SMIv2):
    def __init__(self, mib, offset):
       super(int_t, self).__init__(mib, offset)

    def set(self, value):
        value = value % self.modulo
        struct.pack_into(self.fmt, self.mib.map, self.offset, value)

    def get(self):
        res = struct.unpack_from(self.fmt, self.mib.map, self.offset)
        return res[0]

class int32_t(int_t):
    def __init__(self, mib, offset):
        self.size = 4
        self.fmt = "@i"
        self.modulo = 2**31
        super(int32_t, self).__init__(mib, offset)

class uint32_t(int_t):
    def __init__(self, mib, offset):
        self.size = 4
        self.fmt = "@I"
        self.modulo = 2**32
        super(uint32_t, self).__init__(mib, offset)

class uint64_t(int_t):
    def __init__(self, mib, offset):
        self.size = 8
        self.fmt = "@Q"
        self.modulo = 2**64
        super(uint64_t, self).__init__(mib, offset)

class octetstr_t(SMIv2):
    def __init__(self, mib, offset, length):
        ## Account for length bytes
        self.size = 2 + length
        super(octetstr_t, self).__init__(mib, offset)

    def set(self, value):
        assert(isinstance(value, bytes))
        length = min(len(value), self.size-2)
        struct.pack_into("@H", self.mib.map, self.offset, length)
        struct.pack_into("{0:d}s".format(length),
                         self.mib.map, self.offset+2, value)

    def get(self):
        len = struct.unpack_from("@H", self.mib.map, self.offset)[0]
        res = struct.unpack_from("{0:d}s".format(len),
                                 self.mib.map, self.offset+2)
        return res[0]

types = {
    'Integer32': int32_t,
    'Unsigned32': uint32_t,
    'OctetStr' : octetstr_t,
    'Counter32': uint32_t,
    'Counter64': uint64_t,
    'Gauge32': uint32_t,
    'TimeTicks': uint32_t,
    ## Not yet implemented
    # 'Bits': octetstr_t
}

class MIB:
    def __init__(self, file):
        index_file = file + ".index"
        self.file = file
        self.data_f = io.open(file, "w+b", buffering=0)
        self.index_f = io.open(index_file, "w")
        self.index_f.write(u'MIB:1\n')
        self.index_f.flush()

        self.map = None
        ## Current offset in the data segment where the next object
        ## will be allocated by register()
        self.offset = 0
        self.objects = {}

    def __del__(self):
        self.data_f.close()
        self.index_f.close()
        os.unlink(self.file)
        os.unlink(self.file + ".index")

    def register(self, name, type, value = None, octet_str_len = None):
        assert(type in types.keys())
        assert(name not in self.objects.keys())

        if type == 'OctetStr':
            assert(octet_str_len is not None)
            obj = types[type](self, self.offset, octet_str_len)
        else:
            obj = types[type](self, self.offset)

        self.objects[name] = obj
        self.offset = self.offset + obj.size
        self.index_f.write(u"{0:s}: {1:d}\n".format(name, obj.size))
        self.index_f.flush()

        ## Grow the shared memory region to the new size
        fileno = self.data_f.fileno()
        os.ftruncate(fileno, self.offset)
        if self.map is not None:
            self.map.close()
        self.map = mmap.mmap(fileno, 0, mmap.MAP_SHARED, mmap.PROT_READ|mmap.PROT_WRITE)

        if value is not None:
            obj.set(value)

    def set(self, name, value):
        assert(name in self.objects.keys())
        self.objects[name].set(value)

    def get(self, name):
        assert(name in self.objects.keys())
        return self.objects[name].get()

class ifmib(MIB):
    def __init__(self, file, properties):
        super(ifmib, self).__init__(file)
        p = properties
        self.register('ifDescr', 'OctetStr', p['ifDescr'].encode(),
                      octet_str_len = 255)
        self.register('ifName', 'OctetStr', p['ifName'].encode(), octet_str_len = 255)
        self.register('ifAlias', 'OctetStr', p['ifAlias'].encode(),
                      octet_str_len = 64 ) # interface description
        self.register('ifType', 'Integer32', 6 ) # ethernetCsmacd
        self.register('ifMtu', 'Integer32', p['ifMtu'])

        if p['speed'] > 1000000000:
            self.register('ifSpeed', 'Gauge32', 4294967295) # RFC3635 sec. 3.2.8
        else:
            self.register('ifSpeed', 'Gauge32', p['speed'])
        self.register('ifHighSpeed', 'Gauge32', int(p['speed'] / 1000000))

        self.register('ifPhysAddress', 'OctetStr', octet_str_len = 0)
        self.register('ifAdminStatus', 'Integer32', 2) # down
        self.register('ifOperStatus', 'Integer32', 2) # down
        self.register('ifLastChange', 'TimeTicks', 0)
        self.register('_X_ifLastChange_TicksBase', 'Counter64',
                                           int(time.time()))
        self.register('ifInOctets', 'Counter32', 0)
        self.register('ifInUcastPkts', 'Counter32', 0)
        self.register('ifInDiscards', 'Counter32', 0)
        self.register('ifInErrors', 'Counter32', 0)
        self.register('ifInUnknownProtos', 'Counter32', 0)
        self.register('ifOutOctets', 'Counter32', 0)
        self.register('ifOutUcastPkts', 'Counter32', 0)
        self.register('ifOutDiscards', 'Counter32', 0)
        self.register('ifOutErrors', 'Counter32', 0)

        self.register('ifInMulticastPkts', 'Counter32', 0)
        self.register('ifInBroadcastPkts', 'Counter32', 0)
        self.register('ifOutMulticastPkts', 'Counter32', 0)
        self.register('ifOutBroadcastPkts', 'Counter32', 0)
        self.register('ifHCInOctets', 'Counter64', 0)
        self.register('ifHCInUcastPkts', 'Counter64', 0)
        self.register('ifHCInMulticastPkts', 'Counter64', 0)
        self.register('ifHCInBroadcastPkts', 'Counter64', 0)
        self.register('ifHCOutOctets', 'Counter64', 0)
        self.register('ifHCOutUcastPkts', 'Counter64', 0)
        self.register('ifHCOutMulticastPkts', 'Counter64', 0)
        self.register('ifHCOutBroadcastPkts', 'Counter64', 0)
        self.register('ifLinkUpDownTrapEnable', 'Integer32', 2) # disabled
        self.register('ifPromiscuousMode', 'Integer32', 2) # false
        ## This information is not available from the $PORT table
        self.register('ifConnectorPresent', 'Integer32', 1) # true
        self.register('ifCounterDiscontinuityTime', 'TimeTicks', 0)
        ## Discontinuities per RFC 2233 are not possible for
        ## this platform (interfaces indices are fixed)
        self.register('_X_ifCounterDiscontinuityTime', 'Counter64', 0)

    def update(self, port, stat):
        if port['$PORT_ENABLE']:
            self.set('ifAdminStatus', 1) # up
        else:
            self.set('ifAdminStatus', 2) # down
        old_oper_state = self.get('ifOperStatus')
        if port['$PORT_UP']:
            new_oper_state = 1 # up
        else:
            new_oper_state = 2 # down
        self.set('ifOperStatus', new_oper_state)
        if old_oper_state != new_oper_state:
            self.set('ifLastChange', 0)
            self.set('_X_ifLastChange_TicksBase', int(time.time()))

        self.set('ifInOctets', stat['$OctetsReceivedinGoodFrames'])
        self.set('ifInUcastPkts', stat['$FramesReceivedOK'])
        self.set('ifInDiscards', stat['$FramesDroppedBufferFull'])
        self.set('ifInErrors', stat['$FrameswithanyError'])
        self.set('ifOutOctets', stat['$OctetsTransmittedwithouterror'])
        self.set('ifOutUcastPkts', stat['$FramesTransmittedOK'])
        self.set('ifOutErrors', stat['$FramesTransmittedwithError'])
        self.set('ifInMulticastPkts', stat['$FramesReceivedwithMulticastAddresses'])
        self.set('ifInBroadcastPkts', stat['$FramesReceivedwithBroadcastAddresses'])
        self.set('ifOutMulticastPkts', stat['$FramesTransmittedMulticast'])
        self.set('ifOutBroadcastPkts', stat['$FramesTransmittedBroadcast'])

        self.set('ifHCInOctets', stat['$OctetsReceivedinGoodFrames'])
        self.set('ifHCInUcastPkts', stat['$FramesReceivedOK'])
        self.set('ifHCInMulticastPkts', stat['$FramesReceivedwithMulticastAddresses'])
        self.set('ifHCInBroadcastPkts', stat['$FramesReceivedwithBroadcastAddresses'])
        self.set('ifHCOutOctets', stat['$OctetsTransmittedwithouterror'])
        self.set('ifHCOutUcastPkts', stat['$FramesTransmittedOK'])
        self.set('ifHCOutMulticastPkts', stat['$FramesTransmittedMulticast'])
        self.set('ifHCOutBroadcastPkts', stat['$FramesTransmittedBroadcast'])

        ### Not available from stats
        #self.set('ifInUnknownProtos', 0)
        #self.set('ifOutDiscards', 0)
