# Copyright (c) 2015 OpenStack Foundation.
# All Rights Reserved.
#
#    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 time

from os_ken.controller import handler
from os_ken.controller import ofp_event
from os_ken.controller import ofp_handler
from os_ken.ofproto import ofproto_common
from os_ken.ofproto import ofproto_parser
from os_ken.ofproto import ofproto_v1_3
from os_ken import utils
from oslo_config import cfg
from oslo_log import log

from dragonflow.common import profiler as df_profiler
from dragonflow.controller.common import constants
from dragonflow.controller import dispatcher


LOG = log.getLogger(__name__)


class OsKenDFAdapter(ofp_handler.OFPHandler):
    OFP_VERSIONS = [ofproto_v1_3.OFP_VERSION]
    OF_AUTO_PORT_DESC_STATS_REQ_VER = 0x04

    def __init__(self, switch_backend, nb_api,
                 db_change_callback,
                 neutron_server_notifier=None):
        super(OsKenDFAdapter, self).__init__()
        self.dispatcher = dispatcher.AppDispatcher(cfg.CONF.df.apps_list)
        self.vswitch_api = switch_backend.vswitch_api
        self.nb_api = nb_api
        self.switch_backend = switch_backend
        self.neutron_server_notifier = neutron_server_notifier
        self._datapath = None
        self.table_handlers = {}
        self.first_connect = True
        self.db_change_callback = db_change_callback

    @property
    def datapath(self):
        return self._datapath

    def start(self):
        super(OsKenDFAdapter, self).start()
        self.load(self,
                  switch_backend=self.switch_backend,
                  nb_api=self.nb_api,
                  neutron_server_notifier=self.neutron_server_notifier)
        self.wait_until_ready()

    def load(self, *args, **kwargs):
        self.dispatcher.load(*args, **kwargs)

    def is_ready(self):
        return self.datapath is not None

    def wait_until_ready(self):
        while not self.is_ready():
            time.sleep(3)

    def register_table_handler(self, table_id, handler):
        if table_id in self.table_handlers:
            raise RuntimeError(
                _(
                    'Cannot register handler {new_handler} for table {table},'
                    'occupied by {existing_handler}'
                ).format(
                    table=table_id,
                    new_handler=handler,
                    existing_handler=self.table_handlers[table_id],
                ),
            )
        self.table_handlers[table_id] = handler

    def unregister_table_handler(self, table_id):
        self.table_handlers.pop(table_id, None)

    def notify_switch_sync_finished(self):
        self.dispatcher.dispatch(constants.CONTROLLER_SWITCH_SYNC_FINISHED)

    def notify_switch_sync_started(self):
        self.dispatcher.dispatch(constants.CONTROLLER_SWITCH_SYNC_STARTED)

    @handler.set_ev_handler(ofp_event.EventOFPSwitchFeatures,
                            handler.CONFIG_DISPATCHER)
    def switch_features_handler(self, ev):
        # TODO(oanson) is there a better way to get the datapath?
        self._datapath = ev.msg.datapath
        super(OsKenDFAdapter, self).switch_features_handler(ev)
        version = self.datapath.ofproto.OFP_VERSION
        if version < OsKenDFAdapter.OF_AUTO_PORT_DESC_STATS_REQ_VER:
            # Otherwise, this is done automatically by OFPHandler
            self._send_port_desc_stats_request(self.datapath)

        self.get_sw_async_msg_config()

        self.switch_backend.setup_datapath(self)
        self.dispatcher.dispatch('switch_features_handler', ev)

        if not self.first_connect:
            # For reconnecting to the os_ken controller, df needs a full sync
            # in case any resource added during the disconnection.
            self.db_change_callback(None, None,
                                    constants.CONTROLLER_REINITIALIZE,
                                    None)
        self.first_connect = False
        self.vswitch_api.initialize(self.db_change_callback)

    def _send_port_desc_stats_request(self, datapath):
        ofp_parser = datapath.ofproto_parser
        req = ofp_parser.OFPPortDescStatsRequest(datapath, 0)
        datapath.send_msg(req)

    @handler.set_ev_handler(ofp_event.EventOFPPortDescStatsReply,
                            handler.MAIN_DISPATCHER)
    def port_desc_stats_reply_handler(self, ev):
        self.dispatcher.dispatch('port_desc_stats_reply_handler', ev)

    @handler.set_ev_handler(ofp_event.EventOFPPacketIn,
                            handler.MAIN_DISPATCHER)
    def OF_packet_in_handler(self, event):
        msg = event.msg
        table_id = msg.table_id
        if table_id in self.table_handlers:
            handler = self.table_handlers[table_id]
            with df_profiler.profiler_context('packet_in',
                                              info={"func": handler.__name__}):
                handler(event)
        else:
            LOG.info("No handler for table id %(table)s with message "
                     "%(msg)", {'table': table_id, 'msg': msg})

    @handler.set_ev_handler(ofp_event.EventOFPErrorMsg,
                            handler.MAIN_DISPATCHER)
    def OF_error_msg_handler(self, event):
        msg = event.msg
        try:
            (version, msg_type, msg_len, xid) = ofproto_parser.header(msg.data)
            os_ken_msg = ofproto_parser.msg(
                self._datapath, version, msg_type,
                msg_len - ofproto_common.OFP_HEADER_SIZE, xid, msg.data)
            LOG.error('OFPErrorMsg received: %s', os_ken_msg)
        except Exception:
            LOG.error('Unrecognized OFPErrorMsg received: '
                      'type=0x%(type)02x code=0x%(code)02x '
                      'message=%(msg)s',
                      {'type': msg.type, 'code': msg.code,
                       'msg': utils.hex_array(msg.data)})

    @handler.set_ev_cls(ofp_event.EventOFPGetAsyncReply,
                        handler.MAIN_DISPATCHER)
    def get_async_reply_handler(self, event):
        msg = event.msg
        LOG.debug('OFPGetAsyncReply received: packet_in_mask=0x%08x:0x%08x '
                  'port_status_mask=0x%08x:0x%08x '
                  'flow_removed_mask=0x%08x:0x%08x',
                  msg.packet_in_mask[0], msg.packet_in_mask[1],
                  msg.port_status_mask[0], msg.port_status_mask[1],
                  msg.flow_removed_mask[0], msg.flow_removed_mask[1])
        self.set_sw_async_msg_config_for_ttl(msg)

    def get_sw_async_msg_config(self):
        """Get the configuration of current switch"""
        ofp_parser = self._datapath.ofproto_parser
        req = ofp_parser.OFPGetAsyncRequest(self._datapath)
        self._datapath.send_msg(req)

    def set_sw_async_msg_config_for_ttl(self, cur_config):
        """Configure switch for TTL

        Configure the switch to packet-in TTL invalid packets to controller.
        Note that this method only works in OFP 1.3, however, this os_ken app
        claims that it only supports ofproto_v1_3.OFP_VERSION. So, no check
        will be made here.
        """
        dp = self._datapath
        parser = dp.ofproto_parser
        ofproto = dp.ofproto

        if cur_config.packet_in_mask[0] & 1 << ofproto.OFPR_INVALID_TTL != 0:
            LOG.info('SW config for TTL error packet in has already '
                     'been set')
            return

        packet_in_mask = (cur_config.packet_in_mask[0] |
                          1 << ofproto.OFPR_INVALID_TTL)
        m = parser.OFPSetAsync(
            dp, [packet_in_mask, cur_config.packet_in_mask[1]],
            [cur_config.port_status_mask[0], cur_config.port_status_mask[1]],
            [cur_config.flow_removed_mask[0], cur_config.flow_removed_mask[1]])
        dp.send_msg(m)
        LOG.info('Set SW config for TTL error packet in.')
