import random
import numpy as np
from deap import base, creator, tools, algorithms
import utils_deap_tools as u_tools
class Archive:
    archive = []
    buffer = []
    
    def __init__(self, size, buffer_size):
        self.size = size
        self.buffer_size = buffer_size
        
    def add(self,ind):
        if(len(self.archive)>self.size):
            self.archive.pop(0)
        self.archive.append(ind)
    
    def add_to_buffer(self,ind):
        if(len(self.buffer)>self.buffer_size):
            self.archive +=self.buffer
            if(len(self.archive)>self.size):
                self.archive=self.archive[-self.size:]
            self.buffer=[]
        self.buffer.append(ind)

# Define the evaluation function

def objective_func(individual):
    # Simulate an evaluation (replace this with your Minecraft server interaction)
    return sum(individual)

# Define the novelty metric (Hamming distance in this example)
def hamming_distance(ind1, ind2):
    return sum(b1 != b2 for b1, b2 in zip(ind1, ind2))

def novelty_func(ind, archive, k):
    distances = [hamming_distance(ind, arch_ind) for arch_ind in archive.archive]
    return np.mean(np.sort(distances)[:k]) if distances else 0

def evaluate(individual, archive, k):
    individual.objective.values = objective_func(individual),
    archive.add_to_buffer(individual)
    return novelty_func(individual, archive, k),
    # return novelty_func(individual, archive, k)+individual.objective.values[0],
    
# Number of generations
n_gen = 50

# Population size
pop_size = 50

# Archive size (novel solutions are added here)
archive_size = 200

k_nearest = 10

# Create types and individuals
creator.create("FitnessMax", base.Fitness, weights=(1.0,))
creator.create("Onemax", base.Fitness, weights=(1.0,))
# creator.create("Individual", list, fitness=creator.FitnessMax)
creator.create("Individual", list, fitness=creator.FitnessMax, objective=creator.Onemax )

# Create a toolbox
toolbox = base.Toolbox()

# Register genetic operators
toolbox.register("bit", random.randint, 0, 1)
toolbox.register("individual", tools.initRepeat, creator.Individual, toolbox.bit, n=10)
toolbox.register("population", tools.initRepeat, list, toolbox.individual)

toolbox.register("evaluate", evaluate, archive=Archive(archive_size,pop_size), k=k_nearest)
toolbox.register("mate", tools.cxTwoPoint)
toolbox.register("mutate", tools.mutFlipBit, indpb=0.05)
toolbox.register("select", tools.selTournament, tournsize=3)

pop = toolbox.population(n=pop_size)
# Evaluate the initial population
stats_fit = tools.Statistics(key=lambda ind: ind.fitness.values)
stats_obj = tools.Statistics(key=lambda ind: ind.objective.values)
stats_pop = tools.Statistics()
fstats = tools.MultiStatistics(fitness=stats_fit, objective=stats_obj)
mstats = tools.MultiStatistics(performance=fstats, population=stats_pop)
fstats.register("avg", np.mean)
fstats.register("std", np.std)
fstats.register("min", np.min)
fstats.register("max", np.max)
stats_pop.register("unique", lambda pop: len(np.unique(pop,axis=0)))

hof = u_tools.HallOfFameGeneral(5, fit_attr="objective")
# Main evolution loop
pop, log =algorithms.eaSimple(pop, toolbox, 0.5, 0.2, n_gen, stats=mstats, halloffame=hof, verbose=True)

# Final results
best_individual_fit = tools.selBest(pop, 1)[0]
best_individual_obj = tools.selBest(pop, 1, fit_attr='objective')[0]

print(np.unique(pop,axis=0))
print("Best Individual:")
print(f"\tFitness:\t{best_individual_fit} f={best_individual_fit.fitness.values[0]:.2f} o={best_individual_fit.objective.values[0]}")
print(f"\tObjective:\t{best_individual_obj} f={best_individual_obj.fitness.values[0]:.2f} o={best_individual_obj.objective.values[0]}")

print("Hall Of Fame:")
for best_ind in hof:
    print(f"\tFitness:\t{best_ind} f={best_ind.fitness.values[0]:.2f} o={best_ind.objective.values[0]}")

