# Copyright (c) Barefoot Networks, Inc.
# Licensed under the Apache License, Version 2.0 (the "License")

from p4_hlir.hlir import p4_match_type, p4_field, p4_table, p4_header_instance
from programSerializer import ProgramSerializer
from compilationException import *
import ebpfProgram
import ebpfInstance
import ebpfCounter
import ebpfStructType
import ebpfAction


class EbpfTableKeyField(object):
    def __init__(self, fieldname, instance, field, mask):
        assert isinstance(instance, ebpfInstance.EbpfInstanceBase)
        assert isinstance(field, ebpfStructType.EbpfField)

        self.keyFieldName = fieldname
        self.instance = instance
        self.field = field
        self.mask = mask

    def serializeType(self, serializer):
        assert isinstance(serializer, ProgramSerializer)
        ftype = self.field.type
        serializer.emitIndent()
        ftype.declare(serializer, self.keyFieldName, False)
        serializer.endOfStatement(True)

    def serializeConstruction(self, keyName, serializer, program):
        assert isinstance(serializer, ProgramSerializer)
        assert isinstance(keyName, str)
        assert isinstance(program, ebpfProgram.EbpfProgram)

        if self.mask is not None:
            maskExpression = " & {0}".format(self.mask)
        else:
            maskExpression = ""

        if isinstance(self.instance, ebpfInstance.EbpfMetadata):
            base = program.metadataStructName
        else:
            base = program.headerStructName

        if isinstance(self.instance, ebpfInstance.SimpleInstance):
            source = "{0}.{1}.{2}".format(
                base, self.instance.name, self.field.name)
        else:
            assert isinstance(self.instance, ebpfInstance.EbpfHeaderStack)
            source = "{0}.{1}[{2}].{3}".format(
                base, self.instance.name,
                self.instance.hlirInstance.index, self.field.name)
        destination = "{0}.{1}".format(keyName, self.keyFieldName)
        size = self.field.widthInBits()

        serializer.emitIndent()
        if size <= 32:
            serializer.appendFormat("{0} = ({1}){2};",
                                    destination, source, maskExpression)
        else:
            if maskExpression != "":
                raise NotSupportedException(
                    "{0} Mask wider than 32 bits", self.field.hlirType)
            serializer.appendFormat(
                "memcpy(&{0}, &{1}, {2});", destination, source, size / 8)

        serializer.newline()


class EbpfTableKey(object):
    def __init__(self, match_fields, program):
        assert isinstance(program, ebpfProgram.EbpfProgram)

        self.expressions = []
        self.fields = []
        self.masks = []
        self.fieldNamePrefix = "key_field_"
        self.program = program

        fieldNumber = 0
        for f in match_fields:
            field = f[0]
            matchType = f[1]
            mask = f[2]

            if ((matchType is p4_match_type.P4_MATCH_TERNARY) or
                (matchType is p4_match_type.P4_MATCH_LPM) or
                (matchType is p4_match_type.P4_MATCH_RANGE)):
                raise NotSupportedException(
                    False, "Match type {0}", matchType)

            if matchType is p4_match_type.P4_MATCH_VALID:
                # we should be really checking the valid field;
                # p4_field is a header instance
                assert isinstance(field, p4_header_instance)
                instance = field
                fieldname = "valid"
            else:
                assert isinstance(field, p4_field)
                instance = field.instance
                fieldname = field.name

            if ebpfProgram.EbpfProgram.isArrayElementInstance(instance):
                ebpfStack = program.getStackInstance(instance.base_name)
                assert isinstance(ebpfStack, ebpfInstance.EbpfHeaderStack)
                basetype = ebpfStack.basetype
                eInstance = program.getStackInstance(instance.base_name)
            else:
                ebpfHeader = program.getInstance(instance.name)
                assert isinstance(ebpfHeader, ebpfInstance.SimpleInstance)
                basetype = ebpfHeader.type
                eInstance = program.getInstance(instance.name)

            ebpfField = basetype.getField(fieldname)
            assert isinstance(ebpfField, ebpfStructType.EbpfField)

            fieldName = self.fieldNamePrefix + str(fieldNumber)
            fieldNumber += 1
            keyField = EbpfTableKeyField(fieldName, eInstance, ebpfField, mask)

            self.fields.append(keyField)
            self.masks.append(mask)

    @staticmethod
    def fieldRank(field):
        assert isinstance(field, EbpfTableKeyField)
        return field.field.type.alignment()

    def serializeType(self, serializer, keyTypeName):
        assert isinstance(serializer, ProgramSerializer)
        serializer.emitIndent()
        serializer.appendFormat("struct {0} ", keyTypeName)
        serializer.blockStart()

        # Sort fields in decreasing size; this will ensure that
        # there is no padding.
        # Padding may cause the ebpf verification to fail,
        # since padding fields are not initialized
        fieldOrder = sorted(
            self.fields, key=EbpfTableKey.fieldRank, reverse=True)
        for f in fieldOrder:
            assert isinstance(f, EbpfTableKeyField)
            f.serializeType(serializer)

        serializer.blockEnd(False)
        serializer.endOfStatement(True)

    def serializeConstruction(self, serializer, keyName, program):
        serializer.emitIndent()
        serializer.appendLine("/* construct key */")

        for f in self.fields:
            f.serializeConstruction(keyName, serializer, program)


class EbpfTable(object):
    # noinspection PyUnresolvedReferences
    def __init__(self, hlirtable, program, config):
        assert isinstance(hlirtable, p4_table)
        assert isinstance(program, ebpfProgram.EbpfProgram)

        self.name = hlirtable.name
        self.hlirtable = hlirtable
        self.config = config

        self.defaultActionMapName = (program.reservedPrefix +
                                     self.name + "_miss")
        self.key = EbpfTableKey(hlirtable.match_fields, program)
        self.size = hlirtable.max_size
        if self.size is None:
            program.emitWarning(
                "{0} does not specify a max_size; using 1024", hlirtable)
            self.size = 1024
        self.isHash = True  # TODO: try to guess arrays when possible
        self.dataMapName = self.name
        self.actionEnumName = program.generateNewName(self.name + "_actions")
        self.keyTypeName = program.generateNewName(self.name + "_key")
        self.valueTypeName = program.generateNewName(self.name + "_value")
        self.actions = []

        if hlirtable.action_profile is not None:
            raise NotSupportedException("{0}: action_profile tables",
                                        hlirtable)
        if hlirtable.support_timeout:
            program.emitWarning("{0}: table timeout {1}; ignoring",
                                hlirtable, NotSupportedException.archError)

        self.counters = []
        if (hlirtable.attached_counters is not None):
            for c in hlirtable.attached_counters:
                ctr = program.getCounter(c.name)
                assert isinstance(ctr, ebpfCounter.EbpfCounter)
                self.counters.append(ctr)

        if (len(hlirtable.attached_meters) > 0 or
            len(hlirtable.attached_registers) > 0):
            program.emitWarning("{0}: meters/registers {1}; ignored",
                                hlirtable, NotSupportedException.archError)

        for a in hlirtable.actions:
            action = program.getAction(a)
            self.actions.append(action)

    def serializeKeyType(self, serializer):
        assert isinstance(serializer, ProgramSerializer)
        self.key.serializeType(serializer, self.keyTypeName)

    def serializeActionArguments(self, serializer, action):
        assert isinstance(serializer, ProgramSerializer)
        assert isinstance(action, ebpfAction.EbpfActionBase)
        action.serializeArgumentsAsStruct(serializer)

    def serializeValueType(self, serializer):
        assert isinstance(serializer, ProgramSerializer)
        #  create an enum with tags for all actions
        serializer.emitIndent()
        serializer.appendFormat("enum {0} ", self.actionEnumName)
        serializer.blockStart()

        for a in self.actions:
            name = a.name
            serializer.emitIndent()
            serializer.appendFormat("{0}_{1},", self.name, name)
            serializer.newline()

        serializer.blockEnd(False)
        serializer.endOfStatement(True)

        # a type-safe union: a struct with a tag and an union
        serializer.emitIndent()
        serializer.appendFormat("struct {0} ", self.valueTypeName)
        serializer.blockStart()

        serializer.emitIndent()
        #serializer.appendFormat("enum {0} action;", self.actionEnumName)
        # teporary workaround bcc bug
        serializer.appendFormat("{0}32 action;",
                                self.config.uprefix)
        serializer.newline()

        serializer.emitIndent()
        serializer.append("union ")
        serializer.blockStart()

        for a in self.actions:
            self.serializeActionArguments(serializer, a)

        serializer.blockEnd(False)
        serializer.space()
        serializer.appendLine("u;")
        serializer.blockEnd(False)
        serializer.endOfStatement(True)

    def serialize(self, serializer, program):
        assert isinstance(serializer, ProgramSerializer)
        assert isinstance(program, ebpfProgram.EbpfProgram)

        self.serializeKeyType(serializer)
        self.serializeValueType(serializer)

        self.config.serializeTableDeclaration(
            serializer, self.dataMapName, self.isHash,
            "struct " + self.keyTypeName,
            "struct " + self.valueTypeName, self.size)
        self.config.serializeTableDeclaration(
            serializer, self.defaultActionMapName, False,
            program.arrayIndexType, "struct " + self.valueTypeName, 1)

    def serializeCode(self, serializer, program, nextNode):
        assert isinstance(serializer, ProgramSerializer)
        assert isinstance(program, ebpfProgram.EbpfProgram)

        hitVarName = program.reservedPrefix + "hit"
        keyname = "key"
        valueName = "value"

        serializer.newline()
        serializer.emitIndent()
        serializer.appendFormat("{0}:", program.getLabel(self))
        serializer.newline()

        serializer.emitIndent()
        serializer.blockStart()

        serializer.emitIndent()
        serializer.appendFormat("{0}8 {1};", program.config.uprefix, hitVarName)
        serializer.newline()

        serializer.emitIndent()
        serializer.appendFormat("struct {0} {1} = {{}};", self.keyTypeName, keyname)
        serializer.newline()

        serializer.emitIndent()
        serializer.appendFormat(
            "struct {0} *{1};", self.valueTypeName, valueName)
        serializer.newline()

        self.key.serializeConstruction(serializer, keyname, program)

        serializer.emitIndent()
        serializer.appendFormat("{0} = 1;", hitVarName)
        serializer.newline()

        serializer.emitIndent()
        serializer.appendLine("/* perform lookup */")
        serializer.emitIndent()
        program.config.serializeLookup(
            serializer, self.dataMapName, keyname, valueName)
        serializer.newline()

        serializer.emitIndent()
        serializer.appendFormat("if ({0} == NULL) ", valueName)
        serializer.blockStart()

        serializer.emitIndent()
        serializer.appendFormat("{0} = 0;", hitVarName)
        serializer.newline()

        serializer.emitIndent()
        serializer.appendLine("/* miss; find default action */")
        serializer.emitIndent()
        program.config.serializeLookup(
            serializer, self.defaultActionMapName,
            program.zeroKeyName, valueName)
        serializer.newline()
        serializer.blockEnd(True)

        if len(self.counters) > 0:
            serializer.emitIndent()
            serializer.append("else ")
            serializer.blockStart()
            for c in self.counters:
                assert isinstance(c, ebpfCounter.EbpfCounter)
                if c.autoIncrement:
                    serializer.emitIndent()
                    serializer.blockStart()
                    c.serializeCode(keyname, serializer, program)
                    serializer.blockEnd(True)
            serializer.blockEnd(True)

        serializer.emitIndent()
        serializer.appendFormat("if ({0} != NULL) ", valueName)
        serializer.blockStart()
        serializer.emitIndent()
        serializer.appendLine("/* run action */")
        self.runAction(serializer, self.name, valueName, program, nextNode)

        nextNode = self.hlirtable.next_
        if "hit" in nextNode:
            node = nextNode["hit"]
            if node is None:
                node = nextNode
            label = program.getLabel(node)
            serializer.emitIndent()
            serializer.appendFormat("if (hit) goto {0};", label)
            serializer.newline()

            node = nextNode["miss"]
            if node is None:
                node = nextNode
            label = program.getLabel(node)
            serializer.emitIndent()
            serializer.appendFormat("else goto {0};", label)
            serializer.newline()

        serializer.blockEnd(True)
        if not "hit" in nextNode:
            # Catch-all
            serializer.emitIndent()
            serializer.appendFormat("goto end;")
            serializer.newline()

        serializer.blockEnd(True)

    def runAction(self, serializer, tableName, valueName, program, nextNode):
        serializer.emitIndent()
        serializer.appendFormat("switch ({0}->action) ", valueName)
        serializer.blockStart()

        for a in self.actions:
            assert isinstance(a, ebpfAction.EbpfActionBase)

            serializer.emitIndent()
            serializer.appendFormat("case {0}_{1}: ", tableName, a.name)
            serializer.newline()
            serializer.emitIndent()
            serializer.blockStart()
            a.serializeBody(serializer, valueName, program)
            serializer.blockEnd(True)
            serializer.emitIndent()

            nextNodes = self.hlirtable.next_
            if a.hliraction in nextNodes:
                node = nextNodes[a.hliraction]
                if node is None:
                    node = nextNode
                label = program.getLabel(node)
                serializer.appendFormat("goto {0};", label)
            else:
                serializer.appendFormat("break;")
            serializer.newline()

        serializer.blockEnd(True)
