import numpy as np
import random


def de_cp(
    fobj,  # objective function, serial eval
    bounds,  # bounds array for the population
    pop=[],  # already initialized population
    history=[],  # previous history that will be appended to
    it_start=0,  # the start index of the iteration
    mut=0.8,  # mutation rate
    crossp=0.7,  # crossover
    popsize=20,  # the population size
    its=1000,  # the number of iterations needed to run for
    **kwargs
):
    """
        This function performs diff evolution using fobj evaluated in parallel  
        returns the  last pop, fitness of pop, best individual and opt history 
    """
    #mut = 0.8 + 0.6*np.random.random()
    dimensions = len(bounds)
    history=[]
    min_b, max_b = np.asarray(bounds).T
    if pop == []: 
        pop = np.random.rand(popsize, dimensions)
    diff = np.fabs(min_b - max_b)
    pop_denorm = min_b + pop * diff
    fitness = np.asarray(fobj(pop_denorm, **kwargs))
    best_idx = np.argmin(fitness)
    best = pop_denorm[best_idx]
    for i in range(its):
        trialarr = np.zeros((popsize, dimensions))
        for j in range(popsize):
            idxs = [idx for idx in range(popsize) if idx != j]
            a, b, c = pop[np.random.choice(idxs, 3, replace = False)]
            #mutant = np.clip(a + mut * (b - c), 0, 1)
            mutant = np.clip(pop[best_idx] + mut * (b - c), 0, 1)
            cross_points = np.random.rand(dimensions) < crossp
            if not np.any(cross_points):
                cross_points[np.random.randint(0, dimensions)] = True
            trialarr[j] = np.where(cross_points, mutant, pop[j])
        trial_denorm = min_b + trialarr * diff
        f = fobj(trial_denorm, **kwargs)
        for j in range(popsize):
            if f[j] < fitness[j]:
                fitness[j] = f[j]
                pop[j] = trialarr[j]
                if f[j] < fitness[best_idx]:
                    best_idx = j
                    best = trial_denorm[j]

        if (i+1)%20 == 0:
            print(i, fitness[best_idx])
        history.append([i, fitness[best_idx]])
    return pop, best_idx, fitness[best_idx], best

    
    
    

def de_c(
    fobj,  # objective function, serial eval
    bounds,  # bounds array for the population
    pop=[],  # already initialized population
    history=[],  # previous history that will be appended to
    it_start=0,  # the start index of the iteration
    mut=0.8,  # mutation rate
    crossp=0.8,  # crossover
    popsize=20,  # the population size
    its=1000,  # the number of iterations needed to run for
    **kwargs
):
    """
        This function performs diff evolution using fobj evaluated serially.
        returns the  best, fitness of the best, and opt history
    """
    dimensions = len(bounds)
    min_b, max_b = np.asarray(bounds).T
    diff = np.fabs(min_b - max_b)
    if pop == []: 
        pop = np.random.rand(popsize, dimensions)
    pop_denorm = min_b + pop * diff
    fitness = np.asarray([fobj(ind, **kwargs) for ind in pop_denorm])
    best_idx = np.argmin(fitness)
    best = pop_denorm[best_idx]
    for i in range(its):
        for j in range(popsize):
            idxs = [idx for idx in range(popsize) if idx != j]
            a, b, c = pop[np.random.choice(idxs, 3, replace=False)]
            mutant = np.clip(a + mut * (b - c), 0, 1)
#             #TODO: Here we can chage the strategy for instance
#             if i >=150:
#                 if random.random() >=0.82: 
#                     mutant = np.clip(a + mut * (b - c), 0, 1)
#             else:
#                 mutant = np.clip(pop[best_idx] + mut * (b - c), 0, 1)
            cross_points = np.random.rand(dimensions) < crossp
            if not np.any(cross_points):
                cross_points[np.random.randint(0, dimensions)] = True
            trial = np.where(cross_points, mutant, pop[j])
            trial_denorm = min_b + trial * diff
            f = fobj(trial_denorm, **kwargs)
            if f < fitness[j]:
                fitness[j] = f
                pop[j] = trial
                if f < fitness[best_idx]:
                    best_idx = j
                    best = trial_denorm
        if i % 5 == 0:
            print(i, fitness[best_idx])
        history.append([i + it_start, fitness[best_idx]])
    return best, fitness[best_idx], history


def de_g(
    fobj,  # objsctive function, parallel eval
    bounds,  # bounds array for the population
    mut=0.8,  # mutation rate
    crossp=0.7,  # crossover
    popsize=20,  # the population size
    its=1000,  # the number of iterations needed to run for
    **kwargs
):
    """
        This function performs diff evolution using fobj evaluated in parallel  
        returns the  last pop, fitness of pop, best individual and opt history 
    """
    dimensions = len(bounds)
    history=[]
    pop = np.random.rand(popsize, dimensions)
    min_b, max_b = np.asarray(bounds).T
    diff = np.fabs(min_b - max_b)
    pop_denorm = min_b + pop * diff
    fitness = np.asarray(fobj(pop_denorm, **kwargs))
    best_idx = np.argmin(fitness)
    best = pop_denorm[best_idx]
    for i in range(its):
        trialarr = np.zeros((popsize, dimensions))
        for j in range(popsize):
            idxs = [idx for idx in range(popsize) if idx != j]
            a, b, c = pop[np.random.choice(idxs, 3, replace = False)]
            #mutant = np.clip(a + mut * (b - c), 0, 1)
            mutant = np.clip(pop[best_idx] + mut * (b - c), 0, 1)
            cross_points = np.random.rand(dimensions) < crossp
            if not np.any(cross_points):
                cross_points[np.random.randint(0, dimensions)] = True
            trialarr[j] = np.where(cross_points, mutant, pop[j])
        trial_denorm = min_b + trialarr * diff
        f = fobj(trial_denorm, **kwargs)
        for j in range(popsize):
            if f[j] < fitness[j]:
                fitness[j] = f[j]
                pop[j] = trialarr[j]
                if f[j] < fitness[best_idx]:
                    best_idx = j
                    best = trial_denorm[j]
#         if i%50 == 0:
#             print(i, fitness[best_idx])
        history.append([i, fitness[best_idx]])
    return pop, fitness, best, history