"""Session 3

Complexity analysis
Algorithm Design

"""
from math import isqrt
import random
import timeit
import unittest

import matplotlib.pyplot as plt
import numpy as np
import sympy

# pylint: disable=missing-docstring


def even(num):
    return num % 2 == 0


def divisors(num):
    """
    m < n ==> m = k n | k in (0, 1)
    T = T1 + T2 + n (T3 + T4) + k n T5 ==> O(n)
      = Tb + n (T3 + T4 + k T5) = Tb + n Ta
    T = n Ta + Tb

    O(n/2) = O(0.5 n) ==> O(n)
    T = 0.5 n Ta + Tb
    """
    assert isinstance(num, int)     # T1
    divs = []                     # T2
    # range --> [1, n + 1) ==> [1, n]
    for x_num in range(1, num + 1):     # n * (T3 + T4) + m * (T5), m < n, n > 1
        if num % x_num == 0:            # T3 + T4
            divs.append(x_num)        # T5 **special** if previous condition
    return divs


def divisors_improved(num):
    """
    f(n) = isqrt(n)
    h(n) = k f(n)
    T = T1 + T2 + f(n) (T3 + T4) + h(n) T6 ==> O(g(n)) < O(n)
    𝒪(sqrt(n))
    """
    assert isinstance(num, int)
    divs = []
    for x_n in range(1, isqrt(num) + 1):  # range --> [1, n + 1) ==> [1, n]
        if num % x_n == 0:
            divs.extend([x_n, num // x_n])
    return divs


def demo():
    max_count = 5
    data = np.zeros((max_count, 3))
    for count in range(max_count):
        target = 60 * (10 ** count)  # 000
        number = 10
        div1 = lambda num=target: divisors(num)
        div2 = lambda num=target: divisors_improved(num)
        std_perf = timeit.timeit(div1, number=number)
        imp_perf = timeit.timeit(div2, number=number)
        data[count] = (target, 1e6 * std_perf/number, 1e6 * imp_perf/number)
        print(f'Std vs improved: {data[count]}')

    n_x, y_ori, y_imp = data.T
    fig, axs = plt.subplots(2)
    fig.suptitle('Divisors Algorithm Analysis and Design')
    axs[0].plot(n_x, y_ori, label=r'$\mathcal{O}(n)$, divisor')
    axs[0].plot(n_x, y_imp, label='divisor_imp', color='tab:orange')
    axs[0].legend()
    axs[0].set_ylabel(r'$t$ ($\mu$s)')
    axs[1].plot(n_x, y_imp, label='divisor_imp', color='tab:orange')
    axs[1].legend()
    axs[1].set_ylabel(r'$t$ ($\mu$s)')
    plt.xlabel(r'$n$')
    plt.show()


class TestSession03(unittest.TestCase):

    def test_even(self):
        # Z⁺
        for val in range(10000):
            self.assertTrue(even(2*val))

    def test_divisors(self):
        vals = [random.randint(1e2, 1e4) for _ in range(5)]
        for val in vals:
            self.assertEqual(divisors(val), sympy.divisors(val))
            self.assertSetEqual(set(divisors(val)), set(sympy.divisors(val)))

    def test_divisors_improved(self):
        vals = {sympy.randprime(1, 100) for _ in range(9)}
        num = int(np.prod(np.asarray(list(vals))))
        res = set(divisors_improved(num))
        self.assertTrue(vals.issubset(res))  # 1, num
        self.assertEqual(res, set(sympy.divisors(num)))
        self.func_support()

    def test_general(self):
        target = 10
        set_l = {x for x in divisors(target) if even(x)}
        set_limp = {x for x in divisors_improved(target) if even(x)}
        self.assertIsInstance(set_l, set)
        self.assertGreaterEqual(len(set_l), 1 if target == 1 else 2)
        self.assertSetEqual(set_l, set_limp)

    def func_support(self):
        print('+-' * 10)

    @unittest.expectedFailure
    def test_fallo(self):
        self.assertEqual(1, 0)

    @unittest.expectedFailure
    def test_fallo2(self):
        self.assertEqual(1, 0)


if __name__ == "__main__":
    #demo()
    unittest.main(verbosity=2)
