#!/usr/bin/env python
#
# Public Domain 2014-present MongoDB, Inc.
# Public Domain 2008-2014 WiredTiger, Inc.
#
# This is free and unencumbered software released into the public domain.
#
# Anyone is free to copy, modify, publish, use, compile, sell, or
# distribute this software, either in source code form or as a compiled
# binary, for any purpose, commercial or non-commercial, and by any
# means.
#
# In jurisdictions that recognize copyright laws, the author or authors
# of this software dedicate any and all copyright interest in the
# software to the public domain. We make this dedication for the benefit
# of the public at large and to the detriment of our heirs and
# successors. We intend this dedication to be an overt act of
# relinquishment in perpetuity of all present and future rights to this
# software under copyright law.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.

import testscenarios
import suite_random

# wtscenarios.py
#    Support scenarios based testing
def powerrange(start, stop, mult):
    """
    Like range, generates a range from start to stop.
    Unlike range, the range is inclusive of stop,
    each step is multiplicative, and as a special case,
    the stop value is returned as the last item.
    """
    val = start
    while val <= stop:
        yield val
        newval = val * mult
        if val < stop and newval > stop:
            val = stop
        else:
            val = newval

def log2chr(val):
    """
    For the log-base 2 of val, return the numeral or letter
    corresponding to val (which is < 36).  Hence, 1 return '0',
    2 return '1', 2*15 returns 'f', 2*16 returns 'g', etc.
    """
    p = 0
    while val >= 2:
        p += 1
        val /= 2
    if p < 10:
        return chr(ord('0') + p)
    else:
        return chr(ord('a') + p - 10)

megabyte = 1024 * 1024

def make_scenarios(*args, **kwargs):
    """
    The standard way to create scenarios for WT tests.
    Scenarios can be combined by listing them all as arguments.
    If some scenario combinations should not be included,
    a include= argument function may be listed, which given a name and
    dictionary argument, returns True if the scenario should be included.
    A final prune= and/or prunelong= argument may be given that
    forces the list of entries in the scenario to be pruned.
    The result is a (combined) scenario that has been checked
    for name duplicates and has been given names and numbers.
    """
    scenes = multiply_scenarios('.', *args)
    pruneval = None
    prunelong = None
    includefunc = None
    for key in kwargs:
        if key == 'prune':
            pruneval = kwargs[key]
        elif key == 'prunelong':
            prunelong = kwargs[key]
        elif key == 'include':
            includefunc = kwargs[key]
        else:
            raise AssertionError(
                'make_scenarios: unexpected named arg: ' + key)
    if includefunc:
        scenes = [(name, d) for (name, d) in scenes if includefunc(name, d)]
    if pruneval != None or prunelong != None:
        pruneval = pruneval if pruneval != None else -1
        prunelong = prunelong if prunelong != None else -1
        scenes = prune_scenarios(scenes, pruneval, prunelong)
    return number_scenarios(scenes)

def check_scenarios(scenes):
    """
    Make sure all scenarios have unique case insensitive names
    """
    assert len(scenes) == len(dict((k.lower(), v) for k, v in scenes))
    return scenes

def multiply_scenarios(sep, *args):
    """
    Create the cross product of two lists of scenarios
    """
    result = None
    for scenes in args:
        if result == None:
            result = scenes
        else:
            total = []
            for scena in result:
                for scenb in scenes:
                    # Create a merged scenario with a concatenated name
                    name = scena[0] + sep + scenb[0]
                    tdict = {}
                    tdict.update(scena[1])
                    tdict.update(scenb[1])

                    # If there is a 'P' value, it represents the
                    # probability that we want to use this scenario
                    # If both scenarios list a probability, multiply them.
                    if 'P' in scena[1] and 'P' in scenb[1]:
                        P = scena[1]['P'] * scenb[1]['P']
                        tdict['P'] = P
                    total.append((name, tdict))
            result = total
    return check_scenarios(result)

def prune_sorter_key(scene):
    """
    Used by prune_scenerios to extract key for sorting.
    The key is the saved random value multiplied by
    the probability of choosing.
    """
    p = 1.0
    if 'P' in scene[1]:
        p = scene[1]['P']
    return p * scene[1]['_rand']

def prune_resort_key(scene):
    """
    Used by prune_scenerios to extract the original ordering key for sorting.
    """
    return scene[1]['_order']

def set_long_run(islong):
    global _is_long_run
    _is_long_run = islong

def prune_scenarios(scenes, default_count = -1, long_count = -1):
    """
    Use listed probabilities for pruning the list of scenarios.
    That is, the highest probability (value of P in the scendario)
    are chosen more often.  With just one argument, only scenarios
    with P > .5 are returned half the time, etc. A second argument
    limits the number of scenarios. When a third argument is present,
    it is a separate limit for a long run.
    """
    global _is_long_run
    r = suite_random.suite_random()
    result = []
    if default_count == -1:
        # Missing second arg - return those with P == .3 at
        # 30% probability, for example.
        for scene in scenes:
            if 'P' in scene[1]:
                p = scene[1]['P']
                if p < r.rand_float():
                    continue
            result.append(scene)
        return result
    else:
        # With at least a second arg present, we'll want a specific count
        # of items returned.  So we'll sort them all and choose
        # the top number.  Not the most efficient solution,
        # but it's easy.
        if _is_long_run and long_count != -1:
            count = long_count
        else:
            count = default_count

        l = len(scenes)
        if l <= count:
            return scenes
        if count == 0:
            return []
        order = 0
        for scene in scenes:
            scene[1]['_rand'] = r.rand_float()
            scene[1]['_order'] = order
            order += 1
        scenes = sorted(scenes, key=prune_sorter_key) # random sort driven by P
        scenes = scenes[l-count:l]                    # truncate to get best
        scenes = sorted(scenes, key=prune_resort_key) # original order
        for scene in scenes:
            del scene[1]['_rand']
            del scene[1]['_order']
        return check_scenarios(scenes)

def filter_scenarios(scenes, pred):
    """
    Filter scenarios that match a predicate
    """
    return [s for s in scenes if pred(*s)]

def number_scenarios(scenes):
    """
    Add a 'scenario_number' and 'scenario_name' variable to each scenario.
    The hash table for each scenario is altered!
    """
    count = 0
    for scene in scenes:
        scene[1]['scenario_name'] = scene[0]
        scene[1]['scenario_number'] = count
        count += 1
    return check_scenarios(scenes)

def quick_scenarios(fieldname, values, probabilities):
    """
    Quickly build common scenarios, like:
       [('foo', dict(somefieldname='foo')),
       ('bar', dict(somefieldname='bar')),
       ('boo', dict(somefieldname='boo'))]
    via a call to:
       quick_scenario('somefieldname', ['foo', 'bar', 'boo'])
    """
    result = []
    if probabilities == None:
        plen = 0
    else:
        plen = len(probabilities)
    ppos = 0
    for value in values:
        if ppos >= plen:
            d = dict([[fieldname, value]])
        else:
            p = probabilities[ppos]
            ppos += 1
            d = dict([[fieldname, value],['P', p]])
        result.append((str(value), d))
    return result

class wtscenario:
    """
    A set of generators for different test scenarios
    """

    @staticmethod
    def session_create_scenario():
        """
        Return a set of scenarios with the name of this method
        'session_create_scenario' as the name of instance
        variable containing a wtscenario object.  The wtscenario
        object can be queried to get a config string.
        Each scenario is named according to the shortName() method.
        """
        s = [
            ('default', dict(session_create_scenario=wtscenario())) ]
        for imin in powerrange(512, 512*megabyte, 1024):
            for imax in powerrange(imin, 512*megabyte, 1024):
                for lmin in powerrange(512, 512*megabyte, 1024):
                    for lmax in powerrange(lmin, 512*megabyte, 1024):
                        for cache in [megabyte, 32*megabyte, 1000*megabyte]:
                            scen = wtscenario()
                            scen.ioverflow = max(imin // 40, 40)
                            scen.imax = imax
                            scen.loverflow = max(lmin // 40, 40)
                            scen.lmax = lmax
                            scen.cache_size = cache
                            s.append((scen.shortName(), dict(session_create_scenario=scen)))
        return make_scenarios(s)

    def shortName(self):
        """
        Return a name of a scenario, based on the 'log2chr-ed numerals'
        representing the four values for {internal,leaf} {minimum, maximum}
        page size.
        """
        return 'scen_' + log2chr(self.ioverflow) + log2chr(self.imax) + log2chr(self.loverflow) + log2chr(self.lmax) + log2chr(self.cache_size)

    def configString(self):
        """
        Return the associated configuration string
        """
        res = ''
        if hasattr(self, 'ioverflow'):
            res += ',internal_item_max=' + str(self.ioverflow)
        if hasattr(self, 'imax'):
            res += ',internal_page_max=' + str(self.imax)
            if self.imax < 4*1024:
                res += ',allocation_size=512'
        if hasattr(self, 'loverflow'):
            res += ',leaf_item_max=' + str(self.loverflow)
        if hasattr(self, 'lmax'):
            res += ',leaf_page_max=' + str(self.lmax)
            if self.lmax < 4*1024:
                res += ',allocation_size=512'
        return res
