#! /usr/bin/env python3

###############################################################################
#
# 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 argparse, grpc, os, sys, socket, logging, mib, re, linecache, shutil, inspect
from time import sleep

SDE = os.environ.get("SDE", "~/bf-sde-9.11.0")
SDE_INSTALL = os.environ.get("SDE_INSTALL", SDE + "/install")
BF_RUNTIME_LIB = SDE_INSTALL + "/lib/python3.9/site-packages/tofino/"
BSP_FILE_PATH = SDE_INSTALL + "/lib/libpltfm_mgr.so"
P4_BASE="%s/share/tofinopd/" % SDE_INSTALL

# set our lib path
sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), "./", BF_RUNTIME_LIB))
sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), "./", BF_RUNTIME_LIB+"bfrt_grpc"))

import bfrt_grpc.bfruntime_pb2 as bfruntime_pb2
import bfrt_grpc.client as gc

PROGRAM_NAME = os.path.basename(sys.argv[0])

log_level = logging.WARNING

logger = logging.getLogger(PROGRAM_NAME)
if not len(logger.handlers):
    logger.addHandler(logging.StreamHandler())
    logger.setLevel(log_level)

logger.warn("SDE=%s" % SDE)

SINGLE = 1
DUAL = 2
QUAD = 4

def logException(location, e):
    logger.error("%s: %s error: %s at line number %s" % (location,
                                                         type(e).__name__,
                                                         e,
                                                         format(sys.exc_info()[-1].tb_lineno)))

class BfRuntimeGrpcClient:
    def __init__(self, grpc_addr, p4_base, p4_program_list, device_id, client_id, profile_name):
        self.class_name = type(self).__name__
        self.grpc_addr = grpc_addr # "localhost:50052"
        self.p4_base = p4_base
        self.p4_name_list = p4_program_list
        self.device_id = device_id
        self.client_id = client_id
        self.profile_name = profile_name
        self.notification = None
        self.is_master = True
        self.interface = None
        logger.warn("GRPC_ADDRESS: %s" % self.grpc_addr)
        logger.warn("P4_BASE: %s" % self.p4_base)
        logger.warn("P4_NAME_LIST: %s" % self.p4_name_list)
        logger.warn("DEVICE_ID: %s" % self.device_id)
        logger.warn("CLIENT_ID: %s" % self.client_id)
        logger.warn("PROFILE_NAME: %s" % self.profile_name)

        try:
            cfg_list = []
            self.setUpGrpcClient()
            cfg_list = self.setP4ProgramList()
            self.sendP4ProgramList(cfg_list)

            # Bind p4 program but I'm not sure if this is uselful
            # as we will tear down the grpc connection afterwards
            #self.interface.bind_pipeline_config(self.p4_name)
            #logger.warn("p4 program %s uploaded to %s", self.p4_name_list,self.grpc_addr)

            # Tearing down grpc connection disconnecting client from device
            # as we are only loading config (no p4 dataplane processing)

            self.tearDown()

        except Exception as e:
            logger.error("Error in %s. Is grpc server running at %s" % sys._getframe().f_code.co_name, self.grpc_addr)
            self.tearDown()
            sys.exit(1)

    def setUpGrpcClient(self):
        try:
            logger.warn("Binding client[%s] to device[%s]@%s with MASTER role", self.client_id, self.device_id, self.grpc_addr)
            self.interface = gc.ClientInterface(self.grpc_addr, client_id=self.client_id,
                             device_id=self.device_id, is_master=self.is_master, notifications=self.notification)
        except Exception as e:
            logger.error("Error in %s. Is grpc server running at %s ?" % (sys._getframe().f_code.co_name, self.grpc_addr))


    def setP4ProgramList(self):
        cfg_list = []
        # By default program is loaded on all TOFINO pipelines
        pipe_id_list = [0,1,2,3]

        try:
            if len(self.p4_name_list) == SINGLE:
                p4_name = self.p4_name_list[0]
                logger.warn("Setting config for: %s" % p4_name)
                cfg_list = [gc.ForwardingConfig(p4_name,
                             self.bfrt_fp(self.p4_base,p4_name),
                             [gc.ProfileInfo(self.profile_name,
                                             self.cxt_fp(self.p4_base, p4_name, self.profile_name),
                                             self.tna_fp(self.p4_base, p4_name, self.profile_name),
                                             pipe_id_list)]
                                             )]
                logger.warn("p4_base = %s" % self.p4_base)
            else:
                pipe_index = 0
                for p4_name in self.p4_name_list:
                    logger.warn("Setting config for: %s" % p4_name)
                    pfl_info_list = [gc.ProfileInfo(self.profile_name,
                                                    self.cxt_fp(self.p4_base, p4_name, self.profile_name),
                                                    self.tna_fp(self.p4_base, p4_name, self.profile_name),
                                                    [pipe_index])]

                    fwd_cfg = gc.ForwardingConfig(p4_name,
                                                  self.bfrt_fp(self.p4_base,p4_name),
                                                  pfl_info_list)

                    cfg_list.append(fwd_cfg)
                    pipe_index = pipe_index + 1

            return cfg_list

        except Exception as e:
            logException(sys._getframe().f_code.co_name,e)

    def sendP4ProgramList(self,cfg_list):

        try:
            logger.warn("Sending VERIFY, VERIFY_AND_WARM_INIT_BEGIN and WARM_INIT_END")

            action = bfruntime_pb2.SetForwardingPipelineConfigRequest.VERIFY_AND_WARM_INIT_BEGIN_AND_END

            success = self.interface.send_set_forwarding_pipeline_config_request(
                                action,
                                self.p4_base,
                                cfg_list)

            if not success:
                raise RuntimeError("Failed to load p4 configuration(s) to %s" % self.grpc_addr)

            logger.warn("Config sent for %s !" % self.p4_name_list)

        except Exception as e:
            logException(sys._getframe().f_code.co_name, e)

    def tearDown(self):
        if self.interface is not None:
            logger.warn("Tearing down gracefully grpc session with %s" % self.grpc_addr)
            self.interface.tear_down_stream()

    def bfrt_fp(self, base_path, p4_name):
        return base_path + "/" + p4_name + "/bf-rt.json"

    def cxt_fp(self, base_path, p4_name, profile_name):
        return base_path + "/" + p4_name + "/" + profile_name + "/context.json"

    def tna_fp(self, base_path, p4_name, profile_name):
        return base_path + "/" + p4_name + "/" + profile_name + "/" + "tofino" + ".bin"


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="BfRuntime controller")

    group = parser.add_mutually_exclusive_group()

    group.add_argument(
        "--single",
        help="Load single p4 program on the entire switch",
        action="store_true",
        required=False,
        default=True,
    )
    group.add_argument(
        "--multi",
        help="Number of pipe available (depends on TOFINO NPU model)",
        type=str,
        action="store",
        choices=["dual","quad"],
        required=False,
        default="",
    )
    parser.add_argument(
        "--bfruntime-address",
        help="BfRuntime address",
        type=str,
        action="store",
        required=False,
        default="localhost:50052",
    )
    parser.add_argument(
        "--p4-program",
        help="P4 program to load",
        type=str,
        action="store",
        required=False,
        default="",
    )
    parser.add_argument(
        "--p4-p0",
        help="P4 program to load on logical pipe 0",
        type=str,
        action="store",
        required=False,
        default="",
    )
    parser.add_argument(
        "--p4-p1",
        help="P4 program to load on logical pipe 1",
        type=str,
        action="store",
        required=False,
        default="",
    )
    parser.add_argument(
        "--p4-p2",
        help="P4 program to load on logical pipe 2",
        type=str,
        action="store",
        required=False,
        default="",
    )
    parser.add_argument(
        "--p4-p3",
        help="P4 program to load on logical pipe 3",
        type=str,
        action="store",
        required=False,
        default="",
    )
    parser.add_argument(
        "--device-id",
        help="p4 switch device-id",
        type=int,
        action="store",
        required=False,
        default=0,
    )
    parser.add_argument(
        "--client-id",
        help="grpc client-id",
        type=int,
        action="store",
        required=False,
        default=0,
    )
    parser.add_argument(
        "--profile-name",
        help="TOFINO profile name",
        type=str,
        action="store",
        required=False,
        default="pipe",
    )

    args = parser.parse_args()
    p4_program_list = []

    try:
        DUAL_PIPE = "dual"
        QUAD_PIPE = "quad"
        pipe_list = None


        if (args.multi == "dual") or (args.multi == "quad"):
            args.single = False
            logger.warn("%s running on NPU (%s pipe mode)" % (PROGRAM_NAME,args.multi))
        else:
            logger.warn("%s running on NPU (single pipe mode)" % (PROGRAM_NAME))

        logger.debug("args.single = %s" % args.single)
        logger.debug("args.multi = %s" % args.multi)

        if args.single is True:
            if args.p4_program != "":
                p4_program_list.append(args.p4_program)
            else:
                raise Exception("--p4-program name is mandatory")
        elif args.multi == DUAL_PIPE:
            if (args.p4_p0 != "") and (args.p4_p1 != ""):
                p4_program_list.append(args.p4_p0)
                p4_program_list.append(args.p4_p1)
            else:
                raise Exception("DUAL PIPE configuration requires --p4-p0 and --p4-p1 programs be allocated on per pipe id basis")
        elif args.multi == QUAD_PIPE:
            if (args.p4_p0 != "") and \
               (args.p4_p1 != "") and \
               (args.p4_p2 != "") and \
               (args.p4_p3 != ""):
                p4_program_list.append(args.p4_p0)
                p4_program_list.append(args.p4_p1)
                p4_program_list.append(args.p4_p2)
                p4_program_list.append(args.p4_p3)
            else:
                raise Exception("QUAD PIPES configuration requires --p4-p0, --p4-p1, --p4-p2 and --p4-p3 programs be allocated on per pipe id basis")
        else:
            raise Exception("More than 4 pipes !!!!")

        bf_client = BfRuntimeGrpcClient(args.bfruntime_address,
                                    P4_BASE,
                                    p4_program_list,
                                    args.device_id,
                                    args.client_id,
                                    args.profile_name)

    except Exception as e:
        logException(PROGRAM_NAME,e)
        #logger.error("%s: %s error: %s at line number %s" % (PROGRAM_NAME,type(e).__name__, e, format(sys.exc_info()[-1].tb_lineno)))
        sys.exit(1)

