#    Copyright 2016 Mirantis, Inc.
#
#    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 abc
import logging
import threading

import futurist
import six

from oslo_messaging._drivers.zmq_driver.client import zmq_response
from oslo_messaging._drivers.zmq_driver import zmq_async
from oslo_messaging._drivers.zmq_driver import zmq_names

LOG = logging.getLogger(__name__)

zmq = zmq_async.import_zmq()


@six.add_metaclass(abc.ABCMeta)
class ReceiverBase(object):
    """Base response receiving interface."""

    def __init__(self, conf):
        self.conf = conf
        self._lock = threading.Lock()
        self._requests = {}
        self._poller = zmq_async.get_poller()
        self._executor = zmq_async.get_executor(method=self._run_loop)
        self._executor.execute()

    @abc.abstractproperty
    def message_types(self):
        """A set of supported incoming response types."""

    def register_socket(self, socket):
        """Register a socket for receiving data."""
        self._poller.register(socket, recv_method=self.recv_response)

    def unregister_socket(self, socket):
        """Unregister a socket from receiving data."""
        self._poller.unregister(socket)

    @abc.abstractmethod
    def recv_response(self, socket):
        """Receive a response and return a tuple of the form
        (reply_id, message_type, message_id, response).
        """

    def track_request(self, request):
        """Track a request via already registered sockets and return
        a dict of futures for monitoring all types of responses.
        """
        futures = {}
        for message_type in self.message_types:
            future = futurist.Future()
            self._set_future(request.message_id, message_type, future)
            futures[message_type] = future
        return futures

    def untrack_request(self, request):
        """Untrack a request and stop monitoring any responses."""
        for message_type in self.message_types:
            self._pop_future(request.message_id, message_type)

    def stop(self):
        self._poller.close()
        self._executor.stop()

    def _get_future(self, message_id, message_type):
        with self._lock:
            return self._requests.get((message_id, message_type))

    def _set_future(self, message_id, message_type, future):
        with self._lock:
            self._requests[(message_id, message_type)] = future

    def _pop_future(self, message_id, message_type):
        with self._lock:
            return self._requests.pop((message_id, message_type), None)

    def _run_loop(self):
        data, socket = self._poller.poll(
            timeout=self.conf.oslo_messaging_zmq.rpc_poll_timeout)
        if data is None:
            return
        reply_id, message_type, message_id, response = data
        assert message_type in self.message_types, \
            "%s is not supported!" % zmq_names.message_type_str(message_type)
        future = self._get_future(message_id, message_type)
        if future is not None:
            LOG.debug("Received %(msg_type)s for %(msg_id)s",
                      {"msg_type": zmq_names.message_type_str(message_type),
                       "msg_id": message_id})
            future.set_result((reply_id, response))


class ReplyReceiver(ReceiverBase):

    message_types = {zmq_names.REPLY_TYPE}


class ReplyReceiverProxy(ReplyReceiver):

    def recv_response(self, socket):
        empty = socket.recv()
        assert empty == b'', "Empty expected!"
        reply_id = socket.recv()
        assert reply_id is not None, "Reply ID expected!"
        message_type = int(socket.recv())
        assert message_type == zmq_names.REPLY_TYPE, "Reply expected!"
        message_id = socket.recv_string()
        reply_body, failure = socket.recv_loaded()
        reply = zmq_response.Reply(
            message_id=message_id, reply_id=reply_id,
            reply_body=reply_body, failure=failure
        )
        return reply_id, message_type, message_id, reply


class ReplyReceiverDirect(ReplyReceiver):

    def recv_response(self, socket):
        empty = socket.recv()
        assert empty == b'', "Empty expected!"
        raw_reply = socket.recv_loaded()
        assert isinstance(raw_reply, dict), "Dict expected!"
        reply = zmq_response.Reply(**raw_reply)
        return reply.reply_id, reply.msg_type, reply.message_id, reply


class AckAndReplyReceiver(ReceiverBase):

    message_types = {zmq_names.ACK_TYPE, zmq_names.REPLY_TYPE}


class AckAndReplyReceiverProxy(AckAndReplyReceiver):

    def recv_response(self, socket):
        empty = socket.recv()
        assert empty == b'', "Empty expected!"
        reply_id = socket.recv()
        assert reply_id is not None, "Reply ID expected!"
        message_type = int(socket.recv())
        assert message_type in (zmq_names.ACK_TYPE, zmq_names.REPLY_TYPE), \
            "Ack or reply expected!"
        message_id = socket.recv_string()
        if message_type == zmq_names.REPLY_TYPE:
            reply_body, failure = socket.recv_loaded()
            reply = zmq_response.Reply(
                message_id=message_id, reply_id=reply_id,
                reply_body=reply_body, failure=failure
            )
            response = reply
        else:
            response = None
        return reply_id, message_type, message_id, response


class AckAndReplyReceiverDirect(AckAndReplyReceiver):

    def recv_response(self, socket):
        # acks are not supported yet
        empty = socket.recv()
        assert empty == b'', "Empty expected!"
        raw_reply = socket.recv_loaded()
        assert isinstance(raw_reply, dict), "Dict expected!"
        reply = zmq_response.Reply(**raw_reply)
        return reply.reply_id, reply.msg_type, reply.message_id, reply
