# -*- coding: utf-8 -*-
"""
Complete this code for continuous optimization  problem

Please remove author name if generated by machine automatically
Keep you code anonymous

"""

# Use standard python package only.
import random 
import math
import numpy as np
import matplotlib.pyplot as plt



# MINIMUM GLOBAL VARIABLES TO BE USED
POPULATION_SIZE = 100 # Change POPULATION_SIZE to obtain better fitness.

GENERATIONS = 100 # Change GENERATIONS to obtain better fitness.
SOLUTION_FOUND = False

CORSSOVER_RATE = 0.8 # Change CORSSOVER_RATE  to obtain better fitness.
MUTATION_RATE = 0.2 # Change MUTATION_RATE to obtain better fitness.


LOWER_BOUND = -10
UPPER_BOUND = 10
FITNESS_TARGET = 0

'''
    CHANGE SELECT_METHOD
    AVAILABLE METHODS ARE: 

    Tournament
    Random
    Roulette

'''
SELECT_METHOD = 'Tournament'

'''
    CHANGE FITNESS_FUNCTION
    AVAILABLE METHODS ARE:

    -- CHECK compute_fitness function FOR DETAILS

'''
FITNESS_FUNCTION = 'Sum of squares'
ELITISM = True


"""
    POPULATION GENERATIONS
"""
def generate_population(size, lower_bound, upper_bound):
    
    population = [] # holds the population

    for i in range(size):
        
        # uniform generates a float value between the two bounds
        individual = { 
            "x" : random.uniform(lower_bound, upper_bound),
            "y" : random.uniform(lower_bound, upper_bound)
        }

        population.append(individual) # adds the generated individual to the population

    return population

def next_generation(previous_population):
    
    if ELITISM:

        elite = sort_population(previous_population)
        elite_size = int(len(elite) * 0.1)

        next_generation = [] # Stores the individuals for the next generation
        next_generation.extend(elite[:elite_size]) # get best 10% of population

        for i in range(POPULATION_SIZE - elite_size):
            # Select Parents
            first_parent = selection(previous_population)
            second_parent = selection(previous_population)

            # Crossover & Mutate offspring
            if i <= ((POPULATION_SIZE-elite_size) * CORSSOVER_RATE):
                offspring = crossover(first_parent, second_parent)
            else:
                offspring = mutation(random_individual(previous_population))
        

            next_generation.append(offspring)
    
    else:

        next_generation = []

        for i in range(POPULATION_SIZE):
            first_parent = selection(previous_population)
            second_parent = selection(previous_population)

            if i <= ((POPULATION_SIZE) * CORSSOVER_RATE):
                offspring = crossover(first_parent, second_parent)
            else:
                offspring = mutation(random_individual(previous_population))

            next_generation.append(offspring)

    return next_generation


"""
    FITNESS COMPUTATIONS
"""
def compute_fitness(individual):

    x = individual['x']
    y = individual['y']

    '''Optimal Search Domain: -10 <= xi <= 10'''
    if FITNESS_FUNCTION == "Sum of squares":
        return (x**2 + y**2)

    '''Optimal Search Domain: -10 <= xi <= 10'''
    '''Optimal 4 dimensions'''
    if FITNESS_FUNCTION == "Colville":
        term1 = 100 * (x**2-y)**2;
        term2 = (x-1)**2
        return term1 + term2

    '''Optimal Search Domain: -10 <= xi <= 10'''
    if FITNESS_FUNCTION == "Matyas":
        return 0.26*(x**2 + y ** 2)-0.48*x*y

    '''Optimal Search Domain: -10 <= xi <= 10'''
    if FITNESS_FUNCTION == "Levy":
        term1 = (math.sin(3*math.pi*x))**2;
        term2 = (x-1)**2 * (1+(math.sin(3*math.pi*y))**2)
        term3 = (x-1)**2 * (1+(math.sin(2*math.pi*y))**2)

        return term1 + term2 + term3

    if FITNESS_FUNCTION == "Booth":
        term1 = (x + 2*y - 7)**2
        term2 = (2*x + y - 5)**2

        return (term1 + term2)

    '''Optimal Search Domain: -5 <= xi <= 5'''
    if FITNESS_FUNCTION == "Hump":
        return 1.0316285+4*x**2-2.1*x**4+x**6/3+x*y-4*y**2+4*y**4

    '''Optimal Search Domain: -5 <= xi <= 10'''
    if FITNESS_FUNCTION == "Rosenbrock":
        return 100*(y-x**2)**2+(x-1)**2

    '''Optimal Search Domain: -5 <= xi <= 5'''
    if FITNESS_FUNCTION == "Three-hump camel":
        term1 = 2*x**2
        term2 = -1.05*x**4
        term3 = x**6 / 6
        term4 = x*y
        term5 = y**2

        return term1 + term2 + term3 + term4 + term5;

    '''Optimal Search Domain: -4.5 <= xi <= 4.5'''
    if FITNESS_FUNCTION == "Beale":
        a = (1.5 - x + (x*y)) ** 2
        b = (2.25 - x + (x*(y ** 2))) ** 2
        c = (2.625 - x + (x*(y ** 3))) ** 2
        return (a + b + c)

        return -math.cos(x)*math.cos(y)*math.exp(-(x-math.pi)**2-(y-math.pi)**2);

    '''Optimal Search Domain: -100 <= xi <= 100'''
    if FITNESS_FUNCTION == "Bohachevsky":
        return x**2+2*y**2-0.3*math.cos(3*math.pi*x)-0.4*math.cos(4*math.pi*y)+0.7

def fitness_sum(population):
    return sum(compute_fitness(individual) for individual in population)


"""
    SORTING & PRINTING
"""
def sort_population(population):
    return sorted(population, key=compute_fitness, reverse = False) #lowest first
    # return [print(f"{individual} + fl: {compute_fitness(individual)}") for  individual in sorted(population, key=compute_fitness)]

def printpop(population):
    # return [print(f"{individual} + fl: {compute_fitness(individual)}") for  individual in population]
    return [print(f"{individual} -- {compute_fitness(individual)}") for  individual in population]


"""
    SELECTION METHODS
"""
def selection(population):
            
    if SELECT_METHOD == "Tournament":
        return tournament(population)

    if SELECT_METHOD == "Random":
        return random_individual(population)

    if SELECT_METHOD == "Roulette":
        return roulette(population)

def tournament(population):
    
    first_pick = random.choice(population)
    second_pick = random.choice(population)

    first_pick_fitness = compute_fitness(first_pick)
    second_pick_fitness = compute_fitness(second_pick)

    '''The Battle'''
    if first_pick_fitness > second_pick_fitness:
        # return first_pick
        return second_pick
    else:
        # return second_pick
        return first_pick
    '''The Battle'''

def random_individual(population):
    return random.choices(population)[0]   

def roulette(population):

    draw = random.random()

    power = math.pow(draw,5)
    random_index = int(POPULATION_SIZE * power)

    population_sorted = sort_population(population)
    return population_sorted[random_index]
   

"""
    OPERANTS
"""
def crossover(first_parent, second_parent):
    
    # could also be average: x of parent a + x of parent b / 2
    first_parent_X = first_parent["x"]
    first_parent_Y = first_parent["y"]

    second_parent_X = second_parent["x"]
    second_parent_Y = second_parent["y"]

    # offspring = {
    #     "x": ((first_parent_X + second_parent_X) / 2),
    #     "y": ((first_parent_Y + second_parent_Y) / 2)
    # }

    offspring = {
        "x": ((second_parent_X)),
        "y": ((first_parent_Y))
    }
    return offspring

def mutation(individual):
    
    #TODO : Write your own code to for choice  of your mutation operator - you can use if condition to write more tan one ime of crossover operator
    random_mutate_upper = 0.85
    random_mutate_lower = 1.15

    mutated_x = individual['x'] * random.uniform(random_mutate_lower, random_mutate_upper)
    mutated_y = individual['y'] * random.uniform(random_mutate_lower, random_mutate_upper)

    mutated_x = min(max(mutated_x, LOWER_BOUND), UPPER_BOUND)
    mutated_y = min(max(mutated_y, LOWER_BOUND), UPPER_BOUND)

    mutated_individual = {
        "x" : mutated_x,
        "y" : mutated_y
    }
    
    return mutated_individual


"""
    GRAPHS
"""
def graphs(best_per_gen):
    
    gen_values = []
    fitness_values = []

    
    for individual in best_per_gen:
        fitness_values.append(compute_fitness(individual))

    plt.suptitle("Fitness of best individuals per generation")
    plt.title("Selection: " + SELECT_METHOD + " Fitness F(x): " + FITNESS_FUNCTION + " Elitism: " + str(ELITISM))
    plt.xlabel("Generations")
    plt.ylabel("Fitness")
    plt.plot(fitness_values)
    plt.show()


    # x,y = zip(best_per_gen)

def graph_avg_fitness(avg_fitnes_per_gen):

    plt.suptitle("Average Generation fitness per generation", fontweight ='bold')
    # plt.title("Selection: " + SELECT_METHOD + " Fitness F(x): " + FITNESS_FUNCTION + " Elitism: " + str(ELITISM))

    plt.xlabel("Generations")
    plt.ylabel("Fitness")
    params = f"Selection: {SELECT_METHOD}\nFitness F(x): {FITNESS_FUNCTION}\nElitism: {ELITISM}\nRanges: {UPPER_BOUND},{LOWER_BOUND}"
    plt.text(GENERATIONS/1.7, max(avg_fitnes_per_gen)/1.35, params, bbox=dict(facecolor='White', alpha=0.1))

    line1, = plt.plot(avg_fitnes_per_gen, label="Average Fitness", linestyle='solid', linewidth = 2, color="red")
    plt.legend(handles=[line1], loc='upper right')
    
    # plt.plot(line1)
    plt.show()
    
# USE THIS MAIN FUNCTION TO COMPLETE YOUR CODE - MAKE SURE IT WILL RUN FROM COMOND LINE   
def main(): 
    global POPULATION_SIZE 
    global GENERATIONS
    global SOLUTION_FOUND
    global LOWER_BOUND
    global UPPER_BOUND
    global FITNESS_TARGET
    global FITNESS_FUNCTION

    generation = 0 # Keeps track of the current gen iteration
    best_per_gen = [] # Keeps track of the best individual per generation
    avg_fitness_per_gen = []
    keep_avg_sum = 0
    keep_avg_sum_per_indv = 0


    """
    ==== Step 0: Initial Population ====
    """
    population = generate_population(POPULATION_SIZE, LOWER_BOUND, UPPER_BOUND) # Create initial Population
    print('Step 0 Completed. Initializing genetic algorithm...')
    """
    ====================================
    """

    goat = random.choice(population)
    while (True):  # TODO: write your termination condition here or within the loop 
        
        # print(f"Generation: {generation}") # print current generation
        
        """
        For testing purposes
        """
    

        best_of_current_gen = sort_population(population)[0] # get best of gen
        print(f"Gen({generation}): {best_of_current_gen} - Fitness : {compute_fitness(best_of_current_gen)}")


        if compute_fitness(best_of_current_gen) == FITNESS_TARGET:
            break
        
        if compute_fitness(best_of_current_gen) < compute_fitness(goat):
            goat = best_of_current_gen
        
        
        # if current_gen_iteration == GENERATIONS:
        if generation == GENERATIONS:          
            break
            
        else:
            generation += 1
    

    print(f"Best Ever: {goat} - Fitness : {compute_fitness(goat)}")
 

if __name__ == '__main__': 
    main()