/******************************************************************************
 ** $Id: Turnier.java 1646 2019-01-12 05:33:11Z wmh $
 ** Diese Datei ist Bestandteil der Java-Quelltexte des Würfelspiels JaFuffy.
 ******************************************************************************
 ** 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.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.ListIterator;
import java.util.Random;

import jafuffy.bedienung.auswahl.Auswahl;
import jafuffy.logik.auswertung.Auswertung;

/** Ablaufsteuerung eines Turniers. */
public class Turnier extends Aenderungen<CEAblauf> implements ActionListener, Serializable {

    /** Die Fortschrittsdaten des Turniers. */
    private static class Fortschritt implements Serializable {

        /** Zur Serialisierung. */
        private static final long serialVersionUID = 8968279815699993227L;

        /** Spieler, der gerade dran ist. Der Wert "null" bedeutet, dass das Turnier beendet ist. */
        private Spieler aktiver;
        /**
         * Index des Spielers pro Runde, der gerade an der Reihe ist. Zählung fängt bei Null mit dem Spieler an, welcher
         * die Runde eröffnet. Der Index ist nicht identisch mit dem Index des Containers, welcher die Spieler enthält.
         */
        private int spielerindex = 0;
        /** Anzahl der vollendeten Spiele, entspricht dem Index des laufenden Spieles im Turnier. */
        private int spielindex = 0;
        /** Anzahl der gesetzten Einträge, wobei eine Runde beendet sein muss. */
        private int eintragsanzahl = 0;

        /**
         * Setze Eintrag mit gegebenen Wert und merke den Schritt als Zwischenschritt für eine spätere Rücknahme.
         * 
         * @param schritt
         *            Fortschritt in diesem Schritt zwischenspeichern.
         * @param kategorie
         *            Der für den aktiven Spieler momentan zu setzende Eintrag.
         * @param wert
         *            Der zu setzende Wert.
         * @return Kopie des Spielers im Zwischenschritt.
         */
        Spieler setze(Fortschritt schritt, Kategorie kategorie, int wert) {
            schritt.aktiver = aktiver;
            schritt.spielerindex = spielerindex;
            schritt.spielindex = spielindex;
            schritt.eintragsanzahl = eintragsanzahl;
            Spieler spieler = aktiver.clone();
            aktiver.belege(kategorie, wert);
            aktiver.beende();
            return spieler;
        }

        /**
         * Nimmt einen Schritt zurück.
         * 
         * @param schritt
         *            Der Schritt, zu dem die Rücknahme erfolgt.
         * @param spieler
         *            Der Stand des Spielers, zu dem die Rücknahme erfolgt.
         * @param kategorie
         *            Eintrag in der Tabelle, der zurückgenommen wird.
         * @param auswertung
         *            Die Auswertung zur Abschaltung der Tipps.
         */
        void annulliere(Fortschritt schritt, Spieler spieler, Kategorie kategorie, Auswertung auswertung) {
            aktiver.beende();
            aktiver = schritt.aktiver;
            spielerindex = schritt.spielerindex;
            spielindex = schritt.spielindex;
            eintragsanzahl = schritt.eintragsanzahl;
            aktiver.annulliere(spieler, kategorie);
            auswertung.reagiereAufWurf(aktiver);
            auswertung.aktiviereTipps(false);
        }
    }

    /** Zur Serialisierung. */
    private static final long serialVersionUID = -8157448193770055452L;

    /** Anzahl der möglichen Einträge auf Gewinnkarte . */
    private static final int EINTRAEGE = 13;
    /** Zur zufälligen Ermittlung des eröffnenden Spielers. */
    private static final Random ZUFALL = new Random();

    /** Die Anzahl der Würfel, die für das Spiel verwendet werden. */
    public static final int WUERFEL = 5;
    /** Die Anzahl der möglichen Runden pro Durchgang. */
    public static final int RUNDEN = 3;

    /** Würfel im Spiel. */
    private final Wuerfel[] wuerfelsatz;
    /** Spielregelvariante, die für das Turnier benutzt wird. */
    private final Variante variante;
    /** Alle Mitspieler. */
    private final ArrayList<Spieler> teilnehmer;
    /** Auswertungsregel für dieses Turnier. */
    private final Auswertung auswertung;
    /** Anzahl der Spiele im Turnier. */
    private final int maximalanzahl;
    /** Hält fest, wer das nächste Spiel beginnt. */
    private final Beginner beginnmodus;
    /** Der aktuelle Fortschritt im Turnier. */
    private final Fortschritt fortschritt = new Fortschritt();
    /** Der zwischengespeicherte Fortschritt im Turnier für eine spätere Rücknahme. */
    private final Fortschritt zwischenschritt = new Fortschritt();

    /** Der Turnierverlauf, alle Spieler pro Turnier werden gespeichert, was die Erstellung eines Reports ermöglicht. */
    private ArrayList<ArrayList<Integer>> verlaeufe;
    /** Der aktuelle Turnierstand summiert über alle abgeschlossenen Spiele, für jeden Mitspieler eine Punktezahl. */
    private ArrayList<Integer> turnierstaende;
    /** Hiermit wird der letzte gesetzte Eintrag gemerkt für eine etwaige Rücknahme des Zuges. */
    private Kategorie eintragsmerker;
    /** Hiermit wird der Stand des letzten Spielers gemerkt für eine etwaige Rücknahme des Zuges. */
    private Spieler standmerker;

    /** Statistik. Muss wiederhergestellt werden, da bei jedem Programmlauf aktualisiert. */
    private transient Statistik statistik;
    /**
     * Punktestand. Muss wiederhergestellt werden, da das Modell Listener enthält, die bei jedem Programmlauf neu
     * erstellt werden.
     */
    private transient Punkte punkte;

    /**
     * Konstruktor. Muss von Initialisierungsfunktion gefolgt werden.
     *
     * @param variante
     *            Spielregelvariante
     * @param teilnehmer
     *            Alle teilnehmenden Spieler
     * @param erster
     *            Spieler, welcher das Turnier eröffnet
     * @param beginnmodus
     *            Wer beginnt das nächste Spiel?
     * @param anzahl
     *            Anzahl der Spiele im Turnier (0=unendlich)
     */
    public Turnier(Variante variante, ArrayList<Spieler> teilnehmer, Spieler erster, Beginner beginnmodus, int anzahl) {
        this.variante = variante;
        this.teilnehmer = teilnehmer;
        this.beginnmodus = beginnmodus;
        this.maximalanzahl = anzahl;
        wuerfelsatz = new Wuerfel[Turnier.WUERFEL];
        for (int i = 0; i < Turnier.WUERFEL; i++) {
            wuerfelsatz[i] = new Wuerfel(i + 1);
        }
        auswertung = variante.auswertung(wuerfelsatz);
        for (Spieler spieler : teilnehmer) {
            spieler.initialisiere();
        }
        fortschritt.aktiver = erster;
        erster.aktiviere();
    }

    @Override
    public void actionPerformed(ActionEvent event) {
        String ac = event.getActionCommand();
        if (ac.equals("Gewuerfelt")) {
            wuerfle();
        } else if (ac.equals("Rueckgaengig")) {
            annulliere();
        } else if (ac.equals("Vorschlagen")) {
            auswertung.erstelleVorschlag();
        } else if (ac.equals("Beenden")) {
            beende();
        }
    }

    /** Liefert den momentan aktiven Spieler. */
    public Spieler aktiver() {
        return fortschritt.aktiver;
    }

    /** Liefert das Objekt, welches die Auswahl und Bearbeitung einer Zelle handhabt. */
    public Auswahl auswahl() {
        return variante.auswahl(this);
    }

    /** Liefert das Objekt, welches die Auswertung von Würfen durchführt. */
    public Auswertung auswertung() {
        return auswertung;
    }

    /** Sichert gegebenenfalls den Turnierstand und passt die Darstellung an. */
    public void beende() {
        if (fortschritt.aktiver != null) {
            auswertung.aktiviereTipps(false);
            fireStateChanged(
                    fortschritt.spielindex == maximalanzahl && maximalanzahl > 0 ? CEAblauf.ENDE : CEAblauf.ABBRUCH);
            fortschritt.aktiver.beende();
            fortschritt.aktiver = null;
        }
    }

    /** Gibt an, ob das Turnier beendet ist. */
    public boolean beendet() {
        return fortschritt.aktiver == null;
    }

    /**
     * Belegt einen gegebenen Eintrag in der Tabelle mit dem erwürfelten Resultat.
     * 
     * @param kategorie
     *            Der Eintrag, welcher belegt werden soll.
     */
    public void belege(Kategorie kategorie) {
        fortschritt.aktiver.belege(kategorie, auswertung.analyse().wert(kategorie));
    }

    /** Liefert eine für das Sichern von Turnieren geeignete Turnierbeschreibung. */
    public String beschreibung() {
        String s = new String("Mitspieler: ");
        ListIterator<Spieler> iterator = teilnehmer.listIterator();
        while (iterator.hasNext()) {
            s += iterator.next().toString();
            if (iterator.hasNext()) {
                s += ", ";
            } else {
                s += ".";
            }
        }
        s += "\n" + auswertung.toString() + ". " + (teilnehmer.size() > 1 ? beginnmodus + ". " : "");
        s += maximalanzahl == 0 ? "Beliebig viele Spiele."
                : "Im " + (fortschritt.spielindex + 1) + ". von " + maximalanzahl
                        + (maximalanzahl != 1 ? " Spielen" : " Spiel") + ".";
        return s;
    }

    /** Liefert maximale Anzahl der Spiele pro Turnier, wobei Null Unbeschränktheit bedeutet. */
    public int maximalanzahl() {
        return maximalanzahl;
    }

    /** Legt eine Pause ein, so dass keine Aktionen mehr entgegen genommen werden. */
    public void pausiere() {
        fireStateChanged(CEAblauf.PAUSE);
    }

    /** Liefert das Punktemodell. */
    public Punkte punkte() {
        return punkte;
    }

    /** Gibt an, ob der letzte Setzvorgang zurückgenommen werden kann. */
    public boolean ruecknahmefaehig() {
        return zwischenschritt.aktiver.interaktiv();
    }

    /**
     * Setzt den gewünschten Eintrag, so dass der nächste Spieler an die Reihe kommt.
     *
     * @param kategorie
     *            Der zu setzende Eintrag, für den der vorliegende Wurf ausgewertet wird.
     */
    public void setze(Kategorie kategorie) {
        standmerker = fortschritt.setze(zwischenschritt, kategorie, auswertung.analyse().wert(kategorie));
        eintragsmerker = kategorie;
        punkte.benachrichtige(fortschritt.aktiver.index());
        beendeSetzen();
    }

    /**
     * Weiter mit dem Turnier nach Turnierpause. Erforderlich wegen Serialisierung, bringt Turniersteuerung und
     * Wurfauswertung auf aktuellen Stand.
     *
     * @param statistik
     *            Aktuelles Statistikobjekt, das vom Turnier verwendet und gepflegt werden soll.
     */
    public void setzeFort(Statistik statistik) {
        this.statistik = statistik;
        auswertung.reagiereAufWurf(fortschritt.aktiver);
        punkte = new Punkte(teilnehmer, auswertung);
        if (verlaeufe == null) {
            verlaeufe = new ArrayList<>(teilnehmer.size());
            turnierstaende = new ArrayList<>(teilnehmer.size());
            for (Spieler spieler : teilnehmer) {
                verlaeufe.add(spieler.index(), new ArrayList<>());
                verlaeufe.get(spieler.index()).add(0);
                turnierstaende.add(spieler.index(), 0);
            }
        }
        statistik.verfolge(variante, maximalanzahl, teilnehmer, auswertung.mittelwert(), auswertung.abweichung(),
                verlaeufe, turnierstaende);
    }

    /** Liefert die Nummer des laufenden Spiels. */
    public int spielnummer() {
        return fortschritt.spielindex + 1;
    }

    /** Startet dieses Turnier. */
    public void starte() {
        fireStateChanged(CEAblauf.START);
    }

    /** Liefert aller Teilnehmer diese Turniers. */
    public ArrayList<Spieler> teilnehmer() {
        return teilnehmer;
    }

    /** Gibt an, ob für einen Wurf ein Vorschlag gemacht werden kann. */
    public boolean vorschlagbar() {
        return fortschritt.aktiver.gewuerfelt();
    }

    /**
     * Liefert den Würfel zu einem Index.
     * 
     * @param i
     *            Index des Würfels
     * @return Würfel
     */
    public Wuerfel wuerfel(int i) {
        return wuerfelsatz[i];
    }

    /** Liefert den Würfelsatz vom Turnier. */
    public Wuerfel[] wuerfelsatz() {
        return wuerfelsatz;
    }

    /** Nimmt den Eintrag eines Spielers in die Punktetabelle zurück. */
    private void annulliere() {
        if (fortschritt.eintragsanzahl != zwischenschritt.eintragsanzahl) {
            statistik.annulliere();
        }
        fortschritt.annulliere(zwischenschritt, standmerker, eintragsmerker, auswertung);
        for (Wuerfel wuerfel : wuerfelsatz) {
            wuerfel.darstellungsmodell().setSelected(false);
        }
        fireStateChanged(CEAblauf.RUECKGAENGIG);
    }

    /** Runde beenden. */
    private void beendeRunde() {
        fortschritt.eintragsanzahl++;
        fortschritt.spielerindex = 0;
        statistik.beendeRunde();
    }

    /**
     * Spieler hat gesetzt, der nächste ist dran. Es werden keine for/while-Schleifen benutzt, da diese Methode bei
     * jedem Setz-Ereignis angesprungen wird.
     */
    private void beendeSetzen() {
        fortschritt.aktiver = naechster();
        fortschritt.aktiver.aktiviere();
        fortschritt.spielerindex++;
        if (fortschritt.spielerindex >= teilnehmer.size()) {
            beendeRunde();
            if (fortschritt.eintragsanzahl >= EINTRAEGE) {
                fortschritt.aktiver.beende(); // Nach Ende eines Spiels soll keine Tabellenspalte markiert sein
                auswertung.aktiviereTipps(false);
                fireStateChanged(CEAblauf.RESULTAT);
                beendeSpiel();
            }
        }
        if (maximalanzahl > 0 && fortschritt.spielindex == maximalanzahl) {
            beende();
        } else {
            if (fortschritt.spielindex == zwischenschritt.spielindex) {
                auswertung.aktiviereTipps(false);
                fireStateChanged(CEAblauf.GESETZT);
            } else {
                fireStateChanged(CEAblauf.SPIEL);
            }
        }
    }

    /** Spiel beenden, nächsten Spieleröffner bestimmen. */
    private void beendeSpiel() {
        Spieler erster = eroeffner(); // Berechnungen erfordern Aufruf vor der Initialisierung der Spieler
        statistik.beendeSpiel();
        for (Spieler spieler : teilnehmer) {
            spieler.initialisiere();
        }
        fortschritt.aktiver = erster;
        fortschritt.aktiver.aktiviere();
        fortschritt.spielindex++;
        fortschritt.eintragsanzahl = 0;
    }

    /**
     * Bestimmt den Spieler, welcher das nächste Spiel im Turnier eröffnet.
     *
     * @return Eröffnender Spieler.
     */
    private Spieler eroeffner() {
        Spieler verlierer = teilnehmer.get(0);
        Spieler gewinner = verlierer;
        for (Spieler spieler : teilnehmer) {
            if (spieler.endsumme() < verlierer.endsumme()) {
                verlierer = spieler;
            }
            if (spieler.endsumme() > gewinner.endsumme()) {
                gewinner = spieler;
            }
        }
        Spieler eroeffner;
        switch (beginnmodus) {
        case VERLIERER:
            eroeffner = verlierer;
            break;
        case GEWINNER:
            eroeffner = gewinner;
            break;
        case REIHUM:
            eroeffner = naechster();
            break;
        case ZUFALL:
            eroeffner = teilnehmer.get(ZUFALL.nextInt(teilnehmer.size()));
            break;
        case DERSELBE:
            eroeffner = fortschritt.aktiver;
            break;
        default:
            eroeffner = null; // Fataler Fehler, führt zum Abbruch. Auftreten weist auf Programmierfehler hin.
            break;
        }
        return eroeffner;
    }

    /**
     * Bestimmt den Spieler, der als nächstes reihum drankommt.
     * 
     * @return Nächster Spieler, der an der Reihe ist.
     */
    private Spieler naechster() {
        return teilnehmer.get((fortschritt.aktiver.index() + 1) % teilnehmer.size());
    }

    /** Führt einen Würfelvorgang des aktiven Spielers durch. */
    private void wuerfle() {
        for (Wuerfel wuerfel : wuerfelsatz) {
            if (wuerfel.darstellungsmodell().isSelected()) {
                wuerfel.wirf();
            }
        }
        fireStateChanged(CEAblauf.WUERFELN);
        fortschritt.aktiver.reagiere();
        auswertung.reagiereAufWurf(fortschritt.aktiver);
        auswertung.aktiviereTipps(false);
        fireStateChanged(CEAblauf.GEWUERFELT);
    }

    /**
     * Liefert den Spieler zu einem gegebenen Index.
     * 
     * @param i
     *            Index
     * @return Spieler
     */
    Spieler spieler(int i) {
        return teilnehmer.get(i);
    }

}
