/**
 * Implementiert einen tabellengetriebenen endlichen Automaten. 
 * Instanzen dieser Klasse sind DEAs, die Wörter über einem 
 * vorgegebenen Alphabet akzeptieren oder ablehnen können.
 * 
 * Die Aufgabe wurde vom schriftlichen Abitur 2020 in 
 * Baden-Württemberg inspiriert.
 * 
 * @author Urs Lautebach
 * @version 2020-Oktober
 */
/* Lehrertipps:
 * 
 * 1. 
 * Die Implementierung setzt etwas Erfahrung mit mehrdimensionalen 
 * Arrays sowie die Handhabung von String und char voraus.  
 * 
 * 2.
 * Die Schüler sollten vor der Implementierung dieser Klasse 
 * schon Transitionstabellen von Hand erstellt (in der 
 * Regel aus einem Übergangsgraphen) und idealerweise auch 
 * schon "Läufe" der so beschriebenen DEAs von Hand simuliert
 * haben. Wenn sie das sogar ohne den Übergangsgraphen tun, 
 * entsteht leichter die richtige Grundvorstellung davon, warum 
 * dieser "allgemeine" DEA beliebige konkrete DEAs simulieren
 * kann. 
 */
public class EndlicherAutomat {
    private int [][] uebergaenge = null; 
    private int[] endzustaende = null;
    private char[] alphabet = null;

    /**
     * Konstruiert einen tabellengetriebenen DEA. Nach dem 
     * erfolgreichen Aufruf dieses Konstruktors kann man den DEA 
     * anschließend durch einen Aufruf der Methode
     * gehoertZuSprache(String):boolean
     * beliebig viele Wörter testen lassen.
     * 
     * @param tabelle Transitionstabelle des DEA. Die erste 
     * Dimension bezeichnet die Zustände; die zweite Dimension die 
     * Eingabezeichen. Die Einträge der Tabelle bezeichnen jeweils 
     * den Zustand, in den der Automat (abhängig vom vorherigen 
     * Zustand und der Eingabe) wechseln soll.
     * 
     * Der Zustand 0 gilt als Fehlerzustand. Zustand 1 ist immer 
     * Startzustand (so dass der Startzustand, anders als bei der 
     * formalen Beschreibung eines DEA, nicht explizit angegeben 
     * werden muss). 
     * 
     * @param finalStates Ein int[], das die Endzustände enthält.
     * 
     * @param alphabet ein Array von char, das das Alphabet des 
     * Automaten enthält. Die Reihenfolge der Zeichen im Alphabet 
     * muss der Reihenfolge der Zeichen an der entsprechenden 
     * Achse der Transitionstabelle entsprechen.
     */
    EndlicherAutomat(final char[] alphabet, 
            final int[][] uebergaenge, final int[] endzustaende ) {
        this.alphabet = alphabet;
        this.uebergaenge = uebergaenge;
        this.endzustaende = endzustaende;
        // Die Methode plausibilitaetPruefen ist fertig vorgegeben; 
        // sie fängt einige offensichtliche Fehler ab:
        plausibilitaetPruefen();
    }

    /**
     * Die Methode prüft, ob die Attribute transitionstabelle, 
     * alphabet und endzustaende zueinander passen. Sie ist fertig 
     * implementiert und dafür gedacht, am Ende des Konstruktors 
     * aufgerufen zu werden.
     */
    private void plausibilitaetPruefen() {
        if (uebergaenge == null) {
            throw new IllegalArgumentException(
                    "Die Übergangstabelle ist null oder leer!");
        }
        if (alphabet == null) {
            throw new IllegalArgumentException(
                    "Das Alphabet ist null oder leer!");
        }
        if (endzustaende == null) {
            throw new IllegalArgumentException(
                    "endzustaende ist null oder leer!");
        }
        int tabelleHoehe = this.uebergaenge.length;
        int tabelleBreite = uebergaenge[0].length;
        // Hat die Übergangstabelle eine Zeile pro Alphabetzeichen?
        if (tabelleBreite != alphabet.length) {
            throw new IllegalArgumentException("Das Alphabet hat " + 
                    alphabet.length + " Zeichen, die " + 
                    " Übergangstabelle aber " + tabelleBreite + 
                    " Spalten!");
        }
        // Sind alle Zeilen der Transitionstabelle gleich lang?
        for (int[] zeile : uebergaenge) {
            if (zeile.length != tabelleBreite) {
                throw new IllegalArgumentException(
                        "Übergangstabelle ist nicht rechteckig!");
            }
        }
        // Hat einer der Endzustände eine unplausible Nummer?
        for (int zustand : endzustaende) {
            if (zustand < 1 || zustand > tabelleHoehe - 1) {
                throw new IllegalArgumentException(
                        "Der Endzustand " + zustand + 
                        " kommt in der Übergangstabelle nicht vor!");
            }
        }
    }

    /**
     * Ermittelt die Nummer (den Index) des angegebenen Zeichens 
     * innerhalb des Alphabetes dieses Automaten.
     * 
     * @param input Zeichen, dessen Position ermittelt werden soll.
     * @returns Position des Zeichens input innerhalb des Alphabets
     * dieses Automaten; oder -1, falls es im Alphabet nicht 
     * enthalten ist.
     */
    public int indexImAlphabet(char input) {
        /* Man geht das Alphabet durch und prüft, welches seiner 
         * Zeichen dem "input" entspricht. Falls man es findet, 
         * gibt man dessen Index zurück.
         */
        for (int index = 0; index < alphabet.length; index++) {
            if (input == alphabet[index])
                return index;
        }
        return -1;
    }

    /**
     * Überprüft, ob der Zustand mit der übergebenen 
     * Zustandsnummer einer der Endzustände dieses Automaten ist.
     * 
     * @param nr Nummer des fraglichen Zustandes.
     * @return ob der Zustand ein Endzustand ist.
     */
    public boolean istEndzustand(int nr) {
        for (int endzustand : endzustaende) {
            // Gehe durch alle Endzustände. Wenn nr einer ist...
            if (nr == endzustand) {
                return true;
            }
        }
        return false;
    }

    /**
     * Die Methode kann mit einem String als Parameter aufgerufen 
     * werden, was oft bequemer ist als ein char[].
     * 
     * @param s String mit dem zu prüfenden Wort.
     * @return ob s von diesem Automaten erkannt wird.
     */
    public boolean gehoertZuSprache(String s) {
        // Hier ist nichts zu implementieren.
        return gehoertZuSprache(s.toCharArray());
    }

    /**
     * Übergibt dem DEA ein ganzes Wort und ermittelt, ob er es 
     * akzeptiert oder nicht.
     * 
     * @param eingabe Die komplette zu analysierende Eingabe als 
     * char[].
     * @returns true, wenn das Wort vom DEA akzeptiert wird; 
     * false sonst.
     */
    public boolean gehoertZuSprache(final char[] wort) {
        /*
         * Um zu prüfen, ob wort akzeptiert wird, muss der DEA... 
         * 
         * - in Zustand 1 beginnen;
         * 
         * - das Eingabewort Zeichen für Zeichen durchgehen;
         * 
         * - prüfen, ob das Zeichen im Alphabet vorkommt;
         * 
         * - nach dessen Position im Alphabet und altem Zustand  
         * einen neuen Zustand aus der Tabelle uebergaenge wählen;
         * 
         * - false antworten, wenn er im Fehlerzustand landet, 
         * 
         * - am Schluss der Eingabe entscheiden, ob der erreichte 
         * Zustand ein Endzustand ist.
         */
        // Startzustand ist immer Nr. 1:
        int zustand = 1;
        // Gehe durch alle Zeichen durch:
        for (char zeichen : wort) {
            int alphIndex = indexImAlphabet(zeichen);
            // Falls das Zeichen gar nicht im Alphabet ist...:
            if (alphIndex == -1) {
                return false;
            } else {
                // wechsle in neuen Zustand:
                zustand = uebergaenge[zustand][alphIndex];
            }
            // Bin ich im Fehlerzustand?
            if (zustand == 0) {
                return false;
            }
        }
        // Die Eingabe ist verbraucht. Falls der aktuelle Zustand 
        // ein Endzustand ist, akzeptieren wir die Eingabe:
        return istEndzustand(zustand);
    }
}

