"""Manages movement of packets through the faucet pipeline."""

# Copyright (C) 2013 Nippon Telegraph and Telephone Corporation.
# Copyright (C) 2015 Brad Cowie, Christopher Lorier and Joe Stringer.
# Copyright (C) 2015 Research and Education Advanced Network New Zealand Ltd.
# Copyright (C) 2015--2019 The Contributors
#
# 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 copy
import functools
import faucet.faucet_metadata as faucet_md
from faucet import valve_of
from faucet.valve_manager_base import ValveManagerBase


class ValvePipeline(ValveManagerBase):
    """Responsible for maintaing the integrity of the Faucet pipeline for a
    single valve.

    Controls what packets a module sees in its tables and how it can pass
    packets through the pipeline.

    Responsible for installing flows in the vlan, egress and classification
    tables"""

    def __init__(self, dp):
        self.dp = dp
        self.vlan_table = dp.tables["vlan"]
        self.classification_table = dp.classification_table()
        self.output_table = dp.output_table()
        self.egress_table = None
        self.egress_acl_table = None
        if dp.egress_pipeline:
            self.egress_table = dp.tables["egress"]
            self.egress_acl_table = dp.tables.get("egress_acl")
        self.filter_priority = self._FILTER_PRIORITY
        self.select_priority = self._HIGH_PRIORITY

    @staticmethod
    @functools.lru_cache(maxsize=1024)
    def _accept_to_table(table, actions):
        inst = [table.goto_this()]
        if actions:
            inst.append(valve_of.apply_actions(actions))
        return tuple(inst)

    @functools.lru_cache(maxsize=1024)
    def accept_to_vlan(self, actions=None):
        """Get instructions to forward packet through the pipeline to
        vlan table.
        args:
            actions: (optional) list of actions to apply to packet.
        returns:
            list of instructions
        """
        return self._accept_to_table(self.vlan_table, actions)

    @functools.lru_cache(maxsize=1024)
    def accept_to_classification(self, actions=None):
        """Get instructions to forward packet through the pipeline to
        classification table.
        args:
            actions: (optional) list of actions to apply to packet.
        returns:
            list of instructions
        """
        return self._accept_to_table(self.classification_table, actions)

    @functools.lru_cache(maxsize=1024)
    def accept_to_l2_forwarding(self, actions=None):
        """Get instructions to forward packet through the pipeline to l2
        forwarding.
        args:
            actions: (optional) list of actions to apply to packet.
        returns:
            list of instructions
        """
        return self._accept_to_table(self.output_table, actions)

    @functools.lru_cache(maxsize=1024)
    def accept_to_egress(self, actions=None):
        """Get instructions to forward packet through the pipeline to egress
        table

        Raises an assertion error if egress pipeline is not configured

        args:
            actions: (optional) list of actions to apply to the packet
        returns:
            list of instructions:
        """
        assert self.egress_table is not None
        return self._accept_to_table(self.egress_table, actions)

    def output(self, port, vlan, hairpin=False, external_forwarding_requested=None):
        """Get instructions list to output a packet through the regular
        pipeline.

        args:
            port: Port object of port to output packet to
            vlan: Vlan object of vlan to output packet on
            hairpin: if True, hairpinning is required
            apply_egress_acl: if True the packet will be sent to the egress acl
                table before being output
        returns:
            list of Instructions
        """
        instructions = []
        if self.egress_table:
            metadata, metadata_mask = faucet_md.get_egress_metadata(
                port.number, vlan.vid
            )
            if self.egress_acl_table:
                instructions.extend(
                    valve_of.metadata_goto_table(
                        metadata, metadata_mask, self.egress_acl_table
                    )
                )
            else:
                instructions.extend(
                    valve_of.metadata_goto_table(
                        metadata, metadata_mask, self.egress_table
                    )
                )
        else:
            instructions.append(
                valve_of.apply_actions(
                    vlan.output_port(
                        port,
                        hairpin=hairpin,
                        output_table=self.output_table,
                        external_forwarding_requested=external_forwarding_requested,
                    )
                )
            )
        return tuple(instructions)

    def initialise_tables(self):
        """Install rules to initialise the classification_table"""
        ofmsgs = []
        # drop broadcast sources
        if self.dp.drop_broadcast_source_address:
            ofmsgs.extend(self.filter_packets({"eth_src": valve_of.mac.BROADCAST_STR}))

        ofmsgs.extend(
            self.filter_packets(
                {"eth_type": valve_of.ECTP_ETH_TYPE}, priority_offset=10
            )
        )

        return ofmsgs

    def _add_egress_table_rule(self, port, vlan, pop_vlan=True):
        metadata, metadata_mask = faucet_md.get_egress_metadata(port.number, vlan.vid)
        actions = copy.copy(port.mirror_actions())
        if pop_vlan:
            actions.append(valve_of.pop_vlan())
        actions.append(valve_of.output_port(port.number))
        inst = (valve_of.apply_actions(tuple(actions)),)
        return self.egress_table.flowmod(
            self.egress_table.match(
                vlan=vlan, metadata=metadata, metadata_mask=metadata_mask
            ),
            priority=self.dp.high_priority,
            inst=inst,
        )

    def add_port(self, port):
        ofmsgs = []
        if self.egress_table is None:
            return ofmsgs
        for vlan in port.tagged_vlans:
            ofmsgs.append(self._add_egress_table_rule(port, vlan, pop_vlan=False))
        if port.native_vlan is not None:
            ofmsgs.append(self._add_egress_table_rule(port, port.native_vlan))
        return ofmsgs

    def del_port(self, port):
        ofmsgs = []
        if self.egress_table:
            mask = faucet_md.PORT_METADATA_MASK
            ofmsgs.append(
                self.egress_table.flowdel(
                    self.egress_table.match(
                        metadata=port.number & mask, metadata_mask=mask
                    )
                )
            )
        return ofmsgs

    def filter_packets(self, match_dict, priority_offset=0):
        """get a list of flow modification messages to filter packets from
        the pipeline.
        args:
            match_dict: a dictionary specifying the match fields
            priority_offset: used to prevent overlapping entries
        """
        return [
            self.classification_table.flowdrop(
                self.classification_table.match(**match_dict),
                priority=self.filter_priority + priority_offset,
            )
        ]

    def select_packets(self, target_table, match_dict, actions=None, priority_offset=0):
        """retrieve rules to redirect packets matching match_dict to table"""
        inst = [target_table.goto_this()]
        if actions is not None:
            inst.append(valve_of.apply_actions(actions))
        return [
            self.classification_table.flowmod(
                self.classification_table.match(**match_dict),
                priority=self.select_priority + priority_offset,
                inst=tuple(inst),
            )
        ]

    def remove_filter(self, match_dict, strict=True, priority_offset=0):
        """retrieve flow mods to remove a filter from the classification table"""
        priority = None
        if strict:
            priority = self.filter_priority + priority_offset
        return [
            self.classification_table.flowdel(
                self.classification_table.match(**match_dict),
                priority=priority,
                strict=strict,
            )
        ]
