/******************************************************************************
 ** $Id: Turnier.java 1188 2017-04-16 01:00:49Z 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.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.ListIterator;
import java.util.Random;

import jafuffy.bedienung.Auswahl;

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

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

    /** Anzahl der möglichen Einträge auf Gewinnkarte . */
    private static final int EINTRAEGE = 13;

    /** 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[] wuerfel;
    /** 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;
    /** Benötigt für die Rücknahme eines Zuges, wobei die Spielerdaten hiermit gemerkt werden. */
    private final Spieler kopieAktiver = new Spieler("", 0);

    /** Spieler, der gerade dran ist. Der Wert "null" bedeutet, dass das Turnier beendet ist. */
    private Spieler aktiver;
    /** 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;
    /**
     * 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;
    /** Bestimmt, wer als Erster eine Spielrunde eröffnet. */
    private int startindex;
    /** Der Turnierverlauf, alle Spieler pro Turnier werden gespeichert, was die Erstellung eines Reports ermöglicht. */
    private HashMap<Spieler, ArrayList<Integer>> verlaeufe;
    /** Der aktuelle Turnierstand summiert über alle abgeschlossenen Spiele, für jeden Mitspieler eine Punktezahl. */
    private HashMap<Spieler, Integer> turnierstaende;

    /** 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;

    /** Benötigt für die Rücknahme eines Zuges. */
    private transient Spieler merkerAktiver;
    /** Benötigt für die Rücknahme eines Zuges. */
    private transient int merkerSpielanzahl;
    /** Benötigt für die Rücknahme eines Zuges. */
    private transient int merkerEintragsanzahl;
    /** Benötigt für die Rücknahme eines Zuges. */
    private transient int merkerSpielerindex;

    /**
     * Konstruktor. Muss von Initialisierungsfunktion gefolgt werden.
     *
     * @param variante
     *            Spielregelvariante
     * @param spieler
     *            Liste aller Mitspieler
     * @param startindex
     *            Nummer des ersten Spielers
     * @param beginnmodus
     *            Wer beginnt das nächste Spiel?
     * @param anzahl
     *            Anzahl der Spiele im Turnier (0=unendlich)
     */
    public Turnier(Variante variante, ArrayList<Spieler> spieler, int startindex, Beginner beginnmodus, int anzahl) {
        this.variante = variante;
        this.teilnehmer = spieler;
        this.startindex = startindex;
        this.beginnmodus = beginnmodus;
        this.maximalanzahl = anzahl;

        // Würfel vorbereiten
        wuerfel = new Wuerfel[Turnier.WUERFEL];
        for (int i = 0; i < Turnier.WUERFEL; i++) {
            wuerfel[i] = new Wuerfel(i + 1);
        }

        // Auswertung je nach Spielregel erstellen
        auswertung = variante.auswertung(wuerfel);

        // Spieler vorbereiten, Beginner heraussuchen
        for (Spieler s : spieler) {
            s.initialisiere();
        }
        aktiviere(startindex);
    }

    /**
     * Spieler hat gewürfelt oder Setzen rückgängig gemacht.
     *
     * @param e
     */
    @Override
    public void actionPerformed(ActionEvent e) {
        String ac = e.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();
        }
    }

    /**
     * @return aktiver Spieler
     */
    public Spieler aktiver() {
        return aktiver;
    }

    /**
     * @return Auswahl
     */
    public Auswahl auswahl() {
        return variante.auswahl(this);
    }

    /**
     * @return Auswertungsobjekt
     */
    public Auswertung auswertung() {
        return auswertung;
    }

    /**
     * Aktion bei Turnierabbruch: Sichern des Turnierstands unter Standardnamen.
     */
    public void beende() {
        if (aktiver != null) {
            auswertung.aktiviereTipps(false);
            fireStateChanged(spielindex == maximalanzahl && maximalanzahl > 0 ? CEAblauf.ENDE : CEAblauf.ABBRUCH);
            aktiver.warte(); // keine Spaltenmarkierung in der Tabelle mehr
            aktiver = null;
        }
    }

    /** @return Turnier beendet? */
    public boolean beendet() {
        return aktiver == null;
    }

    /**
     * @return 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 " + (spielindex + 1) + ". von " + maximalanzahl + (maximalanzahl != 1 ? " Spielen" : " Spiel")
                        + ".";
        return s;
    }

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

    /**
     * Pause einlegen (keine Aktionen mehr entgegennehmen).
     */
    public void pausiere() {
        fireStateChanged(CEAblauf.PAUSE);
    }

    /**
     * @return Punktemodell
     */
    public Punkte punkte() {
        return punkte;
    }

    /**
     * Spieler hat gesetzt. Weiter im Spielgeschehen.
     *
     * @param eintrag
     *            Eintrag
     */
    public void setze(Tabzeile eintrag) {
        setze(eintrag, auswertung.analyse().wert(eintrag));
    }

    /**
     * Wurf des aktiven Spielers setzen. Weiter im Spielgeschehen.
     *
     * @param eintrag
     *            Eintrag
     * @param wert
     *            Punktewert
     */
    public void setze(Tabzeile eintrag, int wert) {
        merke();
        aktiver.setze(eintrag, wert, true);
        beendeSetzen();
    }

    /**
     * Weiter mit dem Turnier nach Turnierpause. Erforderlich wegen Serialisierung.
     *
     * @param statistik
     *            Aktuelles Statistikobjekt, das vom Turnier verwendet und gepflegt werden soll.
     */
    public void setzeFort(Statistik statistik) {
        this.statistik = statistik;
        auswertung.reagiereAufWurf(aktiver); // wertet Wurf des aktiven Spielers aus um auf aktuellem Stand zu sein
        punkte = new Punkte(teilnehmer, auswertung);
        if (verlaeufe == null) {
            verlaeufe = new HashMap<>(teilnehmer.size());
            turnierstaende = new HashMap<>(teilnehmer.size());
            for (Spieler spieler : teilnehmer) {
                verlaeufe.put(spieler, new ArrayList<>());
                verlaeufe.get(spieler).add(0);
                turnierstaende.put(spieler, 0);
            }
        }
        statistik.verfolge(variante, maximalanzahl, teilnehmer, auswertung.mittelwert(), auswertung.abweichung(),
                verlaeufe, turnierstaende);
    }

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

    /** @return Nummer des laufenden Spiels */
    public int spielnummer() {
        return spielindex + 1;
    }

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

    /**
     * @return Feld aller Würfel
     */
    public Wuerfel[] wuerfel() {
        return wuerfel;
    }

    /**
     * @param i
     *            Nummer des Würfels
     * @return Würfel
     */
    public Wuerfel wuerfel(int i) {
        return wuerfel[i];
    }

    /** Nimmt den Eintrag eines Spielers in die Punktetabelle zurück. */
    private void annulliere() {
        if (eintragsanzahl != merkerEintragsanzahl) {
            statistik.annulliere();
        }
        aktiver.warte();
        spielerindex = merkerSpielerindex;
        eintragsanzahl = merkerEintragsanzahl;
        spielindex = merkerSpielanzahl;
        aktiver = merkerAktiver;
        kopieAktiver.sichere(aktiver);
        aktiver.aktualisiere();
        auswertung.reagiereAufWurf(aktiver);
        for (Wuerfel w : wuerfel) {
            w.darstellungsmodell().setSelected(false);
        }
        auswertung.aktiviereTipps(false);
        fireStateChanged(CEAblauf.RUECKGAENGIG);
    }

    /** Runde beenden. */
    private void beendeRunde() {
        eintragsanzahl++;
        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() {
        aktiviere((teilnehmer.indexOf(aktiver) + 1) % teilnehmer.size());
        spielerindex++;
        if (spielerindex >= teilnehmer.size()) {
            beendeRunde();
            if (eintragsanzahl >= EINTRAEGE) {
                // in der Tabelle soll kein Spieler markiert sein
                aktiver.warte();
                auswertung.aktiviereTipps(false);
                fireStateChanged(CEAblauf.RESULTAT);
                beendeSpiel();
            }
        }
        if (maximalanzahl > 0 && spielindex == maximalanzahl) {
            beende();
        } else {
            if (spielindex == merkerSpielanzahl) {
                auswertung.aktiviereTipps(false);
                fireStateChanged(CEAblauf.GESETZT);
            } else {
                fireStateChanged(CEAblauf.SPIEL);
            }
        }
    }

    /** Spiel beenden, nächsten Spieleröffner bestimmen. */
    private void beendeSpiel() {
        Spieler verlierer, gewinner;

        // suche Gewinner und Verlierer
        verlierer = teilnehmer.get(0);
        gewinner = verlierer;
        for (Spieler spieler : teilnehmer) {
            if (spieler.endsumme() < verlierer.endsumme()) {
                verlierer = spieler;
            }
            if (spieler.endsumme() > gewinner.endsumme()) {
                gewinner = spieler;
            }
        }

        // Suche nächsten Spielbeginner
        switch (beginnmodus) {
        case VERLIERER:
            startindex = teilnehmer.indexOf(verlierer);
            break;
        case GEWINNER:
            startindex = teilnehmer.indexOf(gewinner);
            break;
        case REIHUM:
            startindex++;
            startindex %= teilnehmer.size();
            break;
        case ZUFALL:
            startindex = new Random().nextInt(teilnehmer.size());
            break;
        default:
            break;
        }

        // Daten zurücksetzen
        statistik.beendeSpiel();
        for (Spieler spieler : teilnehmer) {
            spieler.initialisiere();
        }

        // Weiterschaltung
        aktiviere(startindex);
        spielindex++;
        eintragsanzahl = 0;
    }

    /** Merken für Rücknahme. */
    private void merke() {
        merkerSpielerindex = spielerindex;
        merkerEintragsanzahl = eintragsanzahl;
        merkerSpielanzahl = spielindex;
        merkerAktiver = aktiver;
        aktiver.sichere(kopieAktiver);
    }

    /** Spieler würfelt. */
    private void wuerfle() {
        for (Wuerfel w : wuerfel) {
            if (w.darstellungsmodell().isSelected()) {
                w.wirf();
            }
        }
        fireStateChanged(CEAblauf.WUERFELN);
        aktiver.reagiere();
        auswertung.reagiereAufWurf(aktiver);
        auswertung.aktiviereTipps(false);
        fireStateChanged(CEAblauf.GEWUERFELT);
    }

    /**
     * Spieler aktivieren.
     *
     * @param index
     *            Spielerindex im Spielerfeld
     */
    void aktiviere(int index) {
        aktiver = teilnehmer.get(index);
        aktiver.aktiviere();
    }

    /**
     * @param i
     *            Nummer
     * @return Spieler
     */
    Spieler spieler(int i) {
        return teilnehmer.get(i);
    }

}
