#
# 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 logging
import struct
import socket
import random

from ryu.lib.packet import ethernet
from ryu.lib.packet import ipv4
from ryu.lib.packet import tcp
from ryu.lib import addrconv
from ryu.base import app_manager
from ryu.controller import ofp_event
from ryu.controller.handler import MAIN_DISPATCHER, CONFIG_DISPATCHER, \
    HANDSHAKE_DISPATCHER
from ryu.controller.handler import set_ev_cls
import ryu.ofproto.ofproto_v1_3 as ofp
import ryu.ofproto.ofproto_v1_3_parser as ofparser
import ryu.ofproto.openstate_v1_0 as osp
import ryu.ofproto.openstate_v1_0_parser as osparser
from ryu.lib.packet import packet
from ryu.topology import event

LOG = logging.getLogger('app.openstate.forwarding_consistency_many_to_many_ctrl')

SWITCH_PORTS = 6
IPV4 = ipv4.ipv4.__name__
TCP = tcp.tcp.__name__

class OSLoadBalancing(app_manager.RyuApp):
    OFP_VERSIONS = [ofp.OFP_VERSION]

    def __init__(self, *args, **kwargs):
        LOG.info("OpenState Forwarding Consistency sample app initialized")
        LOG.info("Supporting MAX %d ports per switch" % SWITCH_PORTS)
        super(OSLoadBalancing, self).__init__(*args, **kwargs)
        self.mac_to_port = {}
        self.counter = 0

    @set_ev_cls(ofp_event.EventOFPPacketIn, MAIN_DISPATCHER)
    def _packet_in_handler(self, ev):
        msg = ev.msg
        datapath = msg.datapath

        in_port = msg.match['in_port']
        ip_dst = None
        ip_src = None
        tcp_dst = None
        tcp_src = None
        data = None

        pkt = packet.Packet(msg.data)
        
        header_list = dict((p.protocol_name, p)
                           for p in pkt.protocols if type(p) != str)

        out_port_m_to_m = random.randint(4,6)

        if IPV4 in header_list:
            ip_dst = self.ip_addr_ntoa(header_list[IPV4].dst)
            ip_src = self.ip_addr_ntoa(header_list[IPV4].src)
            
            if TCP in header_list:
                tcp_src = header_list[TCP].src_port
                tcp_dst = header_list[TCP].dst_port
            

                if datapath.id == 4:
                    self.add_flow_many_to_many(datapath, in_port, out_port_m_to_m, ip_src, ip_dst, tcp_src, tcp_dst)
                    actions = [ofparser.OFPActionOutput(out_port_m_to_m, 0)]
            
                if msg.buffer_id == ofp.OFP_NO_BUFFER:
                        data = msg.data
                    
                out = ofparser.OFPPacketOut(datapath=datapath, buffer_id=msg.buffer_id,
                                          in_port=in_port, actions=actions, data=data)
                datapath.send_msg(out)

    @set_ev_cls(ofp_event.EventOFPSwitchFeatures, CONFIG_DISPATCHER)
    def switch_features_handler(self, ev):
        msg = ev.msg
        datapath = msg.datapath

        options ={1: self.one_to_many_switch,
                  2: self.middleswitch,
                  3: self.middleswitch,
                  4: self.many_to_many_switch,
                  5: self.middleswitch,
                  6: self.middleswitch,
                  7: self.many_to_one_switch
        }


        options[datapath.id](datapath)

    def one_to_many_switch(self, datapath):
        self.send_features_request(datapath)
        self.send_group_mod(datapath)
        self.send_table_mod(datapath)

        self.send_key_lookup_1(datapath)
        self.send_key_update_1(datapath)
      
        self.add_flow_1_to_many(datapath)
        
    def middleswitch(self, datapath):
        self.send_features_request(datapath)
        self.add_middleswitch_default(datapath)

    #STATELESS (ma anche no...)
    def many_to_many_switch(self, datapath):
        self.send_features_request(datapath)
        self.send_table_mod(datapath)

        self.send_key_lookup_1(datapath)
        self.send_key_update_1(datapath)

        self.add_flow_default_many_to_many(datapath)

    def many_to_one_switch(self, datapath):
        self.send_features_request(datapath)
        self.send_table_mod(datapath)
        
        self.send_key_lookup_2(datapath)
        self.send_key_update_2(datapath)

        self.add_flow_many_to_1(datapath)


    def add_flow_1_to_many(self, datapath, table_miss=False):
        LOG.info("Configuring flow table for switch %d" % datapath.id)     

        if table_miss:
            LOG.debug("Installing table miss...")
            actions = [ofparser.OFPActionOutput(
                ofp.OFPP_CONTROLLER, ofp.OFPCML_NO_BUFFER)]
            match = ofparser.OFPMatch()
            inst = [ofparser.OFPInstructionActions(
                ofp.OFPIT_APPLY_ACTIONS, actions)]
            mod = ofparser.OFPFlowMod(
                datapath=datapath, cookie=0, cookie_mask=0, table_id=0,
                command=ofp.OFPFC_ADD, idle_timeout=0, hard_timeout=0,
                priority=0, buffer_id=ofp.OFP_NO_BUFFER,
                out_port=ofp.OFPP_ANY,
                out_group=ofp.OFPG_ANY,
                flags=0, match=match, instructions=inst)

            datapath.send_msg(mod)

        else:

            # ARP packets flooding
            match = ofparser.OFPMatch(eth_type=0x0806)
            actions = [
                ofparser.OFPActionOutput(ofp.OFPP_FLOOD)]
            inst = [ofparser.OFPInstructionActions(
                ofp.OFPIT_APPLY_ACTIONS, actions)]
            mod = ofparser.OFPFlowMod(
                datapath=datapath, cookie=0, cookie_mask=0, table_id=0,
                command=ofp.OFPFC_ADD, idle_timeout=0, hard_timeout=0,
                priority=32768, buffer_id=ofp.OFP_NO_BUFFER,
                out_port=ofp.OFPP_ANY,
                out_group=ofp.OFPG_ANY,
                flags=0, match=match, instructions=inst)
            datapath.send_msg(mod)

            
            # Reverse path flow
            for in_port in range(2, 5):
                match = ofparser.OFPMatch(in_port=in_port)
                actions = [
                    ofparser.OFPActionOutput(1,0)]
                inst = [ofparser.OFPInstructionActions(
                    ofp.OFPIT_APPLY_ACTIONS, actions)]
                mod = ofparser.OFPFlowMod(
                    datapath=datapath, cookie=0, cookie_mask=0, table_id=0,
                    command=ofp.OFPFC_ADD, idle_timeout=0, hard_timeout=0,
                    priority=32767, buffer_id=ofp.OFP_NO_BUFFER,
                    out_port=ofp.OFPP_ANY,
                    out_group=ofp.OFPG_ANY,
                    flags=0, match=match, instructions=inst)
                datapath.send_msg(mod)

            # the state of a flow is the selected output port for that flow
            for state in range(4):
                if state == 0:
                    # if state=DEFAULT => send it to the first group entry in the group table
                    actions = [
                            ofparser.OFPActionGroup(1)]
                    match = ofparser.OFPMatch(
                            in_port=1, state=state, eth_type=0x800)
                else:
                    # state x means output port x+1
                    actions = [
                        ofparser.OFPActionOutput(state+1, 0)]
                    match = ofparser.OFPMatch(
                        in_port=1, state=state, eth_type=0x800)
                inst = [
                    ofparser.OFPInstructionActions(
                        ofp.OFPIT_APPLY_ACTIONS, actions)]
                mod = ofparser.OFPFlowMod(
                    datapath=datapath, cookie=0, cookie_mask=0, table_id=0,
                    command=ofp.OFPFC_ADD, idle_timeout=0,
                    hard_timeout=0, priority=32767,
                    buffer_id=ofp.OFP_NO_BUFFER,
                    out_port=ofp.OFPP_ANY, out_group=ofp.OFPG_ANY,
                    flags=0, match=match, instructions=inst)
                datapath.send_msg(mod)

    # MANY TO MANY RULES
    def add_flow_default_many_to_many(self, datapath):
        LOG.info("Configuring default flow entries for switch %d" % datapath.id)      

        #table miss
        actions = [ofparser.OFPActionOutput(
            ofp.OFPP_CONTROLLER, ofp.OFPCML_NO_BUFFER)]
        match = ofparser.OFPMatch()
        inst = [ofparser.OFPInstructionActions(
            ofp.OFPIT_APPLY_ACTIONS, actions)]
        mod = ofparser.OFPFlowMod(
            datapath=datapath, cookie=0, cookie_mask=0, table_id=0,
            command=ofp.OFPFC_ADD, idle_timeout=0, hard_timeout=0,
            priority=0, buffer_id=ofp.OFP_NO_BUFFER,
            out_port=ofp.OFPP_ANY,
            out_group=ofp.OFPG_ANY,
            flags=0, match=match, instructions=inst)

        datapath.send_msg(mod)

        # ARP packets flooding
        match = ofparser.OFPMatch(eth_type=0x0806)
        actions = [
            ofparser.OFPActionOutput(ofp.OFPP_FLOOD)]
        inst = [ofparser.OFPInstructionActions(
            ofp.OFPIT_APPLY_ACTIONS, actions)]
        mod = ofparser.OFPFlowMod(
            datapath=datapath, cookie=0, cookie_mask=0, table_id=0,
            command=ofp.OFPFC_ADD, idle_timeout=0, hard_timeout=0,
            priority=32768, buffer_id=ofp.OFP_NO_BUFFER,
            out_port=ofp.OFPP_ANY,
            out_group=ofp.OFPG_ANY,
            flags=0, match=match, instructions=inst)
        datapath.send_msg(mod)

    

    #   MANY TO MANY
    def add_flow_many_to_many(self, datapath, in_port, out_port, ip_src, ip_dst, tcp_src, tcp_dst):        
        self.counter+=1
        #LOG.info('Installing new forward rule for switch %d (rule # %d)' % (datapath.id, self.counter)) 
        
        # reverse path rule
        actions = [ofparser.OFPActionOutput(in_port, 0)]
        match = ofparser.OFPMatch(
            in_port=out_port, eth_type=0x800, ip_proto=6, ipv4_src=ip_dst, ipv4_dst=ip_src, tcp_src=tcp_dst, tcp_dst=tcp_src)
        inst = [
            ofparser.OFPInstructionActions(
                ofp.OFPIT_APPLY_ACTIONS, actions)]
        mod = ofparser.OFPFlowMod(
            datapath=datapath, cookie=0, cookie_mask=0, table_id=0,
            command=ofp.OFPFC_ADD, idle_timeout=0,
            hard_timeout=0, priority=32767,