import java.util.ArrayList;
import java.util.Random;

/**
 * Das Spielfeld selbst
 * 
 * @author Rainer Helfrich
 * @version Oktober 2020
 */
public class FreecellGame
{
    /**
     * Die vier Zellen oben links
     */
    private Karte[] zellen;

    /**
     * Die vier Stapel oben rechts
     */
    private Stack<Karte>[] ablagestapel;

    /**
     * Die acht Stapel unten, auf denen die Karten zu Beginn liegen
     */
    private Stack<Karte>[] kaskaden; 

    /**
     * Konstanten, die für die verschiedenen Ablegebereiche stehen.
     */
    public static final int ZELLEN = 0;
    public static final int ABLAGESTAPEL = 1;
    public static final int KASKADEN = 2;
    public static final int KEINER = -1;

    /**
     * Erzeugt ein neues Spiel mit einer zufälligen Ausgangssituation.
     */
    public FreecellGame()
    {
        this(new Random());
    }

    /**
     * Erzeugt ein neues Spiel mit einer vorgegebenen Nummer.
     * Ein Spiel mit einer bestimmten Nummer beginnt immer in der gleichen Ausgangssituation.
     * @param nummer Legt fest, welche Ausgangssituation verwendet wird.
     */
    public FreecellGame(int nummer)
    {
        this(new Random(nummer));
    }

    /**
     * Erzeugt ein neues Spiel
     * @param r Der Zufallsgenerator, der zum zufälligen Füllen der Kaskaden verwendet wird.
     */
    private FreecellGame(Random r)
    {
        zellen = new Karte[4];
        ablagestapel = new Stack[4];
        kaskaden = new Stack[8];
        ArrayList<Integer> zahlen = new ArrayList<Integer>(52);
        for (int i = 0; i < 52; i++) {
            zahlen.add(i);
        } 
        //# TODO: Erzeugen Sie die Stacks für den Ablagestapel und die Kaskaden
        for (int i = 0; i < ablagestapel.length; i++) {
            ablagestapel[i] = new ArrayStack<Karte>();
        } 
        for (int i = 0; i < kaskaden.length; i++) {
            kaskaden[i] = new ArrayStack<Karte>();
        }
        //# Ende TODO        
        for (int i = 0; i < kaskaden.length; i++) 
        {
            for (int j = 0; j < ((i < 4) ? 7 : 6); j++) {
                int rnd = r.nextInt(zahlen.size());
                Karte k = new Karte(zahlen.get(rnd));
                zahlen.remove(rnd);
                if (kaskaden[i] != null)
                {
                    kaskaden[i].push(k);
                }
            } 
        } 
        new FreecellFrame(this);
    }

    /**
     * Gibt die oberste Karte aus dem angegebenen Bereich zurück
     * @param bereich Kann ZELLEN, ABLAGESTAPEL oder KASKADEN sein.
     * @param index Kann 0 bis 7 (für Kaskaden) bzw. 0 bis 3 (andere Bereiche) sein. Gibt an, welches Feld des Bereichs gewünscht wird.
     * @return Die oberste Karte, sofern eine dort liegt. null sonst.
     */
    public Karte getKarte(int bereich, int index)
    {
        //# TODO: Sorgen Sie dafür, dass die (oberste) Karte aus dem gewünschten Bereich zurückgegeben wird.
        //# Wenn ein Stapel leer ist, soll null zurückgegeben werden.
        if (bereich == ZELLEN)
        {
            return zellen[index];
        }
        if (bereich == KASKADEN && !kaskaden[index].isEmpty())
        {
            return kaskaden[index].top();
        }
        if (bereich == ABLAGESTAPEL && !ablagestapel[index].isEmpty())
        {
            return ablagestapel[index].top();
        }
        return null;
    }

    /**
     * Gibt die Höhe der Kaskade mit der gegebenen Nummer zurück
     * @param index Kann 0 bis 7 sein. Gibt an, welche Kaskade gewünscht wird.
     * @return Die Höhe der Kaskade.
     */
    public int getStapelHoehe(int index)
    {
        //# TODO: Lassen Sie zurückgeben, wie hoch der Stapel der Kaskade mit der Nummer index ist.
        int size = 0;
        Stack<Karte> temp = new ArrayStack<Karte>();
        Stack<Karte> s = kaskaden[index];
        while(!s.isEmpty())
        {
            temp.push(s.pop());
            size++;
        }
        while(!temp.isEmpty())
        {
            s.push(temp.pop());
        }
        return size;
    }

    /**
     * Gibt die n-te Karte von oben in der Kaskade mit der Nummer index zurück.
     * Die Parameter sind immer in gültigen Bereichen, wenn die Methode von FreecellFrame aufgerufen wird.
     * @param index Kann 0 bis 7 sein. Gibt an, welche Kaskade gewünscht wird.
     * @param n Kann 0 bis getStapelHoehe(index)-1 sein. Gibt an, welche Karte darin gewünscht wird.
     * @return Die gewünschte Karte
     */
    public Karte getKarteInStapel(int index, int n)
    {
        //# TODO: Lassen Sie die n-te Karte aus der Kaskade mit der Nummer index zurückgeben.
        //#       n ist dabei von oben gezählt, d.h. die oberste Karte hat den Index 0.
        //#       n ist immer ein gültiger Index, d.h. die Zahl liegt zwischen 0 und Höhe-1

        Stack<Karte> temp = new ArrayStack<Karte>();
        Stack<Karte> s = kaskaden[index];
        for (int i = 0; i < n; i++)
        {
            temp.push(s.pop());
        }
        Karte k = s.top();
        while(!temp.isEmpty())
        {
            s.push(temp.pop());
        }
        return k;
    }

    /**
     * Gibt zurück, ob das Spiel gewonnen ist.
     * @return true, wenn das Spiel gewonnen ist; false sonst
     */
    public boolean istGewonnen()
    {
        //# TODO: Lassen Sie zurückgeben, ob das Spiel gewonnen ist. 
        //#       Das Spiel ist gewonnen, wenn die Zellen keine Karte enthalten und alle Kaskaden leer sind.
        //#       Umgekehrt: Falls eine Zelle oder eine Kaskade noch eine Karte enthält, ist das Spiel noch nicht gewonnen.
        for (Karte k : zellen)
        {
            if (k != null)
            {
                return false;
            }
        }
        for (Stack<Karte> s : kaskaden)
        {
            if (!s.isEmpty())
            {
                return false;
            }
        }
        return true;
    }

    /**
     * Verschiebt eine Karte
     * @param vonBereich Kann ZELLEN oder KASKADEN sein. Gibt an, <b>aus</b> welchem Bereich die Karte verschoben wird.
     * @param vonIndex Kann 0 bis 7 (für Kaskaden) bzw. 0 bis 3 (für Zellen) sein. Gibt an, <b>von</b> welchem Feld des Bereichs die Karte verschoben wird.
     * @param nachBereich Kann ZELLEN, ABLAGESTAPEL oder KASKADEN sein. Gibt an, <b>zu</b> welchem Bereich die Karte verschoben wird.
     * @param nachIndex Kann 0 bis 7 (für Kaskaden) bzw. 0 bis 3 (andere Bereiche) sein. Gibt an, <b>zu</b> welchem Feld des Bereichs die Karte verschoben wird.
     * @return true, wenn die Verschiebung gültig und erfolgreich war; false sonst
     */
    public boolean verschiebe(int vonBereich, int vonIndex, int nachBereich, int nachIndex)
    {
        //# TODO: Verschieben Sie eine Karte.
        //#       vonBereich kann dabei ZELLEN oder KASKADEN sein, vonIndex ist ein jeweils gültiger Index (0 bis 3 für Zellen, 0 bis 7 für Kaskaden)
        //#       nachBereich kann ZELLEN, KASKADEN oder ABLAGESTAPEL sein, nachIndex ist ein jeweils gültiger Index
        //#       Sorgen Sie dafür, dass die Karten an Ihrem Ursprungsort auch entfernt wird!
        //#       Geben Sie true zurück, wenn die Verschiebung gültig und erfolgreich war, sonst false
        Karte k = null;
        if (vonBereich == ZELLEN)
        {
            k = zellen[vonIndex];
        }
        else if (vonBereich == KASKADEN && !kaskaden[vonIndex].isEmpty())
        {
            k = kaskaden[vonIndex].top();
        }
        else
        {
            return false;
        }
        boolean erfolg = false;
        if (nachBereich == ZELLEN)
        {
            erfolg = (zellen[nachIndex] == null);
            if (erfolg)
            {
                zellen[nachIndex] = k;
            }
        }
        if (nachBereich == KASKADEN)
        {
            if (kaskaden[nachIndex].isEmpty())
            {
                erfolg = true;
            }
            else
            {
                Karte kk = kaskaden[nachIndex].top();
                erfolg = (kk.getWert() == k.getWert()+1 && kk.istRot() != k.istRot());
            }
            if (erfolg)
            {
                kaskaden[nachIndex].push(k);
            }
        }
        if (nachBereich == ABLAGESTAPEL)
        {
            if (ablagestapel[nachIndex].isEmpty())
            {
                erfolg = (k.getWert() == 1);
            }
            else 
            {
                Karte kk = ablagestapel[nachIndex].top();
                erfolg = (kk.getFarbe() == k.getFarbe() && kk.getWert() == k.getWert()-1);
            }
            if (erfolg)
            {
                ablagestapel[nachIndex].push(k);
            }
        }
        if (erfolg)
        {
            if (vonBereich == ZELLEN)
            {
                zellen[vonIndex] = null;
            }
            else
            {
                kaskaden[vonIndex].pop();
            }
        }
        return erfolg;
    }

    /**
     * Führt einen automatischen Zug aus, wenn dies möglich ist.
     * @return true, wenn ein automatischer Zug möglich war; false sonst
     */
    public boolean automatischerZug()
    {
        //# TODO: (optional) Lassen Sie einen automatischen Zug ausführen.
        //#       Eine Karte kann automatisch von einer Zelle oder einer Kaskade auf einen Ablagestapel gelegt werden, wenn...
        //#       - sie ein As oder eine 2 ist oder
        //#       - ihr Wert um höchstens 2 größer ist als die niederwertigste Karte auf allen Ablagestapeln 
        //#       Geben Sie true zurück, wenn ein automatischer Zug möglich war, sonst false.

        int min = 15;
        for (Stack<Karte> s : ablagestapel)
        {
            if (s.isEmpty())
            {
                min = 0;
                break;
            }
            if (s.top().getWert() < min)
            {
                min = s.top().getWert();
            }
        }
        for (int i = 0; i < zellen.length; i++)
        {
            if (zellen[i] != null && zellen[i].getWert() <= min+2 && verschiebeAutomatisch(zellen[i]))
            {
                zellen[i] = null;
                return true;
            }
        }
        for (Stack<Karte> s : kaskaden)
        {
            if (!s.isEmpty() && s.top().getWert() <= min+2 && verschiebeAutomatisch(s.top()))
            {
                s.pop();
                return true;
            }
        }
        return false;        
    }

    private boolean verschiebeAutomatisch(Karte k)
    {
        if (k.getWert() == 1)
        {
            for (Stack<Karte> s: ablagestapel)
            {
                if (s.isEmpty())
                {
                    s.push(k);
                    return true;
                }
            }
        }
        else
        {
            for (Stack<Karte> s: ablagestapel)
            {
                if (!s.isEmpty() && s.top().getFarbe() == k.getFarbe() && s.top().getWert()+1 == k.getWert())
                {
                    s.push(k);
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Führt einen schnellen Zug aus, wenn dies möglich ist.
     * @param bereich Kann ZELLEN oder KASKADEN sein. Gibt an, <b>aus</b> welchem Bereich die Karte verschoben wird.
     * @param index Kann 0 bis 7 (für Kaskaden) bzw. 0 bis 3 (für Zellen) sein. Gibt an, <b>von</b> welchem Feld des Bereichs die Karte verschoben wird.
     * @return true, wenn ein schneller Zug möglich war; false sonst
     */
    public boolean schnellerZug(int bereich, int index)
    {
        //# TODO: (optional) Lassen Sie einen "schnellen Zug" ausführen. "Schnelle Züge" werden durch einen Doppelklick ausgeführt.
        //#       Ein schneller Zug verschiebt die Karte 
        //#       - von einer Kaskade auf eine freie Zelle oder
        //#       - von einer Zelle auf einen passenden Ablagestapel
        //#       bereich kann dabei ZELLEN oder KASKADEN sein, index ist ein jeweils gültiger Index (0 bis 3 für Zellen, 0 bis 7 für Kaskaden)
        //#       Geben Sie true zurück, wenn ein schneller Zug möglich war, sonst false.
        if (bereich == ZELLEN && zellen[index] != null)
        {
            Karte k = zellen[index];
            zellen[index] = null;
            return verschiebeAutomatisch(k);
        }
        if (bereich == KASKADEN && !kaskaden[index].isEmpty())
        {
            for (int i = 0; i < zellen.length; i++)
            {
                if (zellen[i] == null)
                {
                    zellen[i] = kaskaden[index].pop();
                    return true;
                }
            }
        }
        return false;
    }

}
