#
# This file is part of pysmi software.
#
# Copyright (c) 2015-2020, Ilya Etingof <etingof@gmail.com>
# License: https://www.pysnmp.com/pysmi/license.html
#
# Build an internally used symbol table for each passed MIB.
#
import sys
from keyword import iskeyword
from pysmi.mibinfo import MibInfo
from pysmi.codegen.base import AbstractCodeGen, dorepr
from pysmi import config, error
from pysmi import debug


class SymtableCodeGen(AbstractCodeGen):
    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 = {
        "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
    }

    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._rows = set()
        self._cols = {}  # k, v = name, datatype
        self._exports = set()
        self._postponedSyms = {}  # k, v = symbol, (parents, properties)
        self._parentOids = set()
        self._importMap = {}  # k, v = symbol, MIB
        self._symsOrder = []
        self._out = {}  # k, v = symbol, properties
        self.moduleName = ["DUMMY"]
        self._moduleRevision = None
        self.genRules = {"text": True}

    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):
        # convertion 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._importMap.update([(self.transOpers(s), module) for s in symbols])

        return {}, tuple(sorted(imports))

    def allParentsExists(self, parents):
        parentsExists = True
        for parent in parents:
            if not (
                parent in self._out
                or parent in self._importMap
                or parent in self.baseTypes
                or parent in ("MibTable", "MibTableRow", "MibTableColumn")
                or parent in self._rows
            ):
                parentsExists = False
                break

        return parentsExists

    def regSym(self, symbol, symProps, parents=()):
        if (
            symbol in self._out
            or symbol in self._postponedSyms
            or (config.STRICT_MODE and symbol in self._importMap)
        ):
            raise error.PySmiSemanticError(f"Duplicate symbol found: {symbol}")

        if self.allParentsExists(parents):
            self._out[symbol] = symProps
            self._symsOrder.append(symbol)
            self.regPostponedSyms()

        else:
            self._postponedSyms[symbol] = (parents, symProps)

    def regPostponedSyms(self):
        regedSyms = []
        for sym, val in self._postponedSyms.items():
            parents, symProps = val

            if self.allParentsExists(parents):
                self._out[sym] = symProps
                self._symsOrder.append(sym)
                regedSyms.append(sym)

        for sym in regedSyms:
            self._postponedSyms.pop(sym)

        # Clause handlers

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

        pysmiName = self.transOpers(origName)

        symProps = {"type": "AgentCapabilities", "oid": oid, "origName": origName}

        self.regSym(pysmiName, symProps)

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

        pysmiName = self.transOpers(origName)

        symProps = {"type": "ModuleIdentity", "oid": oid, "origName": origName}

        if revisions:
            self._moduleRevision = revisions[0]

        self.regSym(pysmiName, symProps)

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

        pysmiName = self.transOpers(origName)

        symProps = {"type": "ModuleCompliance", "oid": oid, "origName": origName}

        self.regSym(pysmiName, symProps)

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

        pysmiName = self.transOpers(origName)

        symProps = {"type": "NotificationGroup", "oid": oid, "origName": origName}

        self.regSym(pysmiName, symProps)

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

        pysmiName = self.transOpers(origName)

        symProps = {"type": "NotificationType", "oid": oid, "origName": origName}

        self.regSym(pysmiName, symProps)

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

        pysmiName = self.transOpers(origName)

        symProps = {"type": "ObjectGroup", "oid": oid, "origName": origName}

        self.regSym(pysmiName, symProps)

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

        pysmiName = self.transOpers(origName)

        symProps = {"type": "ObjectIdentity", "oid": oid, "origName": origName}

        self.regSym(pysmiName, symProps)

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

        pysmiName = self.transOpers(origName)

        symProps = {
            "type": "ObjectType",
            "oid": oid,
            "syntax": syntax,  # (type, module), subtype
            "origName": origName,
        }

        parents = [syntax[0][0]]

        if augmention:
            parents.append(self.transOpers(augmention))

        if defval:  # XXX
            symProps["defval"] = defval

        if index and index[1]:
            namepart, fakeIndexes, fakeSymSyntax = index
            for fakeIdx, fakeSyntax in zip(fakeIndexes, fakeSymSyntax):
                fakeName = namepart + str(fakeIdx)

                fakeSymProps = {
                    "type": "fakeColumn",
                    "oid": oid + (fakeIdx,),
                    "syntax": fakeSyntax,
                    "origName": fakeName,
                }

                self.regSym(fakeName, fakeSymProps)

        self.regSym(pysmiName, symProps, parents)

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

        pysmiName = self.transOpers(origName)

        symProps = {
            "type": "NotificationType",
            "oid": enterprise + (0, value),
            "origName": origName,
        }

        self.regSym(pysmiName, symProps)

    # noinspection PyUnusedLocal
    def genTypeDeclaration(self, data, classmode=False):
        origName, declaration = data

        pysmiName = self.transOpers(origName)

        if declaration:
            parentType, attrs = declaration
            if parentType:  # skipping SEQUENCE case
                symProps = {
                    "type": "TypeDeclaration",
                    "syntax": declaration,  # (type, module), subtype
                    "origName": origName,
                }

                self.regSym(pysmiName, symProps, [declaration[0][0]])

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

        pysmiName = self.transOpers(origName)

        symProps = {"type": "MibIdentifier", "oid": oid, "origName": origName}

        self.regSym(pysmiName, symProps)

    # Subparts generation functions
    # noinspection PyUnusedLocal,PyMethodMayBeStatic
    def genBitNames(self, data, classmode=False):
        names = data[0]
        return names

    # noinspection PyUnusedLocal,PyMethodMayBeStatic
    def genBits(self, data, classmode=False):
        bits = data[0]
        return ("Bits", ""), bits

    # noinspection PyUnusedLocal,PyUnusedLocal,PyMethodMayBeStatic
    def genCompliances(self, data, classmode=False):
        return ""

    # noinspection PyUnusedLocal
    def genConceptualTable(self, data, classmode=False):
        row = data[0]
        if row[0] and row[0][0]:
            self._rows.add(self.transOpers(row[0][0]))
        return ("MibTable", ""), ""

    # noinspection PyUnusedLocal,PyUnusedLocal,PyMethodMayBeStatic
    def genContactInfo(self, data, classmode=False):
        return ""

    # noinspection PyUnusedLocal,PyUnusedLocal,PyMethodMayBeStatic
    def genDisplayHint(self, data, classmode=False):
        return ""

    # noinspection PyUnusedLocal
    def genDefVal(self, data, classmode=False):  # XXX should be fixed, see pysnmp.py
        defval = data[0]

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

        elif self.isHex(defval):  # hex
            val = 'hexValue="' + defval[1:-2] + '"'  # not working for Integer baseTypes

        elif self.isBinary(defval):  # binary
            binval = defval[1:-2]
            hexval = binval and hex(int(binval, 2))[2:] or ""
            val = 'hexValue="' + hexval + '"'

        elif isinstance(defval, list):  # bits list
            val = defval

        elif defval[0] == defval[-1] and defval[0] == '"':  # quoted strimg
            val = dorepr(defval[1:-1])

        else:  # symbol (oid as defval) or name for enumeration member
            if defval in self._out or defval in self._importMap:
                val = defval + ".getName()"
            else:
                val = dorepr(defval)

        return val

    # noinspection PyUnusedLocal,PyUnusedLocal,PyMethodMayBeStatic
    def genDescription(self, data, classmode=False):
        return ""

    def genReference(self, data, classmode=False):
        return ""

    def genStatus(self, data, classmode=False):
        return ""

    def genProductRelease(self, data, classmode=False):
        return ""

    def genEnumSpec(self, data, classmode=False):
        return self.genBits(data, classmode=classmode)[1]

    def genIndex(self, data, classmode=False):
        indexes = data[0]

        fakeIdxName = "pysmiFakeCol"
        fakeIndexes, fakeSymsSyntax = [], []

        for idx in indexes:
            idxName = idx[1]
            if idxName in self.smiv1IdxTypes:  # SMIv1 support
                idxType = idxName

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

                fakeIndexes.append(self.fakeidx)
                fakeSymsSyntax.append((("MibTableColumn", ""), objType))
                self.fakeidx += 1

        return fakeIdxName, fakeIndexes, fakeSymsSyntax

    # noinspection PyUnusedLocal,PyUnusedLocal,PyMethodMayBeStatic
    def genIntegerSubType(self, data, classmode=False):
        return ""

    # noinspection PyUnusedLocal,PyUnusedLocal,PyMethodMayBeStatic
    def genMaxAccess(self, data, classmode=False):
        return ""

    # noinspection PyUnusedLocal,PyUnusedLocal,PyMethodMayBeStatic
    def genOctetStringSubType(self, data, classmode=False):
        return ""

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

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

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

            else:
                raise error.PySmiSemanticError(f"unknown datatype for OID: {el}")

        return out

    # noinspection PyUnusedLocal,PyUnusedLocal,PyMethodMayBeStatic
    def genObjects(self, data, classmode=False):
        return ""

    # noinspection PyUnusedLocal,PyUnusedLocal,PyMethodMayBeStatic
    def genTime(self, data, classmode=False):
        return ""

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

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

    # noinspection PyUnusedLocal,PyUnusedLocal,PyMethodMayBeStatic
    def genRevisions(self, data, classmode=False):
        lastRevision, lastDescription = data[0][0][0], data[0][0][1][1]
        return lastRevision, lastDescription

    def genRow(self, data, classmode=False):
        row = data[0]
        row = self.transOpers(row)
        return (
            row in self._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 "", ""

    # noinspection PyUnusedLocal
    def genSimpleSyntax(self, data, classmode=False):
        objType = data[0]

        module = ""

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

        if objType not in self.baseTypes:
            module = self._importMap.get(objType, self.moduleName[0])

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

        return (objType, module), subtype

    # noinspection PyUnusedLocal,PyMethodMayBeStatic
    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

        return parentType, attrs

    # noinspection PyUnusedLocal,PyUnusedLocal,PyMethodMayBeStatic
    def genUnits(self, data, classmode=False):
        return ""

    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": genIndex,
        "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,
    }

    def genCode(self, ast, symbolTable, **kwargs):
        self.genRules["text"] = kwargs.get("genTexts", False)
        self._rows.clear()
        self._cols.clear()
        self._parentOids.clear()
        self._symsOrder = []
        self._postponedSyms.clear()
        self._importMap.clear()
        self._out = {}  # should be new object, do not use `clear` method
        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
                )

        if self._postponedSyms:
            raise error.PySmiSemanticError(
                f"Unknown parents for symbols: {', '.join(self._postponedSyms)}"
            )

        for sym in self._parentOids:
            if sym not in self._out and sym not in self._importMap:
                raise error.PySmiSemanticError(f"Unknown parent symbol: {sym}")

        self._out["_symtable_order"] = list(self._symsOrder)
        self._out["_symtable_cols"] = list(self._cols)
        self._out["_symtable_rows"] = list(self._rows)

        debug.logger & debug.flagCodegen and debug.logger(
            f"canonical MIB name {self.moduleName[0]} ({moduleOid}), imported MIB(s) {','.join(importedModules) or '<none>'}, Symbol table size {len(self._out)} symbols"
        )

        return (
            MibInfo(
                oid=None,
                name=self.moduleName[0],
                revision=self._moduleRevision,
                imported=tuple(x for x in importedModules),
            ),
            self._out,
        )
