/******************************************************************************
 ** $Id: Statistik.java 1195 2017-04-17 12:33:52Z wmh $
 ** Diese Datei ist Bestandteil der Java-Quelltexte des Würfelspiels JaFuffy.
 ** Lauffähig ab Java 7.
 ******************************************************************************
 ** Copyright (C) Wolfgang Hauck <wolfgang.hauck@3kelvin.de>
 ******************************************************************************
 ** This program is free software: you can redistribute it and/or modify
 ** it under the terms of the GNU General Public License as published by
 ** the Free Software Foundation, either version 3 of the License, or
 ** (at your option) any later version.
 **
 ** This program is distributed in the hope that it will be useful,
 ** but WITHOUT ANY WARRANTY; without even the implied warranty of
 ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 ** GNU General Public License for more details.
 **
 ** You should have received a copy of the GNU General Public License
 ** along with this program.  If not, see <http://www.gnu.org/licenses/>.
 ******************************************************************************
 ** Die aktuellste Version von JaFuffy findet sich im Internet unter
 ** <http://jafuffy.3kelvin.de>.
 **
 ** Kommentare, Fehler oder Erweiterungswünsche bitte per E-Mail senden an
 ** <jafuffy@3kelvin.de>.
 ******************************************************************************/
package jafuffy.logik;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.ListIterator;
import java.util.Locale;

import com.orsoncharts.data.DefaultKeyedValues;
import com.orsoncharts.data.category.StandardCategoryDataset3D;

/**
 * Speicherung aller statistischen Daten. 10 beste Punktzahlen, Anzahl aller Spiele, Durchschnitt, Standardabweichung.
 * Sortiert nach Spielvarianten.
 */
public class Statistik extends Aenderungen<CEStatistik> implements Serializable {

    /** Ermöglicht Serialisierung. */
    private static final long serialVersionUID = -4577948675428850797L;

    /**
     * Klasse für Mitspieler gleichen Ranges. Ein Rang ist durch die Punktzahl bestimmt, kann aber mehrere gleiche
     * Punkteinträge für mehrere Spieler enthalten.
     */
    public static class Rang implements Serializable {

        private static final long serialVersionUID = 8690127540463640879L;

        public static class Eintrag implements Serializable {
            private static final long serialVersionUID = -6067121882006385994L;
            private final String name;
            private final String datum;
            private String kommentar = null;

            Eintrag(String name, String datum) {
                this.name = name;
                this.datum = datum;
            }

            public void kommentar(String kommentar) {
                this.kommentar = kommentar;
            }

            public String name() {
                return name;
            }

            String datum() {
                return datum;
            }

            String kommentar() {
                return kommentar;
            }
        }

        private final ArrayList<Eintrag> eintraege = new ArrayList<>();

        private int punkte;
        private int rangnummer;

        public Eintrag neueintrag() {
            return eintraege.get(eintraege.size() - 1);
        }

        public int rangnummer() {
            return rangnummer;
        }

        ArrayList<Eintrag> eintraege() {
            return eintraege;
        }

        void notiere(Spieler spieler) {
            punkte = spieler.endsumme();
            eintraege.add(new Eintrag(spieler.toString(),
                    DateFormat.getDateInstance(DateFormat.MEDIUM, Locale.GERMANY).format(new Date())));
        }

        void platziere(int rangnummer) {
            this.rangnummer = rangnummer;
        }

        int punkte() {
            return punkte;
        }

    }

    /** Name der Datei, in welcher die Statistikdaten abgelegt werden. */
    public static final String STAT = "Statistik.obj";

    /** Gibt an, über wie viele Ränge Buch geführt wird. */
    private static final int MAXRAENGE = 10;

    /** Gibt an, welche Variante der Spielregeln momentan verwendet wird. */
    private transient Variante variante;
    /** Alle derzeitigen Mitspieler, welche im laufende Turnier teilnehmen. */
    private transient ArrayList<Spieler> teilnehmer;
    /** Merkt sich alle Ränge. */
    private transient ArrayList<Rang> raenge;

    /** Anzahl der maximal möglichen Spiele im laufenden Turniers, wobei Null unbegrenzt bedeutet. */
    private transient int maximalanzahl;
    /** Geschätzter Mittelwert pro Spiel für die aktuelle Regelvariante. */
    private transient int mittelwert;
    /** Geschätzte Standardabweichung pro Spiel für die aktuelle Regelvariante. */
    private transient int abweichung;

    /** Merkt sich den letzten Spielstand, um die letzte Runde rückgängig machen zu können. */
    private transient HashMap<Spieler, Integer> ruecknahmen;
    /** Die aktuellen Stände im Spiel, für jedes laufende Spiel für jeden Spieler für abgeschlossene Runden. */
    private transient HashMap<Spieler, Integer> spielstaende;
    /** Die im Turnier aufsummierten Stände im Spiel, für jedes Spiel für jeden Spieler für abgeschlossene Spiele. */
    private transient HashMap<Spieler, Integer> turnierstaende;
    /** Turnierverlauf, enthält für jeden Spieler alle Stände aller Spiele. */
    private transient HashMap<Spieler, ArrayList<Integer>> verlaeufe;
    /** Spielindex des laufenden Spiels, entspricht der Anzahl der vollendeten Spiele. */
    private transient int spielindex;
    /**
     * Der Datensatz besteht aus Datenreihen, die durch den Spieler indiziert sind; eine Datenreihe besteht aus den
     * Punktzahlen für einen Spieler über das bisherige Turnier.
     */
    private transient StandardCategoryDataset3D<Spieler, Spieler, Integer> datensatz;

    /** Datenmodelle für Bestenlisten, entsprechend der Variante. */
    private transient HashMap<Variante, Bestenlistenmodell> bestenlistenmodelle;

    /** Für jede Variante eine Rangliste mit Einträgen der Klasse "Rang". */
    private final HashMap<Variante, ArrayList<Rang>> ranglisten = new HashMap<>(Variante.ANZAHL);
    /** Speichert die Anzahl der vollendeten Spiele pro Regelvariante. */
    private final HashMap<Variante, Integer> ns = new HashMap<>(Variante.ANZAHL);
    /** Speichert die Summe der Punkte aller vollendeten Spiele pro Regelvariante. */
    private final HashMap<Variante, Integer> summen = new HashMap<>(Variante.ANZAHL);
    /** Speichert die Summe der quadrierten Punkte aller vollendeten Spiele pro Regelvariante. */
    private final HashMap<Variante, Long> quadratsummen = new HashMap<>(Variante.ANZAHL);

    /**
     * Konstruktor für den erstmaligen Start von JaFuffy, danach wird das Objekt aus der Serialisierung rekonstruiert.
     */
    public Statistik() {
        for (Variante variante : Variante.values()) {
            ns.put(variante, 0);
            summen.put(variante, 0);
            quadratsummen.put(variante, 0L);
            ranglisten.put(variante, new ArrayList<Rang>(MAXRAENGE));
        }
        erzeugeBestenlistenmodelle();
    }

    /** @return Geschätzte Abweichung pro Spiel */
    public int abweichung() {
        return abweichung;
    }

    /** @return Modelle für Bestenlisten für alle Spielregelvarianten */
    public HashMap<Variante, Bestenlistenmodell> bestenlistenmodelle() {
        return bestenlistenmodelle;
    }

    public StandardCategoryDataset3D<Spieler, Spieler, Integer> datensatz() {
        return datensatz;
    }

    /**
     * @param variante
     *            Spielvariante
     * @return Durchschnittliche Punktzahl aus erfassten Daten berechnet
     */
    public double durchschnitt(Variante variante) {
        return (double) summen.get(variante) / (double) ns.get(variante);
    }

    /** @return Anzahl der maximal möglichen Spiele im laufenden Turnier, wobei Null unbegrenzt bedeutet */
    public int maximalanzahl() {
        return maximalanzahl;
    }

    /** @return Geschätzter Mittelwert pro Spiel für die Regelvariante des aktuell laufenden Turniers */
    public int mittelwert() {
        return mittelwert;
    }

    /**
     * @param variante
     *            Spielvariante
     * @return Gesamtzahl aller Spieler in genannter Variante
     */
    public int n(Variante variante) {
        return ns.get(variante);
    }

    /** @return Alle Ränge, die sich in Bestenliste eintragen können */
    public ArrayList<Rang> raenge() {
        return raenge;
    }

    /**
     * @return Restliche Anzahl der vollständigen Spiele pro Turnier, wobei bei unbeschränkten Turnieren immer Eins
     *         zurückgegeben wird.
     */
    public int rest() {
        return maximalanzahl == 0 ? 1 : maximalanzahl - spielindex;
    }

    /**
     * @param spieler
     *            Spieler
     * @return Zwischenstand für genannten Spieler
     */
    public int stand(Spieler spieler) {
        return turnierstaende.get(spieler) + spielstaende.get(spieler);
    }

    /**
     * @param variante
     *            Spielvariante
     * @return Standardabweichung der Punktzahlen aus Daten berechnet
     */
    public double standardabweichung(Variante variante) {
        double quadratsumme = quadratsummen.get(variante);
        double summe = summen.get(variante);
        int n = ns.get(variante);
        return Math.sqrt((quadratsumme - summe * summe / n) / (n - 1));
    }

    /** @return Die am aktuell laufenden Turnier teilnehmenden Spieler */
    public ArrayList<Spieler> teilnehmer() {
        return teilnehmer;
    }

    /**
     * Ermittelt den Titel des laufenden Turniers, welcher den Statistikstand beschreibt.
     *
     * @param statistik
     *            Darzustellende Statistik
     *
     * @return Fenstertitel.
     */
    public String titel() {
        if (maximalanzahl == 0) {
            return spielanzahl() + ". Spiel im Turnier";
        } else if (rest() > 0) {
            if (maximalanzahl == 1) {
                return "Einziges Spiel im Turnier";
            } else {
                return spielanzahl() + ". von " + maximalanzahl + " Spielen im Turnier";
            }
        } else {
            return "Turnierende";
        }
    }

    /** @return Die Anzahl der begonnenen oder vollendeten Spiele im Turnier. */
    public int spielanzahl() {
        if (rest() > 0) {
            return spielindex + 1;
        } else {
            return maximalanzahl;
        }
    }

    /** @return Regelvariante */
    public Variante variante() {
        return variante;
    }

    /** @return Turnierverlauf */
    public HashMap<Spieler, ArrayList<Integer>> verlaeufe() {
        return verlaeufe;
    }

    /** Erstellt Modelle für die Bestenlisten für alle Spielregelvarianten. */
    private void erzeugeBestenlistenmodelle() {
        bestenlistenmodelle = new HashMap<>(Variante.ANZAHL);
        for (Variante variante : Variante.values()) {
            bestenlistenmodelle.put(variante, new Bestenlistenmodell(ranglisten.get(variante)));
        }
    }

    /**
     * Rekonstruiere das serialisierte Statistik-Objekt aus einem Datenstrom.
     *
     * @param in
     *            Eingehender Datenstrom
     */
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        erzeugeBestenlistenmodelle();
    }

    /**
     * Annulliere Zug (nur zu rufen, falls in alte Runde zurückgefallen). Zuvor muss die Runde beendet worden sein.
     */
    void annulliere() {
        for (Spieler spieler : teilnehmer) {
            spielstaende.put(spieler, ruecknahmen.get(spieler));
            verlaeufe.get(spieler).set(spielindex, spielstaende.get(spieler));
        }
        fireStateChanged(CEStatistik.STAND);
    }

    /** Runde beenden. */
    void beendeRunde() {
        for (Spieler spieler : teilnehmer) {
            ruecknahmen.put(spieler, spielstaende.get(spieler));
            spielstaende.put(spieler, spieler.endsumme());
            verlaeufe.get(spieler).set(spielindex, spielstaende.get(spieler));
            datensatz.setValue(spieler.endsumme(), spieler, spieler, spielanzahl());
        }
        fireStateChanged(CEStatistik.STAND);
    }

    /**
     * Beende das Spiel: Merken des Punktstandes, Erstellung und Anzeige der Rangliste, Aktualisierung der statistischen
     * Kenngrößen und Eingabe von Kommentaren.
     */
    void beendeSpiel() {

        // Für die Suche in Listen
        ListIterator<Rang> rangiterator;
        // Neuer Eintrag in Bestenliste nötig?
        boolean aktualisieren = false;
        // Für das Einordnen in Listen
        Rang rang;

        // Punktstand & Verlauf merken, Spieler in Bestenliste einsortieren
        raenge = new ArrayList<>(teilnehmer.size());
        for (Spieler spieler : teilnehmer) {
            int j = 0, punkte;
            boolean eingeordnet = false;
            punkte = spieler.endsumme();
            turnierstaende.put(spieler, turnierstaende.get(spieler) + punkte);
            spielstaende.put(spieler, 0);
            // Platz in Rangliste suchen und bei Bedarf neuen Rang erzeugen
            rangiterator = ranglisten.get(variante).listIterator();
            rang = null;
            while (rangiterator.hasNext() && !eingeordnet) {
                j = rangiterator.nextIndex();
                rang = rangiterator.next();
                eingeordnet = rang.punkte <= punkte;
            }
            // Neuer Rang gefunden?
            if (eingeordnet && rang.punkte < punkte) {
                if (ranglisten.get(variante).size() == MAXRAENGE) {
                    // falls letzter Rang aus Bestenliste gelöscht wird, auch
                    // aus Rangmerker löschen
                    raenge.remove(ranglisten.get(variante).get(MAXRAENGE - 1));
                    ranglisten.get(variante).remove(MAXRAENGE - 1);
                }
                rang = new Rang();
                ranglisten.get(variante).add(j, rang);
            }
            // Niedrigster Punktstand, aber noch Platz in Bestenliste?
            if (!eingeordnet && ranglisten.get(variante).size() < MAXRAENGE) {
                rang = new Rang();
                ranglisten.get(variante).add(rang);
                eingeordnet = true;
            }
            // Eintrag zum Rang hinzufügen, Rang und Eintrag merken für Kommentareingabe
            if (eingeordnet) {
                aktualisieren = true;
                rang.notiere(spieler);
                raenge.add(rang);
            }

        }

        // Hat sich die Bestenliste verändert?
        if (aktualisieren) {
            // Korrektur der Rangnummern
            int rangnummer = 1;
            for (Rang rangliste : ranglisten.get(variante)) {
                rangliste.platziere(rangnummer);
                rangnummer += rangliste.eintraege.size();
            }
            // Eingabe der Kommentare
            fireStateChanged(CEStatistik.BESTER);
        }

        // Aktualisierung der statistischen Kenngrößen
        int ergebnis;
        for (Spieler spieler : teilnehmer) {
            ergebnis = spieler.endsumme();
            summen.put(variante, summen.get(variante) + ergebnis);
            quadratsummen.put(variante, quadratsummen.get(variante) + ergebnis * ergebnis);
        }
        ns.put(variante, ns.get(variante) + teilnehmer.size());

        spielindex++;
        if (rest() > 0) {
            for (Spieler spieler : teilnehmer) {
                verlaeufe.get(spieler).add(0);
                datensatz.setValue(0, spieler, spieler, spielanzahl());
            }
        }

        // Oberfläche an neue Daten anpassen
        fireStateChanged(CEStatistik.SPIEL_ENDE);
    }

    /**
     * Verfolgt in der Statistik ein fortgesetztes Turnier.
     *
     * @param variante
     *            Regelvariante
     * @param maximalanzahl
     *            Maximale Anzahl der Spiele pro Turnier
     * @param teilnehmer
     *            Alle Mitspieler
     * @param mittelwert
     *            Geschätzter Mittelwert pro Spiel
     * @param abweichung
     *            Geschätzte Abweichung pro Spiel
     * @param verlaeufe
     *            Historie für Balkengrafik
     * @param turnierstaende
     *            Turnierstand der letzten vollendeten Runde
     */
    void verfolge(Variante variante, int maximalanzahl, ArrayList<Spieler> teilnehmer, int mittelwert, int abweichung,
            HashMap<Spieler, ArrayList<Integer>> verlaeufe, HashMap<Spieler, Integer> turnierstaende) {
        this.teilnehmer = teilnehmer;
        this.variante = variante;
        this.maximalanzahl = maximalanzahl;
        this.mittelwert = mittelwert;
        this.abweichung = abweichung;
        this.verlaeufe = verlaeufe;
        this.turnierstaende = turnierstaende;
        spielindex = verlaeufe.get(teilnehmer.get(0)).size() - 1;
        ruecknahmen = new HashMap<>(teilnehmer.size());
        spielstaende = new HashMap<>(teilnehmer.size());
        for (Spieler spieler : teilnehmer) {
            spielstaende.put(spieler, verlaeufe.get(spieler).get(spielindex));
        }
        datensatz = new StandardCategoryDataset3D<>();
        for (Spieler spieler : teilnehmer) {
            DefaultKeyedValues<Integer, Integer> reihe = new DefaultKeyedValues<>();
            for (int i = 0; i < spielanzahl(); i++) {
                reihe.put(i + 1, verlaeufe.get(spieler).get(i));
            }
            datensatz.addSeriesAsRow(spieler, reihe);
        }
        if (spielindex == 0) {
            fireStateChanged(CEStatistik.TURNIER_START);
        } else {
            fireStateChanged(CEStatistik.TURNIER_WEITER);
        }
    }

}
