# -*- coding: utf-8 -*-
import logging
import six
import utool as ut
from six.moves import builtins
from utool._internal.meta_util_six import get_funcname

print, rrr, profile = ut.inject2(__name__)
logger = logging.getLogger('wbia')

DEBUG_ADDERS = False
DEBUG_SETTERS = False
DEBUG_GETTERS = False
# DEBUG_ADDERS  = ut.get_argflag(('--debug-adders', '--verbadd'))
# DEBUG_SETTERS = ut.get_argflag(('--debug-setters', '--verbset'))
# DEBUG_GETTERS = ut.get_argflag(('--debug-getters', '--verbget'))
VERB_CONTROL = ut.get_argflag(('--verb-control'))

# DEV_CACHE = ut.get_argflag(('--dev-cache', '--devcache'))
# DEBUG_API_CACHE = ut.get_argflag('--debug-api-cache')
DEV_CACHE = False
DEBUG_API_CACHE = False
RELEASE_MODE = True

if RELEASE_MODE:
    # API Cache is only for when you can gaurentee one instance of the
    # Controller will be running. This is not safe to use in production.  Use
    # only for local testing.
    # API_CACHE = ut.get_argflag('--api-cache')
    # ASSERT_API_CACHE = not ut.get_argflag(('--noassert-api-cache', '--naac'))
    API_CACHE = False
    ASSERT_API_CACHE = False
else:
    # API_CACHE = not ut.get_argflag('--no-api-cache')
    # ASSERT_API_CACHE = ut.get_argflag(('--assert-api-cache', '--naac'))
    API_CACHE = False
    ASSERT_API_CACHE = False


if ut.VERBOSE:
    if ut.in_main_process():
        if API_CACHE:
            logger.info('[accessor_decors] API_CACHE IS ENABLED')
        else:
            logger.info('[accessor_decors] API_CACHE IS DISABLED')
#
# -----------------
# IBEIS DECORATORS
# -----------------


# DECORATORS::ADDER


def init_tablecache():
    r"""
    Returns:
       defaultdict: tablecache

    CommandLine:
        python -m wbia.control.accessor_decors --test-init_tablecache

    Example:
        >>> # ENABLE_DOCTEST
        >>> from wbia.control.accessor_decors import *  # NOQA
        >>> result = init_tablecache()
        >>> print(result)
    """
    # 4 levels of dictionaries
    # tablename, colname, kwargs, and then rowids
    tablecache = ut.ddict(lambda: ut.ddict(lambda: ut.ddict(dict)))
    return tablecache


def cache_getter(tblname, colname=None, cfgkeys=None, force=False, debug=False):
    """
    Creates a getter cacher
    the class must have a table_cache property
    varargs are currently unallowed

    Args:
        tblname (str):
        colname (str):

    Returns:
        function: closure_getter_cacher

    CommandLine:
        python -m wbia.control.accessor_decors --test-cache_getter

    Example0:
        >>> # ENABLE_DOCTEST
        >>> from wbia.control.accessor_decors import *  # NOQA
        >>> import wbia
        >>> from wbia import constants as const
        >>> ibs = wbia.opendb('testdb1')
        >>> #ibs = wbia.opendb('PZ_MTEST')
        >>> valid_nids = ibs.get_valid_nids()
        >>> tblname = const.NAME_TABLE
        >>> colname = 'annot_rowid'
        >>> rowid_list = valid_nids
        >>> rowid_list1 = rowid_list[::2]
        >>> rowid_list2 = rowid_list[::3]
        >>> rowid_list3 = rowid_list[1::2]
        >>> kwargs = {}
        >>> getter_func = ut.get_method_func(ibs.get_name_aids)
        >>> wrp_getter_cacher = cache_getter(tblname, colname, force=True, debug=False)(getter_func)
        >>> ### Test Getter (caches)
        >>> val_list1 = getter_func(ibs, rowid_list1)
        >>> val_list2 = wrp_getter_cacher(ibs, rowid_list1)
        >>> print(ut.repr2(ibs.table_cache))
        >>> val_list3 = wrp_getter_cacher(ibs, rowid_list1)
        >>> val_list4 = wrp_getter_cacher(ibs, rowid_list2)
        >>> print(ut.repr2(ibs.table_cache))
        >>> val_list5 = wrp_getter_cacher(ibs, rowid_list3)
        >>> val_list  = wrp_getter_cacher(ibs, rowid_list)
        >>> ut.assert_eq(val_list1, val_list2, 'run1')
        >>> ut.assert_eq(val_list1, val_list2, 'run2')
        >>> print(ut.repr2(ibs.table_cache))
        >>> ### Test Setter (invalidates)
        >>> setter_func = ibs.set_name_texts
        >>> wrp_cache_invalidator = cache_invalidator(tblname, force=True)(lambda *a: None)
        >>> wrp_cache_invalidator(ibs, rowid_list1)
        >>> print(ut.repr2(ibs.table_cache))

    Example1:
        >>> # ENABLE_DOCTEST
        >>> from wbia.control.accessor_decors import *  # NOQA
        >>> import wbia
        >>> from wbia import constants as const
        >>> from wbia.control.manual_feat_funcs import FEAT_KPTS
        >>> ibs = wbia.opendb('testdb1')
        >>> tblname = const.FEATURE_TABLE,
        >>> colname = FEAT_KPTS
        >>> aid_list = ibs.get_valid_aids()[0:1]
        >>> # Check that config2 actually gets you different vectors in the cache
        >>> qreq_ = ibs.new_query_request(aid_list, aid_list, cfgdict={'affine_invariance': False})
        >>> config2_ = qreq_.extern_query_config2
        >>> kpts_list1 = ibs.get_annot_kpts(aid_list, config2_=None)
        >>> kpts_list2 = ibs.get_annot_kpts(aid_list, config2_=config2_)
        >>> kp1 = kpts_list1[0][0:1]
        >>> kp2 = kpts_list2[0][0:1]
        >>> assert kp1.T[3] != 0
        >>> assert kp2.T[3] == 0
        >>> assert kp2.T[2] == kp2.T[4]

    Ignore:
        %timeit getter_func(ibs, rowid_list)
        %timeit wrp_getter_cacher(ibs, rowid_list)
    """
    assert colname is not None, 'must specify a single colname'

    def closure_getter_cacher(getter_func):
        if not API_CACHE and not force:
            # Turn of API Cache
            return getter_func

        def debug_cache_hits(ismiss_list, rowid_list):
            num_miss = sum(ismiss_list)
            num_total = len(rowid_list)
            num_hit = num_total - num_miss
            logger.info(
                '\n[get] %s.%s %d / %d cache hits'
                % (tblname, colname, num_hit, num_total)
            )

        def assert_cache_hits(ibs, ismiss_list, rowid_list, kwargs_hash, **kwargs):
            cached_rowid_list = ut.filterfalse_items(rowid_list, ismiss_list)
            cache_ = ibs.table_cache[tblname][colname][kwargs_hash]
            # Load cached values for each rowid
            cache_vals_list = ut.dict_take_list(cache_, cached_rowid_list, None)
            db_vals_list = getter_func(ibs, cached_rowid_list, **kwargs)
            # Assert everything is valid
            msg_fmt = ut.codeblock(
                """
                [assert_cache_hits] tblname = %r
                [assert_cache_hits] colname = %r
                [assert_cache_hits] cfgkeys = %r
                [assert_cache_hits] CACHE INVALID: %r != %r
                """
            )
            msg = msg_fmt % (tblname, colname, cfgkeys, cache_vals_list, db_vals_list)
            try:
                list1 = cache_vals_list
                list2 = db_vals_list
                assert ut.lists_eq(list1, list2), msg
                # if isinstance(db_vals_list, list):
                #    assert cache_vals_list == db_vals_list, msg
                # else:
                #    assert np.all(cache_vals_list == db_vals_list), msg
            except AssertionError as ex:
                raise ex
            except Exception as ex2:
                logger.info(type(cache_vals_list))
                logger.info(type(db_vals_list))
                ut.printex(ex2)
                ut.embed()
                raise

        if False:
            # @profile cannot profile this because it is alrady being profiled by
            def wrp_getter_cacher(ibs, rowid_list, **kwargs):
                """
                Wrapper function that caches rowid values in a dictionary
                """
                # HACK TAKE OUT GETTING DEBUG OUT OF KWARGS
                debug_ = kwargs.pop('debug', False)
                if cfgkeys is not None:
                    # kwargs_hash = ut.get_dict_hashid(ut.dict_take_list(kwargs, cfgkeys, None))
                    kwargs_hash = ut.get_dict_hashid(
                        [kwargs.get(key, None) for key in cfgkeys]
                    )
                    # ut.dict_take_list(kwargs, cfgkeys, None))
                else:
                    kwargs_hash = None
                # +----------------------------
                # There are 3 levels of caches
                # +----------------------------
                # All caches for this table
                # colscache_ = ibs.table_cache[tblname]
                # # All caches for the this column
                # kwargs_cache_ = colscache_[colname]
                # # All caches for this kwargs configuration
                # cache_ = kwargs_cache_[kwargs_hash]
                cache_ = ibs.table_cache[tblname][colname][kwargs_hash]
                # L____________________________

                # Load cached values for each rowid
                # vals_list = ut.dict_take_list(cache_, rowid_list, None)
                vals_list = [cache_.get(rowid, None) for rowid in rowid_list]
                # Mark rowids with cache misses
                ismiss_list = [val is None for val in vals_list]
                if debug or debug_:
                    debug_cache_hits(ismiss_list, rowid_list)
                    # logger.info('[cache_getter] "debug_cache_hits" turned off')
                # HACK !!! DEBUG THESE GETTERS BY ASSERTING INFORMATION IN CACHE IS CORRECT
                if ASSERT_API_CACHE:
                    assert_cache_hits(ibs, ismiss_list, rowid_list, kwargs_hash, **kwargs)
                # END HACK
                if any(ismiss_list):
                    miss_indices = ut.list_where(ismiss_list)
                    miss_rowids = ut.compress(rowid_list, ismiss_list)
                    # call wrapped function
                    miss_vals = getter_func(ibs, miss_rowids, **kwargs)
                    # overwrite missed output
                    for index, val in zip(miss_indices, miss_vals):
                        vals_list[index] = val  # Output write
                    # cache save
                    for rowid, val in zip(miss_rowids, miss_vals):
                        cache_[rowid] = val  # Cache write
                return vals_list

        else:

            def handle_cache_misses(
                ibs, getter_func, rowid_list, ismiss_list, vals_list, cache_, kwargs
            ):
                miss_indices = ut.list_where(ismiss_list)
                miss_rowids = ut.compress(rowid_list, ismiss_list)
                # call wrapped function
                miss_vals = getter_func(ibs, miss_rowids, **kwargs)
                # overwrite missed output
                for index, val in zip(miss_indices, miss_vals):
                    vals_list[index] = val  # Output write
                # cache save
                for rowid, val in zip(miss_rowids, miss_vals):
                    cache_[rowid] = val  # Cache write

            def wrp_getter_cacher(ibs, rowid_list, **kwargs):
                """
                Wrapper function that caches rowid values in a dictionary
                """
                kwargs.pop('debug', False)
                kwargs_hash = (
                    None
                    if cfgkeys is None
                    else ut.get_dict_hashid([kwargs.get(key, None) for key in cfgkeys])
                )
                # There are 3 levels of caches
                # All caches for this table, caches for the this column, and caches for this kwargs configuration
                cache_ = ibs.table_cache[tblname][colname][kwargs_hash]
                # Load cached values for each rowid
                vals_list = [cache_.get(rowid, None) for rowid in rowid_list]
                # Mark rowids with cache misses
                ismiss_list = [val is None for val in vals_list]
                # END HACK
                if any(ismiss_list):
                    handle_cache_misses(
                        ibs,
                        getter_func,
                        rowid_list,
                        ismiss_list,
                        vals_list,
                        cache_,
                        kwargs,
                    )
                return vals_list

        wrp_getter_cacher = ut.preserve_sig(wrp_getter_cacher, getter_func)
        return wrp_getter_cacher

    return closure_getter_cacher


def cache_invalidator(tblname, colnames=None, rowidx=None, force=False):
    """cacher decorator

    Args:
        tablename (str): the table that the owns the underlying cache
        colnames (list): the list of cached column that this function will invalidate
        rowidx (int): the position (not including self) of the invalidated
                      table's native rowid in the writer function's argument
                      signature. If this does not exist you should use None.
                      (default=None)
    """
    colnames = [colnames] if isinstance(colnames, six.string_types) else colnames

    def closure_cache_invalidator(writer_func):
        """
        writer_func is either a setter, deleter, or an adder, something that writes to
        the database.
        """
        if not API_CACHE and not force:
            return writer_func

        def wrp_cache_invalidator(self, *args, **kwargs):
            # the class must have a table_cache property
            colscache_ = self.table_cache[tblname]
            colnames_ = list(six.iterkeys(colscache_)) if colnames is None else colnames
            if DEBUG_API_CACHE:
                indenter = ut.Indenter('[%s]' % (tblname,))
                indenter.start()
                logger.info('+------')
                logger.info(
                    'INVALIDATING tblname=%r, colnames=%r, rowidx=%r, force=%r'
                    % (tblname, colnames, rowidx, force)
                )
                logger.info('self = %r' % (self,))
                logger.info('args = %r' % (args,))
                logger.info('kwargs = %r' % (kwargs,))
                logger.info('colscache_ = ' + ut.repr2(colscache_, truncate=1))

            # Clear the cache of any specified colname
            # when the invalidator is called
            if rowidx is None:
                for colname in colnames_:
                    kwargs_cache_ = colscache_[colname]
                    # We dont know the rowsids so clear everything
                    for cache_ in six.itervalues(kwargs_cache_):
                        cache_.clear()
            else:
                rowid_list = args[rowidx]
                for colname in colnames_:
                    kwargs_cache_ = colscache_[colname]
                    # We know the rowids to delete
                    # iterate over all getter kwargs values
                    for cache_ in six.itervalues(kwargs_cache_):
                        ut.delete_dict_keys(cache_, rowid_list)

            # Preform set/delete action
            if DEBUG_API_CACHE:
                logger.info('After:')
                logger.info('colscache_ = ' + ut.repr2(colscache_, truncate=1))
                logger.info('L__________')

            writer_result = writer_func(self, *args, **kwargs)

            if DEBUG_API_CACHE:
                indenter.stop()
            return writer_result

        wrp_cache_invalidator = ut.preserve_sig(wrp_cache_invalidator, writer_func)
        return wrp_cache_invalidator

    return closure_cache_invalidator


def dev_cache_getter(tblname, colname, *args, **kwargs):
    """ cache getter for when the database is gaurenteed not to change """

    def closure_dev_getter_cacher(getter_func):
        if not DEV_CACHE:
            return getter_func
        return cache_getter(tblname, colname, *args, **kwargs)(getter_func)

    return closure_dev_getter_cacher


# @decorator.decorator
def adder(func):
    @ut.accepts_scalar_input
    @ut.ignores_exc_tb
    def wrp_adder(*args, **kwargs):
        if DEBUG_ADDERS or VERB_CONTROL:
            logger.info('+------')
            logger.info('[ADD]: ' + get_funcname(func))
            funccall_str = ut.func_str(func, args, kwargs, packed=True)
            logger.info('\n' + funccall_str + '\n')
            logger.info('L------')
        if VERB_CONTROL:
            logger.info('[ADD]: ' + get_funcname(func))
            builtins.print('\n' + ut.func_str(func, args, kwargs) + '\n')
        return func(*args, **kwargs)

    wrp_adder = ut.preserve_sig(wrp_adder, func)
    # wrp_adder = ut.on_exception_report_input(wrp_adder)
    return wrp_adder


# DECORATORS::DELETER

# @decorator.decorator
def deleter(func):
    @ut.accepts_scalar_input
    @ut.ignores_exc_tb
    def wrp_deleter(*args, **kwargs):
        if VERB_CONTROL:
            logger.info('[DELETE]: ' + get_funcname(func))
            builtins.print('\n' + ut.func_str(func, args, kwargs) + '\n')
        return func(*args, **kwargs)

    wrp_deleter = ut.preserve_sig(wrp_deleter, func)
    return wrp_deleter


# DECORATORS::SETTER

# @decorator.decorator
def setter(func):
    @ut.accepts_scalar_input2(argx_list=[0, 1], outer_wrapper=False)
    @ut.ignores_exc_tb
    def wrp_setter(*args, **kwargs):
        if DEBUG_SETTERS or VERB_CONTROL:
            logger.info('+------')
            logger.info('[SET]: ' + get_funcname(func))
            logger.info('[SET]: called by: ' + ut.get_caller_name(range(1, 7)))
            funccall_str = ut.func_str(func, args, kwargs, packed=True)
            logger.info('\n' + funccall_str + '\n')
            logger.info('L------')
            # builtins.print('\n' + funccall_str + '\n')
        # logger.info('set: funcname=%r, args=%r, kwargs=%r' % (get_funcname(func), args, kwargs))
        return func(*args, **kwargs)

    wrp_setter = ut.preserve_sig(wrp_setter, func)
    # wrp_setter = ut.on_exception_report_input(wrp_setter)
    return wrp_setter


# DECORATORS::GETTER


def getter(func):
    """
    Getter decorator for functions which takes as the first input a unique id
    list and returns a heterogeous list of values
    """

    @ut.accepts_scalar_input
    @ut.ignores_exc_tb
    def wrp_getter(*args, **kwargs):
        # if ut.DEBUG:
        #    logger.info('[IN GETTER] args=%r' % (args,))
        #    logger.info('[IN GETTER] kwargs=%r' % (kwargs,))
        if DEBUG_GETTERS or VERB_CONTROL:
            logger.info('+------')
            logger.info('[GET]: ' + get_funcname(func))
            funccall_str = ut.func_str(func, args, kwargs, packed=True)
            logger.info('\n' + funccall_str + '\n')
            logger.info('L------')
        return func(*args, **kwargs)

    wrp_getter = ut.preserve_sig(wrp_getter, func)
    # wrp_getter = ut.on_exception_report_input(wrp_getter)
    return wrp_getter


# @decorator.decorator
def getter_vector_output(func):
    """
    Getter decorator for functions which takes as the first input a unique id
    list and returns a homogenous list of values
    """

    @ut.accepts_scalar_input_vector_output
    @ut.ignores_exc_tb
    def getter_vector_wrp(*args, **kwargs):
        return func(*args, **kwargs)

    getter_vector_wrp = ut.preserve_sig(getter_vector_wrp, func)
    return getter_vector_wrp


getter_1toM = getter_vector_output
getter_1to1 = getter
getter_1to1 = getter


# @decorator.decorator
def getter_numpy(func):
    """
    Getter decorator for functions which takes as the first input a unique id
    list and returns a heterogeous list of values
    """
    # getter_func = getter(func)

    @ut.accepts_numpy
    @ut.accepts_scalar_input
    @ut.ignores_exc_tb
    def getter_numpy_wrp(*args, **kwargs):
        return func(*args, **kwargs)

    getter_numpy_wrp = ut.preserve_sig(getter_numpy_wrp, func)
    # getter_numpy_wrp = ut.on_exception_report_input(getter_numpy_wrp)
    return getter_numpy_wrp


# @decorator.decorator
def getter_numpy_vector_output(func):
    """
    Getter decorator for functions which takes as the first input a unique
    id list and returns a heterogeous list of values
    """
    # getter_func = getter_vector_output(func)

    @ut.accepts_numpy
    @ut.accepts_scalar_input_vector_output
    @ut.ignores_exc_tb
    def getter_numpy_vector_wrp(*args, **kwargs):
        return func(*args, **kwargs)

    getter_numpy_vector_wrp = ut.preserve_sig(getter_numpy_vector_wrp, func)
    return getter_numpy_vector_wrp


def ider(func):
    """ This function takes returns ids subject to conditions """
    ider_func = ut.preserve_sig(func, func)
    return ider_func
