# @GPL_HEADER_START
# Use of the RAVE and related source code is subject to the terms
# of the following licenses:
# 
# GNU Public License (GPL) Rights pursuant to Version 2, June 1991
# Government Purpose License Rights (GPLR) pursuant to DFARS 252.225-7013
# 
# NO WARRANTY
# 
# ANY INFORMATION, MATERIALS, SERVICES, INTELLECTUAL PROPERTY OR OTHER
# PROPERTY OR RIGHTS GRANTED OR PROVIDED BY CARNEGIE MELLON UNIVERSITY
# PURSUANT TO THIS LICENSE (HEREINAFTER THE "DELIVERABLES") ARE ON AN
# "AS-IS" BASIS. CARNEGIE MELLON UNIVERSITY MAKES NO WARRANTIES OF ANY
# KIND, EITHER EXPRESS OR IMPLIED AS TO ANY MATTER INCLUDING, BUT NOT
# LIMITED TO, WARRANTY OF FITNESS FOR A PARTICULAR PURPOSE,
# MERCHANTABILITY, INFORMATIONAL CONTENT, NONINFRINGEMENT, OR ERROR-FREE
# OPERATION. CARNEGIE MELLON UNIVERSITY SHALL NOT BE LIABLE FOR INDIRECT,
# SPECIAL OR CONSEQUENTIAL DAMAGES, SUCH AS LOSS OF PROFITS OR INABILITY
# TO USE SAID INTELLECTUAL PROPERTY, UNDER THIS LICENSE, REGARDLESS OF
# WHETHER SUCH PARTY WAS AWARE OF THE POSSIBILITY OF SUCH DAMAGES.
# LICENSEE AGREES THAT IT WILL NOT MAKE ANY WARRANTY ON BEHALF OF
# CARNEGIE MELLON UNIVERSITY, EXPRESS OR IMPLIED, TO ANY PERSON
# CONCERNING THE APPLICATION OF OR THE RESULTS TO BE OBTAINED WITH THE
# DELIVERABLES UNDER THIS LICENSE.
# 
# Licensee hereby agrees to defend, indemnify, and hold harmless Carnegie
# Mellon University, its trustees, officers, employees, and agents from
# all claims or demands made against them (and any related losses,
# expenses, or attorney's fees) arising out of, or relating to Licensee's
# and/or its sub licensees' negligent use or willful misuse of or
# negligent conduct or willful misconduct regarding the Software,
# facilities, or other rights or assistance granted by Carnegie Mellon
# University under this License, including, but not limited to, any
# claims of product liability, personal injury, death, damage to
# property, or violation of any laws or regulations.
# 
# Carnegie Mellon University Software Engineering Institute authored
# documents are sponsored by the U.S. Department of Defense under
# Contract F19628-00-C-0003. Carnegie Mellon University retains
# copyrights in all material produced under this contract. The U.S.
# Government retains a non-exclusive, royalty-free license to publish or
# reproduce these documents, or allow others to do so, for U.S.
# Government purposes only pursuant to the copyright license under the
# contract clause at 252.227.7013.
# 
# @GPL_HEADER_END
"""
Argument manipulation, specifically:
    1. Conversion of (usually) string values to appropriate types given a
       type mapping
    2. Normalization of function arguments to a canonical form (so they can
       be used for caching).
"""
import sys
import rave.log as rlog
get_log = rlog.log_factory("rave.plugins.decorators")

import copy
import inspect
import pdb

__all__ = ["reconcile", "coerce", "UnmappedValueError"]

def reconcile_noargnames(
        reconciled, varargs, varkwargs, funcname, args, kwargs):
    "Do reconciliation when the function uses no static arguments."
    if not (varargs or varkwargs):
    #   No arguments at all. Return empty reconciled.
        if args or kwargs:
            raise TypeError(
                "%s() takes no arguments (%d given)"
                    % (funcname, len(args) + len(kwargs))
            )
            return reconciled
    elif varargs and not varkwargs:
    #   Just positional arguments
        if len(kwargs):
            k, v = kwargs.popitem()
            raise TypeError(
                "%s() got an unexpected keyword argument '%s'" % (
                    funcname, str(k))
            )
        reconciled['positional'] = list(args)
    elif varkwargs and not varargs:
    #   Just keyword arguments
        if len(args):
            raise TypeError(
                "%s() takes 0 arguments (%d given)" % (
                    funcname, len(args))
            )
        reconciled['kw'] = kwargs
    else:
    #   Both positional and keyword args
        reconciled['positional'] = list(args)
        reconciled['kw'] = kwargs
    return reconciled


def reconcile(funcname, argspec, args, kwargs, is_typemap=False):
    """
    Match invocation arguments up with their names in the function.

    If there is a parameter mismatch, this function throws an TypeError,
    just as if the mismatch were noticed when the real function is called.
    Some of the messages are slightly different; however, they should still
    be useful for debugging purposes.

    Parameters:
    funcname: str
        Function name as a string. Used for logging and error handling
    argspec: tuple | list
        A tuple containing argument specification information for the
        function. The semantics of the tuple values are identical to those
        of the inspect.getargspec function.
    args: tuple | list
        A sequence of the positional arguments used to invoke the function.
    kwargs: dict
        A dictionary of keyword arguments used to invoke the function
    is_typemap:
        If True, the invocation arguments are expected to be type mapping
        functions. In this case, not supplying a value for an argument is 
        an error, even if the function supplies a default value.
    """
#   We change kwargs later on, so make a private copy now
    kwargs = copy.copy(kwargs)
    argnames, varargs, varkwargs, defaults = argspec
#   Convert defaults to a dictionary mapping argument names to their
#   default values.
    if defaults is None:
        defaults = {}
    else:
        defaults = dict(zip(argnames[len(defaults) * -1:], defaults))

    reconciled = {'mapped':{}, 'positional' : [], 'kw': {}}
    if not argnames:
        return reconcile_noargnames(
            reconciled, varargs, varkwargs, funcname, args, kwargs)

#   We have one or more positional arguments in the spec,
#   plus possibly some varargs or varkwargs

#   Positional arguments (This will completely consume args)
    num_argnames = len(argnames)
    if len(args) > num_argnames and not varargs:
        raise TypeError(
            "%s() takes %d arguments (%d given)" % (
                funcname, len(argnames), len(args) + len(kwargs))
        )
    for arg in args:
        if len(argnames):
            reconciled['mapped'][argnames.pop(0)] = arg
        else:
            reconciled['positional'].append(arg)

#   Are there keyword arguments with the same name as a positional
#   argument? Error.
    for k in reconciled['mapped'].keys():
        if k in kwargs:
            raise TypeError(
                "%s() got multiple values for keyword argument '%s'" % (
                    funcname, k)
            )

#   Positional arguments passed in as keyword arguments
    unused_argnames = []
    for argname in argnames:
        if argname in kwargs:
            reconciled['mapped'][argname] = kwargs.pop(argname)
        else:
            unused_argnames.append(argname)

#   (Save for an error message later)
    pre_default_mapped_values = len(reconciled['mapped'])

#   Default values
    really_unused_argnames = []
    if is_typemap:
        really_unused_argnames = unused_argnames
    else:
        for argname in unused_argnames:
            if argname in defaults:
                reconciled['mapped'][argname] = defaults[argname]
            else:
                really_unused_argnames.append(argname)

#   Unused argument names even after going over args and kwargs and defaults?
#   That's an error.
    if len(really_unused_argnames):
        raise TypeError(
            "%s() takes exactly %d non-keyword arguments (%d given)" % (
            funcname, num_argnames, pre_default_mapped_values)
        )

#   We've eaten all our positional args, and all the kwargs that we
#   can associate with a name. Stuff the rest in kw, unless the
#   function doesn't expect arbitrary keyword arguments.
    if len(kwargs):
        if varkwargs:
            reconciled['kw'] = kwargs
        else:
            k, v = kwargs.popitem()
            raise TypeError(
                "%s() got an unexpected keyword argument '%s'" % (
                    funcname, str(k))
            )
    return reconciled

class UnmappedValueError(Exception):
    "An error has occurred in the coerce function."
    pass

def convert(src, converter):
    def none_converter(src):
        if src is not None:
            raise TypeError("Expected None; got '%s'" % type(src))
        else:
            return None
    if converter is None:
        return none_converter(src)
    else:
        return converter(src)

def coerce(op, reconciled):
    """
    Coerce reconciled parameters to the types specified in typemap.
    """
    get_log().debug("Coercing %s", reconciled)
    converted = {'mapped':{}, 'positional':[], 'kw':{}}
    mapped_types, varargs_types = op.typemap
    get_log().debug("Mapping to %s", mapped_types)

#   Converting values mapped to keywords is straightforward
#   (IMPORTANT: We assume all of the typemap's arguments will
#   map to named parameters in the function. IOW, 'kw' and
#   'positional' should be irrelevant.)
    for k, v in reconciled['mapped'].items():
        if k in mapped_types['mapped']:
            converter = mapped_types['mapped'][k]
            get_log().debug("kwargs['%s'] = %s(%s)", k, converter, v)
            converted['mapped'][k] = convert(v, converter)
        else:
            raise UnmappedValueError(k)
#   Convert unmapped positional arguments (e.g., *args) using
#   the contents of varargs_types in sequence, recycling as necessary
#   (If varargs_types is unsupplied, leave the args as they are.)
    if len(varargs_types):
        for i in xrange(len(reconciled['positional'])):
            val = reconciled['positional'][i]
            converter = varargs_types[i % (len(varargs_types))]
            converted['positional'].append(convert(val, converter))
#   Pass unmodified any unmapped keyword arguments (e.g., **kwargs).
#   This could potentially result in different arguments being passed
#   into the function (and used in the cache key) depending on the
#   origin of the call. Caveat programmor.
    kwargs = {}
    kwargs.update(converted['mapped'])
    kwargs.update(converted['kw'])
    args = []
    args.extend(converted['positional'])
    return args, kwargs, converted
