# 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 datetime
import logging

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
from os_ken.exception import OSKenException
from os_ken.exception import OFPUnknownVersion
from os_ken.lib import hub
from os_ken.lib import mac
from os_ken.lib.dpid import dpid_to_str
from os_ken.lib.packet import bpdu
from os_ken.lib.packet import ethernet
from os_ken.lib.packet import llc
from os_ken.lib.packet import packet
from os_ken.ofproto import ofproto_v1_0
from os_ken.ofproto import ofproto_v1_2
from os_ken.ofproto import ofproto_v1_3


MAX_PORT_NO = 0xfff

# for OpenFlow 1.2/1.3
BPDU_PKT_IN_PRIORITY = 0xffff
NO_PKT_IN_PRIORITY = 0xfffe


# Result of compared config BPDU priority.
SUPERIOR = -1
REPEATED = 0
INFERIOR = 1

# Port role
DESIGNATED_PORT = 0  # The port which sends BPDU.
ROOT_PORT = 1  # The port which receives BPDU from a root bridge.
NON_DESIGNATED_PORT = 2  # The port which blocked.

""" How to decide the port roles.

     Root bridge:
       a bridge has smallest bridge ID is chosen as a root.
       it sends original config BPDU.
     Non Root bridge:
       forwards config BPDU received from the root bridge.

                   +-----------------------+
                   |      Root bridge      |
                   +-----------------------+
                     (D)               (D)
                      |                 |
                      |                 |
                     (R)               (R)
        +-----------------+          +-----------------+
        | Non Root bridge |(D)---(ND)| Non Root bridge |
        +-----------------+          +-----------------+

     ROOT_PORT(R):
       the nearest port to a root bridge of the bridge.
       it is determined by the cost of the path, etc.
     DESIGNATED_PORT(D):
       the port of the side near the root bridge of each link.
       it is determined by the cost of the path, etc.
     NON_DESIGNATED_PORT(ND):
       the port other than a ROOT_PORT and DESIGNATED_PORT.
"""


# Port state
#  DISABLE: Administratively down or link down by an obstacle.
#  BLOCK  : Not part of spanning tree.
#  LISTEN : Not learning or relaying frames.
#  LEARN  : Learning but not relaying frames.
#  FORWARD: Learning and relaying frames.
PORT_STATE_DISABLE = 0
PORT_STATE_BLOCK = 1
PORT_STATE_LISTEN = 2
PORT_STATE_LEARN = 3
PORT_STATE_FORWARD = 4

# for OpenFlow 1.0
PORT_CONFIG_V1_0 = {PORT_STATE_DISABLE: (ofproto_v1_0.OFPPC_NO_RECV_STP
                                         | ofproto_v1_0.OFPPC_NO_RECV
                                         | ofproto_v1_0.OFPPC_NO_FLOOD
                                         | ofproto_v1_0.OFPPC_NO_FWD),
                    PORT_STATE_BLOCK: (ofproto_v1_0.OFPPC_NO_RECV
                                       | ofproto_v1_0.OFPPC_NO_FLOOD
                                       | ofproto_v1_0.OFPPC_NO_FWD),
                    PORT_STATE_LISTEN: (ofproto_v1_0.OFPPC_NO_RECV
                                        | ofproto_v1_0.OFPPC_NO_FLOOD),
                    PORT_STATE_LEARN: ofproto_v1_0.OFPPC_NO_FLOOD,
                    PORT_STATE_FORWARD: 0}

# for OpenFlow 1.2
PORT_CONFIG_V1_2 = {PORT_STATE_DISABLE: (ofproto_v1_2.OFPPC_NO_RECV
                                         | ofproto_v1_2.OFPPC_NO_FWD),
                    PORT_STATE_BLOCK: (ofproto_v1_2.OFPPC_NO_FWD
                                       | ofproto_v1_2.OFPPC_NO_PACKET_IN),
                    PORT_STATE_LISTEN: ofproto_v1_2.OFPPC_NO_PACKET_IN,
                    PORT_STATE_LEARN: ofproto_v1_2.OFPPC_NO_PACKET_IN,
                    PORT_STATE_FORWARD: 0}

# for OpenFlow 1.3
PORT_CONFIG_V1_3 = {PORT_STATE_DISABLE: (ofproto_v1_3.OFPPC_NO_RECV
                                         | ofproto_v1_3.OFPPC_NO_FWD),
                    PORT_STATE_BLOCK: (ofproto_v1_3.OFPPC_NO_FWD
                                       | ofproto_v1_3.OFPPC_NO_PACKET_IN),
                    PORT_STATE_LISTEN: ofproto_v1_3.OFPPC_NO_PACKET_IN,
                    PORT_STATE_LEARN: ofproto_v1_3.OFPPC_NO_PACKET_IN,
                    PORT_STATE_FORWARD: 0}

""" Port state machine

    +------------------------<--------------------------+
    |                                                   |*2
    +--> [BLOCK] -----+--> [LISTEN] ----> [LEARN] ------+----> [FORWARD]
                  *3  |        |    15sec    |    15sec   *1       |
                      |        |*3           |*3                   |*3
                      +----<---+------<------+----------<----------+

          *1 if port role == DESIGNATED_PORT or ROOT_PORT
          *2 if port role == NON_DESIGNATED_PORT
          *3 re-calculation of Spanning tree occurred.

      When bridge has started, each port state is set to [LISTEN]
       except port configuration is disable.
      If port configuration is disable or link down occurred,
       the port state is set to [DISABLE]
"""


# Throw this event when network topology is changed.
# Flush filtering database, when you receive this event.
class EventTopologyChange(event.EventBase):
    def __init__(self, dp):
        super(EventTopologyChange, self).__init__()
        self.dp = dp


# Throw this event when port status is changed.
class EventPortStateChange(event.EventBase):
    def __init__(self, dp, port):
        super(EventPortStateChange, self).__init__()
        self.dp = dp
        self.port_no = port.ofport.port_no
        self.port_state = port.state


# Event for receive packet in message except BPDU packet.
class EventPacketIn(event.EventBase):
    def __init__(self, msg):
        super(EventPacketIn, self).__init__()
        self.msg = msg


# For Python3 compatibility
# Note: The following is the official workaround for cmp() in Python2.
# https://docs.python.org/3.0/whatsnew/3.0.html#ordering-comparisons
def cmp(a, b):
    return (a > b) - (a < b)


class Stp(app_manager.OSKenApp):
    """ STP(spanning tree) library. """

    OFP_VERSIONS = [ofproto_v1_0.OFP_VERSION,
                    ofproto_v1_2.OFP_VERSION,
                    ofproto_v1_3.OFP_VERSION]

    def __init__(self):
        super(Stp, self).__init__()
        self.name = 'stplib'
        self._set_logger()
        self.config = {}
        self.bridge_list = {}

    def close(self):
        for dpid in self.bridge_list:
            self._unregister_bridge(dpid)

    def _set_logger(self):
        self.logger.propagate = False
        hdlr = logging.StreamHandler()
        fmt_str = '[STP][%(levelname)s] dpid=%(dpid)s: %(message)s'
        hdlr.setFormatter(logging.Formatter(fmt_str))
        self.logger.addHandler(hdlr)

    def set_config(self, config):
        """ Use this API if you want to set up configuration
             of each bridge and ports.
            Set configuration with 'config' parameter as follows.

             config = {<dpid>: {'bridge': {'priority': <value>,
                                           'sys_ext_id': <value>,
                                           'max_age': <value>,
                                           'hello_time': <value>,
                                           'fwd_delay': <value>}
                                'ports': {<port_no>: {'priority': <value>,
                                                      'path_cost': <value>,
                                                      'enable': <True/False>},
                                          <port_no>: {...},,,}}
                       <dpid>: {...},
                       <dpid>: {...},,,}

             NOTE: You may omit each field.
                    If omitted, a default value is set up.
                   It becomes effective when a bridge starts.

             Default values:
             ------------------------------------------------------
             | bridge | priority   | bpdu.DEFAULT_BRIDGE_PRIORITY |
             |        | sys_ext_id | 0                            |
             |        | max_age    | bpdu.DEFAULT_MAX_AGE         |
             |        | hello_time | bpdu.DEFAULT_HELLO_TIME      |
             |        | fwd_delay  | bpdu.DEFAULT_FORWARD_DELAY   |
             |--------|------------|------------------------------|
             | port   | priority   | bpdu.DEFAULT_PORT_PRIORITY   |
             |        | path_cost  | (Set up automatically        |
             |        |            |   according to link speed.)  |
             |        | enable     | True                         |
             ------------------------------------------------------
        """
        assert isinstance(config, dict)
        self.config = config

    @set_ev_cls(ofp_event.EventOFPStateChange,
                [handler.MAIN_DISPATCHER, handler.DEAD_DISPATCHER])
    def dispacher_change(self, ev):
        assert ev.datapath is not None
        if ev.state == handler.MAIN_DISPATCHER:
            self._register_bridge(ev.datapath)
        elif ev.state == handler.DEAD_DISPATCHER:
            self._unregister_bridge(ev.datapath.id)

    def _register_bridge(self, dp):
        self._unregister_bridge(dp.id)

        dpid_str = {'dpid': dpid_to_str(dp.id)}
        self.logger.info('Join as stp bridge.', extra=dpid_str)
        try:
            bridge = Bridge(dp, self.logger,
                            self.config.get(dp.id, {}),
                            self.send_event_to_observers)
        except OFPUnknownVersion as message:
            self.logger.error(str(message), extra=dpid_str)
            return

        self.bridge_list[dp.id] = bridge

    def _unregister_bridge(self, dp_id):
        if dp_id in self.bridge_list:
            self.bridge_list[dp_id].delete()
            del self.bridge_list[dp_id]
            self.logger.info('Leave stp bridge.',
                             extra={'dpid': dpid_to_str(dp_id)})

    @set_ev_cls(ofp_event.EventOFPPacketIn, handler.MAIN_DISPATCHER)
    def packet_in_handler(self, ev):
        if ev.msg.datapath.id in self.bridge_list:
            bridge = self.bridge_list[ev.msg.datapath.id]
            bridge.packet_in_handler(ev.msg)

    @set_ev_cls(ofp_event.EventOFPPortStatus, handler.MAIN_DISPATCHER)
    def port_status_handler(self, ev):
        dp = ev.msg.datapath
        dpid_str = {'dpid': dpid_to_str(dp.id)}
        port = ev.msg.desc
        reason = ev.msg.reason
        link_down_flg = port.state & 0b1

        if dp.id in self.bridge_list:
            bridge = self.bridge_list[dp.id]

            if reason is dp.ofproto.OFPPR_ADD:
                self.logger.info('[port=%d] Port add.',
                                 port.port_no, extra=dpid_str)
                bridge.port_add(port)
            elif reason is dp.ofproto.OFPPR_DELETE:
                self.logger.info('[port=%d] Port delete.',
                                 port.port_no, extra=dpid_str)
                bridge.port_delete(port)
            else:
                assert reason is dp.ofproto.OFPPR_MODIFY
                if bridge.ports_state[port.port_no] == port.state:
                    # Do nothing
                    self.logger.debug('[port=%d] Link status not changed.',
                                      port.port_no, extra=dpid_str)
                    return
                if link_down_flg:
                    self.logger.info('[port=%d] Link down.',
                                     port.port_no, extra=dpid_str)
                    bridge.link_down(port)
                else:
                    self.logger.info('[port=%d] Link up.',
                                     port.port_no, extra=dpid_str)
                    bridge.link_up(port)

    @staticmethod
    def compare_root_path(path_cost1, path_cost2, bridge_id1, bridge_id2,
                          port_id1, port_id2):
        """ Decide the port of the side near a root bridge.
            It is compared by the following priorities.
             1. root path cost
             2. designated bridge ID value
             3. designated port ID value """
        result = Stp._cmp_value(path_cost1, path_cost2)
        if not result:
            result = Stp._cmp_value(bridge_id1, bridge_id2)
            if not result:
                result = Stp._cmp_value(port_id1, port_id2)
        return result

    @staticmethod
    def compare_bpdu_info(my_priority, my_times, rcv_priority, rcv_times):
        """ Check received BPDU is superior to currently held BPDU
             by the following comparison.
             - root bridge ID value
             - root path cost
             - designated bridge ID value
             - designated port ID value
             - times """
        if my_priority is None:
            result = SUPERIOR
        else:
            result = Stp._cmp_value(rcv_priority.root_id.value,
                                    my_priority.root_id.value)
            if not result:
                result = Stp.compare_root_path(
                    rcv_priority.root_path_cost,
                    my_priority.root_path_cost,
                    rcv_priority.designated_bridge_id.value,
                    my_priority.designated_bridge_id.value,
                    rcv_priority.designated_port_id.value,
                    my_priority.designated_port_id.value)
                if not result:
                    result1 = Stp._cmp_value(
                        rcv_priority.designated_bridge_id.value,
                        mac.haddr_to_int(
                            my_priority.designated_bridge_id.mac_addr))
                    result2 = Stp._cmp_value(
                        rcv_priority.designated_port_id.value,
                        my_priority.designated_port_id.port_no)
                    if not result1 and not result2:
                        result = SUPERIOR
                    else:
                        result = Stp._cmp_obj(rcv_times, my_times)
        return result

    @staticmethod
    def _cmp_value(value1, value2):
        result = cmp(value1, value2)
        if result < 0:
            return SUPERIOR
        elif result == 0:
            return REPEATED
        else:
            return INFERIOR

    @staticmethod
    def _cmp_obj(obj1, obj2):
        for key in obj1.__dict__.keys():
            if (not hasattr(obj2, key)
                    or getattr(obj1, key) != getattr(obj2, key)):
                return SUPERIOR
        return REPEATED


class Bridge(object):
    _DEFAULT_VALUE = {'priority': bpdu.DEFAULT_BRIDGE_PRIORITY,
                      'sys_ext_id': 0,
                      'max_age': bpdu.DEFAULT_MAX_AGE,
                      'hello_time': bpdu.DEFAULT_HELLO_TIME,
                      'fwd_delay': bpdu.DEFAULT_FORWARD_DELAY}

    def __init__(self, dp, logger, config, send_ev_func):
        super(Bridge, self).__init__()
        self.dp = dp
        self.logger = logger
        self.dpid_str = {'dpid': dpid_to_str(dp.id)}
        self.send_event = send_ev_func

        # Bridge data
        bridge_conf = config.get('bridge', {})
        values = self._DEFAULT_VALUE
        for key, value in bridge_conf.items():
            values[key] = value
        system_id = list(dp.ports.values())[0].hw_addr

        self.bridge_id = BridgeId(values['priority'],
                                  values['sys_ext_id'],
                                  system_id)
        self.bridge_times = Times(0,  # message_age
                                  values['max_age'],
                                  values['hello_time'],
                                  values['fwd_delay'])
        # Root bridge data
        self.root_priority = Priority(self.bridge_id, 0, None, None)
        self.root_times = self.bridge_times
        # Ports
        self.ports = {}
        self.ports_state = {}
        self.ports_conf = config.get('ports', {})
        for ofport in dp.ports.values():
            self.port_add(ofport)

        # Install BPDU PacketIn flow. (OpenFlow 1.2/1.3)
        if dp.ofproto == ofproto_v1_2 or dp.ofproto == ofproto_v1_3:
            ofctl = OfCtl_v1_2later(self.dp)
            ofctl.add_bpdu_pkt_in_flow()

    @property
    def is_root_bridge(self):
        return bool(self.bridge_id.value == self.root_priority.root_id.value)

    def delete(self):
        for port in self.ports.values():
            port.delete()

    def port_add(self, ofport):
        if ofport.port_no <= MAX_PORT_NO:
            port_conf = self.ports_conf.get(ofport.port_no, {})
            self.ports[ofport.port_no] = Port(self.dp, self.logger,
                                              port_conf, self.send_event,
                                              self.recalculate_spanning_tree,
                                              self.topology_change_notify,
                                              self.bridge_id,
                                              self.bridge_times,
                                              ofport)
            self.ports_state[ofport.port_no] = ofport.state

    def port_delete(self, ofp_port):
        self.link_down(ofp_port)
        self.ports[ofp_port.port_no].delete()
        del self.ports[ofp_port.port_no]
        del self.ports_state[ofp_port.port_no]

    def link_up(self, ofp_port):
        port = self.ports[ofp_port.port_no]
        port.up(DESIGNATED_PORT, self.root_priority, self.root_times)
        self.ports_state[ofp_port.port_no] = ofp_port.state

    def link_down(self, ofp_port):
        """ DESIGNATED_PORT/NON_DESIGNATED_PORT: change status to DISABLE.
            ROOT_PORT: change status to DISABLE and recalculate STP. """
        port = self.ports[ofp_port.port_no]
        init_stp_flg = bool(port.role is ROOT_PORT)

        port.down(PORT_STATE_DISABLE, msg_init=True)
        self.ports_state[ofp_port.port_no] = ofp_port.state
        if init_stp_flg:
            self.recalculate_spanning_tree()

    def packet_in_handler(self, msg):
        dp = msg.datapath
        if dp.ofproto == ofproto_v1_0:
            in_port_no = msg.in_port
        else:
            assert dp.ofproto == ofproto_v1_2 or dp.ofproto == ofproto_v1_3
            in_port_no = None
            for match_field in msg.match.fields:
                if match_field.header == dp.ofproto.OXM_OF_IN_PORT:
                    in_port_no = match_field.value
                    break
        if in_port_no not in self.ports:
            return

        in_port = self.ports[in_port_no]
        if in_port.state == PORT_STATE_DISABLE:
            return

        pkt = packet.Packet(msg.data)
        if bpdu.ConfigurationBPDUs in pkt:
            # Received Configuration BPDU.
            # - If received superior BPDU:
            #    Re-calculates spanning tree.
            # - If received Topology Change BPDU:
            #    Throws EventTopologyChange.
            #    Forwards Topology Change BPDU.
            (bpdu_pkt, ) = pkt.get_protocols(bpdu.ConfigurationBPDUs)
            if bpdu_pkt.message_age > bpdu_pkt.max_age:
                log_msg = 'Drop BPDU packet which message_age exceeded.'
                self.logger.debug(log_msg, extra=self.dpid_str)
                return

            rcv_info, rcv_tc = in_port.rcv_config_bpdu(bpdu_pkt)

            if rcv_info is SUPERIOR:
                self.logger.info('[port=%d] Receive superior BPDU.',
                                 in_port_no, extra=self.dpid_str)
                self.recalculate_spanning_tree(init=False)

            elif rcv_tc:
                self.send_event(EventTopologyChange(self.dp))

            if in_port.role is ROOT_PORT:
                self._forward_tc_bpdu(rcv_tc)

        elif bpdu.TopologyChangeNotificationBPDUs in pkt:
            # Received Topology Change Notification BPDU.
            # Send Topology Change Ack BPDU and throws EventTopologyChange.
            # - Root bridge:
            #    Sends Topology Change BPDU from all port.
            # - Non root bridge:
            #    Sends Topology Change Notification BPDU to root bridge.
            in_port.transmit_ack_bpdu()
            self.topology_change_notify(None)

        elif bpdu.RstBPDUs in pkt:
            # Received Rst BPDU.
            # TODO: RSTP
            pass

        else:
            # Received non BPDU packet.
            # Throws EventPacketIn.
            self.send_event(EventPacketIn(msg))

    def recalculate_spanning_tree(self, init=True):
        """ Re-calculation of spanning tree. """
        # All port down.
        for port in self.ports.values():
            if port.state is not PORT_STATE_DISABLE:
                port.down(PORT_STATE_BLOCK, msg_init=init)

        # Send topology change event.
        if init:
            self.send_event(EventTopologyChange(self.dp))

        # Update tree roles.
        port_roles = {}
        self.root_priority = Priority(self.bridge_id, 0, None, None)
        self.root_times = self.bridge_times

        if init:
            self.logger.info('Root bridge.', extra=self.dpid_str)
            for port_no in self.ports:
                port_roles[port_no] = DESIGNATED_PORT
        else:
            (port_roles,
             self.root_priority,
             self.root_times) = self._spanning_tree_algorithm()

        # All port up.
        for port_no, role in port_roles.items():
            if self.ports[port_no].state is not PORT_STATE_DISABLE:
                self.ports[port_no].up(role, self.root_priority,
                                       self.root_times)

    def _spanning_tree_algorithm(self):
        """ Update tree roles.
             - Root bridge:
                all port is DESIGNATED_PORT.
             - Non root bridge:
                select one ROOT_PORT and some DESIGNATED_PORT,
                and the other port is set to NON_DESIGNATED_PORT."""
        port_roles = {}

        root_port = self._select_root_port()

        if root_port is None:
            # My bridge is a root bridge.
            self.logger.info('Root bridge.', extra=self.dpid_str)
            root_priority = self.root_priority
            root_times = self.root_times

            for port_no in self.ports:
                if self.ports[port_no].state is not PORT_STATE_DISABLE:
                    port_roles[port_no] = DESIGNATED_PORT
        else:
            # Other bridge is a root bridge.
            self.logger.info('Non root bridge.', extra=self.dpid_str)
            root_priority = root_port.designated_priority
            root_times = root_port.designated_times

            port_roles[root_port.ofport.port_no] = ROOT_PORT

            d_ports = self._select_designated_port(root_port)
            for port_no in d_ports:
                port_roles[port_no] = DESIGNATED_PORT

            for port in self.ports.values():
                if port.state is not PORT_STATE_DISABLE:
                    port_roles.setdefault(port.ofport.port_no,
                                          NON_DESIGNATED_PORT)

        return port_roles, root_priority, root_times

    def _select_root_port(self):
        """ ROOT_PORT is the nearest port to a root bridge.
            It is determined by the cost of path, etc. """
        root_port = None

        for port in self.ports.values():
            root_msg = (self.root_priority if root_port is None
                        else root_port.designated_priority)
            port_msg = port.designated_priority
            if port.state is PORT_STATE_DISABLE or port_msg is None:
                continue
            if root_msg.root_id.value > port_msg.root_id.value:
                result = SUPERIOR
            elif root_msg.root_id.value == port_msg.root_id.value:
                if root_msg.designated_bridge_id is None:
                    result = INFERIOR
                else:
                    result = Stp.compare_root_path(
                        port_msg.root_path_cost,
                        root_msg.root_path_cost,
                        port_msg.designated_bridge_id.value,
                        root_msg.designated_bridge_id.value,
                        port_msg.designated_port_id.value,
                        root_msg.designated_port_id.value)
            else:
                result = INFERIOR

            if result is SUPERIOR:
                root_port = port

        return root_port

    def _select_designated_port(self, root_port):
        """ DESIGNATED_PORT is a port of the side near the root bridge
            of each link. It is determined by the cost of each path, etc
            same as ROOT_PORT. """
        d_ports = []
        root_msg = root_port.designated_priority

        for port in self.ports.values():
            port_msg = port.designated_priority
            if (port.state is PORT_STATE_DISABLE
                    or port.ofport.port_no == root_port.ofport.port_no):
                continue
            if (port_msg is None or
                    (port_msg.root_id.value != root_msg.root_id.value)):
                d_ports.append(port.ofport.port_no)
            else:
                result = Stp.compare_root_path(
                    root_msg.root_path_cost,
                    port_msg.root_path_cost - port.path_cost,
                    self.bridge_id.value,
                    port_msg.designated_bridge_id.value,
                    port.port_id.value,
                    port_msg.designated_port_id.value)
                if result is SUPERIOR:
                    d_ports.append(port.ofport.port_no)

        return d_ports

    def topology_change_notify(self, port_state):
        notice = False
        if port_state is PORT_STATE_FORWARD:
            for port in self.ports.values():
                if port.role is DESIGNATED_PORT:
                    notice = True
                    break
        else:
            notice = True

        if notice:
            self.send_event(EventTopologyChange(self.dp))
            if self.is_root_bridge:
                self._transmit_tc_bpdu()
            else:
                self._transmit_tcn_bpdu()

    def _transmit_tc_bpdu(self):
        for port in self.ports.values():
            port.transmit_tc_bpdu()

    def _transmit_tcn_bpdu(self):
        root_port = None
        for port in self.ports.values():
            if port.role is ROOT_PORT:
                root_port = port
                break
        if root_port:
            root_port.transmit_tcn_bpdu()

    def _forward_tc_bpdu(self, fwd_flg):
        for port in self.ports.values():
            port.send_tc_flg = fwd_flg


class Port(object):
    _DEFAULT_VALUE = {'priority': bpdu.DEFAULT_PORT_PRIORITY,
                      'path_cost': bpdu.PORT_PATH_COST_10MB,
                      'enable': True}

    def __init__(self, dp, logger, config, send_ev_func, timeout_func,
                 topology_change_func, bridge_id, bridge_times, ofport):
        super(Port, self).__init__()
        self.dp = dp
        self.logger = logger
        self.dpid_str = {'dpid': dpid_to_str(dp.id)}
        self.config_enable = config.get('enable',
                                        self._DEFAULT_VALUE['enable'])
        self.send_event = send_ev_func
        self.wait_bpdu_timeout = timeout_func
        self.topology_change_notify = topology_change_func
        self.ofctl = (OfCtl_v1_0(dp) if dp.ofproto == ofproto_v1_0
                      else OfCtl_v1_2later(dp))

        # Bridge data
        self.bridge_id = bridge_id
        # Root bridge data
        self.port_priority = None
        self.port_times = None
        # ofproto_v1_X_parser.OFPPhyPort data
        self.ofport = ofport
        # Port data
        values = self._DEFAULT_VALUE
        path_costs = {dp.ofproto.OFPPF_10MB_HD: bpdu.PORT_PATH_COST_10MB,
                      dp.ofproto.OFPPF_10MB_FD: bpdu.PORT_PATH_COST_10MB,
                      dp.ofproto.OFPPF_100MB_HD: bpdu.PORT_PATH_COST_100MB,
                      dp.ofproto.OFPPF_100MB_FD: bpdu.PORT_PATH_COST_100MB,
                      dp.ofproto.OFPPF_1GB_HD: bpdu.PORT_PATH_COST_1GB,
                      dp.ofproto.OFPPF_1GB_FD: bpdu.PORT_PATH_COST_1GB,
                      dp.ofproto.OFPPF_10GB_FD: bpdu.PORT_PATH_COST_10GB}
        for rate in sorted(path_costs, reverse=True):
            if ofport.curr & rate:
                values['path_cost'] = path_costs[rate]
                break
        for key, value in values.items():
            values[key] = value
        self.port_id = PortId(values['priority'], ofport.port_no)
        self.path_cost = values['path_cost']
        self.state = (None if self.config_enable else PORT_STATE_DISABLE)
        self.role = None
        # Receive BPDU data
        self.designated_priority = None
        self.designated_times = None
        # BPDU handling threads
        self.send_bpdu_thread = PortThread(self._transmit_bpdu)
        self.wait_bpdu_thread = PortThread(self._wait_bpdu_timer)
        self.send_tc_flg = None
        self.send_tc_timer = None
        self.send_tcn_flg = None
        self.wait_timer_event = None
        # State machine thread
        self.state_machine = PortThread(self._state_machine)
        self.state_event = None

        self.up(DESIGNATED_PORT,
                Priority(bridge_id, 0, None, None),
                bridge_times)

        self.state_machine.start()
        self.logger.debug('[port=%d] Start port state machine.',
                          self.ofport.port_no, extra=self.dpid_str)

    def delete(self):
        self.state_machine.stop()
        self.send_bpdu_thread.stop()
        self.wait_bpdu_thread.stop()
        if self.state_event is not None:
            self.state_event.set()
            self.state_event = None
        if self.wait_timer_event is not None:
            self.wait_timer_event.set()
            self.wait_timer_event = None
        self.logger.debug('[port=%d] Stop port threads.',
                          self.ofport.port_no, extra=self.dpid_str)

    def up(self, role, root_priority, root_times):
        """ A port is started in the state of LISTEN.  """
        self.port_priority = root_priority
        self.port_times = root_times

        state = (PORT_STATE_LISTEN if self.config_enable
                 else PORT_STATE_DISABLE)
        self._change_role(role)
        self._change_status(state)

    def down(self, state, msg_init=False):
        """ A port will be in the state of DISABLE or BLOCK,
             and be stopped.  """
        assert (state is PORT_STATE_DISABLE
                or state is PORT_STATE_BLOCK)
        if not self.config_enable:
            return

        if msg_init:
            self.designated_priority = None
            self.designated_times = None

        self._change_role(DESIGNATED_PORT)
        self._change_status(state)

    def _state_machine(self):
        """ Port state machine.
             Change next status when timer is exceeded
             or _change_status() method is called."""
        role_str = {ROOT_PORT: 'ROOT_PORT          ',
                    DESIGNATED_PORT: 'DESIGNATED_PORT    ',
                    NON_DESIGNATED_PORT: 'NON_DESIGNATED_PORT'}
        state_str = {PORT_STATE_DISABLE: 'DISABLE',
                     PORT_STATE_BLOCK: 'BLOCK',
                     PORT_STATE_LISTEN: 'LISTEN',
                     PORT_STATE_LEARN: 'LEARN',
                     PORT_STATE_FORWARD: 'FORWARD'}

        if self.state is PORT_STATE_DISABLE:
            self.ofctl.set_port_status(self.ofport, self.state)

        while True:
            self.logger.info('[port=%d] %s / %s', self.ofport.port_no,
                             role_str[self.role], state_str[self.state],
                             extra=self.dpid_str)

            self.state_event = hub.Event()
            timer = self._get_timer()
            if timer:
                timeout = hub.Timeout(timer)
                try:
                    self.state_event.wait()
                except hub.Timeout as t:
                    if t is not timeout:
                        err_msg = 'Internal error. Not my timeout.'
                        raise OSKenException(msg=err_msg)
                    new_state = self._get_next_state()
                    self._change_status(new_state, thread_switch=False)
                finally:
                    timeout.cancel()
            else:
                self.state_event.wait()

            self.state_event = None

    def _get_timer(self):
        timer = {PORT_STATE_DISABLE: None,
                 PORT_STATE_BLOCK: None,
                 PORT_STATE_LISTEN: self.port_times.forward_delay,
                 PORT_STATE_LEARN: self.port_times.forward_delay,
                 PORT_STATE_FORWARD: None}
        return timer[self.state]

    def _get_next_state(self):
        next_state = {PORT_STATE_DISABLE: None,
                      PORT_STATE_BLOCK: None,
                      PORT_STATE_LISTEN: PORT_STATE_LEARN,
                      PORT_STATE_LEARN: (PORT_STATE_FORWARD
                                         if (self.role is ROOT_PORT or
                                             self.role is DESIGNATED_PORT)
                                         else PORT_STATE_BLOCK),
                      PORT_STATE_FORWARD: None}
        return next_state[self.state]

    def _change_status(self, new_state, thread_switch=True):
        if new_state is not PORT_STATE_DISABLE:
            self.ofctl.set_port_status(self.ofport, new_state)

        if (new_state is PORT_STATE_FORWARD
           or (self.state is PORT_STATE_FORWARD
               and (new_state is PORT_STATE_DISABLE
                    or new_state is PORT_STATE_BLOCK))):
            self.topology_change_notify(new_state)

        if (new_state is PORT_STATE_DISABLE
                or new_state is PORT_STATE_BLOCK):
            self.send_tc_flg = False
            self.send_tc_timer = None
            self.send_tcn_flg = False
            self.send_bpdu_thread.stop()
        elif new_state is PORT_STATE_LISTEN:
            self.send_bpdu_thread.start()

        self.state = new_state
        self.send_event(EventPortStateChange(self.dp, self))

        if self.state_event is not None:
            self.state_event.set()
            self.state_event = None
        if thread_switch:
            hub.sleep(0)  # For thread switching.

    def _change_role(self, new_role):
        if self.role is new_role:
            return
        self.role = new_role
        if (new_role is ROOT_PORT
                or new_role is NON_DESIGNATED_PORT):
            self.wait_bpdu_thread.start()
        else:
            assert new_role is DESIGNATED_PORT
            self.wait_bpdu_thread.stop()

    def rcv_config_bpdu(self, bpdu_pkt):
        # Check received BPDU is superior to currently held BPDU.
        root_id = BridgeId(bpdu_pkt.root_priority,
                           bpdu_pkt.root_system_id_extension,
                           bpdu_pkt.root_mac_address)
        root_path_cost = bpdu_pkt.root_path_cost
        designated_bridge_id = BridgeId(bpdu_pkt.bridge_priority,
                                        bpdu_pkt.bridge_system_id_extension,
                                        bpdu_pkt.bridge_mac_address)
        designated_port_id = PortId(bpdu_pkt.port_priority,
                                    bpdu_pkt.port_number)

        msg_priority = Priority(root_id, root_path_cost,
                                designated_bridge_id,
                                designated_port_id)
        msg_times = Times(bpdu_pkt.message_age,
                          bpdu_pkt.max_age,
                          bpdu_pkt.hello_time,
                          bpdu_pkt.forward_delay)

        rcv_info = Stp.compare_bpdu_info(self.designated_priority,
                                         self.designated_times,
                                         msg_priority, msg_times)
        if rcv_info is SUPERIOR:
            self.designated_priority = msg_priority
            self.designated_times = msg_times

        chk_flg = False
        if ((rcv_info is SUPERIOR or rcv_info is REPEATED)
                and (self.role is ROOT_PORT
                     or self.role is NON_DESIGNATED_PORT)):
            self._update_wait_bpdu_timer()
            chk_flg = True
        elif rcv_info is INFERIOR and self.role is DESIGNATED_PORT:
            chk_flg = True

        # Check TopologyChange flag.
        rcv_tc = False
        if chk_flg:
            tc_flag_mask = 0b00000001
            tcack_flag_mask = 0b10000000
            if bpdu_pkt.flags & tc_flag_mask:
                self.logger.debug('[port=%d] receive TopologyChange BPDU.',
                                  self.ofport.port_no, extra=self.dpid_str)
                rcv_tc = True
            if bpdu_pkt.flags & tcack_flag_mask:
                self.logger.debug('[port=%d] receive TopologyChangeAck BPDU.',
                                  self.ofport.port_no, extra=self.dpid_str)
                if self.send_tcn_flg:
                    self.send_tcn_flg = False

        return rcv_info, rcv_tc

    def _update_wait_bpdu_timer(self):
        if self.wait_timer_event is not None:
            self.wait_timer_event.set()
            self.wait_timer_event = None
            self.logger.debug('[port=%d] Wait BPDU timer is updated.',
                              self.ofport.port_no, extra=self.dpid_str)
        hub.sleep(0)  # For thread switching.

    def _wait_bpdu_timer(self):
        time_exceed = False

        while True:
            self.wait_timer_event = hub.Event()
            message_age = (self.designated_times.message_age
                           if self.designated_times else 0)
            timer = self.port_times.max_age - message_age
            timeout = hub.Timeout(timer)
            try:
                self.wait_timer_event.wait()
            except hub.Timeout as t:
                if t is not timeout:
                    err_msg = 'Internal error. Not my timeout.'
                    raise OSKenException(msg=err_msg)
                self.logger.info('[port=%d] Wait BPDU timer is exceeded.',
                                 self.ofport.port_no, extra=self.dpid_str)
                time_exceed = True
            finally:
                timeout.cancel()
                self.wait_timer_event = None

            if time_exceed:
                break

        if time_exceed:  # Bridge.recalculate_spanning_tree
            hub.spawn(self.wait_bpdu_timeout)

    def _transmit_bpdu(self):
        while True:
            # Send config BPDU packet if port role is DESIGNATED_PORT.
            if self.role == DESIGNATED_PORT:
                now = datetime.datetime.today()
                if self.send_tc_timer and self.send_tc_timer < now:
                    self.send_tc_timer = None
                    self.send_tc_flg = False

                if not self.send_tc_flg:
                    flags = 0b00000000
                    log_msg = '[port=%d] Send Config BPDU.'
                else:
                    flags = 0b00000001
                    log_msg = '[port=%d] Send TopologyChange BPDU.'
                bpdu_data = self._generate_config_bpdu(flags)
                self.ofctl.send_packet_out(self.ofport.port_no, bpdu_data)
                self.logger.debug(log_msg, self.ofport.port_no,
                                  extra=self.dpid_str)

            # Send Topology Change Notification BPDU until receive Ack.
            if self.send_tcn_flg:
                bpdu_data = self._generate_tcn_bpdu()
                self.ofctl.send_packet_out(self.ofport.port_no, bpdu_data)
                self.logger.debug('[port=%d] Send TopologyChangeNotify BPDU.',
                                  self.ofport.port_no, extra=self.dpid_str)

            hub.sleep(self.port_times.hello_time)

    def transmit_tc_bpdu(self):
        """ Set send_tc_flg to send Topology Change BPDU. """
        if not self.send_tc_flg:
            timer = datetime.timedelta(seconds=self.port_times.max_age
                                       + self.port_times.forward_delay)
            self.send_tc_timer = datetime.datetime.today() + timer
            self.send_tc_flg = True

    def transmit_ack_bpdu(self):
        """ Send Topology Change Ack BPDU. """
        ack_flags = 0b10000001
        bpdu_data = self._generate_config_bpdu(ack_flags)
        self.ofctl.send_packet_out(self.ofport.port_no, bpdu_data)

    def transmit_tcn_bpdu(self):
        self.send_tcn_flg = True

    def _generate_config_bpdu(self, flags):
        src_mac = self.ofport.hw_addr
        dst_mac = bpdu.BRIDGE_GROUP_ADDRESS
        length = (bpdu.bpdu._PACK_LEN + bpdu.ConfigurationBPDUs.PACK_LEN
                  + llc.llc._PACK_LEN + llc.ControlFormatU._PACK_LEN)

        e = ethernet.ethernet(dst_mac, src_mac, length)
        l = llc.llc(llc.SAP_BPDU, llc.SAP_BPDU, llc.ControlFormatU())
        b = bpdu.ConfigurationBPDUs(
            flags=flags,
            root_priority=self.port_priority.root_id.priority,
            root_mac_address=self.port_priority.root_id.mac_addr,
            root_path_cost=self.port_priority.root_path_cost + self.path_cost,
            bridge_priority=self.bridge_id.priority,
            bridge_mac_address=self.bridge_id.mac_addr,
            port_priority=self.port_id.priority,
            port_number=self.ofport.port_no,
            message_age=self.port_times.message_age + 1,
            max_age=self.port_times.max_age,
            hello_time=self.port_times.hello_time,
            forward_delay=self.port_times.forward_delay)

        pkt = packet.Packet()
        pkt.add_protocol(e)
        pkt.add_protocol(l)
        pkt.add_protocol(b)
        pkt.serialize()

        return pkt.data

    def _generate_tcn_bpdu(self):
        src_mac = self.ofport.hw_addr
        dst_mac = bpdu.BRIDGE_GROUP_ADDRESS
        length = (bpdu.bpdu._PACK_LEN
                  + bpdu.TopologyChangeNotificationBPDUs.PACK_LEN
                  + llc.llc._PACK_LEN + llc.ControlFormatU._PACK_LEN)

        e = ethernet.ethernet(dst_mac, src_mac, length)
        l = llc.llc(llc.SAP_BPDU, llc.SAP_BPDU, llc.ControlFormatU())
        b = bpdu.TopologyChangeNotificationBPDUs()

        pkt = packet.Packet()
        pkt.add_protocol(e)
        pkt.add_protocol(l)
        pkt.add_protocol(b)
        pkt.serialize()

        return pkt.data


class PortThread(object):
    def __init__(self, function):
        super(PortThread, self).__init__()
        self.function = function
        self.thread = None

    def start(self):
        self.stop()
        self.thread = hub.spawn(self.function)

    def stop(self):
        if self.thread is not None:
            hub.kill(self.thread)
            hub.joinall([self.thread])
            self.thread = None


class BridgeId(object):
    def __init__(self, priority, system_id_extension, mac_addr):
        super(BridgeId, self).__init__()
        self.priority = priority
        self.system_id_extension = system_id_extension
        self.mac_addr = mac_addr
        self.value = bpdu.ConfigurationBPDUs.encode_bridge_id(
            priority, system_id_extension, mac_addr)


class PortId(object):
    def __init__(self, priority, port_no):
        super(PortId, self).__init__()
        self.priority = priority
        self.port_no = port_no
        self.value = bpdu.ConfigurationBPDUs.encode_port_id(priority, port_no)


class Priority(object):
    def __init__(self, root_id, root_path_cost,
                 designated_bridge_id, designated_port_id):
        super(Priority, self).__init__()
        self.root_id = root_id
        self.root_path_cost = root_path_cost
        self.designated_bridge_id = designated_bridge_id
        self.designated_port_id = designated_port_id


class Times(object):
    def __init__(self, message_age, max_age, hello_time, forward_delay):
        super(Times, self).__init__()
        self.message_age = message_age
        self.max_age = max_age
        self.hello_time = hello_time
        self.forward_delay = forward_delay


class OfCtl_v1_0(object):
    def __init__(self, dp):
        super(OfCtl_v1_0, self).__init__()
        self.dp = dp

    def send_packet_out(self, out_port, data):
        actions = [self.dp.ofproto_parser.OFPActionOutput(out_port, 0)]
        self.dp.send_packet_out(buffer_id=self.dp.ofproto.OFP_NO_BUFFER,
                                in_port=self.dp.ofproto.OFPP_CONTROLLER,
                                actions=actions, data=data)

    def set_port_status(self, port, state):
        ofproto_parser = self.dp.ofproto_parser
        mask = 0b1111111
        msg = ofproto_parser.OFPPortMod(self.dp, port.port_no, port.hw_addr,
                                        PORT_CONFIG_V1_0[state], mask,
                                        port.advertised)
        self.dp.send_msg(msg)


class OfCtl_v1_2later(OfCtl_v1_0):
    def __init__(self, dp):
        super(OfCtl_v1_2later, self).__init__(dp)

    def set_port_status(self, port, state):
        ofp = self.dp.ofproto
        parser = self.dp.ofproto_parser
        config = {ofproto_v1_2: PORT_CONFIG_V1_2,
                  ofproto_v1_3: PORT_CONFIG_V1_3}

        # Only turn on the relevant bits defined on OpenFlow 1.2+, otherwise
        # some switch that follows the specification strictly will report
        # OFPPMFC_BAD_CONFIG error.
        mask = 0b1100101
        msg = parser.OFPPortMod(self.dp, port.port_no, port.hw_addr,
                                config[ofp][state], mask, port.advertised)
        self.dp.send_msg(msg)

        if config[ofp][state] & ofp.OFPPC_NO_PACKET_IN:
            self.add_no_pkt_in_flow(port.port_no)
        else:
            self.del_no_pkt_in_flow(port.port_no)

    def add_bpdu_pkt_in_flow(self):
        ofp = self.dp.ofproto
        parser = self.dp.ofproto_parser

        match = parser.OFPMatch(eth_dst=bpdu.BRIDGE_GROUP_ADDRESS)
        actions = [parser.OFPActionOutput(ofp.OFPP_CONTROLLER,
                                          ofp.OFPCML_NO_BUFFER)]
        inst = [parser.OFPInstructionActions(ofp.OFPIT_APPLY_ACTIONS,
                                             actions)]
        mod = parser.OFPFlowMod(self.dp, priority=BPDU_PKT_IN_PRIORITY,
                                match=match, instructions=inst)
        self.dp.send_msg(mod)

    def add_no_pkt_in_flow(self, in_port):
        parser = self.dp.ofproto_parser

        match = parser.OFPMatch(in_port=in_port)
        mod = parser.OFPFlowMod(self.dp, priority=NO_PKT_IN_PRIORITY,
                                match=match)
        self.dp.send_msg(mod)

    def del_no_pkt_in_flow(self, in_port):
        ofp = self.dp.ofproto
        parser = self.dp.ofproto_parser

        match = parser.OFPMatch(in_port=in_port)
        mod = parser.OFPFlowMod(self.dp, command=ofp.OFPFC_DELETE_STRICT,
                                out_port=ofp.OFPP_ANY, out_group=ofp.OFPG_ANY,
                                priority=NO_PKT_IN_PRIORITY, match=match)
        self.dp.send_msg(mod)
