# Copyright (c) 2014 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""RDB request managers and requests.

RDB request managers: Call an rdb api_method with a list of RDBRequests, and
match the requests to the responses returned.

RDB Request classes: Used in conjunction with the request managers. Each class
defines the set of fields the rdb needs to fulfill the request, and a hashable
request object the request managers use to identify a response with a request.
"""

import collections

import common
from autotest_lib.scheduler import rdb_utils


class RDBRequestManager(object):
    """Base request manager for RDB requests.

    Each instance of a request manager is associated with one request, and
    one api call. All subclasses maintain a queue of unexecuted requests, and
    and expose an api to add requests/retrieve the response for these requests.
    """


    def __init__(self, request, api_call):
        """
        @param request: A subclass of rdb_utls.RDBRequest. The manager can only
            manage requests of one type.
        @param api_call: The rdb api call this manager is expected to make.
            A manager can only send requests of type request, to this api call.
        """
        self.request = request
        self.api_call = api_call
        self.request_queue = []


    def add_request(self, **kwargs):
        """Add an RDBRequest to the queue."""
        self.request_queue.append(self.request(**kwargs).get_request())


    def response(self):
        """Execute the api call and return a response for each request.

        The order of responses is the same as the order of requests added
        to the queue.

        @yield: A response for each request added to the queue after the
            last invocation of response.
        """
        if not self.request_queue:
            raise rdb_utils.RDBException('No requests. Call add_requests '
                    'with the appropriate kwargs, before calling response.')

        result = self.api_call(self.request_queue)
        requests = self.request_queue
        self.request_queue = []
        for request in requests:
            yield result.get(request) if result else None


class BaseHostRequestManager(RDBRequestManager):
    """Manager for batched get requests on hosts."""


    def response(self):
        """Yields a popped host from the returned host list."""

        # As a side-effect of returning a host, this method also removes it
        # from the list of hosts matched up against a request. Eg:
        #    hqes: [hqe1, hqe2, hqe3]
        #    client requests: [c_r1, c_r2, c_r3]
        #    generate requests in rdb: [r1 (c_r1 and c_r2), r2]
        #    and response {r1: [h1, h2], r2:[h3]}
        # c_r1 and c_r2 need to get different hosts though they're the same
        # request, because they're from different queue_entries.
        for hosts in super(BaseHostRequestManager, self).response():
            yield hosts.pop() if hosts else None


class RDBRequestMeta(type):
    """Metaclass for constructing rdb requests.

    This meta class creates a read-only request template by combining the
    request_arguments of all classes in the inheritence hierarchy into a
    namedtuple.
    """
    def __new__(cls, name, bases, dctn):
        for base in bases:
            try:
                dctn['_request_args'].update(base._request_args)
            except AttributeError:
                pass
        dctn['template'] = collections.namedtuple('template',
                                                  dctn['_request_args'])
        return type.__new__(cls, name, bases, dctn)


class RDBRequest(object):
    """Base class for an rdb request.

    All classes inheriting from RDBRequest will need to specify a list of
    request_args necessary to create the request, and will in turn get a
    request that the rdb understands.
    """
    __metaclass__ = RDBRequestMeta
    __slots__ = set(['_request_args', '_request'])
    _request_args = set([])


    def __init__(self, **kwargs):
        for key,value in kwargs.iteritems():
            try:
                hash(value)
            except TypeError as e:
                raise rdb_utils.RDBException('All fields of a %s must be. '
                        'hashable %s: %s, %s failed this test.' %
                        (self.__class__, key, type(value), value))
        try:
            self._request = self.template(**kwargs)
        except TypeError:
            raise rdb_utils.RDBException('Creating %s requires args %s got %s' %
                    (self.__class__, self.template._fields, kwargs.keys()))


    def get_request(self):
        """Returns a request that the rdb understands.

        @return: A named tuple with all the fields necessary to make a request.
        """
        return self._request


class HashableDict(dict):
    """A hashable dictionary.

    This class assumes all values of the input dict are hashable.
    """

    def __hash__(self):
        return hash(tuple(sorted(self.items())))


class HostRequest(RDBRequest):
    """Basic request for information about a single host.

    Eg: HostRequest(host_id=x): Will return all information about host x.
    """
    _request_args =  set(['host_id'])


class UpdateHostRequest(HostRequest):
    """Defines requests to update hosts.

    Eg:
        UpdateHostRequest(host_id=x, payload={'afe_hosts_col_name': value}):
            Will update column afe_hosts_col_name with the given value, for
            the given host_id.

    @raises RDBException: If the input arguments don't contain the expected
        fields to make the request, or are of the wrong type.
    """
    _request_args = set(['payload'])


    def __init__(self, **kwargs):
        try:
            kwargs['payload'] = HashableDict(kwargs['payload'])
        except (KeyError, TypeError) as e:
            raise rdb_utils.RDBException('Creating %s requires args %s got %s' %
                    (self.__class__, self.template._fields, kwargs.keys()))
        super(UpdateHostRequest, self).__init__(**kwargs)


class AcquireHostRequest(HostRequest):
    """Defines requests to acquire hosts.

    Eg:
        AcquireHostRequest(host_id=None, deps=[d1, d2], acls=[a1, a2],
                priority=None, parent_job_id=None): Will acquire and return a
                host that matches the specified deps/acls.
        AcquireHostRequest(host_id=x, deps=[d1, d2], acls=[a1, a2]) : Will
            acquire and return host x, after checking deps/acls match.

    @raises RDBException: If the the input arguments don't contain the expected
        fields to make a request, or are of the wrong type.
    """
    # TODO(beeps): Priority and parent_job_id shouldn't be a part of the
    # core request.
    _request_args = set(['priority', 'deps', 'preferred_deps', 'acls',
                         'parent_job_id', 'suite_min_duts'])


    def __init__(self, **kwargs):
        try:
            kwargs['deps'] = frozenset(kwargs['deps'])
            kwargs['preferred_deps'] = frozenset(kwargs['preferred_deps'])
            kwargs['acls'] = frozenset(kwargs['acls'])

            # parent_job_id defaults to NULL but always serializing it as an int
            # fits the rdb's type assumptions. Note that job ids are 1 based.
            if kwargs['parent_job_id'] is None:
                kwargs['parent_job_id'] = 0
        except (KeyError, TypeError) as e:
            raise rdb_utils.RDBException('Creating %s requires args %s got %s' %
                    (self.__class__, self.template._fields, kwargs.keys()))
        super(AcquireHostRequest, self).__init__(**kwargs)