/******************************************************************************
 ** $Id: Statistik.java 739 2015-05-23 16:13:32Z wmh $
 ** Diese Datei ist Bestandteil der Java-Quelltexte des Würfelspiels JaFuffy.
 ** Lauffähig ab Java 6.
 ******************************************************************************
 ** 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 2 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, write to the Free Software
 ** Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 ******************************************************************************
 ** 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.ListIterator;
import java.util.Locale;

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

    private static final long serialVersionUID = 3612572422999296411L;

    /** Name der Statistikdatei. */
    public static final String STAT = "Statistik.obj";

    /** Über wie viele Ränge wird Buch geführt? */
    private static final int MAXRAENGE = 10;

    /**
     * Klasse für Mitspieler gleichen Ranges. Ein Rang ist durch die Punktzahl bestimmt, kann
     * aber mehrere gleiche Punkteinträge enthalten.
     */
    public class Rang implements Serializable {
        private static final long serialVersionUID = 8690127540463640879L;
        private final ArrayList<Eintrag> eintraege = new ArrayList<Eintrag>();
        private int punkte;
        private int rangnummer;

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

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

            public String name() {
                return name;
            }

            String datum() {
                return datum;
            }

            String kommentar() {
                return kommentar;
            }

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

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

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

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

        int punkte() {
            return punkte;
        }

        public int rangnummer() {
            return rangnummer;
        }

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

    }

    // Welche Spielregel?
    private transient int variante;

    // alle Mitspieler
    private transient ArrayList<Spieler> spieler;
    // Merke alle Ränge, die verändert werden
    private transient ArrayList<Rang> rangmerker;

    // aktueller Spielstand (vorhergehende Spiele+aktueller Zwischenstand),
    // Anzahl der Spiele im Turnier, Mittelwert & Abweichung pro Spiel
    private transient int[] rueckgaengig;
    private transient int[] spielstand;
    private transient int anzahl;
    private transient int mittelwert;
    private transient int abweichung;
    private transient int[] turnierstand;

    // für jede Variante eine Rangliste mit Einträgen der Klasse "Rang"
    private final ArrayList<Rang>[] ranglisten;
    // Datenmodelle für Bestenlisten, entsprechend der Variante
    private transient BestenlisteModell[] bestenlisteModelle;
    // Turnierverlauf (pro Spieler ein Feldeintrag, nämlich ArrayList)
    private transient ArrayList<Integer>[] verlauf;

    // für statistische Kenngrößen
    private final int[] n = new int[Variante.values().length];
    private final int[] summe = new int[Variante.values().length];
    private final long[] quadratsumme = new long[Variante.values().length];

    /** Konstruktor. */
    @SuppressWarnings("unchecked")
    public Statistik() {
        ranglisten = new ArrayList[Variante.values().length];
        for (int i = 0; i < Variante.values().length; i++) {
            ranglisten[i] = new ArrayList<Rang>(MAXRAENGE);
        }
        bestenlistenmodelle();
    }

    /**
     * Statistik verfolgt fortgesetztes Turnier.
     *
     * @param variante
     *            Regelvariante
     * @param anzahl
     *            Zahl der Spiele pro Turnier
     * @param spieler
     *            alle Mitspieler
     * @param mittelwert
     *            geschätzter Mittelwert pro Spiel
     * @param abweichung
     *            geschätzte Abweichung pro Spiel
     * @param verlauf
     *            Historie für Balkengrafik
     * @param turnierstand
     *            Turnierstand der letzten vollendeten Runde
     */
    void verfolgen(int variante, int anzahl, ArrayList<Spieler> spieler, int mittelwert,
            int abweichung, ArrayList<Integer>[] verlauf, int[] turnierstand) {
        initialisieren(variante, anzahl, spieler, mittelwert, abweichung);
        this.verlauf = verlauf;
        this.turnierstand = turnierstand;
        for (int i = 0; i < spieler.size(); i++) {
            spielstand[i] = punkte(i);
        }
        fireStateChanged(CEStatistik.TURNIER_WEITER);
    }

    /**
     * Statistik verfolgt neues Turnier.
     *
     * @param variante
     *            Regelvariante
     * @param anzahl
     *            Zahl der Spiele pro Turnier
     * @param spieler
     *            alle Mitspieler
     * @param mittelwert
     *            geschätzter Mittelwert pro Spiel
     * @param abweichung
     *            geschätzte Abweichung pro Spiel
     */
    @SuppressWarnings("unchecked")
    void verfolgen(int variante, int anzahl, ArrayList<Spieler> spieler, int mittelwert,
            int abweichung) {
        initialisieren(variante, anzahl, spieler, mittelwert, abweichung);
        verlauf = new ArrayList[spieler.size()];
        turnierstand = new int[spieler.size()];
        for (int i = 0; i < spieler.size(); i++) {
            turnierstand[i] = 0;
            spielstand[i] = 0;
            verlauf[i] = new ArrayList<Integer>();
            verlauf[i].add(new Integer(0));
        }
        fireStateChanged(CEStatistik.TURNIER_START);
    }

    /** Runde beenden. */
    void beendeRunde() {
        for (int s = 0; s < spieler.size(); s++) {
            rueckgaengig[s] = spielstand[s];
            spielstand[s] = punkte(s);
            verlauf[s].set(verlauf[0].size() - 1, new Integer(spielstand[s]));
        }
        fireStateChanged(CEStatistik.STAND);
    }

    /**
     * Zug rückgängig machen (nur zu rufen, falls in alte Runde zurückgefallen). Zuvor muss
     * beendeRunde() aufgerufen worden sein.
     */
    void rueckgaengig() {
        for (int s = 0; s < spieler.size(); s++) {
            spielstand[s] = rueckgaengig[s];
            verlauf[s].set(verlauf[0].size() - 1, new Integer(spielstand[s]));
        }
        fireStateChanged(CEStatistik.STAND);
    }

    /**
     * Spiel beenden. Merken des Punktstandes. Erstellung und Anzeige der Rangliste.
     * Aktualisierung der statistischen Kenngrößen. 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
        rangmerker = new ArrayList<Rang>(spieler.size());
        for (int i = 0; i < spieler.size(); i++) {
            int j = 0, punkte;
            boolean eingeordnet = false;
            punkte = punkte(i);
            turnierstand[i] += punkte;
            spielstand[i] = 0;
            // Platz in Rangliste suchen und bei Bedarf neuen Rang erzeugen
            rangiterator = ranglisten[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[variante].size() == MAXRAENGE) {
                    // falls letzter Rang aus Bestenliste gelöscht wird, auch
                    // aus Rangmerker löschen
                    rangmerker.remove(ranglisten[variante].get(MAXRAENGE - 1));
                    ranglisten[variante].remove(MAXRAENGE - 1);
                }
                rang = new Rang();
                ranglisten[variante].add(j, rang);
            }
            // Niedrigster Punktstand, aber noch Platz in Bestenliste?
            if (!eingeordnet && ranglisten[variante].size() < MAXRAENGE) {
                rang = new Rang();
                ranglisten[variante].add(rang);
                eingeordnet = true;
            }
            // Eintrag zum Rang hinzufügen, Rang und Eintrag merken für
            // Kommentareingabe
            if (eingeordnet) {
                aktualisieren = true;
                rang.anhaengen(spieler.get(i));
                rangmerker.add(rang);
            }

        }

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

        // Aktualisierung der statistischen Kenngrößen
        int ergebnis;
        for (Spieler s : spieler) {
            ergebnis = s.endsumme();
            summe[variante] += ergebnis;
            quadratsumme[variante] += ergebnis * ergebnis;
        }
        n[variante] += spieler.size();

        // Platz für nächstes Spiel
        for (ArrayList<Integer> v : verlauf) {
            v.add(new Integer(0));
        }

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

    /**
     * @return Anzahl der Spiele pro Turnier
     */
    public int anzahl() {
        return anzahl;
    }

    /**
     * @return restliche Anzahl der vollständigen Spiele pro Turnier
     */
    public int rest() {
        return anzahl - verlauf[0].size();
    }

    /**
     * @return Spielvariante
     */
    public int variante() {
        return variante;
    }

    /**
     * @return geschätzter Mittelwert pro Spiel
     */
    public int mittelwert() {
        return mittelwert;
    }

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

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

    /**
     * @return Spieler
     */
    public ArrayList<Spieler> spieler() {
        return spieler;
    }

    /**
     * @return Modelle für Bestenlisten
     */
    public BestenlisteModell[] bestenlisteModelle() {
        return bestenlisteModelle;
    }

    /**
     * @return Spielverlauf
     */
    public ArrayList<Integer>[] verlauf() {
        return verlauf;
    }

    /**
     * @param s
     *            Nummer des Spielers
     * @return Punktzahl des s-ten Spielers
     */
    int punkte(int s) {
        return spieler.get(s).endsumme();
    }

    /**
     * @return Zwischenstandmerker
     */
    int[] turnierstand() {
        return turnierstand;
    }

    /**
     * @param s
     *            Nummer des Spielers
     * @return Zwischenstand des s-ten Spielers
     */
    public int stand(int s) {
        return turnierstand[s] + spielstand[s];
    }

    /**
     * @param i
     *            Spielvariante
     * @return Gesamtzahl aller Spieler in genannter Variante
     */
    public int n(int i) {
        return n[i];
    }

    /**
     * @param i
     *            Spielvariante
     * @return durchschnittliche Punktzahl aus Daten berechnet
     */
    public double durchschnitt(int i) {
        return (double) summe[i] / (double) n[i];
    }

    /**
     * @param i
     *            Spielvariante
     * @return Standardabweichung der Punktzahlen aus Daten berechnet
     */
    public double standardabweichung(int i) {
        double qi = quadratsumme[i];
        double si = summe[i];
        int ni = n[i];
        return Math.sqrt((qi - si * si / ni) / (ni - 1));
    }

    /**
     * Initialisierung.
     *
     * @param variante
     *            Regelvariante
     * @param anzahl
     *            Zahl der Spiele pro Turnier
     * @param spieler
     *            alle Mitspieler
     * @param mittelwert
     *            geschätzter Mittelwert pro Spiel
     * @param abweichung
     *            geschätzte Abweichung pro Spiel
     */
    private void initialisieren(int variante, int anzahl, ArrayList<Spieler> spieler,
            int mittelwert, int abweichung) {
        // Spieleigenschaften
        this.spieler = spieler;
        this.variante = variante;
        this.anzahl = anzahl;
        this.mittelwert = mittelwert;
        this.abweichung = abweichung;

        // interne Daten
        rueckgaengig = new int[spieler.size()];
        spielstand = new int[spieler.size()];
    }

    /**
     * Erstellung der Modelle für die Bestenlisten
     */
    private void bestenlistenmodelle() {
        bestenlisteModelle = new BestenlisteModell[Variante.values().length];
        for (int i = 0; i < Variante.values().length; i++) {
            bestenlisteModelle[i] = new BestenlisteModell(ranglisten[i]);
        }
    }

    /**
     * Serialisiertes Objekt wieder einlesen.
     *
     * @param in
     */
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        bestenlistenmodelle();
    }

}
