import time
from typing import TypeVar, List

from jmetal.config import store
from jmetal.core.algorithm import EvolutionaryAlgorithm, Algorithm
from jmetal.core.operator import Mutation, Crossover, Selection
from jmetal.core.problem import Problem
from jmetal.operator.parameter_update import DiversityBasedControl
from jmetal.util.solutions.evaluator import Evaluator
from jmetal.util.solutions.generator import Generator
from jmetal.util.termination_criterion import TerminationCriterion
from jmetal.algorithm.data.diversity import DimensionWiseDiversity
import numpy as np
import pandas as pd

S = TypeVar('S')
R = TypeVar('R')

"""
.. module:: genetic_algorithm
   :platform: Unix, Windows
   :synopsis: Implementation of Genetic Algorithms.
.. moduleauthor:: <NAME> <<EMAIL>>, <NAME> <<EMAIL>>
"""


class GeneticAlgorithm(EvolutionaryAlgorithm[S, R]):

    def __init__(self,
                 problem: Problem,
                 population_size: int,
                 offspring_population_size: int,
                 mutation: Mutation,
                 crossover: Crossover,
                 selection: Selection,
                 termination_criterion: TerminationCriterion = store.default_termination_criteria,
                 population_generator: Generator = store.default_generator,
                 population_evaluator: Evaluator = store.default_evaluator):
        super(GeneticAlgorithm, self).__init__(
            problem=problem,
            population_size=population_size,
            offspring_population_size=offspring_population_size)
        self.mutation_operator = mutation
        self.crossover_operator = crossover
        self.selection_operator = selection

        self.population_generator = population_generator
        self.population_evaluator = population_evaluator

        self.termination_criterion = termination_criterion
        self.observable.register(termination_criterion)

        self.mating_pool_size = \
            self.offspring_population_size * \
            self.crossover_operator.get_number_of_parents() // self.crossover_operator.get_number_of_children()

        if self.mating_pool_size < self.crossover_operator.get_number_of_children():
            self.mating_pool_size = self.crossover_operator.get_number_of_children()

    def create_initial_solutions(self) -> List[S]:
        return [self.population_generator.new(self.problem)
                for _ in range(self.population_size)]

    def evaluate(self, population: List[S]):
        return self.population_evaluator.evaluate(population, self.problem)

    def stopping_condition_is_met(self) -> bool:
        return self.termination_criterion.is_met

    def selection(self, population: List[S]):
        mating_population = []

        for i in range(self.mating_pool_size):
            solution = self.selection_operator.execute(population)
            mating_population.append(solution)

        return mating_population

    def reproduction(self, mating_population: List[S]) -> List[S]:
        number_of_parents_to_combine = self.crossover_operator.get_number_of_parents()

        if len(mating_population) % number_of_parents_to_combine != 0:
            raise Exception('Wrong number of parents')

        offspring_population = []
        for i in range(0, self.offspring_population_size, number_of_parents_to_combine):
            parents = []
            for j in range(number_of_parents_to_combine):
                parents.append(mating_population[i + j])

            offspring = self.crossover_operator.execute(parents)

            for solution in offspring:
                self.mutation_operator.execute(solution)
                offspring_population.append(solution)
                if len(offspring_population) >= self.offspring_population_size:
                    break

        return offspring_population

    def replacement(self, population: List[S], offspring_population: List[S]) -> List[S]:
        population.extend(offspring_population)

        population.sort(key=lambda s: s.objectives[0])

        return population[:self.population_size]

    def get_result(self) -> R:
        return self.solutions[0]

    def get_name(self) -> str:
        return 'GA'

    def get_observable_data(self) -> dict:
        return {'PROBLEM': self.problem,
                'EVALUATIONS': self.evaluations,
                'SOLUTIONS': self.solutions,
                'COMPUTING_TIME': time.time() - self.start_computing_time}


class DiversityBasedGeneticAlgorithm(GeneticAlgorithm):

    def __init__(self, problem: Problem, population_size: int, offspring_population_size: int, mutation: Mutation,
                 crossover: Crossover, selection: Selection,
                 termination_criterion: TerminationCriterion = store.default_termination_criteria,
                 population_generator: Generator = store.default_generator,
                 restart_population_generator: Generator = store.default_generator,
                 population_evaluator: Evaluator = store.default_evaluator, local_search: Algorithm = None):

        super().__init__(problem, population_size, offspring_population_size, mutation, crossover, selection,
                         termination_criterion, population_generator, population_evaluator)
        self.diversity = DimensionWiseDiversity()
        self.parameter_update = DiversityBasedControl(max_evaluations=self.termination_criterion.get_criterion())
        self.diversification_prob = 0
        self.intensification_prob = 0
        self.do_nothing_prob = 1
        self.local_search = local_search
        self.restart_population_generator = restart_population_generator
        self.diversification_counter = []
        self.intensification_counter = []

    def init_progress(self) -> None:
        super().init_progress()
        self.diversity = DimensionWiseDiversity()
        self.parameter_update = DiversityBasedControl(max_evaluations=self.termination_criterion.get_criterion())
        self.diversification_prob = 0
        self.intensification_prob = 0
        self.do_nothing_prob = 1

    def step(self):
        super().step()

        options = ['Exploration', 'Exploitation', 'Do Nothing']
        probabilities = [self.diversification_prob, self.intensification_prob, self.do_nothing_prob]
        result = np.random.choice(3, p=probabilities)

        # self.diversification_step()

        if result == 0:
            self.diversification_counter.append(1)
            self.intensification_counter.append(0)
            self.diversification_step()
        elif result == 1:
            self.diversification_counter.append(0)
            self.intensification_counter.append(1)
            self.intensification_step()
        else:
            self.diversification_counter.append(0)
            self.intensification_counter.append(0)

    def intensification_step(self):
        best_solution_idx = np.argsort([_.objectives[0] for _ in self.solutions])[0:10]

        for idx in best_solution_idx:
            best_solution = self.solutions[idx]

            self.local_search.init_progress()
            self.local_search.solutions = [best_solution]
            self.local_search.run()
            ls_result = self.local_search.get_result()

            if ls_result.objectives[0] <= best_solution.objectives[0]:
                self.solutions[idx] = ls_result

            self.evaluations += self.local_search.evaluations

    def diversification_step(self):
        self.restart_population()

    def restart_population(self):
        amount_ind = int((25/100) * self.population_size)

        worst_solutions = np.argsort([_.objectives[0] for _ in self.solutions])[-amount_ind:]

        for replacement_idx in worst_solutions:
            self.solutions[replacement_idx] = self.population_generator.new(self.problem)

        self.evaluate(np.take(self.solutions, worst_solutions))

    def update_progress(self) -> None:
        super().update_progress()
        self.update_diversity()
        self.update_probabilities()

    def update_diversity(self):
        self.diversity.update(self.evaluations, self.solutions, self.problem.number_of_variables)

    def update_probabilities(self):
        xpl, xpt = self.diversity.get_data()
        data = {"EVALUATIONS": self.evaluations, "XPL": xpl, "XPT": xpt}
        self.do_nothing_prob, self.diversification_prob, self.intensification_prob = self.parameter_update.update(
            **data)

    def get_observable_data(self) -> dict:
        return {'PROBLEM': self.problem,
                'EVALUATIONS': self.evaluations,
                'SOLUTIONS': self.solutions,
                'COMPUTING_TIME': time.time() - self.start_computing_time,
                'DIVERSIFICATION_PERCENTAGE': self.parameter_update.get_diversification_percentage(),
                'INTENSIFICATION_PERCENTAGE': self.parameter_update.get_intensification_percentage(),
                'DO_NOTHING_PERCENTAGE': self.parameter_update.get_do_nothing_percentage()}

    def get_name(self) -> str:
        return "diversity-ga"





import time
from typing import TypeVar, List

from jmetal.config import store
from jmetal.core.algorithm import EvolutionaryAlgorithm, Algorithm
from jmetal.core.operator import Mutation, Crossover, Selection
from jmetal.core.problem import Problem
from jmetal.operator.parameter_update import DiversityBasedControl
from jmetal.util.solutions.evaluator import Evaluator
from jmetal.util.solutions.generator import Generator
from jmetal.util.termination_criterion import TerminationCriterion
from jmetal.algorithm.data.diversity import DimensionWiseDiversity
import numpy as np
import pandas as pd

S = TypeVar('S')
R = TypeVar('R')

"""
.. module:: genetic_algorithm
   :platform: Unix, Windows
   :synopsis: Implementation of Genetic Algorithms.
.. moduleauthor:: <NAME> <<EMAIL>>, <NAME> <<EMAIL>>
"""


class GeneticAlgorithm(EvolutionaryAlgorithm[S, R]):

    def __init__(self,
                 problem: Problem,
                 population_size: int,
                 offspring_population_size: int,
                 mutation: Mutation,
                 crossover: Crossover,
                 selection: Selection,
                 termination_criterion: TerminationCriterion = store.default_termination_criteria,
                 population_generator: Generator = store.default_generator,
                 population_evaluator: Evaluator = store.default_evaluator):
        super(GeneticAlgorithm, self).__init__(
            problem=problem,
            population_size=population_size,
            offspring_population_size=offspring_population_size)
        self.mutation_operator = mutation
        self.crossover_operator = crossover
        self.selection_operator = selection

        self.population_generator = population_generator
        self.population_evaluator = population_evaluator

        self.termination_criterion = termination_criterion
        self.observable.register(termination_criterion)

        self.mating_pool_size = \
            self.offspring_population_size * \
            self.crossover_operator.get_number_of_parents() // self.crossover_operator.get_number_of_children()

        if self.mating_pool_size < self.crossover_operator.get_number_of_children():
            self.mating_pool_size = self.crossover_operator.get_number_of_children()

    def create_initial_solutions(self) -> List[S]:
        return [self.population_generator.new(self.problem)
                for _ in range(self.population_size)]

    def evaluate(self, population: List[S]):
        return self.population_evaluator.evaluate(population, self.problem)

    def stopping_condition_is_met(self) -> bool:
        return self.termination_criterion.is_met

    def selection(self, population: List[S]):
        mating_population = []

        for i in range(self.mating_pool_size):
            solution = self.selection_operator.execute(population)
            mating_population.append(solution)

        return mating_population

    def reproduction(self, mating_population: List[S]) -> List[S]:
        number_of_parents_to_combine = self.crossover_operator.get_number_of_parents()

        if len(mating_population) % number_of_parents_to_combine != 0:
            raise Exception('Wrong number of parents')

        offspring_population = []
        for i in range(0, self.offspring_population_size, number_of_parents_to_combine):
            parents = []
            for j in range(number_of_parents_to_combine):
                parents.append(mating_population[i + j])

            offspring = self.crossover_operator.execute(parents)

            for solution in offspring:
                self.mutation_operator.execute(solution)
                offspring_population.append(solution)
                if len(offspring_population) >= self.offspring_population_size:
                    break

        return offspring_population

    def replacement(self, population: List[S], offspring_population: List[S]) -> List[S]:
        population.extend(offspring_population)

        population.sort(key=lambda s: s.objectives[0])

        return population[:self.population_size]

    def get_result(self) -> R:
        return self.solutions[0]

    def get_name(self) -> str:
        return 'GA'

    def get_observable_data(self) -> dict:
        return {'PROBLEM': self.problem,
                'EVALUATIONS': self.evaluations,
                'SOLUTIONS': self.solutions,
                'COMPUTING_TIME': time.time() - self.start_computing_time}


class DiversityBasedGeneticAlgorithm(GeneticAlgorithm):

    def __init__(self, problem: Problem, population_size: int, offspring_population_size: int, mutation: Mutation,
                 crossover: Crossover, selection: Selection,
                 termination_criterion: TerminationCriterion = store.default_termination_criteria,
                 population_generator: Generator = store.default_generator,
                 restart_population_generator: Generator = store.default_generator,
                 population_evaluator: Evaluator = store.default_evaluator, local_search: Algorithm = None):

        super().__init__(problem, population_size, offspring_population_size, mutation, crossover, selection,
                         termination_criterion, population_generator, population_evaluator)
        self.diversity = DimensionWiseDiversity()
        self.parameter_update = DiversityBasedControl(max_evaluations=self.termination_criterion.get_criterion())
        self.diversification_prob = 0
        self.intensification_prob = 0
        self.do_nothing_prob = 1
        self.local_search = local_search
        self.restart_population_generator = restart_population_generator
        self.diversification_counter = []
        self.intensification_counter = []

    def init_progress(self) -> None:
        super().init_progress()
        self.diversity = DimensionWiseDiversity()
        self.parameter_update = DiversityBasedControl(max_evaluations=self.termination_criterion.get_criterion())
        self.diversification_prob = 0
        self.intensification_prob = 0
        self.do_nothing_prob = 1

    def step(self):
        super().step()

        options = ['Exploration', 'Exploitation', 'Do Nothing']
        probabilities = [self.diversification_prob, self.intensification_prob, self.do_nothing_prob]
        result = np.random.choice(3, p=probabilities)

        # self.diversification_step()

        if result == 0:
            self.diversification_counter.append(1)
            self.intensification_counter.append(0)
            self.diversification_step()
        elif result == 1:
            self.diversification_counter.append(0)
            self.intensification_counter.append(1)
            self.intensification_step()
        else:
            self.diversification_counter.append(0)
            self.intensification_counter.append(0)

    def intensification_step(self):
        best_solution_idx = np.argsort([_.objectives[0] for _ in self.solutions])[0:10]

        for idx in best_solution_idx:
            best_solution = self.solutions[idx]

            self.local_search.init_progress()
            self.local_search.solutions = [best_solution]
            self.local_search.run()
            ls_result = self.local_search.get_result()

            if ls_result.objectives[0] <= best_solution.objectives[0]:
                self.solutions[idx] = ls_result

            self.evaluations += self.local_search.evaluations

    def diversification_step(self):
        self.restart_population()

    def restart_population(self):
        amount_ind = int((25/100) * self.population_size)

        worst_solutions = np.argsort([_.objectives[0] for _ in self.solutions])[-amount_ind:]

        for replacement_idx in worst_solutions:
            self.solutions[replacement_idx] = self.population_generator.new(self.problem)

        self.evaluate(np.take(self.solutions, worst_solutions))

    def update_progress(self) -> None:
        super().update_progress()
        self.update_diversity()
        self.update_probabilities()

    def update_diversity(self):
        self.diversity.update(self.evaluations, self.solutions, self.problem.number_of_variables)

    def update_probabilities(self):
        xpl, xpt = self.diversity.get_data()
        data = {"EVALUATIONS": self.evaluations, "XPL": xpl, "XPT": xpt}
        self.do_nothing_prob, self.diversification_prob, self.intensification_prob = self.parameter_update.update(
            **data)

    def get_observable_data(self) -> dict:
        return {'PROBLEM': self.problem,
                'EVALUATIONS': self.evaluations,
                'SOLUTIONS': self.solutions,
                'COMPUTING_TIME': time.time() - self.start_computing_time,
                'DIVERSIFICATION_PERCENTAGE': self.parameter_update.get_diversification_percentage(),
                'INTENSIFICATION_PERCENTAGE': self.parameter_update.get_intensification_percentage(),
                'DO_NOTHING_PERCENTAGE': self.parameter_update.get_do_nothing_percentage()}

    def get_name(self) -> str:
        return "diversity-ga"





.. module:: genetic_algorithm    :platform: Unix, Windows    :synopsis: Implementation of Genetic Algorithms. .. moduleauthor:: <NAME> <<EMAIL>>, <NAME> <<EMAIL>> # self.diversification_step()