#
# This file is part of pysmi software.
#
# Copyright (c) 2015-2019, Ilya Etingof <etingof@gmail.com>
# License: http://snmplabs.com/pysmi/license.html
#
import sys
import re
from time import strptime, strftime
from keyword import iskeyword
from pysmi.mibinfo import MibInfo
from pysmi.codegen.base import AbstractCodeGen, dorepr
from pysmi import error
from pysmi import debug


if sys.version_info[0] > 2:
    # noinspection PyShadowingBuiltins
    unicode = str
    # noinspection PyShadowingBuiltins
    long = int


class PySnmpCodeGen(AbstractCodeGen):
    """Builds PySNMP-specific Python code representing MIB module supplied
       in form of an Abstract Syntax Tree on input.

       Instance of this class is supposed to be passed to *MibCompiler*,
       the rest is internal to *MibCompiler*.
    """
    defaultMibPackages = ('pysnmp.smi.mibs', 'pysnmp_mibs')

    symsTable = {
        'MODULE-IDENTITY': ('ModuleIdentity',),
        'OBJECT-TYPE': ('MibScalar', 'MibTable', 'MibTableRow', 'MibTableColumn'),
        'NOTIFICATION-TYPE': ('NotificationType',),
        'TEXTUAL-CONVENTION': ('TextualConvention',),
        'MODULE-COMPLIANCE': ('ModuleCompliance',),
        'OBJECT-GROUP': ('ObjectGroup',),
        'NOTIFICATION-GROUP': ('NotificationGroup',),
        'AGENT-CAPABILITIES': ('AgentCapabilities',),
        'OBJECT-IDENTITY': ('ObjectIdentity',),
        'TRAP-TYPE': ('NotificationType',),  # smidump always uses NotificationType
        'BITS': ('Bits',),
    }

    constImports = {
        'ASN1': ('Integer', 'OctetString', 'ObjectIdentifier'),
        'ASN1-ENUMERATION': ('NamedValues',),
        'ASN1-REFINEMENT': ('ConstraintsUnion', 'ConstraintsIntersection', 'SingleValueConstraint',
                            'ValueRangeConstraint', 'ValueSizeConstraint'),
        'SNMPv2-SMI': ('iso',
                       'Bits',  # XXX
                       'Integer32',  # XXX
                       'TimeTicks',  # bug in some IETF MIBs
                       'Counter32',  # bug in some IETF MIBs (e.g. DSA-MIB)
                       'Counter64',  # bug in some MIBs (e.g.A3COM-HUAWEI-LswINF-MIB)
                       'NOTIFICATION-TYPE',  # bug in some MIBs (e.g. A3COM-HUAWEI-DHCPSNOOP-MIB)
                       'Gauge32',  # bug in some IETF MIBs (e.g. DSA-MIB)
                       'MODULE-IDENTITY', 'OBJECT-TYPE', 'OBJECT-IDENTITY', 'Unsigned32', 'IpAddress',  # XXX
                       'MibIdentifier'),  # OBJECT IDENTIFIER
        'SNMPv2-TC': ('DisplayString', 'TEXTUAL-CONVENTION',),  # XXX
        'SNMPv2-CONF': ('MODULE-COMPLIANCE', 'NOTIFICATION-GROUP',),  # XXX
    }

    # never compile these, they either:
    # - define MACROs (implementation supplies them)
    # - or carry conflicting OIDs (so that all IMPORT's of them will be rewritten)
    # - or have manual fixes
    # - or import base ASN.1 types from implementation-specific MIBs
    fakeMibs = ('ASN1',
                'ASN1-ENUMERATION',
                'ASN1-REFINEMENT')
    baseMibs = ('PYSNMP-USM-MIB',
                'SNMP-FRAMEWORK-MIB',
                'SNMP-TARGET-MIB',
                'TRANSPORT-ADDRESS-MIB',
                'INET-ADDRESS-MIB') + AbstractCodeGen.baseMibs

    baseTypes = ['Integer', 'Integer32', 'Bits', 'ObjectIdentifier', 'OctetString']

    typeClasses = {
        'COUNTER32': 'Counter32',
        'COUNTER64': 'Counter64',
        'GAUGE32': 'Gauge32',
        'INTEGER': 'Integer32',  # XXX
        'INTEGER32': 'Integer32',
        'IPADDRESS': 'IpAddress',
        'NETWORKADDRESS': 'IpAddress',
        'OBJECT IDENTIFIER': 'ObjectIdentifier',
        'OCTET STRING': 'OctetString',
        'OPAQUE': 'Opaque',
        'TIMETICKS': 'TimeTicks',
        'UNSIGNED32': 'Unsigned32',
        'Counter': 'Counter32',
        'Gauge': 'Gauge32',
        'NetworkAddress': 'IpAddress',  # RFC1065-SMI, RFC1155-SMI -> SNMPv2-SMI
        'nullSpecific': 'zeroDotZero',  # RFC1158-MIB -> SNMPv2-SMI
        'ipRoutingTable': 'ipRouteTable',  # RFC1158-MIB -> RFC1213-MIB
        'snmpEnableAuthTraps': 'snmpEnableAuthenTraps'  # RFC1158-MIB -> SNMPv2-MIB
    }

    smiv1IdxTypes = ['INTEGER', 'OCTET STRING', 'IPADDRESS', 'NETWORKADDRESS']

    ifTextStr = 'if mibBuilder.loadTexts: '
    indent = ' ' * 4
    fakeidx = 1000  # starting index for fake symbols

    def __init__(self):
        self._snmpTypes = set(self.typeClasses.values())
        self._snmpTypes.add('Bits')
        self._rows = set()
        self._cols = {}  # k, v = name, datatype
        self._exports = set()
        self._seenSyms = set()
        self._importMap = {}
        self._out = {}  # k, v = name, generated code
        self._moduleIdentityOid = None
        self._moduleRevision = None
        self.moduleName = ['DUMMY']
        self.genRules = {'text': True}
        self.symbolTable = {}

    def symTrans(self, symbol):
        if symbol in self.symsTable:
            return self.symsTable[symbol]

        return symbol,

    @staticmethod
    def transOpers(symbol):
        if iskeyword(symbol):
            symbol = 'pysmi_' + symbol

        return symbol.replace('-', '_')

    def prepData(self, pdata, classmode=False):
        data = []

        for el in pdata:
            if not isinstance(el, tuple):
                data.append(el)

            elif len(el) == 1:
                data.append(el[0])

            else:
                data.append(
                    self.handlersTable[el[0]](self, self.prepData(el[1:], classmode=classmode), classmode=classmode)
                )

        return data

    def genImports(self, imports):
        outStr = ''

        # conversion to SNMPv2
        toDel = []
        for module in list(imports):

            if module in self.convertImportv2:

                for symbol in imports[module]:

                    if symbol in self.convertImportv2[module]:
                        toDel.append((module, symbol))

                        for newImport in self.convertImportv2[module][symbol]:
                            newModule, newSymbol = newImport

                            if newModule in imports:
                                imports[newModule].append(newSymbol)
                            else:
                                imports[newModule] = [newSymbol]

        # removing converted symbols
        for d in toDel:
            imports[d[0]].remove(d[1])

        # merging mib and constant imports
        for module in self.constImports:
            if module in imports:
                imports[module] += self.constImports[module]
            else:
                imports[module] = self.constImports[module]

        for module in sorted(imports):
            symbols = ()

            for symbol in set(imports[module]):
                symbols += self.symTrans(symbol)

            if symbols:
                self._seenSyms.update([self.transOpers(s) for s in symbols])
                self._importMap.update([(self.transOpers(s), module) for s in symbols])

                outStr += ', '.join([self.transOpers(s) for s in symbols])
                if len(symbols) < 2:
                    outStr += ','
                outStr += ' = mibBuilder.importSymbols("%s")\n' % ('", "'.join((module,) + symbols))

        return outStr, tuple(sorted(imports))

    def genExports(self, ):
        exports = list(self._exports)
        if not exports:
            return ''

        numFuncCalls = len(exports) // 254 + 1

        outStr = ''

        for idx in range(numFuncCalls):
            outStr += 'mibBuilder.exportSymbols("' + self.moduleName[0] + '", '
            outStr += ', '.join(exports[254 * idx:254 * (idx + 1)]) + ')\n'

        return outStr

    # noinspection PyMethodMayBeStatic
    def genLabel(self, symbol, classmode=False):
        if '-' in symbol or iskeyword(symbol):
            return classmode and 'label = "' + symbol + '"\n' or '.setLabel("' + symbol + '")'

        return ''

    def addToExports(self, symbol, moduleIdentity=0):
        if moduleIdentity:
            self._exports.add('PYSNMP_MODULE_ID=%s' % symbol)

        self._exports.add('%s=%s' % (symbol, symbol))
        self._seenSyms.add(symbol)

    # noinspection PyUnusedLocal
    def regSym(self, symbol, outStr, oidStr=None, moduleIdentity=False):
        if symbol in self._seenSyms and symbol not in self._importMap:
            raise error.PySmiSemanticError('Duplicate symbol found: %s' % symbol)

        self.addToExports(symbol, moduleIdentity)
        self._out[symbol] = outStr

        if moduleIdentity:
            if self._moduleIdentityOid:
                raise error.PySmiSemanticError('Duplicate module identity')
            # TODO: turning literal tuple into a string - hackerish
            self._moduleIdentityOid = '.'.join(oidStr.split(', '))[1:-1]

    def genNumericOid(self, oid):
        numericOid = ()

        for part in oid:
            if isinstance(part, tuple):
                parent, module = part

                if parent == 'iso':
                    numericOid += (1,)
                    continue

                if module not in self.symbolTable:
                    # XXX do getname for possible future borrowed mibs
                    raise error.PySmiSemanticError('no module "%s" in symbolTable' % module)

                if parent not in self.symbolTable[module]:
                    raise error.PySmiSemanticError('no symbol "%s" in module "%s"' % (parent, module))

                numericOid += self.genNumericOid(self.symbolTable[module][parent]['oid'])

            else:
                numericOid += (part,)

        return numericOid

    def getBaseType(self, symName, module):
        if module not in self.symbolTable:
            raise error.PySmiSemanticError('no module "%s" in symbolTable' % module)

        if symName not in self.symbolTable[module]:
            raise error.PySmiSemanticError('no symbol "%s" in module "%s"' % (symName, module))

        symType, symSubtype = self.symbolTable[module][symName].get('syntax', (('', ''), ''))

        if not symType[0]:
            raise error.PySmiSemanticError('unknown type for symbol "%s"' % symName)

        if symType[0] in self.baseTypes:
            return symType, symSubtype

        else:
            baseSymType, baseSymSubtype = self.getBaseType(*symType)

            if isinstance(baseSymSubtype, list):
                if isinstance(symSubtype, list):
                    symSubtype += baseSymSubtype
                else:
                    symSubtype = baseSymSubtype

            return baseSymType, symSubtype

    # Clause generation functions

    # noinspection PyUnusedLocal
    def genAgentCapabilities(self, data, classmode=False):
        name, productRelease, status, description, reference, oid = data

        label = self.genLabel(name)
        name = self.transOpers(name)

        oidStr, parentOid = oid
        outStr = name + ' = AgentCapabilities(' + oidStr + ')' + label + '\n'

        if productRelease:
            outStr += """\
if getattr(mibBuilder, 'version', (0, 0, 0)) > (4, 4, 0):
    %(name)s = %(name)s%(productRelease)s
""" % dict(name=name, productRelease=productRelease)

        if status:
            outStr += """\
if getattr(mibBuilder, 'version', (0, 0, 0)) > (4, 4, 0):
    %(name)s = %(name)s%(status)s
""" % dict(name=name, status=status)

        if self.genRules['text'] and description:
            outStr += self.ifTextStr + name + description + '\n'

        if self.genRules['text'] and reference:
            outStr += name + reference + '\n'

        self.regSym(name, outStr, oidStr)

        return outStr

    # noinspection PyUnusedLocal
    def genModuleIdentity(self, data, classmode=False):
        name, lastUpdated, organization, contactInfo, description, revisionsAndDescrs, oid = data

        label = self.genLabel(name)
        name = self.transOpers(name)

        oidStr, parentOid = oid

        outStr = name + ' = ModuleIdentity(' + oidStr + ')' + label + '\n'

        if revisionsAndDescrs:
            last_revision, revisions, descriptions = revisionsAndDescrs

            self._moduleRevision = last_revision

            if revisions:
                outStr += name + revisions + '\n'

            if self.genRules['text'] and descriptions:
                outStr += """
if getattr(mibBuilder, 'version', (0, 0, 0)) > (4, 4, 0):
    %(ifTextStr)s%(name)s%(descriptions)s
""" % dict(ifTextStr=self.ifTextStr, name=name, descriptions=descriptions)

        if lastUpdated:
            outStr += self.ifTextStr + name + lastUpdated + '\n'

        if organization:
            outStr += self.ifTextStr + name + organization + '\n'

        if self.genRules['text'] and contactInfo:
            outStr += self.ifTextStr + name + contactInfo + '\n'

        if self.genRules['text'] and description:
            outStr += self.ifTextStr + name + description + '\n'

        self.regSym(name, outStr, oidStr, moduleIdentity=True)

        return outStr

    # noinspection PyUnusedLocal
    def genModuleCompliance(self, data, classmode=False):
        name, status, description, reference, compliances, oid = data

        label = self.genLabel(name)
        name = self.transOpers(name)

        oidStr, parentOid = oid
        outStr = name + ' = ModuleCompliance(' + oidStr + ')' + label
        outStr += compliances + '\n'

        if status:
            outStr += """\
if getattr(mibBuilder, 'version', (0, 0, 0)) > (4, 4, 0):
    %(name)s = %(name)s%(status)s
""" % dict(name=name, status=status)

        if self.genRules['text'] and description:
            outStr += self.ifTextStr + name + description + '\n'

        if self.genRules['text'] and reference:
            outStr += self.ifTextStr + name + reference + '\n'

        self.regSym(name, outStr, oidStr)

        return outStr

    # noinspection PyUnusedLocal
    def genNotificationGroup(self, data, classmode=False):
        name, objects, status, description, reference, oid = data

        label = self.genLabel(name)
        name = self.transOpers(name)

        oidStr, parentOid = oid

        outStr = name + ' = NotificationGroup(' + oidStr + ')' + label

        if objects:
            objects = ['("' + self._importMap.get(obj, self.moduleName[0]) + '", "' + self.transOpers(obj) + '")'
                       for obj in objects]

            numFuncCalls = len(objects) // 255 + 1

            if numFuncCalls > 1:
                objStrParts = []

                for idx in range(numFuncCalls):
                    objStrParts.append('[' + ', '.join(objects[255 * idx:255 * (idx + 1)]) + ']')

                outStr += """
for _%(name)s_obj in [%(objects)s]:
    if getattr(mibBuilder, 'version', 0) < (4, 4, 2):
        # WARNING: leading objects get lost here! Upgrade your pysnmp version!
        %(name)s = %(name)s.setObjects(*_%(name)s_obj)
    else:
        %(name)s = %(name)s.setObjects(*_%(name)s_obj, **dict(append=True))\
""" % dict(name=name, objects=', '.join(objStrParts))

            else:
                outStr += '.setObjects(' + ', '.join(objects) + ')'

        outStr += '\n'

        if status:
            outStr += """\
if getattr(mibBuilder, 'version', (0, 0, 0)) > (4, 4, 0):
    %(name)s = %(name)s%(status)s
""" % dict(name=name, status=status)

        if self.genRules['text'] and description:
            outStr += self.ifTextStr + name + description + '\n'

        if self.genRules['text'] and reference:
            outStr += name + reference + '\n'

        self.regSym(name, outStr, oidStr)

        return outStr

    # noinspection PyUnusedLocal
    def genNotificationType(self, data, classmode=False):
        name, objects, status, description, reference, oid = data

        label = self.genLabel(name)
        name = self.transOpers(name)

        oidStr, parentOid = oid

        outStr = name + ' = NotificationType(' + oidStr + ')' + label

        if objects:
            objects = ['("' + self._importMap.get(obj, self.moduleName[0]) + '", "' + self.transOpers(obj) + '")'
                       for obj in objects]

            numFuncCalls = len(objects) // 255 + 1

            if numFuncCalls > 1:
                objStrParts = []

                for idx in range(numFuncCalls):
                    objStrParts.append('[' + ', '.join(objects[255 * idx:255 * (idx + 1)]) + ']')

                outStr += """
for _%(name)s_obj in [%(objects)s]:
    if getattr(mibBuilder, 'version', 0) < (4, 4, 2):
        # WARNING: leading objects get lost here! Upgrade your pysnmp version!
        %(name)s = %(name)s.setObjects(*_%(name)s_obj)
    else:
        %(name)s = %(name)s.setObjects(*_%(name)s_obj, **dict(append=True))\
""" % dict(name=name, objects=', '.join(objStrParts))

            else:
                outStr += '.setObjects(' + ', '.join(objects) + ')'

        outStr += '\n'

        if status:
            outStr += self.ifTextStr + name + status + '\n'

        if self.genRules['text'] and description:
            outStr += self.ifTextStr + name + description + '\n'

        if self.genRules['text'] and reference:
            outStr += self.ifTextStr + name + reference + '\n'

        self.regSym(name, outStr, oidStr)

        return outStr

    # noinspection PyUnusedLocal
    def genObjectGroup(self, data, classmode=False):
        name, objects, status, description, reference, oid = data

        label = self.genLabel(name)
        name = self.transOpers(name)

        oidStr, parentOid = oid

        outStr = name + ' = ObjectGroup(' + oidStr + ')' + label

        if objects:
            objects = ['("' + self._importMap.get(obj, self.moduleName[0]) + '", "' + self.transOpers(obj) + '")'
                       for obj in objects]

            numFuncCalls = len(objects) // 255 + 1

            if numFuncCalls > 1:
                objStrParts = []

                for idx in range(numFuncCalls):
                    objStrParts.append('[' + ', '.join(objects[255 * idx:255 * (idx + 1)]) + ']')

                outStr += """
for _%(name)s_obj in [%(objects)s]:
    if getattr(mibBuilder, 'version', 0) < (4, 4, 2):
        # WARNING: leading objects get lost here!
        %(name)s = %(name)s.setObjects(*_%(name)s_obj)
    else:
        %(name)s = %(name)s.setObjects(*_%(name)s_obj, **dict(append=True))\
""" % dict(name=name, objects=', '.join(objStrParts))

            else:
                outStr += '.setObjects(' + ', '.join(objects) + ')'

        outStr += '\n'

        if status:
            outStr += """\
if getattr(mibBuilder, 'version', (0, 0, 0)) > (4, 4, 0):
    %(name)s = %(name)s%(status)s
""" % dict(name=name, status=status)

        if self.genRules['text'] and description:
            outStr += self.ifTextStr + name + description + '\n'

        if self.genRules['text'] and reference:
            outStr += self.ifTextStr + name + reference + '\n'

        self.regSym(name, outStr, oidStr)

        return outStr

    # noinspection PyUnusedLocal
    def genObjectIdentity(self, data, classmode=False):
        name, status, description, reference, oid = data

        label = self.genLabel(name)
        name = self.transOpers(name)

        oidStr, parentOid = oid
        outStr = name + ' = ObjectIdentity(' + oidStr + ')' + label + '\n'

        if status:
            outStr += self.ifTextStr + name + status + '\n'

        if self.genRules['text'] and description:
            outStr += self.ifTextStr + name + description + '\n'

        if self.genRules['text'] and reference:
            outStr += self.ifTextStr + name + reference + '\n'

        self.regSym(name, outStr, oidStr)

        return outStr

    # noinspection PyUnusedLocal
    def genObjectType(self, data, classmode=False):
        name, syntax, units, maxaccess, status, description, reference, augmention, index, defval, oid = data

        label = self.genLabel(name)
        name = self.transOpers(name)

        oidStr, parentOid = oid

        indexStr, fakeStrlist, fakeSyms = index or ('', '', [])
        subtype = syntax[0] == 'Bits' and 'Bits()' + syntax[1] or syntax[1]  # Bits hack #1

        classtype = self.typeClasses.get(syntax[0], syntax[0])
        classtype = self.transOpers(classtype)
        classtype = syntax[0] == 'Bits' and 'MibScalar' or classtype  # Bits hack #2
        classtype = name in self.symbolTable[self.moduleName[0]]['_symtable_cols'] and 'MibTableColumn' or classtype

        defval = self.genDefVal(defval, objname=name)

        outStr = name + ' = ' + classtype + '(' + oidStr + ', ' + subtype + (defval or '') + ')' + label
        outStr += units or ''
        outStr += maxaccess or ''
        outStr += indexStr or ''
        outStr += '\n'

        if self.genRules['text'] and reference:
            outStr += self.ifTextStr + name + reference + '\n'

        if augmention:
            augmention = self.transOpers(augmention)
            outStr += augmention + '.registerAugmentions(("' + self._importMap.get(name, self.moduleName[0]) + '", "' + name + '"))\n'
            outStr += name + '.setIndexNames(*' + augmention + '.getIndexNames())\n'

        if status:
            outStr += self.ifTextStr + name + status + '\n'

        if self.genRules['text'] and description:
            outStr += self.ifTextStr + name + description + '\n'

        self.regSym(name, outStr, parentOid)

        if fakeSyms:  # fake symbols for INDEX to support SMIv1
            for idx, fakeSym in enumerate(fakeSyms):
                fakeOutStr = fakeStrlist[idx] % oidStr
                self.regSym(fakeSym, fakeOutStr, oidStr)

        return outStr

    # noinspection PyUnusedLocal
    def genTrapType(self, data, classmode=False):
        name, enterprise, objects, description, reference, value = data

        label = self.genLabel(name)
        name = self.transOpers(name)

        enterpriseStr, parentOid = enterprise

        outStr = name + ' = NotificationType(' + enterpriseStr + ' + (0,' + str(value) + '))' + label

        if objects:
            objects = ['("' + self._importMap.get(obj, self.moduleName[0]) + '", "' + self.transOpers(obj) + '")'
                       for obj in objects]

            numFuncCalls = len(objects) // 255 + 1

            if numFuncCalls > 1:
                objStrParts = []

                for idx in range(numFuncCalls):
                    objStrParts.append('[' + ', '.join(objects[255 * idx:255 * (idx + 1)]) + ']')

                outStr += """
for _%(name)s_obj in [%(objects)s]:
    if getattr(mibBuilder, 'version', 0) < (4, 4, 2):
        # WARNING: leading objects get lost here! Upgrade your pysnmp version!
        %(name)s = %(name)s.setObjects(*_%(name)s_obj)
    else:
        %(name)s = %(name)s.setObjects(*_%(name)s_obj, **dict(append=True))\
""" % dict(name=name, objects=', '.join(objStrParts))

            else:
                outStr += '.setObjects(' + ', '.join(objects) + ')'

        outStr += '\n'

        if self.genRules['text'] and description:
            outStr += self.ifTextStr + name + description + '\n'

        if self.genRules['text'] and reference:
            outStr += self.ifTextStr + name + reference + '\n'

        self.regSym(name, outStr, enterpriseStr)

        return outStr

    # noinspection PyUnusedLocal
    def genTypeDeclaration(self, data, classmode=False):
        outStr = ''

        name, declaration = data

        if declaration:
            parentType, attrs = declaration
            if parentType:  # skipping SEQUENCE case
                name = self.transOpers(name)
                outStr = 'class ' + name + '(' + parentType + '):\n' + attrs + '\n'
                self.regSym(name, outStr)

        return outStr

    # noinspection PyUnusedLocal
    def genValueDeclaration(self, data, classmode=False):
        name, oid = data

        label = self.genLabel(name)
        name = self.transOpers(name)

        oidStr, parentOid = oid
        outStr = name + ' = MibIdentifier(' + oidStr + ')' + label + '\n'

        self.regSym(name, outStr, oidStr)

        return outStr

    # Subparts generation functions

    # noinspection PyMethodMayBeStatic,PyUnusedLocal
    def ftNames(self, data, classmode=False):
        names = data[0]
        return names

    def genBitNames(self, data, classmode=False):
        names = data[0]
        return names

    def genBits(self, data, classmode=False):
        bits = data[0]

        namedval = ['("' + bit[0] + '", ' + str(bit[1]) + ')' for bit in bits]

        numFuncCalls = len(namedval) // 255 + 1

        funcCalls = ''
        for idx in range(numFuncCalls):
            funcCalls += 'NamedValues(' + ', '.join(namedval[255 * idx:255 * (idx + 1)]) + ') + '

        funcCalls = funcCalls[:-3]

        outStr = classmode and self.indent + 'namedValues = ' + funcCalls + '\n' or '.clone(namedValues=' + funcCalls + ')'

        return 'Bits', outStr

    # noinspection PyUnusedLocal
    def genCompliances(self, data, classmode=False):
        if not data[0]:
            return ''

        objects = []

        for complianceModule in data[0]:
            name = complianceModule[0] or self.moduleName[0]
            objects += ['("' + name + '", "' + self.transOpers(compl) + '")' for compl in complianceModule[1]]

        outStr = ''

        numFuncCalls = len(objects) // 255 + 1

        if numFuncCalls > 1:
            objStrParts = []

            for idx in range(numFuncCalls):
                objStrParts.append('[' + ', '.join(objects[255 * idx:255 * (idx + 1)]) + ']')

            outStr += """
for _%(name)s_obj in [%(objects)s]:
    if getattr(mibBuilder, 'version', 0) < (4, 4, 2):
        # WARNING: leading objects get lost here! Upgrade your pysnmp version!
        %(name)s = %(name)s.setObjects(*_%(name)s_obj)
    else:
        %(name)s = %(name)s.setObjects(*_%(name)s_obj, **dict(append=True))

""" % dict(name=name, objects=', '.join(objStrParts))

        else:
            outStr += '.setObjects(' + ', '.join(objects) + ')\n'

        return outStr

    # noinspection PyUnusedLocal
    def genConceptualTable(self, data, classmode=False):
        row = data[0]
        if row[1] and row[1][-2:] == '()':
            row = row[1][:-2]
            self._rows.add(row)

        return 'MibTable', ''

    # noinspection PyMethodMayBeStatic,PyUnusedLocal
    def genContactInfo(self, data, classmode=False):
        text = self.textFilter('contact-info', data[0])
        return '.setContactInfo(' + dorepr(text) + ')'

    # noinspection PyUnusedLocal
    def genDisplayHint(self, data, classmode=False):
        return self.indent + 'displayHint = ' + dorepr(data[0]) + '\n'

    # noinspection PyUnusedLocal
    def genDefVal(self, data, classmode=False, objname=None):
        if not data:
            return ''

        if not objname:
            return data

        defval = data[0]
        defvalType = self.getBaseType(objname, self.moduleName[0])

        if isinstance(defval, (int, long)):  # number
            val = str(defval)

        elif self.isHex(defval):  # hex
            if defvalType[0][0] in ('Integer32', 'Integer'):  # common bug in MIBs
                val = str(int(defval[1:-2], 16))
            else:
                val = 'hexValue="' + defval[1:-2] + '"'

        elif self.isBinary(defval):  # binary
            binval = defval[1:-2]
            if defvalType[0][0] in ('Integer32', 'Integer'):  # common bug in MIBs
                val = str(int(binval or '0', 2))
            else:
                hexval = binval and hex(int(binval, 2))[2:] or ''
                val = 'hexValue="' + hexval + '"'

        elif defval[0] == defval[-1] and defval[0] == '"':  # quoted string
            if defval[1:-1] == '' and defvalType != 'OctetString':  # common bug
                # a warning should be here
                return False  # we will set no default value

            val = dorepr(defval[1:-1])

        else:  # symbol (oid as defval) or name for enumeration member
            if (defvalType[0][0] == 'ObjectIdentifier' and
                    (defval in self.symbolTable[self.moduleName[0]] or defval in self._importMap)):  # oid
                module = self._importMap.get(defval, self.moduleName[0])

                try:
                    val = str(self.genNumericOid(self.symbolTable[module][defval]['oid']))
                except:
                    # or no module if it will be borrowed later
                    raise error.PySmiSemanticError('no symbol "%s" in module "%s"' % (defval, module))

            # enumeration
            elif defvalType[0][0] in ('Integer32', 'Integer') and isinstance(defvalType[1], list):
                if isinstance(defval, list):  # buggy MIB: DEFVAL { { ... } }
                    defval = [dv for dv in defval if dv in dict(defvalType[1])]
                    val = defval and dorepr(defval[0]) or ''
                elif defval in dict(defvalType[1]):  # good MIB: DEFVAL { ... }
                    val = dorepr(defval)
                else:
                    val = ''

            elif defvalType[0][0] == 'Bits':
                defvalBits = []
                bits = dict(defvalType[1])

                for bit in defval:
                    bitValue = bits.get(bit, None)
                    if bitValue is not None:
                        defvalBits.append((bit, bitValue))
                    else:
                        raise error.PySmiSemanticError('no such bit as "%s" for symbol "%s"' % (bit, objname))

                return self.genBits([defvalBits])[1]

            else:
                raise error.PySmiSemanticError(
                    'unknown type "%s" for defval "%s" of symbol "%s"' % (defvalType, defval, objname))

        return '.clone(' + val + ')'

    # noinspection PyMethodMayBeStatic,PyUnusedLocal
    def genDescription(self, data, classmode=False):
        text = self.textFilter('description', data[0])
        return classmode and self.indent + 'description = ' + dorepr(text) + '\n' or '.setDescription(' + dorepr(text) + ')'

    # noinspection PyMethodMayBeStatic
    def genReference(self, data, classmode=False):
        text = self.textFilter('reference', data[0])
        return classmode and self.indent + 'reference = ' + dorepr(text) + '\n' or '.setReference(' + dorepr(text) + ')'

    # noinspection PyMethodMayBeStatic
    def genStatus(self, data, classmode=False):
        text = data[0]
        return classmode and self.indent + 'status = ' + dorepr(text) + '\n' or '.setStatus(' + dorepr(text) + ')'

    # noinspection PyMethodMayBeStatic
    def genProductRelease(self, data, classmode=False):
        text = data[0]
        return classmode and self.indent + 'productRelease = ' + dorepr(text) + '\n' or '.setProductRelease(' + dorepr(text) + ')'

    def genEnumSpec(self, data, classmode=False):
        items = data[0]
        singleval = [str(item[1]) for item in items]
        outStr = classmode and self.indent + 'subtypeSpec = %s.subtypeSpec + ' or '.subtype(subtypeSpec='
        numFuncCalls = len(singleval) / 255 + 1
        singleCall = numFuncCalls == 1
        funcCalls = ''

        outStr += not singleCall and 'ConstraintsUnion(' or ''

        for idx in range(int(numFuncCalls)):
            if funcCalls:
                funcCalls += ', '
            funcCalls += 'SingleValueConstraint(' + ', '.join(singleval[255 * idx:255 * (idx + 1)]) + ')'

        outStr += funcCalls
        outStr += not singleCall and (classmode and ')\n' or '))') or (not classmode and ')' or '\n')
        outStr += self.genBits(data, classmode=classmode)[1]

        return outStr

    # noinspection PyUnusedLocal
    def genTableIndex(self, data, classmode=False):
        def genFakeSyms(fakeidx, idxType):
            fakeSymName = 'pysmiFakeCol%s' % fakeidx

            objType = self.typeClasses.get(idxType, idxType)
            objType = self.transOpers(objType)

            return (fakeSymName + ' = MibTableColumn(%s + (' + str(fakeidx) +
                    ', ), ' + objType + '())\n',  # stub for parentOid
                    fakeSymName)

        indexes = data[0]
        idxStrlist, fakeSyms, fakeStrlist = [], [], []
        for idx in indexes:
            idxName = idx[1]
            if idxName in self.smiv1IdxTypes:  # SMIv1 support
                idxType = idxName

                fakeSymStr, idxName = genFakeSyms(self.fakeidx, idxType)
                fakeStrlist.append(fakeSymStr)
                fakeSyms.append(idxName)
                self.fakeidx += 1

            idxStrlist.append('(' + str(idx[0]) + ', "' +
                              self._importMap.get(idxName, self.moduleName[0]) +
                              '", "' + idxName + '")')

        return '.setIndexNames(' + ', '.join(idxStrlist) + ')', fakeStrlist, fakeSyms

    def genIntegerSubType(self, data, classmode=False):
        singleRange = len(data[0]) == 1

        outStr = classmode and self.indent + 'subtypeSpec = %s.subtypeSpec + ' or '.subtype(subtypeSpec='
        outStr += not singleRange and 'ConstraintsUnion(' or ''

        for rng in data[0]:
            vmin, vmax = len(rng) == 1 and (rng[0], rng[0]) or rng
            vmin, vmax = str(self.str2int(vmin)), str(self.str2int(vmax))
            outStr += 'ValueRangeConstraint(' + vmin + ', ' + vmax + ')' + (not singleRange and ', ' or '')

        outStr += not singleRange and (classmode and ')' or '))') or (not classmode and ')' or '\n')

        return outStr

    # noinspection PyMethodMayBeStatic,PyUnusedLocal
    def genMaxAccess(self, data, classmode=False):
        access = data[0].replace('-', '')
        return access != 'notaccessible' and '.setMaxAccess("' + access + '")' or ''

    def genOctetStringSubType(self, data, classmode=False):
        singleRange = len(data[0]) == 1

        outStr = classmode and self.indent + 'subtypeSpec = %s.subtypeSpec + ' or '.subtype(subtypeSpec='
        outStr += not singleRange and 'ConstraintsUnion(' or ''

        for rng in data[0]:
            vmin, vmax = len(rng) == 1 and (rng[0], rng[0]) or rng
            vmin, vmax = str(self.str2int(vmin)), str(self.str2int(vmax))
            outStr += ('ValueSizeConstraint(' + vmin + ', ' + vmax + ')' +
                       (not singleRange and ', ' or ''))

        outStr += not singleRange and (classmode and ')' or '))') or (not classmode and ')' or '\n')

        if data[0]:
            # noinspection PyUnboundLocalVariable
            outStr += (singleRange
                       and vmin == vmax
                       and (classmode and self.indent + 'fixedLength = ' + vmin + '\n' or '.setFixedLength(' + vmin + ')') or '')

        return outStr

    # noinspection PyUnusedLocal
    def genOid(self, data, classmode=False):
        out = ()
        parent = ''
        for el in data[0]:
            if isinstance(el, (str, unicode)):
                parent = self.transOpers(el)
                out += ((parent, self._importMap.get(parent, self.moduleName[0])),)

            elif isinstance(el, (int, long)):
                out += (el,)

            elif isinstance(el, tuple):
                out += (el[1],)  # XXX Do we need to create a new object el[0]?

            else:
                raise error.PySmiSemanticError('unknown datatype for OID: %s' % el)

        return str(self.genNumericOid(out)), parent

    # noinspection PyUnusedLocal
    def genObjects(self, data, classmode=False):
        if data[0]:
            return [self.transOpers(obj) for obj in data[0]]  # XXX self.transOpers or not??
        return []

    # noinspection PyMethodMayBeStatic,PyUnusedLocal
    def genTime(self, data, classmode=False):
        times = []
        for timeStr in data:
            if len(timeStr) == 11:
                timeStr = '19' + timeStr
            # XXX raise in strict mode
            # elif lenTimeStr != 13:
            #  raise error.PySmiSemanticError("Invalid date %s" % t)
            try:
                times.append(strftime('%Y-%m-%d %H:%M', strptime(timeStr, '%Y%m%d%H%MZ')))

            except ValueError:
                # XXX raise in strict mode
                # raise error.PySmiSemanticError("Invalid date %s: %s" % (t, sys.exc_info()[1]))
                timeStr = '197001010000Z'  # dummy date for dates with typos
                times.append(strftime('%Y-%m-%d %H:%M', strptime(timeStr, '%Y%m%d%H%MZ')))

        return times

    # noinspection PyMethodMayBeStatic,PyUnusedLocal
    def genLastUpdated(self, data, classmode=False):
        text = data[0]
        return '.setLastUpdated(' + dorepr(text) + ')'

    # noinspection PyMethodMayBeStatic,PyUnusedLocal
    def genOrganization(self, data, classmode=False):
        text = self.textFilter('organization', data[0])
        return '.setOrganization(' + dorepr(text) + ')'

    # noinspection PyUnusedLocal
    def genRevisions(self, data, classmode=False):
        times = self.genTime([x[0] for x in data[0]])
        times = [dorepr(x) for x in times]

        revisions = '.setRevisions((%s,))' % ', '.join(times)

        descriptions = '.setRevisionsDescriptions((%s,))' % ', '.join(
            [dorepr(self.textFilter('description', x[1][1])) for x in data[0]]
        )

        lastRevision = data[0][0][0]

        return lastRevision, revisions, descriptions

    def genRow(self, data, classmode=False):
        row = data[0]
        row = self.transOpers(row)
        return row in self.symbolTable[self.moduleName[0]]['_symtable_rows'] and (
             'MibTableRow', '') or self.genSimpleSyntax(data, classmode=classmode)

    # noinspection PyUnusedLocal
    def genSequence(self, data, classmode=False):
        cols = data[0]
        self._cols.update(cols)
        return '', ''

    def genSimpleSyntax(self, data, classmode=False):
        objType = data[0]
        objType = self.typeClasses.get(objType, objType)
        objType = self.transOpers(objType)

        subtype = len(data) == 2 and data[1] or ''

        if classmode:
            subtype = '%s' in subtype and subtype % objType or subtype  # XXX hack?
            return objType, subtype

        outStr = objType + '()' + subtype

        return 'MibScalar', outStr

    # noinspection PyUnusedLocal
    def genTypeDeclarationRHS(self, data, classmode=False):
        if len(data) == 1:
            parentType, attrs = data[0]  # just syntax

        else:
            # Textual convention
            display, status, description, reference, syntax = data
            parentType, attrs = syntax

            if parentType in self._snmpTypes:
                parentType = 'TextualConvention, ' + parentType

            if display:
                attrs = display + attrs

            if status:
                attrs = status + attrs

            if self.genRules['text'] and description:
                attrs = description + attrs

            if reference:
                attrs = reference + attrs

        attrs = attrs or self.indent + 'pass\n'

        return parentType, attrs

    # noinspection PyMethodMayBeStatic,PyUnusedLocal
    def genUnits(self, data, classmode=False):
        text = data[0]
        return '.setUnits(' + dorepr(self.textFilter('units', text)) + ')'

    handlersTable = {
        'agentCapabilitiesClause': genAgentCapabilities,
        'moduleIdentityClause': genModuleIdentity,
        'moduleComplianceClause': genModuleCompliance,
        'notificationGroupClause': genNotificationGroup,
        'notificationTypeClause': genNotificationType,
        'objectGroupClause': genObjectGroup,
        'objectIdentityClause': genObjectIdentity,
        'objectTypeClause': genObjectType,
        'trapTypeClause': genTrapType,
        'typeDeclaration': genTypeDeclaration,
        'valueDeclaration': genValueDeclaration,

        'ApplicationSyntax': genSimpleSyntax,
        'BitNames': genBitNames,
        'BITS': genBits,
        'ComplianceModules': genCompliances,
        'conceptualTable': genConceptualTable,
        'CONTACT-INFO': genContactInfo,
        'DISPLAY-HINT': genDisplayHint,
        'DEFVAL': genDefVal,
        'DESCRIPTION': genDescription,
        'REFERENCE': genReference,
        'Status': genStatus,
        'PRODUCT-RELEASE': genProductRelease,
        'enumSpec': genEnumSpec,
        'INDEX': genTableIndex,
        'integerSubType': genIntegerSubType,
        'MaxAccessPart': genMaxAccess,
        'Notifications': genObjects,
        'octetStringSubType': genOctetStringSubType,
        'objectIdentifier': genOid,
        'Objects': genObjects,
        'LAST-UPDATED': genLastUpdated,
        'ORGANIZATION': genOrganization,
        'Revisions': genRevisions,
        'row': genRow,
        'SEQUENCE': genSequence,
        'SimpleSyntax': genSimpleSyntax,
        'typeDeclarationRHS': genTypeDeclarationRHS,
        'UNITS': genUnits,
        'VarTypes': genObjects,
        # 'a': lambda x: genXXX(x, 'CONSTRAINT')
    }

    def genCode(self, ast, symbolTable, **kwargs):
        self.genRules['text'] = kwargs.get('genTexts', False)
        self.textFilter = kwargs.get('textFilter') or (lambda symbol, text: re.sub('\s+', ' ', text))
        self.symbolTable = symbolTable
        self._rows.clear()
        self._cols.clear()
        self._exports.clear()
        self._seenSyms.clear()
        self._importMap.clear()
        self._out.clear()
        self._moduleIdentityOid = None
        self.moduleName[0], moduleOid, imports, declarations = ast

        out, importedModules = self.genImports(imports or {})

        for declr in declarations or []:
            if declr:
                clausetype = declr[0]
                classmode = clausetype == 'typeDeclaration'
                self.handlersTable[declr[0]](self, self.prepData(declr[1:], classmode), classmode)

        for sym in self.symbolTable[self.moduleName[0]]['_symtable_order']:
            if sym not in self._out:
                raise error.PySmiCodegenError('No generated code for symbol %s' % sym)
            out += self._out[sym]

        out += self.genExports()

        if 'comments' in kwargs:
            out = ''.join(['# %s\n' % x for x in kwargs['comments']]) + '#\n' + out
            out = '#\n# PySNMP MIB module %s (http://snmplabs.com/pysmi)\n' % self.moduleName[0] + out

        debug.logger & debug.flagCodegen and debug.logger(
            'canonical MIB name %s (%s), imported MIB(s) %s, Python code size %s bytes' % (
                self.moduleName[0], moduleOid, ','.join(importedModules) or '<none>', len(out)))

        return MibInfo(oid=moduleOid,
                       identity=self._moduleIdentityOid,
                       name=self.moduleName[0],
                       revision=self._moduleRevision,
                       oids=[],
                       enterprise=None,
                       compliance=[],
                       imported=tuple([x for x in importedModules if x not in self.fakeMibs])), out

    def genIndex(self, processed, **kwargs):
        out = '\nfrom pysnmp.proto.rfc1902 import ObjectName\n\noidToMibMap = {\n'
        count = 0
        for module, status in processed.items():
            value = getattr(status, 'oid', None)
            if value:
                out += 'ObjectName("%s"): "%s",\n' % (value, module)
                count += 1
        out += '}\n'

        if 'comments' in kwargs:
            out = ''.join(['# %s\n' % x for x in kwargs['comments']]) + '#\n' + out
            out = '#\n# PySNMP MIB indices (http://snmplabs.com/pysmi)\n' + out

        debug.logger & debug.flagCodegen and debug.logger(
            'OID->MIB index built, %s entries, %s bytes' % (count, len(out)))

        return out

# backward compatibility
baseMibs = PySnmpCodeGen.baseMibs
fakeMibs = PySnmpCodeGen.fakeMibs
