"""
Module utilisé à l'UMONS dans le cadre des cours de Programmation et
Algorithmique 1 et Structure de Données 1 pour faciliter le calcul
des temps CPU.

Auteur: Pierre Hauweele et Hadrien Mélot (Université de Mons), 2016
"""

import timeit
import pickle


def __init_timer__(f, *args):
    fs = pickle.dumps(f)
    argss = pickle.dumps(args)
    setup = \
"""
import pickle
import copy
f = pickle.loads(%s)
args = pickle.loads(%s)
""" % (fs, argss)
    stmt = 'f(*copy.deepcopy(args))'
    return timeit.Timer(stmt, setup)


def __calibrate__(t):
    calibrate_test = 0
    n = 1

    while calibrate_test < 0.1:
        n *= 10
        calibrate_test = t.timeit(n)

    return n, calibrate_test


def cpu_time(f, *args):
    """ Retourne un temps CPU exprimé en millisecondes (ms)
            - f : fonction ou méthode à tester
            - *args : liste d'arguments pour f. Ces arguments ne sont pas
              modifiés, même si la fonction f a des effets de bord (ils sont
              copiés avant l'exécution).

            Exemples :
                cputime(math.sqrt, 4)
                   pour calculer le temps CPU de math.sqrt(4)
                cputime(str.upper, 'hello')
                   pour calculer le temps CPU de 'hello'.upper()
                cputime(myfunc, x, y, z)
                   pour calculer le temps CPU de myfunc(x, y, z)
    """
    t = __init_timer__(f, *args)

    n, cal_time = __calibrate__(t)

    res = min([cal_time] + t.repeat(2, n))

    return (res / n) * 1000


def calibrate(f, *args):
    """ Retourne un nombre de tests qui rend le calcul du temps CPU
        a priori raisonnable.
            - f : fonction ou méthode à tester
            - *args : liste d'arguments pour f. Ces arguments ne sont pas
              modifiés, même si la fonction f a des effets de bord (ils sont
              copiés avant l'exécution).

        Le nombre de tests retourné est une puissance de 10 (au minimum 10). Il
        sera d'autant plus grand si la fonction semble rapide.
    """
    t = __init_timer__(f, *args)

    n, cal_time = __calibrate__(t)

    return n


def cpu_time_without_copy(f, *args):
    """ Retourne un temps CPU exprimé en millisecondes (ms)
            - f : fonction ou méthode à tester
            - *args : liste d'arguments pour f.
            Cette version ne copie pas les arguments:
            il ne faut donc l'utiliser qu'avec des fonctions
            sans effet de bord !
    """
    fs = pickle.dumps(f)
    argss = pickle.dumps(args)
    setup = \
"""
import pickle
f = pickle.loads(%s)
args = pickle.loads(%s)
""" % (fs, argss)
    stmt = 'f(*args)'
    t = timeit.Timer(stmt, setup)

    calibrate_test = 0
    n = 1

    while calibrate_test < 0.1:
        n *= 10
        calibrate_test = t.timeit(n)

    res = min([calibrate_test] + t.repeat(2, n))

    return (res / n) * 1000