

#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Copyright 2012 California Institute of Technology. ALL RIGHTS RESERVED.
# 
# 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.
# 
# United States Government Sponsorship acknowledged. This software is subject to
# U.S. export control laws and regulations and has been classified as 'EAR99 NLR'
# (No [Export] License Required except when exporting to an embargoed country,
# end user, or in support of a prohibited end use). By downloading this software,
# the user agrees to comply with all applicable U.S. export laws and regulations.
# The user has the responsibility to obtain export licenses, or other export
# authority as may be required before exporting this software to any 'EAR99'
# embargoed foreign country or citizen of those countries.
#
# Author: Eric Belz
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~




## \namespace isceobj.Util.decorators Utility decorators
"""This module has generic decorators, such as:

float_setter         (makes sure a setter calls it's args __float__)
type_check           (a type checker for methods)
object_wrapper       (for making methods that interface to a C for Fortran library)
dov                  (deal with dictionaryOfVariables in a DRY way)
profiler             (a method profile-- TBD).
etc
"""
from functools import wraps

## force a setter to use a type
def force(type_):
    def enforcer(setter):
        @wraps(setter)
        def checked_setter(self, value):
            try:
                checked = type_(value)
            except (ValueError, TypeError, AttributeError):
                message = "Expecting a %s, but got: %s, in setter: %s, %s"%(
                    str(type_),
                    str(value),
                    self.__class__.__name__,
                    setter.__name__)
                if hasattr(self, "logger"):
                    self.logger.warning(message)
                else:
                    raise ValueError(message)
                pass
            return setter(self, checked)
        return checked_setter
    return enforcer

## for floating point
float_setter = force(float)

## decorator to wrap a 1-argument **methods** and check if the argument is an instance
# of "cls"
def type_check(cls):
    """decorator=type_check(cls)

    "cls" is a class that the decorated method's sole argument must be an instance of (or
    a TypeError is raised-- a string explaining the problem is included"
    
    "decorator" is the decorator with which to decorate the method-- the decorator
    protocol is that a decorator with arguments returns a decorator.

    USAGE WARNING: CANNOT DECORATE FUNCTIONS or STATICMETHODS (yet).
    """
    ## The checker knows "cls", and takes the method to make, an return, checked_method
    def checker(method):
        ## The interpretor installs this method in method's place--
        ## it checks and raises a TypeEror if needed
        def checked_method(*args):
            obj = args[-1]
            if not isinstance(obj, cls):
                raise TypeError(
                    method.__name__+
                    " excpected: "+
                    cls.__name__ +
                    ", got:" +
                    str(obj.__class__)
                    )
            return method(*args)
        return checked_method
    return checker




## If self.method(*args) return self.object.method(*args), then decorate method with:
## @object_wrapper("object")
def object_wrapper(objname):
    """If self.method(*args) return self.object.method(*args), then decorate method with
    @object_wrapper("object") and make sure that the method looks like this:

    @object_wrapprt("object")
    def method(self, x1, ..., xn.):
        return object.method

    where object.method(x1,..., xn) is object.method's signature. See LineAccesorPy.py
    for a concrete example.
    """

    ## functools.wraps prevents the decorator from overiding the method's:
    ## __name__, __doc__ attributes. 
    def accessor(func):
        """This is a method decorator. The bare method returns "func", while the
        decorated method ("new_method") calls it, with self.<objname> as the implicit 1st
        argument. """
        @wraps(func)
        def new_method(self, *args):
            return func(getattr(self, objname), *args)
        return new_method
    return accessor


## This decorator decorates __init__ methods for classes that need their mandatory and
## optional variables computed -- it may removed when Parameters replace variable tuples.
def dov(init):
    """Usage:

        dictionaryOfVariables = {....}

        @dov
        def __init__(self,...):


    Decorates __init__ so that it takes a STATIC dictionary of variables and computes
    dynamic mandatoryVariables and optionalVariables dictionies. Obviously easy to
    rewrite to take a dynamic dictionaryOfVariables.

    Nevertheless, it should be a class decorator that only handles static variables.
    That's TBD.
    """
    def constructor(self, *args, **kwargs):
        self.descriptionOfVariables = {}
        self.mandatoryVariables = []
        self.optionalVariables = []
        typePos = 2
        for key , val in self.dictionaryOfVariables.items():
            value = val[typePos]
            if value is True or value == 'mandatory':
                self.mandatoryVariables.append(key)
            elif value is False or value == 'optional':
                self.optionalVariables.append(key)
            else:
                raise ValueError(
                    'Error. Variable can only be "optional"/False or "mandatory"/True'
                    )
            pass
        return init(self, *args, **kwargs)
    return constructor




## Decorator to add logging to a class's __init__.
def logged(init):
    """Usage:
    -------------------------------------------------
   | class CLS(object):                             |
   |                                                |
   |      logging_name = 'isce.cls' # or whatever   |
   |                                                |
   |     @logged                                    |
   |     def __init__(self, ...)                    |
   -------------------------------------------------
   This decorator adds a logger names "logging_name"
   to any instance of CLS.
   """
    
    import logging
    def constructor(self, *args, **kwargs):
        init(self, *args, **kwargs)
        self.logger = logging.getLogger(self.__class__.logging_name)
        return None
    return constructor


## The no-pickle list, may be moved to a /library.
DONT_PICKLE_ME = ('logger', '_inputPorts', '_outputPorts')
## Decorator to add pickling to a class without using inheritance
def pickled(cls):
    """Usage:
    -------------------------------------------------
   | @pickled                                       |
   | class CLS(object):                             |
   |                                                |
   |      logging_name = 'isce.cls' # or whatever   |
   |                                                |
   -------------------------------------------------
   This decorator adds pickling to class CLS.
   By default, it also invokes @logged on the CLS.__init__,
   so you need to 
   """

    ## reject objects bases on name
    def __getstate__(self):
        d = dict(self.__dict__)
        ## for future use: modify no pickle list.
        skip = (
            () if not hasattr(self.__class__, 'dont_pickle_me') else
            self.__class__.dont_pickle_me
            )
        for key in DONT_PICKLE_ME+skip:
            if key in d:
                del d[key]
                pass
            pass
        return d
    
    def __setstate__(self,d):
        self.__dict__.update(d)
        import logging
        self.logger = logging.getLogger(self.__class__.logging_name)
        pass

    if not hasattr(cls, '__setstate__'): cls.__setstate__ = __setstate__
    if not hasattr(cls, '__getstate__'): cls.__getstate__ = __getstate__

    return cls


## A decorator for making a port out of a trivial method named add<port>
def port(check):
    """port(check) makes a decorator.
    
    if "check" is a str [type] it enforces:
    hasattr(port, check) [isintanace(port, check)].

    The decorated method should be as follows, for port "spam"

    @port("eggs")
    def addspam(self):
        pass
    
    That will setup:
    
    self.spam from self.inputPorts['spam'] and ensure:
    self.spam.eggs exists.

    Of course, the method canbe notrivial, too.
    """
    def port_decorator(method):
        port_name = method.__name__[3:].lower()
        attr = port_name
        @wraps(method)
        def port_method(self):
            local_object = self.inputPorts[port_name]
            setattr(self, attr, local_object)
            if check is not None:
                if isinstance(check, str):
                    if not hasattr(local_object, check):
                        raise AttributeError(check+" failed")
                    pass
                else:
                    if not isinstance(local_object, check):
                        raise TypeError(str(check)+" failed")
                    pass
                pass
            return method(self) # *args, **kwargs is TBD.
        return port_method
    return port_decorator

##Provide a decorator for those methods that need to use the old api.
##at one point one might just turn it off by simply returning the original function 
def use_api(func):
    from iscesys.ImageApi.DataAccessorPy import DataAccessor
    def use_api_decorator(*args,**kwargs):
        #turn on the use of the old image api
        if DataAccessor._accessorType == 'api':
            leave = True
        else:
            DataAccessor._accessorType = 'api'
            leave = False
        ret = func(*args,**kwargs)
        #turn off. The default will be used, i.e. api for write and gdal for read
        if not leave:
            DataAccessor._accessorType = ''
        return ret
    return use_api_decorator
        
        
