import numpy as np

from pypop7.optimizers.ep.ep import EP


class CEP(EP):
    """Classical Evolutionary Programming with self-adaptive mutation (CEP).

    References
    ----------
    Yao, X., Liu, Y. and Lin, G., 1999.
    Evolutionary programming made faster.
    IEEE Transactions on Evolutionary computation, 3(2), pp.82-102.
    https://ieeexplore.ieee.org/abstract/document/771163
    """
    def __init__(self, problem, options):
        EP.__init__(self, problem, options)
        self.sigma = options.get('sigma')  # initial global step-size
        self.q = options.get('q', 10)  # number of opponents for pairwise comparisons
        # two learning rate factors of individual step-size
        self.tau = options.get('tau', 1.0 / np.sqrt(2.0*np.sqrt(self.ndim_problem)))
        self.tau_apostrophe = options.get('tau_apostrophe', 1.0 / np.sqrt(2.0*self.ndim_problem))

    def initialize(self, args=None):
        x = self.rng_initialization.uniform(self.initial_lower_boundary, self.initial_upper_boundary,
                                            size=(self.n_individuals, self.ndim_problem))
        y = np.empty((self.n_individuals,))
        sigmas = self.sigma*np.ones((self.n_individuals, self.ndim_problem))
        for i in range(self.n_individuals):
            if self._check_terminations():
                break
            y[i] = self._evaluate_fitness(x[i], args)
        return x, y, sigmas

    def iterate(self, x, y, n):
        new_x = np.empty((self.n_individuals, self.ndim_problem))
        new_n = np.empty((self.n_individuals, self.ndim_problem))
        new_y = np.empty((self.n_individuals,))
        rand1 = self.rng_optimization.standard_normal(self.ndim_problem)
        for i in range(self.n_individuals):
            for j in range(self.ndim_problem):
                new_x[i][j] = x[i][j] + n[i][j] * rand1[j]
                new_n[i][j] = n[i][j] * np.exp(self.t1 * self.rng_optimization.standard_normal(1) + self.t * rand1[j])
            new_x[i] = np.clip(new_x[i], self.lower_boundary, self.upper_boundary)
            new_y[i] = self._evaluate_fitness(new_x[i])
        new_x = np.vstack((new_x, x))
        new_y = np.hstack((new_y, y))
        new_n = np.vstack((new_x, x))
        win = np.zeros((2 * self.n_individuals,))
        list1 = list(range(0, 2 * self.n_individuals))
        for i in range(2 * self.n_individuals):
            list2 = random.sample(list1, self.q)
            for j in list2:
                if new_y[i] >= new_y[j]:
                    win[i] += 1
        order = np.argsort(win)
        for i in range(self.n_individuals):
            x[i] = new_x[order[i]]
            y[i] = new_y[order[i]]
            n[i] = new_n[order[i]]
        return x, y, n

    def optimize(self, fitness_function=None, args=None):
        fitness = EP.optimize(self, fitness_function)
        x, y, n = self.initialize()
        fitness.extend(y)
        while True:
            x, y, n = self.iterate(x, y, n)
            if self.record_fitness:
                fitness.extend(y)
            if self._check_terminations():
                break
            self._n_generations += 1
            self._print_verbose_info(y)
        return self._collect_results(fitness)
