# Copyright (c) 2016 Cisco and/or its affiliates.
# 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.

"""Policer utilities library."""

from enum import Enum

from ipaddress import ip_address

from resources.libraries.python.VatExecutor import VatExecutor
from resources.libraries.python.VatJsonUtil import VatJsonUtil
from resources.libraries.python.topology import Topology


class PolicerRateType(Enum):
    """Policer rate types."""
    KBPS = 'kbps'
    PPS = 'pps'

    def __init__(self, string):
        self.string = string


# pylint: disable=invalid-name
class PolicerRoundType(Enum):
    """Policer round types."""
    CLOSEST = 'closest'
    UP = 'up'
    DOWN = 'down'

    def __init__(self, string):
        self.string = string


class PolicerType(Enum):
    """Policer type."""
    P_1R2C = '1r2c'
    P_1R3C = '1r3c'
    P_2R3C_2698 = '2r3c-2698'
    P_2R3C_4115 = '2r3c-4115'
    P_2R3C_MEF5CF1 = '2r3c-mef5cf1'

    def __init__(self, string):
        self.string = string


class PolicerAction(Enum):
    """Policer action."""
    DROP = 'drop'
    TRANSMIT = 'transmit'
    MARK_AND_TRANSMIT = 'mark-and-transmit'

    def __init__(self, string):
        self.string = string


class DSCP(Enum):
    """DSCP for mark-and-transmit action."""
    CS0 = ('CS0', 0)
    CS1 = ('CS1', 8)
    CS2 = ('CS2', 16)
    CS3 = ('CS3', 24)
    CS4 = ('CS4', 32)
    CS5 = ('CS5', 40)
    CS6 = ('CS6', 48)
    CS7 = ('CS7', 56)
    AF11 = ('AF11', 10)
    AF12 = ('AF12', 12)
    AF13 = ('AF13', 14)
    AF21 = ('AF21', 18)
    AF22 = ('AF22', 20)
    AF23 = ('AF23', 22)
    AF31 = ('AF31', 26)
    AF32 = ('AF32', 28)
    AF33 = ('AF33', 30)
    EF = ('EF', 46)

    def __init__(self, string, num):
        self.string = string
        self.num = num


class PolicerClassifyPreColor(Enum):
    """Policer classify precolor."""
    CONFORM_COLOR = 'conform-color'
    EXCEED_COLOR = 'exceed-color'

    def __init__(self, string):
        self.string = string


class PolicerClassifyTableType(Enum):
    """Policer classify table type."""
    IP4_TABLE = 'ip4-table'
    IP6_TABLE = 'ip6-table'
    L2_TABLE = 'l2-table'

    def __init__(self, string):
        self.string = string


# pylint: disable=too-many-instance-attributes
class Policer(object):
    """Policer utilities."""

    def __init__(self):
        self._cir = 0
        self._eir = 0
        self._cb = 0
        self._eb = 0
        self._rate_type = None
        self._round_type = None
        self._policer_type = None
        self._conform_action = None
        self._conform_dscp = None
        self._exceed_action = None
        self._exceed_dscp = None
        self._violate_action = None
        self._violate_dscp = None
        self._color_aware = False
        self._classify_match_ip = ''
        self._classify_match_is_src = True
        self._classify_precolor = None
        self._sw_if_index = 0
        self._node = None
        self._policer_name = ''

    def policer_set_configuration(self):
        """Configure policer on VPP node.

        ...note:: First set all required parameters.
        """
        node = self._node

        # create policer
        color_aware = 'color-aware' if self._color_aware else ''

        # pylint: disable=no-member
        conform_action = self._conform_action.value

        if PolicerAction.MARK_AND_TRANSMIT == self._conform_action:
            conform_action += ' {0}'.format(self._conform_dscp.string)

        exceed_action = self._exceed_action.value
        if PolicerAction.MARK_AND_TRANSMIT == self._exceed_action:
            exceed_action += ' {0}'.format(self._exceed_dscp.string)

        violate_action = self._violate_action.value
        if PolicerAction.MARK_AND_TRANSMIT == self._violate_action:
            violate_action += ' {0}'.format(self._violate_dscp.string)

        out = VatExecutor.cmd_from_template(node,
                                            "policer/policer_add_3c.vat",
                                            name=self._policer_name,
                                            cir=self._cir,
                                            eir=self._eir,
                                            cb=self._cb,
                                            eb=self._eb,
                                            rate_type=self._rate_type.value,
                                            round_type=self._round_type.value,
                                            p_type=self._policer_type.value,
                                            conform_action=conform_action,
                                            exceed_action=exceed_action,
                                            violate_action=violate_action,
                                            color_aware=color_aware)

        VatJsonUtil.verify_vat_retval(
            out[0],
            err_msg='Add policer {0} failed on {1}'.format(self._policer_name,
                                                           node['host']))

        policer_index = out[0].get('policer_index')

        # create classify table
        direction = 'src' if self._classify_match_is_src else 'dst'

        if ip_address(unicode(self._classify_match_ip)).version == 6:
            ip_version = 'ip6'
            table_type = PolicerClassifyTableType.IP6_TABLE
        else:
            ip_version = 'ip4'
            table_type = PolicerClassifyTableType.IP4_TABLE

        out = VatExecutor.cmd_from_template(node,
                                            "classify_add_table.vat",
                                            ip_version=ip_version,
                                            direction=direction)

        VatJsonUtil.verify_vat_retval(
            out[0],
            err_msg='Add classify table failed on {0}'.format(node['host']))

        new_table_index = out[0].get('new_table_index')
        skip_n_vectors = out[0].get('skip_n_vectors')
        match_n_vectors = out[0].get('match_n_vectors')

        # create classify session
        match = 'l3 {0} {1} {2}'.format(ip_version,
                                        direction,
                                        self._classify_match_ip)

        out = VatExecutor.cmd_from_template(
            node,
            "policer/policer_classify_add_session.vat",
            policer_index=policer_index,
            pre_color=self._classify_precolor.value, # pylint: disable=no-member
            table_index=new_table_index,
            skip_n=skip_n_vectors,
            match_n=match_n_vectors,
            match=match)

        VatJsonUtil.verify_vat_retval(
            out[0],
            err_msg='Add classify session failed on {0}'.format(node['host']))

        # set classify interface
        out = VatExecutor.cmd_from_template(
            node,
            "policer/policer_classify_set_interface.vat",
            sw_if_index=self._sw_if_index,
            table_type=table_type.value, # pylint: disable=no-member
            table_index=new_table_index)

        VatJsonUtil.verify_vat_retval(
            out[0],
            err_msg='Set classify interface failed on {0}'.format(node['host']))

    def policer_clear_settings(self):
        """Clear policer settings."""
        self._cir = 0
        self._eir = 0
        self._cb = 0
        self._eb = 0
        self._rate_type = None
        self._round_type = None
        self._policer_type = None
        self._conform_action = None
        self._conform_dscp = None
        self._exceed_action = None
        self._exceed_dscp = None
        self._violate_action = None
        self._violate_dscp = None
        self._color_aware = False
        self._classify_match_ip = ''
        self._classify_match_is_src = True
        self._classify_precolor = None
        self._sw_if_index = 0
        self._node = None
        self._policer_name = ''

    def policer_set_name(self, name):
        """Set policer name.

        :param name: Policer name.
        :type name: str
        """
        self._policer_name = name

    def policer_set_node(self, node):
        """Set node to setup policer on.

        :param node: VPP node.
        :type node: dict
        """
        self._node = node

    def policer_set_cir(self, cir):
        """Set policer CIR.

        :param cir: Committed Information Rate.
        :type cir: int
        """
        self._cir = cir

    def policer_set_eir(self, eir):
        """Set polcier EIR.

        :param eir: Excess Information Rate.
        :type eir: int
        """
        self._eir = eir

    def policer_set_cb(self, cb):
        """Set policer CB.

        :param cb: Committed Burst size.
        :type cb: int
        """
        self._cb = cb

    def policer_set_eb(self, eb):
        """Set policer EB.

        :param eb: Excess Burst size.
        :type eb: int
        """
        self._eb = eb

    def policer_set_rate_type_kbps(self):
        """Set policer rate type to kbps."""
        self._rate_type = PolicerRateType.KBPS

    def policer_set_rate_type_pps(self):
        """Set policer rate type to pps."""
        self._rate_type = PolicerRateType.PPS

    def policer_set_round_type_closest(self):
        """Set policer round type to closest."""
        self._round_type = PolicerRoundType.CLOSEST

    def policer_set_round_type_up(self):
        """Set policer round type to up."""
        self._round_type = PolicerRoundType.UP

    def policer_set_round_type_down(self):
        """Set policer round type to down."""
        self._round_type = PolicerRoundType.DOWN

    def policer_set_type_1r2c(self):
        """Set policer type to 1r2c."""
        self._policer_type = PolicerType.P_1R2C

    def policer_set_type_1r3c(self):
        """Set policer type to 1r3c RFC2697."""
        self._policer_type = PolicerType.P_1R3C

    def policer_set_type_2r3c_2698(self):
        """Set policer type to 2r3c RFC2698."""
        self._policer_type = PolicerType.P_2R3C_2698

    def policer_set_type_2r3c_4115(self):
        """Set policer type to 2r3c RFC4115."""
        self._policer_type = PolicerType.P_2R3C_4115

    def policer_set_type_2r3c_mef5cf1(self):
        """Set policer type to 2r3c MEF5CF1."""
        self._policer_type = PolicerType.P_2R3C_MEF5CF1

    def policer_set_conform_action_drop(self):
        """Set policer conform-action to drop."""
        self._conform_action = PolicerAction.DROP

    def policer_set_conform_action_transmit(self):
        """Set policer conform-action to transmit."""
        self._conform_action = PolicerAction.TRANSMIT

    def policer_set_conform_action_mark_and_transmit(self, dscp):
        """Set policer conform-action to mark-and-transmit.

        :param dscp: DSCP value to mark.
        :type dscp: DSCP
        """
        self._conform_action = PolicerAction.MARK_AND_TRANSMIT
        self._conform_dscp = dscp

    def policer_set_exceed_action_drop(self):
        """Set policer exceed-action to drop."""
        self._exceed_action = PolicerAction.DROP

    def policer_set_exceed_action_transmit(self):
        """Set policer exceed-action to transmit."""
        self._exceed_action = PolicerAction.TRANSMIT

    def policer_set_exceed_action_mark_and_transmit(self, dscp):
        """Set policer exceed-action to mark-and-transmit.

        :param dscp: DSCP value to mark.
        :type dscp: DSCP
        """
        self._exceed_action = PolicerAction.MARK_AND_TRANSMIT
        self._exceed_dscp = dscp

    def policer_set_violate_action_drop(self):
        """Set policer violate-action to drop."""
        self._violate_action = PolicerAction.DROP

    def policer_set_violate_action_transmit(self):
        """Set policer violate-action to transmit."""
        self._violate_action = PolicerAction.TRANSMIT

    def policer_set_violate_action_mark_and_transmit(self, dscp):
        """Set policer violate-action to mark-and-transmit.

        :param dscp: DSCP value to mark.
        :type dscp: DSCP
        """
        self._violate_action = PolicerAction.MARK_AND_TRANSMIT
        self._violate_dscp = dscp

    def policer_enable_color_aware(self):
        """Enable color-aware mode for policer."""
        self._color_aware = True

    def policer_classify_set_precolor_conform(self):
        """Set policer classify pre-color to conform-color."""
        self._classify_precolor = PolicerClassifyPreColor.CONFORM_COLOR

    def policer_classify_set_precolor_exceed(self):
        """Set policer classify pre-color to exceeed-color."""
        self._classify_precolor = PolicerClassifyPreColor.EXCEED_COLOR

    def policer_classify_set_interface(self, interface):
        """Set policer classify interface.

        :param interface: Interface name or sw_if_index.
        :type interface: str or int
        .. note:: First set node with policer_set_node.
        """
        if isinstance(interface, basestring):
            self._sw_if_index = Topology.get_interface_sw_index(self._node,
                                                                interface)
        else:
            self._sw_if_index = interface

    def policer_classify_set_match_ip(self, ip, is_src=True):
        """Set policer classify match source IP address.

        :param ip: IPv4 or IPv6 address.
        :param is_src: Match src IP if True otherwise match dst IP.
        :type ip: str
        :type is_src: bool
        """
        self._classify_match_ip = ip
        self._classify_match_is_src = is_src

    @staticmethod
    def dscp_cs0():
        """Return DSCP CS0.

        :return: DSCP enum CS0 object.
        :rtype: DSCP
        """
        return DSCP.CS0

    @staticmethod
    def dscp_cs1():
        """Return DSCP CS1.

        :return: DSCP enum CS1 object.
        :rtype: DSCP
        """
        return DSCP.CS1

    @staticmethod
    def dscp_cs2():
        """Return DSCP CS2.

        :return: DSCP enum CS2 object.
        :rtype: DSCP
        """
        return DSCP.CS2

    @staticmethod
    def dscp_cs3():
        """Return DSCP CS3.

        :return: DSCP enum CS3 object.
        :rtype: DSCP
        """
        return DSCP.CS3

    @staticmethod
    def dscp_cs4():
        """Return DSCP CS4.

        :return: DSCP enum CS4 object.
        :rtype: DSCP
        """
        return DSCP.CS4

    @staticmethod
    def dscp_cs5():
        """Return DSCP CS5.

        :return: DSCP enum CS5 object.
        :rtype: DSCP
        """
        return DSCP.CS5

    @staticmethod
    def dscp_cs6():
        """Return DSCP CS6.

        :return: DSCP enum CS6 object.
        :rtype: DSCP
        """
        return DSCP.CS6

    @staticmethod
    def dscp_cs7():
        """Return DSCP CS7.

        :return: DSCP enum CS7 object.
        :rtype: DSCP
        """
        return DSCP.CS7

    @staticmethod
    def dscp_ef():
        """Return DSCP EF.

        :return: DSCP enum EF object.
        :rtype: DSCP
        """
        return DSCP.EF

    @staticmethod
    def dscp_af11():
        """Return DSCP AF11.

        :return: DSCP enum AF11 object.
        :rtype: DSCP
        """
        return DSCP.AF11

    @staticmethod
    def dscp_af12():
        """Return DSCP AF12.

        :return: DSCP enum AF12 object.
        :rtype: DSCP
        """
        return DSCP.AF12

    @staticmethod
    def dscp_af13():
        """Return DSCP AF13.

        :return: DSCP enum AF13 object.
        :rtype: DSCP
        """
        return DSCP.AF13

    @staticmethod
    def dscp_af21():
        """Return DSCP AF21.

        :return: DSCP enum AF21 object.
        :rtype: DSCP
        """
        return DSCP.AF21

    @staticmethod
    def dscp_af22():
        """Return DSCP AF22.

        :return: DSCP enum AF22 object.
        :rtype: DSCP
        """
        return DSCP.AF22

    @staticmethod
    def dscp_af23():
        """Return DSCP AF23.

        :return: DSCP enum AF23 object.
        :rtype: DSCP
        """
        return DSCP.AF23

    @staticmethod
    def dscp_af31():
        """Return DSCP AF31.

        :return: DSCP enum AF31 object.
        :rtype: DSCP
        """
        return DSCP.AF31

    @staticmethod
    def dscp_af32():
        """Return DSCP AF32.

        :return: DSCP enum AF32 object.
        :rtype: DSCP
        """
        return DSCP.AF32

    @staticmethod
    def dscp_af33():
        """Return DSCP AF33.

        :return: DSCP enum AF33 object.
        :rtype: DSCP
        """
        return DSCP.AF33

    @staticmethod
    def get_dscp_num_value(dscp):
        """Return DSCP numeric value.

        :param dscp: DSCP enum object.
        :type dscp: DSCP
        :return: DSCP numeric value.
        :rtype: int
        """
        return dscp.num