import argparse
import os
import sys
import numpy as np

# TODO add new example: steadystate.py (analogous to standard.py) OR include steadysteate as a mode in this example or in niching_novelty.py
# TODO extend both standard.py and steadystate.py to support >1 criteria (using DEAP's selNSGA2() and selSPEA2())
# TODO add comments to all examples in this directory
# TODO add to standard.py and steadystate.py evaluating each genotype in HOF N (configurable, default 20) times when the evolution ends, as it is in niching_novelty.py
# TODO "-debug" mode, indent nested steps (pre++, post-- of a static counter?) and print their arguments so it is easy to see what happens during evolution


from FramsticksLib import FramsticksLib
from evolalg.base.union_step import UnionStep
from evolalg.experiment import Experiment
from evolalg.fitness.fitness_step import FitnessStep
from evolalg.mutation_cross.frams_cross_and_mutate import FramsCrossAndMutate
from evolalg.population.frams_population import FramsPopulation
from evolalg.repair.remove.field import FieldRemove
from evolalg.selection.tournament import TournamentSelection
from evolalg.statistics.halloffame_stats import HallOfFameStatistics
from evolalg.statistics.statistics_deap import StatisticsDeap
from evolalg.utils.population_save import PopulationSave



EVAL_LIFESPAN_BEHAVIOR = False  # if False, standard evaluation criteria can be used as fitness as defined by the -opt parameter. If True, it is assumed that the expdef provides custom dictionary fields in evaluation, and they need to be handled specifically in python source code below (this could be parametrized in command-line too, but the syntax would be complex).


def ensureDir(string):
    if os.path.isdir(string):
        return string
    else:
        raise NotADirectoryError(string)


def parseArguments():
    parser = argparse.ArgumentParser(
        description='Run this program with "python -u %s" if you want to disable buffering of its output.' % sys.argv[
            0])
    parser.add_argument('-path', type=ensureDir, required=True, help='Path to the Framsticks library without trailing slash.')
    parser.add_argument('-opt', required=True,
                        help='optimization criteria : vertpos, velocity, distance, vertvel, lifespan, numjoints, numparts, numneurons, numconnections (or other as long as it is provided by the .sim file and its .expdef). Single or multiple criteria.')
    parser.add_argument('-lib', required=False, help="Filename of .so or .dll with the Framsticks library")

    parser.add_argument('-genformat', required=False, default="1",
                        help='Genetic format for the demo run, for example 4, 9, or B. If not given, f1 is assumed.')
    parser.add_argument('-sim', required=False, default="eval-allcriteria.sim", help="Name of the .sim file with all parameter values. If you want to provide more files, separate them with a semicolon ';'.")
    parser.add_argument("-popsize", type=int, default=50, help="Population size, default 50.")
    parser.add_argument('-generations', type=int, default=5, help="Number of generations, default 5.")
    parser.add_argument('-tournament', type=int, default=5, help="Tournament size, default 5.")

    parser.add_argument('-hof_size', type=int, default=10, help="Number of genotypes in Hall of Fame. Default: 10.")
    return parser.parse_args()


def extract_fitness(ind):
    return ind.fitness


def print_population_count(pop):
    print("Current popsize:", len(pop))
    return pop  # Each step must return a population


def main():
    parsed_args = parseArguments()
    frams_lib = FramsticksLib(parsed_args.path, parsed_args.lib, parsed_args.sim.split(";"))

    hall_of_fame = HallOfFameStatistics(parsed_args.hof_size, "fitness")
    statistics_union = UnionStep([
        hall_of_fame,
        StatisticsDeap([
            ("avg", np.mean),
            ("stddev", np.std),
            ("min", np.min),
            ("max", np.max),
            ("count", len)
        ], extract_fitness)
    ])

    fitness_remove = UnionStep(  # evaluate performance and fitness, rename some of the fields, and remove some performance fields that we get from Framsticks, but we don't need them here
        [
        FitnessStep(frams_lib, fields={"velocity": "fitness", "data->recording": "recording"},
                    fields_defaults={"velocity": None, "data->recording": None})  # custom definitions and handling
        if EVAL_LIFESPAN_BEHAVIOR else
        FitnessStep(frams_lib, fields={parsed_args.opt: "fitness"}, fields_defaults={parsed_args.opt: None})
        ]
        +
        ([FieldRemove("recording", None)] if EVAL_LIFESPAN_BEHAVIOR else [FieldRemove("fitness", None)])
        +
        [print_population_count]  # Stages can also be any Callable
    )

    selection = TournamentSelection(parsed_args.tournament, copy=True, fit_attr="fitness")
    new_generation_steps = [
        FramsCrossAndMutate(frams_lib, cross_prob=0.2, mutate_prob=0.9),
        fitness_remove
    ]

    generation_modifications = [
        statistics_union  # Or niching, novelty
    ]

    init_stages = [FramsPopulation(frams_lib, parsed_args.genformat, parsed_args.popsize),
                   fitness_remove,  # It is possible to create smaller population
                   statistics_union]

    end_steps = [PopulationSave("halloffame.gen", provider=hall_of_fame.halloffame,
                                fields={"genotype": "genotype", "fitness": "fitness", "custom": "recording"}
                                if EVAL_LIFESPAN_BEHAVIOR
                                else {"genotype": "genotype", "fitness": "fitness"}
                                )]

    experiment = Experiment(init_population=init_stages,
                            selection=selection,
                            new_generation_steps=new_generation_steps,
                            generation_modification=generation_modifications,
                            end_steps=end_steps,
                            population_size=parsed_args.popsize
                            )
    experiment.init()
    experiment.run(parsed_args.generations)
    for ind in hall_of_fame.halloffame:
        print("%g\t%s" % (ind.fitness, ind.genotype))


if __name__ == '__main__':
    main()
