# -*- coding: utf-8 -*-

# Copyright (c) 2010-2011, GEM Foundation.
#
# OpenQuake is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License version 3
# only, as published by the Free Software Foundation.
#
# OpenQuake is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License version 3 for more details
# (a copy is included in the LICENSE file that accompanied this code).
#
# You should have received a copy of the GNU Lesser General Public License
# version 3 along with OpenQuake.  If not, see
# <http://www.gnu.org/licenses/lgpl-3.0.txt> for a copy of the LGPLv3 License.


"""
Classes dealing with amqp signalling between jobbers, workers and supervisors.
"""
import time

from amqplib import client_0_8 as amqp

from openquake.utils import config


def generate_routing_key(job_id, type_):
    """
    Generate an amqp routing key to route messages of a job.

    :param job_id: the id of the job or '*' for a key matching any job
    :type job_id: int or the string '*'
    :param type_: the type of the message. One of
      - 'failed', to match messages notifying the failure of a job
      - 'succeeded', to match messages notifying the success of job
      - a logging level, e.g. 'ERROR', to match logging messages
      - '*' to match any type of message
    :type type_: string
    :return: the routing key
    :rtype: string
    """
    assert type_ in ('*', 'failed', 'succeeded', 'ERROR', 'FATAL')

    assert isinstance(job_id, int) or job_id == '*'

    return 'log.%s.%s' % (type_, job_id)


def connect():
    """
    Create an amqp channel for signalling using the parameters from
    openquake.cfg.

    Create the exchange too if it doesn't exist yet.

    :return: the tuple (connection, channel)
    """
    cfg = config.get_section("amqp")

    conn = amqp.Connection(host=cfg['host'],
                           userid=cfg['user'],
                           password=cfg['password'],
                           virtual_host=cfg['vhost'])
    chn = conn.channel()
    # I use the vhost as a realm, which seems to be an arbitrary string
    chn.access_request(cfg['vhost'], active=True, read=True, write=True)
    chn.exchange_declare(cfg['exchange'], 'topic', auto_delete=False)

    return conn, chn


def declare_and_bind_queue(job_id, levels, name=''):
    """
    Create an amqp queue for sending/receiving messages and binds it to the
    exchange of specific job and levels.

    It is safe to call this function more than once.  If the exchange, queue or
    bindings already exists, this function won't create them again.

    :param job_id: the id of the job
    :type job_id: int
    :param levels: the signalling levels, e.g. 'ERROR'
    :type levels: iterable of strings
    :param name: the name for the queue, '' (empty string) to give the queue an
                 automatically generated name
    :type name: string
    :return: the name of the created queue
    :rtype: string
    """
    cfg = config.get_section("amqp")

    conn, chn = connect()

    name, _, _ = chn.queue_declare(queue=name, auto_delete=False)

    for level in levels:
        chn.queue_bind(name, cfg['exchange'],
                       routing_key=generate_routing_key(job_id, level))

    chn.close()
    conn.close()

    return name


class LogMessageConsumer(object):
    """
    A class to consume logging messages generated by an OpenQuake job.

    Typical use:

        class MyConsumer(LogMessageConsumer):
            def message_callback(self, msg):
                pass

        with MyConsumer(job_id) as mc:
            mc.run()
    """
    def __init__(self, job_id, levels=None, timeout=None):
        """
        :param job_id: the id of the job whose logging messages we are
                       interested in
        :type job_id: int
        :param levels: the logging levels we are interested in
        :type levels: None for all the levels (translated to a '*' in the
                      routing_key) or an iterable of stings
                      (e.g. ['ERROR', 'FATAL'])
        :param timeout: the optional timeout in seconds. When it expires the
                        `timeout_callback` will be called.
        :type timeout: None or float
        """

        self.job_id = job_id
        self.timeout = timeout

        self.conn, self.chn = connect()

        if levels is None:
            levels = ('*',)

        self.qname = declare_and_bind_queue(self.job_id, levels,
                                            self.get_queue_name())

    def get_queue_name(self):  # pylint: disable=R0201
        """
        The name of the queue that will contain the messages of this consumer.
        """
        return ''

    def __enter__(self):
        return self

    def __exit__(self, *args):
        self.chn.close()
        self.conn.close()

    def run(self):
        """
        Run the loop waiting for messages or timeout expiration and calling the
        appropriate callback.

        The callbacks can stop the loop by raising StopIteration.
        """

        if self.timeout:
            # Resort to polling when a timeout is specified, because of
            # limitations of the amqplib API.
            while True:
                time.sleep(self.timeout)

                try:
                    self.timeout_callback()
                except StopIteration:
                    break

                msg = self.chn.basic_get(self.qname)
                if msg:
                    try:
                        self.message_callback(msg)
                    except StopIteration:
                        self.chn.basic_ack(msg.delivery_tag)
                        break
                    else:
                        self.chn.basic_ack(msg.delivery_tag)
        else:
            tag = self.chn.basic_consume(self.qname,
                                         callback=self.message_callback)

            while self.chn.callbacks:
                try:
                    self.chn.wait()
                except StopIteration:
                    # this will remove the callback from self.chn.callbacks
                    self.chn.basic_cancel(tag)

    def message_callback(self, msg):
        """
        Called by `run` when a message is received.

        Can raise StopIteration to stop the loop inside `run` and let it return
        to the caller.
        """
        raise NotImplementedError()

    def timeout_callback(self):
        """
        Called by `run` each time the timeout expires.

        You need to implement this only if you specify a timeout when creating
        an instance of LogMessageConsumer.

        Can raise StopIteration to stop the loop inside `run` and let it return
        to the caller.
        """
        raise NotImplementedError()


def signal_job_outcome(job_id, outcome):
    """
    Send an amqp message to publish the outcome of a job.

    :param job_id: the id of the job
    :type job_id: int
    :param outcome: the outcome of the job, 'succeeded' or 'failed'
    :type outcome: string
    """
    cfg = config.get_section("amqp")

    conn, chn = connect()

    chn.basic_publish(amqp.Message(), exchange=cfg['exchange'],
                      routing_key=generate_routing_key(job_id, outcome))

    chn.close()
    conn.close()

@@ -1612,24 +1612,70 @@
 ed',
- 'ERROR', 'FATAL
+%0A                     'FATAL', 'ERROR', 'WARN', 'INFO', 'DEBUG
 ')%0A%0A

Completed the list of logging levels