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

import logging

from ryu import utils
from ryu.base import app_manager
from ryu.controller import ofp_event
from ryu.controller.handler import set_ev_cls
from ryu.controller.handler import HANDSHAKE_DISPATCHER, CONFIG_DISPATCHER,\
    MAIN_DISPATCHER

LOG = logging.getLogger('ryu.controller.ofp_handler')

# The state transition: HANDSHAKE -> CONFIG -> MAIN
#
# HANDSHAKE: if it receives HELLO message with the valid OFP version,
# sends Features Request message, and moves to CONFIG.
#
# CONFIG: it receives Features Reply message and moves to MAIN
#
# MAIN: it does nothing. Applications are expected to register their
# own handlers.
#
# Note that at any state, when we receive Echo Request message, send
# back Echo Reply message.


class OFPHandler(app_manager.RyuApp):
    def __init__(self, *args, **kwargs):
        super(OFPHandler, self).__init__(*args, **kwargs)

    @staticmethod
    def hello_failed(datapath, error_desc):
        LOG.error(error_desc)
        error_msg = datapath.ofproto_parser.OFPErrorMsg(datapath)
        error_msg.type = datapath.ofproto.OFPET_HELLO_FAILED
        error_msg.code = datapath.ofproto.OFPHFC_INCOMPATIBLE
        error_msg.data = error_desc
        datapath.send_msg(error_msg)

    @set_ev_cls(ofp_event.EventOFPHello, HANDSHAKE_DISPATCHER)
    def hello_handler(self, ev):
        LOG.debug('hello ev %s', ev)
        msg = ev.msg
        datapath = msg.datapath

        # TODO: check if received version is supported.
        #       pre 1.0 is not supported
        # TODO: OFPHET_VERSIONBITMAP
        usable_versions = [version for version
                           in datapath.supported_ofp_version
                           if version <= msg.version]
        if not usable_versions:
            # send the error
            error_desc = 'unsupported version 0x%x. '
            'If possible, set the switch to use one of the versions %s' % (
                msg.version, datapath.supported_ofp_version.keys())
            self.hello_failed(error_desc)
            return
        version = max(usable_versions)
        if version != min(msg.version, datapath.ofproto.OFP_VERSION):
            # The version of min(msg.version, datapath.ofproto.OFP_VERSION)
            # should be used according to the spec. But we can't.
            # So log it and use max(usable_versions) with the hope that
            # the switch is able to understand lower version.
            # e.g.
            # OF 1.1 from switch
            # OF 1.2 from Ryu and supported_ofp_version = (1.0, 1.2)
            # In this case, 1.1 should be used according to the spec,
            # but 1.1 can't be used.
            #
            # OF1.3.1 6.3.1
            # Upon receipt of this message, the recipient must
            # calculate the OpenFlow protocol version to be used. If
            # both the Hello message sent and the Hello message
            # received contained a OFPHET_VERSIONBITMAP hello element,
            # and if those bitmaps have some common bits set, the
            # negotiated version must be the highest version set in
            # both bitmaps. Otherwise, the negotiated version must be
            # the smaller of the version number that was sent and the
            # one that was received in the version fields.  If the
            # negotiated version is supported by the recipient, then
            # the connection proceeds. Otherwise, the recipient must
            # reply with an OFPT_ERROR message with a type field of
            # OFPET_HELLO_FAILED, a code field of OFPHFC_INCOMPATIBLE,
            # and optionally an ASCII string explaining the situation
            # in data, and then terminate the connection.
            error_desc = 'no compatible version found: '
            'switch 0x%x controller 0x%x, but found usable 0x%x. '
            'If possible, set the switch to use OF version 0x%x' % (
                msg.version, datapath.ofproto.OFP_VERSION, version, version)
            self.hello_failed(error_desc)
            return
        datapath.set_version(version)

        # now send feature
        features_reqeust = datapath.ofproto_parser.OFPFeaturesRequest(datapath)
        datapath.send_msg(features_reqeust)

        # now move on to config state
        LOG.debug('move onto config mode')
        datapath.ev_q.set_dispatcher(CONFIG_DISPATCHER)

    @set_ev_cls(ofp_event.EventOFPSwitchFeatures, CONFIG_DISPATCHER)
    def switch_features_handler(self, ev):
        msg = ev.msg
        datapath = msg.datapath
        LOG.debug('switch features ev %s', msg)

        datapath.id = msg.datapath_id

        # hacky workaround, will be removed. OF1.3 doesn't have
        # ports. An application should not depend on them. But there
        # might be such bad applications so keep this workaround for
        # while.
        if datapath.ofproto.OFP_VERSION < 0x04:
            datapath.ports = msg.ports

        ofproto = datapath.ofproto
        ofproto_parser = datapath.ofproto_parser
        set_config = ofproto_parser.OFPSetConfig(
            datapath, ofproto.OFPC_FRAG_NORMAL,
            128  # TODO:XXX
        )
        datapath.send_msg(set_config)

        LOG.debug('move onto main mode')
        ev.msg.datapath.ev_q.set_dispatcher(MAIN_DISPATCHER)

    @set_ev_cls(ofp_event.EventOFPEchoRequest,
                [HANDSHAKE_DISPATCHER, CONFIG_DISPATCHER, MAIN_DISPATCHER])
    def echo_request_handler(self, ev):
        msg = ev.msg
        datapath = msg.datapath
        echo_reply = datapath.ofproto_parser.OFPEchoReply(datapath)
        echo_reply.xid = msg.xid
        echo_reply.data = msg.data
        datapath.send_msg(echo_reply)

    @set_ev_cls(ofp_event.EventOFPErrorMsg,
                [HANDSHAKE_DISPATCHER, CONFIG_DISPATCHER, MAIN_DISPATCHER])
    def error_msg_handler(self, ev):
        msg = ev.msg
        LOG.debug('error msg ev %s type 0x%x code 0x%x %s',
                  msg, msg.type, msg.code, utils.hex_array(msg.data))