#!/usr/bin/env python
# -*- coding: utf-8 -*-

import copy

from ryu.base import app_manager
from ryu.lib import hub
from ryu.controller import ofp_event
from ryu.controller.handler import MAIN_DISPATCHER, CONFIG_DISPATCHER
from ryu.controller.handler import set_ev_cls
from ryu.lib.packet import packet, ethernet, arp, ipv4, icmp, ether_types, mpls, tcp, udp
from ryu.ofproto import ofproto_v1_3
from ryu.ofproto.ofproto_v1_3 import  OFP_DEFAULT_PRIORITY

from command_sender import CommandSender
from network_monitor import NetworkMonitor
from route_calculator import RouteCalculator
from flow_collector import FlowCollector

'''
###For 2 chapter###
when packet_in then
packet_out ONLY!
comparison for base_app2
----test----
Linear topology
iperfTCP
'''

class HLApp(app_manager.RyuApp):

    OFP_VERSIONS = [ofproto_v1_3.OFP_VERSION]
    _CONTEXTS = {
        'network_monitor': NetworkMonitor,
        'flow_collector':FlowCollector
    }

    def __init__(self, *args, **kwargs):
        super(HLApp, self).__init__(*args, **kwargs)
        self.network_monitor = kwargs['network_monitor'] # context
        self.flow_collector = kwargs['flow_collector'] #context

        self.commandSender = CommandSender.get_instance() # util
        self.routeCalculator = RouteCalculator.get_instance() # util

        self.DISCOVER_PERIOD = 3

        self.network_monitor_thread = hub.spawn(self._monitor)

    # context
    def _monitor(self):
        while True:
            hub.sleep(self.DISCOVER_PERIOD)
            self.network_monitor.pre_adjacency_matrix = copy.deepcopy(self.network_monitor.adjacency_matrix)
            self.network_monitor.update_topology()
            if self.network_monitor.pre_adjacency_matrix != self.network_monitor.adjacency_matrix:
                self.logger.info('***********adjacency_matrix CHANGED***********')
                self.routeCalculator.pre_path_table = copy.deepcopy(self.routeCalculator.path_table)
                self.routeCalculator.path_table = self.routeCalculator.get_path_table(
                                                    self.network_monitor.adjacency_matrix,
                                                    self.network_monitor.dpids_to_access_port)
                self.routeCalculator.pre_route_table = copy.deepcopy(self.routeCalculator.route_table)
                self.routeCalculator.route_table = self.routeCalculator.get_route_table(
                                                    self.network_monitor.adjacency_matrix,
                                                    self.network_monitor.dpids_to_access_port)
                if self.routeCalculator.pre_path_table != self.routeCalculator.path_table:
                    self.logger.info('------path_table CHANGED-------')

    # install table-miss flow entry for each switch
    @set_ev_cls(ofp_event.EventOFPSwitchFeatures, CONFIG_DISPATCHER)
    def switch_features_handler(self, ev):
        datapath = ev.msg.datapath
        ofproto = datapath.ofproto
        parser = datapath.ofproto_parser
        match = parser.OFPMatch()
        actions = [parser.OFPActionOutput(ofproto.OFPP_CONTROLLER,
                                          ofproto.OFPCML_NO_BUFFER)]
        # add miss entry
        self.commandSender.add_flow(datapath, 0, match, actions)


    @set_ev_cls(ofp_event.EventOFPPacketIn, MAIN_DISPATCHER)
    def packet_in_handler(self, ev):
        msg = ev.msg
        buffer_id = msg.buffer_id
        datapath = msg.datapath
        ofproto = datapath.ofproto
        dpid = datapath.id
        in_port = msg.match['in_port']

        pkt = packet.Packet(msg.data)
        eth = pkt.get_protocols(ethernet.ethernet)[0]
        if eth.ethertype == ether_types.ETH_TYPE_LLDP:
            return

        arp_pkt = pkt.get_protocol(arp.arp)
        if isinstance(arp_pkt, arp.arp): #  arp request and reply
            print("----arp-------")
            arp_src_ip = arp_pkt.src_ip
            arp_dst_ip = arp_pkt.dst_ip
            self.network_monitor.dpid_ip_to_port.setdefault(dpid,{})
            self.network_monitor.dpid_ip_to_port[dpid][arp_src_ip] = in_port
            if arp_dst_ip in self.network_monitor.dpid_ip_to_port[dpid]:
                out_port = self.network_monitor.dpid_ip_to_port[dpid][arp_dst_ip]
            else:
                out_port = ofproto.OFPP_FLOOD
            data = msg.data
            self.commandSender.packet_out(datapath, in_port, out_port, data)
            self.__register_access_info(dpid, arp_src_ip, in_port)

        ipv4_pkt = pkt.get_protocol(ipv4.ipv4)
        if isinstance(ipv4_pkt,ipv4.ipv4):
            src_ip = ipv4_pkt.src
            dst_ip = ipv4_pkt.dst
            src_sw = self.__get_host_location(src_ip)
            dst_sw = self.__get_host_location(dst_ip)
            if src_sw and dst_sw:# end-to-end connection
                print('--------------end-to-end connection--------------')
                src_dpid = src_sw[0]
                dst_dpid = dst_sw[0]
                src_in_port = src_sw[1]
                dst_out_port = dst_sw[1]

                icmp_pkt = pkt.get_protocol(icmp.icmp)
                tcp_pkt = pkt.get_protocol(tcp.tcp)

                if isinstance(icmp_pkt,icmp.icmp):
                    print("----icmp-------")
                    if src_dpid == dst_dpid:
                        print("src_dpid == dst_dpid")
                        data = msg.data
                        self.commandSender.packet_out(datapath, in_port, dst_out_port, data, buffer_id)
                    else:
                        print("src_dpid != dst_dpid",src_dpid,dst_dpid)
                        if dpid == src_dpid:
                            route = self.routeCalculator.get_route(src_dpid, dst_dpid)
                            out_port = self.network_monitor.links_dpid_to_port[(route[0],route[1])][0]
                        elif dpid == dst_dpid:
                            out_port = dst_out_port
                        else:
                            route = self.routeCalculator.get_route(src_dpid, dst_dpid)
                            index = 0
                            for i in range(len(route)):
                                if route[i] == dpid:
                                    index = i
                                    break
                            out_port = self.network_monitor.links_dpid_to_port[(route[index],route[index+1])][0]
                        data = msg.data