package com.brackeen.javagamebook.ai;

import java.util.*;
import com.brackeen.javagamebook.ai.pattern.*;
import com.brackeen.javagamebook.path.*;
import com.brackeen.javagamebook.util.MoreMath;
import com.brackeen.javagamebook.bsp2D.BSPTree;

/**
    Klasa EvolutionGenePool utrzymuje kolekcję obiektów klasy
    Brain. Tworzony jest obiekt klasy EvolutionBot, wymaga
    obiektu klasy Brain z puli genów. Obiekt klasy Brain jest
    albo jednym z "mózgów" z tego zbioru, albo mutacją jednego
    z tych mózgów. Utrzymywane są tylko najlepsze mózgi, ich
    ocena jest uzależniona od uszkodzeń spowodowanych u gracza
    przez bota z danym mózgiem. Motowane są więc tylko najlepsze
    mózgi.
*/

public class EvolutionGenePool {

    private static final int NUM_TOP_BRAINS = 5;
    private static final int NUM_TOTAL_BRAINS = 10;

    private class BrainStat extends Brain implements Comparable {

        long totalDamageCaused;
        int numBots;
        int generation;

        public BrainStat() {

        }

        public BrainStat(BrainStat brain) {
            super(brain);
            this.generation = brain.generation;
        }


        /**
            Zwraca średnią uszkodzeń powodowanych przez boty z tym mózgiem.
        */
        public float getAverageDamageCaused() {
            if (numBots == 0) {
                return 0;
            }
            else {
                return (float)totalDamageCaused / numBots;
            }
        }


        /**
            Raportuje o sumie uszkodzeń spowodowanych przez bota z tym
            mózgiem po tym, jak dany bot zostanie zniszczony.
        */
        public void report(long damageCaused) {
            totalDamageCaused+=damageCaused;
            numBots++;
        }


        /**
            Zwraca mniejszą liczbę, jeśli ten mózg spowodował więcej
            zniszczeń niż wskazany obiekt (parametr obj), który także
            powinien być mózgiem.
        */
        public int compareTo(Object obj) {
            BrainStat other = (BrainStat)obj;
            float thisScore = this.getAverageDamageCaused();
            float otherScore = other.getAverageDamageCaused();
            if (thisScore == 0 && otherScore == 0) {
                // mniejsza liczba botów jest lepsza
                return (this.numBots - other.numBots);
            }
            else {
                // im więcej zniszczeń, tym lepiej
                return (int)MoreMath.sign(otherScore - thisScore);
            }

        }


        /**
            Mutuje ten mózg. Przekazana wartość mutationProbability
            jest prawdopodobieństwem zdarzenia polegającego na zmianie
            ("zmutowaniu") wartości wszystkich atrybutów.
        */
        public void mutate(float mutationProbability) {
            if (MoreMath.chance(mutationProbability)) {
                attackProbability = (float)Math.random();
            }
            if (MoreMath.chance(mutationProbability)) {
                dodgeProbability = (float)Math.random();
            }
            if (MoreMath.chance(mutationProbability)) {
                runAwayProbability = (float)Math.random();
            }
            if (MoreMath.chance(mutationProbability)) {
                decisionTime = MoreMath.random(3000, 6000);
            }
            if (MoreMath.chance(mutationProbability)) {
                aimTime = MoreMath.random(300, 2000);
            }
            if (MoreMath.chance(mutationProbability)) {
                hearDistance = MoreMath.random(50, 2000);
            }
            if (MoreMath.chance(mutationProbability)) {
                attackPathFinder = (PathFinder)
                    MoreMath.random(attackPathFinders);
            }
            if (MoreMath.chance(mutationProbability)) {
                dodgePathFinder = (PathFinder)
                    MoreMath.random(dodgePathFinders);
            }
            if (MoreMath.chance(mutationProbability)) {
                aimPathFinder = (PathFinder)
                    MoreMath.random(aimPathFinders);
            }
            if (MoreMath.chance(mutationProbability)) {
                idlePathFinder = (PathFinder)
                    MoreMath.random(idlePathFinders);
            }
            if (MoreMath.chance(mutationProbability)) {
                chasePathFinder = (PathFinder)
                    MoreMath.random(chasePathFinders);
            }
            if (MoreMath.chance(mutationProbability)) {
                runAwayPathFinder = (PathFinder)
                    MoreMath.random(runAwayPathFinders);
            }

            fixProbabilites();
        }


        public Object clone() {
            BrainStat brain = new BrainStat(this);
            brain.generation++;
            return brain;
        }

        public String toString() {
            if (numBots == 0) {
                return "(Nie używany)\n" + super.toString();
            }
            else {
                return "Średnie zniszczenia na bota: " +
                    getAverageDamageCaused() + " " +
                    "(" + numBots + " bots)\n" +
                    "Pokolenie: " + generation + "\n" +
                    super.toString();
            }
        }
    }

    private List brains;

    private List attackPathFinders;
    private List aimPathFinders;
    private List dodgePathFinders;
    private List idlePathFinders;
    private List chasePathFinders;
    private List runAwayPathFinders;

    public EvolutionGenePool(BSPTree bspTree) {

        // stwórz obiekty szukania drogi
        attackPathFinders = Arrays.asList(new Object[] {
            new AttackPatternRush(bspTree),
            new AttackPatternStrafe(bspTree)
        });
        aimPathFinders = Arrays.asList(new Object[] {
            new AimPattern(bspTree)
        });
        dodgePathFinders = Arrays.asList(new Object[] {
            new DodgePatternZigZag(bspTree),
            new DodgePatternRandom(bspTree)
        });
        idlePathFinders = Arrays.asList(new Object[] {
            null
        });
        chasePathFinders = Arrays.asList(new Object[] {
            new AStarSearchWithBSP(bspTree)
        });
        runAwayPathFinders = Arrays.asList(new Object[] {
            new RunAwayPattern(bspTree)
        });

        // stwórz na początku kilka losowych mózgów
        brains = new ArrayList();
        for (int i=0; i<NUM_TOTAL_BRAINS; i++) {
            BrainStat brain = new BrainStat();
            // wylosuj (zmutuj) wszystkie atrybuty mózgu
            brain.mutate(1);
            brains.add(brain);
        }
    }

    /**
        Drzewo BSP wykorzystywane przez pewne wzorce (np. algorytm
        znajdowania najkrótszej drogi wykorzystywany przez wzorzec
        pościgu)
    */
    public void setBSPTree(BSPTree bspTree) {
        ((AStarSearchWithBSP)chasePathFinders.get(0)).
            setBSPTree(bspTree);
    }


    public void resetEvolution() {
        brains.clear();
    }


    /**
        GZwraca nowy mózg z puli genów. Nowy mózg jest albo brany
        bezpośrednio ze szczytu puli, albo jest brany ze szczytu
        tej puli i odpowiednio mutowany.
    */
    public Brain getNewBrain() {

        // 50% szansy na stworzenie nowego, zmutowanego mózgu
        if (MoreMath.chance(.5f)) {
            BrainStat brain = (BrainStat)getRandomTopBrain().clone();

            // 10% do 25% szans na zmianę poszczególnych atrybutów
            float p = MoreMath.random(0.10f, 0.25f);
            brain.mutate(p);
            return brain;
        }
        else {
            return getRandomTopBrain();
        }
    }


    /**
        Zwraca losowo wybrany spośród najlepszych mózgów.
    */
    public Brain getRandomTopBrain() {
        int index = MoreMath.random(NUM_TOP_BRAINS-1);
        return (Brain)brains.get(index);
    }


    /**
        Informuje o zniszczeniu obiektu z danym mózgiem.
        Zapisuje dane statystyczne zniszczonego obiektu
        (osiągnięcia mózgu). Jeśli te dane są zbliżone do
        danych mózgów na szczycie puli genów, zachowujemy
        ten mózg na przyszłość.
    */
    public void notifyDead(Brain brain, long damageCaused) {
        // aktualizuje statystyki dla tego mózgu
        if (brain instanceof BrainStat) {
            BrainStat stat = (BrainStat)brain;

            // raportuje o spowodowanych zniszczeniach
            stat.report(damageCaused);

            // sortuje i przycina listę
            if (!brains.contains(stat)) {
                brains.add(stat);
            }
            Collections.sort(brains);
            while (brains.size() > NUM_TOTAL_BRAINS) {
                brains.remove(NUM_TOTAL_BRAINS);
            }
        }
    }



    public String toString() {

        // wyświetl najlepsze mózgi
        String retVal = "Najlepsze " + NUM_TOP_BRAINS + " mózgów:\n";
        for (int i=0; i<NUM_TOP_BRAINS; i++) {
            retVal+= (i+1) + ".\n";
            retVal+=brains.get(i) + "\n";
        }

        return retVal;
    }
}
