# Copyright (C) 2012, 2013 Nippon Telegraph and Telephone Corporation.
# Copyright (C) 2012 Isaku Yamahata <yamahata at valinux co jp>
#
# 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.

"""
Manage switches.

Planned to be replaced by os_ken/topology.
"""

import logging
import warnings

from os_ken.base import app_manager
from os_ken.controller import event
from os_ken.controller import handler
from os_ken.controller import ofp_event
from os_ken.controller.handler import set_ev_cls
import os_ken.exception as os_ken_exc

from os_ken.lib.dpid import dpid_to_str

LOG = logging.getLogger('os_ken.controller.dpset')

DPSET_EV_DISPATCHER = "dpset"


class EventDPBase(event.EventBase):
    def __init__(self, dp):
        super(EventDPBase, self).__init__()
        self.dp = dp


class EventDP(EventDPBase):
    """
    An event class to notify connect/disconnect of a switch.

    For OpenFlow switches, one can get the same notification by observing
    os_ken.controller.ofp_event.EventOFPStateChange.
    An instance has at least the following attributes.

    ========= =================================================================
    Attribute Description
    ========= =================================================================
    dp        A os_ken.controller.controller.Datapath instance of the switch
    enter     True when the switch connected to our controller.  False for
              disconnect.
    ports     A list of port instances.
    ========= =================================================================
    """

    def __init__(self, dp, enter_leave):
        # enter_leave
        # True: dp entered
        # False: dp leaving
        super(EventDP, self).__init__(dp)
        self.enter = enter_leave
        self.ports = []  # port list when enter or leave


class EventDPReconnected(EventDPBase):
    def __init__(self, dp):
        super(EventDPReconnected, self).__init__(dp)
        # port list, which should not change across reconnects
        self.ports = []


class EventPortBase(EventDPBase):
    def __init__(self, dp, port):
        super(EventPortBase, self).__init__(dp)
        self.port = port


class EventPortAdd(EventPortBase):
    """
    An event class for switch port status "ADD" notification.

    This event is generated when a new port is added to a switch.
    For OpenFlow switches, one can get the same notification by observing
    os_ken.controller.ofp_event.EventOFPPortStatus.
    An instance has at least the following attributes.

    ========= =================================================================
    Attribute Description
    ========= =================================================================
    dp        A os_ken.controller.controller.Datapath instance of the switch
    port      port number
    ========= =================================================================
    """

    def __init__(self, dp, port):
        super(EventPortAdd, self).__init__(dp, port)


class EventPortDelete(EventPortBase):
    """
    An event class for switch port status "DELETE" notification.

    This event is generated when a port is removed from a switch.
    For OpenFlow switches, one can get the same notification by observing
    os_ken.controller.ofp_event.EventOFPPortStatus.
    An instance has at least the following attributes.

    ========= =================================================================
    Attribute Description
    ========= =================================================================
    dp        A os_ken.controller.controller.Datapath instance of the switch
    port      port number
    ========= =================================================================
    """

    def __init__(self, dp, port):
        super(EventPortDelete, self).__init__(dp, port)


class EventPortModify(EventPortBase):
    """
    An event class for switch port status "MODIFY" notification.

    This event is generated when some attribute of a port is changed.
    For OpenFlow switches, one can get the same notification by observing
    os_ken.controller.ofp_event.EventOFPPortStatus.
    An instance has at least the following attributes.

    ========= ====================================================================
    Attribute Description
    ========= ====================================================================
    dp        A os_ken.controller.controller.Datapath instance of the switch
    port      port number
    ========= ====================================================================
    """

    def __init__(self, dp, new_port):
        super(EventPortModify, self).__init__(dp, new_port)


class PortState(dict):
    def __init__(self):
        super(PortState, self).__init__()

    def add(self, port_no, port):
        self[port_no] = port

    def remove(self, port_no):
        del self[port_no]

    def modify(self, port_no, port):
        self[port_no] = port


# this depends on controller::Datapath and dispatchers in handler
class DPSet(app_manager.OSKenApp):
    """
    DPSet application manages a set of switches (datapaths)
    connected to this controller.

    Usage Example::

        # ...(snip)...
        from os_ken.controller import dpset


        class MyApp(app_manager.OSKenApp):
            _CONTEXTS = {
                'dpset': dpset.DPSet,
            }

            def __init__(self, *args, **kwargs):
                super(MyApp, self).__init__(*args, **kwargs)
                # Stores DPSet instance to call its API in this app
                self.dpset = kwargs['dpset']

            def _my_handler(self):
                # Get the datapath object which has the given dpid
                dpid = 1
                dp = self.dpset.get(dpid)
                if dp is None:
                    self.logger.info('No such datapath: dpid=%d', dpid)
    """

    def __init__(self, *args, **kwargs):
        super(DPSet, self).__init__(*args, **kwargs)
        self.name = 'dpset'

        self.dps = {}   # datapath_id => class Datapath
        self.port_state = {}  # datapath_id => ports

    def _register(self, dp):
        LOG.debug('DPSET: register datapath %s', dp)
        assert dp.id is not None

        # while dpid should be unique, we need to handle duplicates here
        # because it's entirely possible for a switch to reconnect us
        # before we notice the drop of the previous connection.
        # in that case,
        # - forget the older connection as it likely will disappear soon
        # - do not send EventDP leave/enter events
        # - keep the PortState for the dpid
        send_dp_reconnected = False
        if dp.id in self.dps:
            self.logger.warning('DPSET: Multiple connections from %s',
                                dpid_to_str(dp.id))
            self.logger.debug('DPSET: Forgetting datapath %s', self.dps[dp.id])
            (self.dps[dp.id]).close()
            self.logger.debug('DPSET: New datapath %s', dp)
            send_dp_reconnected = True
        self.dps[dp.id] = dp
        if dp.id not in self.port_state:
            self.port_state[dp.id] = PortState()
            ev = EventDP(dp, True)
            with warnings.catch_warnings():
                warnings.simplefilter('ignore')
                for port in dp.ports.values():
                    self._port_added(dp, port)
                    ev.ports.append(port)
            self.send_event_to_observers(ev)
        if send_dp_reconnected:
            ev = EventDPReconnected(dp)
            ev.ports = self.port_state.get(dp.id, {}).values()
            self.send_event_to_observers(ev)

    def _unregister(self, dp):
        # see the comment in _register().
        if dp not in self.dps.values():
            return
        LOG.debug('DPSET: unregister datapath %s', dp)
        assert self.dps[dp.id] == dp

        # Now datapath is already dead, so port status change event doesn't
        # interfere us.
        ev = EventDP(dp, False)
        for port in list(self.port_state.get(dp.id, {}).values()):
            self._port_deleted(dp, port)
            ev.ports.append(port)

        self.send_event_to_observers(ev)

        del self.dps[dp.id]
        del self.port_state[dp.id]

    def get(self, dp_id):
        """
        This method returns the os_ken.controller.controller.Datapath
        instance for the given Datapath ID.
        """
        return self.dps.get(dp_id)

    def get_all(self):
        """
        This method returns a list of tuples which represents
        instances for switches connected to this controller.
        The tuple consists of a Datapath ID and an instance of
        os_ken.controller.controller.Datapath.

        A return value looks like the following::

            [ (dpid_A, Datapath_A), (dpid_B, Datapath_B), ... ]
        """
        return list(self.dps.items())

    def _port_added(self, datapath, port):
        self.port_state[datapath.id].add(port.port_no, port)

    def _port_deleted(self, datapath, port):
        self.port_state[datapath.id].remove(port.port_no)

    @set_ev_cls(ofp_event.EventOFPStateChange,
                [handler.MAIN_DISPATCHER, handler.DEAD_DISPATCHER])
    def dispatcher_change(self, ev):
        datapath = ev.datapath
        assert datapath is not None
        if ev.state == handler.MAIN_DISPATCHER:
            self._register(datapath)
        elif ev.state == handler.DEAD_DISPATCHER:
            self._unregister(datapath)

    @set_ev_cls(ofp_event.EventOFPSwitchFeatures, handler.CONFIG_DISPATCHER)
    def switch_features_handler(self, ev):
        msg = ev.msg
        datapath = msg.datapath
        # ofp_handler.py does the following so we could remove...
        if datapath.ofproto.OFP_VERSION < 0x04:
            datapath.ports = msg.ports

    @set_ev_cls(ofp_event.EventOFPPortStatus, handler.MAIN_DISPATCHER)
    def port_status_handler(self, ev):
        msg = ev.msg
        reason = msg.reason
        datapath = msg.datapath
        port = msg.desc
        ofproto = datapath.ofproto

        if reason == ofproto.OFPPR_ADD:
            LOG.debug('DPSET: A port was added.' +
                      '(datapath id = %s, port number = %s)',
                      dpid_to_str(datapath.id), port.port_no)
            self._port_added(datapath, port)
            self.send_event_to_observers(EventPortAdd(datapath, port))
        elif reason == ofproto.OFPPR_DELETE:
            LOG.debug('DPSET: A port was deleted.' +
                      '(datapath id = %s, port number = %s)',
                      dpid_to_str(datapath.id), port.port_no)
            self._port_deleted(datapath, port)
            self.send_event_to_observers(EventPortDelete(datapath, port))
        else:
            assert reason == ofproto.OFPPR_MODIFY
            LOG.debug('DPSET: A port was modified.' +
                      '(datapath id = %s, port number = %s)',
                      dpid_to_str(datapath.id), port.port_no)
            self.port_state[datapath.id].modify(port.port_no, port)
            self.send_event_to_observers(EventPortModify(datapath, port))

    def get_port(self, dpid, port_no):
        """
        This method returns the os_ken.controller.dpset.PortState
        instance for the given Datapath ID and the port number.
        Raises os_ken_exc.PortNotFound if no such a datapath connected to
        this controller or no such a port exists.
        """
        try:
            return self.port_state[dpid][port_no]
        except KeyError:
            raise os_ken_exc.PortNotFound(dpid=dpid, port=port_no,
                                       network_id=None)

    def get_ports(self, dpid):
        """
        This method returns a list of os_ken.controller.dpset.PortState
        instances for the given Datapath ID.
        Raises KeyError if no such a datapath connected to this controller.
        """
        return list(self.port_state[dpid].values())


handler.register_service('os_ken.controller.dpset')
