# Copyright (C) 2013 Nippon Telegraph and Telephone Corporation.
#
# 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

from ryu.base import app_manager
from ryu.controller import event
from ryu.controller import ofp_event
from ryu.controller.handler import DEAD_DISPATCHER
from ryu.controller.handler import MAIN_DISPATCHER
from ryu.controller.handler import set_ev_cls
from ryu.ofproto import ether
from ryu.ofproto import inet
from ryu.ofproto import ofproto_v1_0
from ryu.ofproto import ofproto_v1_2
from ryu.ofproto import ofproto_v1_3
from ryu.lib import addrconv
from ryu.lib import hub
from ryu.lib.dpid import dpid_to_str
from ryu.lib.packet import packet
from ryu.lib.packet import ethernet
from ryu.lib.packet import ipv4
from ryu.lib.packet import igmp


class EventPacketIn(event.EventBase):
    """a PacketIn event class using except IGMP."""
    def __init__(self, msg):
        """initialization."""
        super(EventPacketIn, self).__init__()
        self.msg = msg


MG_GROUP_ADDED = 1
MG_MEMBER_CHANGED = 2
MG_GROUP_REMOVED = 3


class EventMulticastGroupStateChanged(event.EventBase):
    """a event class that notifies the changes of the statuses of the
    multicast groups."""

    def __init__(self, reason, address, src, dsts):
        """
        ========= =====================================================
        Attribute Description
        ========= =====================================================
        reason    why the event occurs. use one of MG_*.
        address   a multicast group address.
        src       a port number in which a querier exists.
        dsts      a list of port numbers in which the members exist.
        ========= =====================================================
        """
        super(EventMulticastGroupStateChanged, self).__init__()
        self.reason = reason
        self.address = address
        self.src = src
        self.dsts = dsts


class IgmpLib(app_manager.RyuApp):
    """IGMP snooping library."""

    # -------------------------------------------------------------------
    # PUBLIC METHODS
    # -------------------------------------------------------------------
    def __init__(self):
        """initialization."""
        super(IgmpLib, self).__init__()
        self.name = 'igmplib'
        self._querier = IgmpQuerier()
        self._snooper = IgmpSnooper(self.send_event_to_observers)

    def set_querier_mode(self, dpid, server_port):
        """set a datapath id and server port number to the instance
        of IgmpQuerier.

        ============ ==================================================
        Attribute    Description
        ============ ==================================================
        dpid         the datapath id that will operate as a querier.
        server_port  the port number linked to the multicasting server.
        ============ ==================================================
        """
        self._querier.set_querier_mode(dpid, server_port)

    # -------------------------------------------------------------------
    # PUBLIC METHODS ( EVENT HANDLERS )
    # -------------------------------------------------------------------
    @set_ev_cls(ofp_event.EventOFPPacketIn, MAIN_DISPATCHER)
    def packet_in_handler(self, evt):
        """PacketIn event handler. when the received packet was IGMP,
        proceed it. otherwise, send a event."""
        msg = evt.msg
        dpid = msg.datapath.id

        req_pkt = packet.Packet(msg.data)
        req_igmp = req_pkt.get_protocol(igmp.igmp)
        if req_igmp:
            if self._querier.dpid == dpid:
                self._querier.packet_in_handler(req_igmp, msg)
            else:
                self._snooper.packet_in_handler(req_pkt, req_igmp, msg)
        else:
            self.send_event_to_observers(EventPacketIn(msg))

    @set_ev_cls(ofp_event.EventOFPStateChange,
                [MAIN_DISPATCHER, DEAD_DISPATCHER])
    def state_change_handler(self, evt):
        """StateChange event handler."""
        datapath = evt.datapath
        assert datapath is not None
        if datapath.id == self._querier.dpid:
            if evt.state == MAIN_DISPATCHER:
                self._querier.start_loop(datapath)
            elif evt.state == DEAD_DISPATCHER:
                self._querier.stop_loop()


class IgmpBase(object):
    """IGMP abstract class library."""

    # -------------------------------------------------------------------
    # PUBLIC METHODS
    # -------------------------------------------------------------------
    def __init__(self):
        self._set_flow_func = {
            ofproto_v1_0.OFP_VERSION: self._set_flow_entry_v1_0,
            ofproto_v1_2.OFP_VERSION: self._set_flow_entry_v1_2,
            ofproto_v1_3.OFP_VERSION: self._set_flow_entry_v1_2,
        }
        self._del_flow_func = {
            ofproto_v1_0.OFP_VERSION: self._del_flow_entry_v1_0,
            ofproto_v1_2.OFP_VERSION: self._del_flow_entry_v1_2,
            ofproto_v1_3.OFP_VERSION: self._del_flow_entry_v1_2,
        }

    # -------------------------------------------------------------------
    # PROTECTED METHODS ( RELATED TO OPEN FLOW PROTOCOL )
    # -------------------------------------------------------------------
    def _set_flow_entry_v1_0(self, datapath, actions, in_port, dst,
                             src=None):
        ofproto = datapath.ofproto
        parser = datapath.ofproto_parser

        match = parser.OFPMatch(
            dl_type=ether.ETH_TYPE_IP, in_port=in_port,
            nw_src=self._ipv4_text_to_int(src),
            nw_dst=self._ipv4_text_to_int(dst))
        mod = parser.OFPFlowMod(
            datapath=datapath, match=match, cookie=0,
            command=ofproto.OFPFC_ADD, actions=actions)
        datapath.send_msg(mod)

    def _set_flow_entry_v1_2(self, datapath, actions, in_port, dst,
                             src=None):
        ofproto = datapath.ofproto
        parser = datapath.ofproto_parser

        match = parser.OFPMatch(
            eth_type=ether.ETH_TYPE_IP, in_port=in_port, ipv4_dst=dst)
        if src is not None:
            match.append_field(ofproto.OXM_OF_IPV4_SRC, src)
        inst = [parser.OFPInstructionActions(
            ofproto.OFPIT_APPLY_ACTIONS, actions)]
        mod = parser.OFPFlowMod(
            datapath=datapath, command=ofproto.OFPFC_ADD,
            priority=65535, match=match, instructions=inst)
        datapath.send_msg(mod)

    def _set_flow_entry(self, datapath, actions, in_port, dst, src=None):
        """set a flow entry."""
        set_flow = self._set_flow_func.get(datapath.ofproto.OFP_VERSION)
        assert set_flow
        set_flow(datapath, actions, in_port, dst, src)

    def _del_flow_entry_v1_0(self, datapath, in_port, dst, src=None):
        ofproto = datapath.ofproto
        parser = datapath.ofproto_parser

        match = parser.OFPMatch(
            dl_type=ether.ETH_TYPE_IP, in_port=in_port,
            nw_src=self._ipv4_text_to_int(src),
            nw_dst=self._ipv4_text_to_int(dst))