import logging
import multiprocessing
from setproctitle import setproctitle
import datetime
from threading import Thread
from motorway.mixins import GrouperMixin, SendMessageMixin, ConnectionMixin
from motorway.utils import set_timeouts_on_socket
import zmq
import time


logger = logging.getLogger(__name__)


class Ramp(GrouperMixin, SendMessageMixin, ConnectionMixin, object):
    """
    All messages must at some point start at a ramp, which ingests data into the pipeline from an external system or
    generates data it self (such as random words in the tutorial)
    """

    send_control_messages = True
    sleep_time = 0

    def __init__(self, runs_on_controller=False, process_uuid=None):
        super(Ramp, self).__init__()
        self.runs_on_controller = runs_on_controller
        self.send_socks = {}
        self.send_grouper = None
        self.receive_port = None
        self.process_name = multiprocessing.current_process().name
        self.process_uuid = "_ramp-%s" % process_uuid
        self.grouper_instance = None

    def next(self):
        """
        This function is called continuously by the ramp.

        .. WARNING::
            Do not block this for a long time or make a while True loop inside it. Betwen every :func:`motorway.ramp.Ramp.next` run,
            some essential operations are run, including receiving acks from the :class:`motorway.controller.Controller`

        :yield: :class:`motorway.messages.Message` instance
        """
        raise NotImplementedError()

    def __iter__(self):
        yield self.next()

    def success(self, _id):
        """
        Called when a message was successfully ack'ed through the entire pipeline.

        :param _id: The id of the message that was successful
        """
        pass

    def failed(self, _id):
        """
        Called when a message failed somewhere in the pipeline. The message might not be entirely finished processing
        at this point and this function might be called multiple times.

        :param _id: The id of the message that failed
        """
        pass

    @classmethod
    def run(cls, queue, refresh_connection_stream=None, runs_on_controller=False, process_uuid=None):

        self = cls(runs_on_controller=runs_on_controller, process_uuid=process_uuid)

        logger.info("Running %s" % self.process_name)
        logger.debug("%s pushing to %s" % (self.process_name, queue))
        setproctitle("data-pipeline: %s" % self.process_name, )

        context = zmq.Context()

        # Create Thread Factories :-)

        thread_update_connections_factory = lambda: Thread(target=self.connection_thread, name="connection_thread", kwargs={
            'refresh_connection_stream': refresh_connection_stream,
            'context': context,
            'input_queue': self.process_uuid,
            'output_queue': queue,
        })
        #self, context=None, refresh_connection_stream=None, input_queue=None, output_queue=None, process_id=None,
                          #process_name=None, grouper_cls=None, set_controller_sock=True

        thread_main_factory = lambda: Thread(target=self.message_producer, name="message_producer", kwargs={
            'context': context,
            'process_id': self.process_uuid,
        })

        thread_results_factory = lambda: Thread(target=self.receive_replies, name="results", kwargs={
            'context': context,
        })

        # Run threads

        thread_update_connections = thread_update_connections_factory()
        thread_update_connections.start()

        thread_main = thread_main_factory()
        thread_main.start()

        thread_results = thread_results_factory()
        thread_results.start()

        while True:
            if not thread_update_connections.is_alive():
                logger.error("Thread thread_update_connections crashed in %s" % self.__class__.__name__)
                thread_update_connections = thread_update_connections_factory()
                thread_update_connections.start()
            if not thread_main.is_alive():
                logger.error("Thread thread_main crashed in %s" % self.__class__.__name__)
                thread_main = thread_main_factory()
                thread_main.start()
            if not thread_results.is_alive():
                logger.error("Thread thread_results crashed in %s" % self.__class__.__name__)
                thread_results = thread_results_factory()
                thread_results.start()
            time.sleep(10)

    def receive_replies(self, context=None):
        result_sock = context.socket(zmq.PULL)
        self.receive_port = result_sock.bind_to_random_port("tcp://*")
        logger.debug("Result port is %s" % self.receive_port)
        set_timeouts_on_socket(result_sock)

        while True:
            try:
                message_reply = result_sock.recv_json()
                if message_reply['status'] == 'success':
                    self.success(message_reply['id'])
                elif message_reply['status'] == 'fail':
                    self.failed(message_reply['id'])
                else:
                    logger.warn("Received unknown status feedback %s" % message_reply['status'])
            except zmq.Again:
                time.sleep(1)

    def should_run(self):
        """
        Subclass to define rules whether this tap should run or not. Mainly used for ensuring a tap only runs once
         across the network

        :return: bool
        """
        return self.runs_on_controller

    def message_producer(self, context=None, process_id=None):

        while True:
            if not self.send_socks:
                # 'self.send_socks' is set in the 'ConnectionMixin' once the corresponding intersection
                # has successfully connected to the ConnectionIntersection and marked itself as a consumer
                # of the input stream, which is the ramp in this case.
                logger.debug("Waiting for send_socks")
                time.sleep(1)
            elif self.should_run():
                start_time = datetime.datetime.now()
                try:
                    for received_message_result in self:
                        for generated_message in received_message_result:
                            if generated_message is not None:
                                self.send_message(generated_message, process_id, time_consumed=datetime.datetime.now() - start_time, sender='ramp')
                            start_time = datetime.datetime.now()
                except Exception as e:  # Never let user code crash the whole thing!!
                    logger.exception(e)
                if self.sleep_time:
                    time.sleep(self.sleep_time)
