package shapes77.geom;
// Příliš žluťoučký kůň úpěl ďábelské ó - PŘÍLIŠ ŽLUŤOUČKÝ KŮŇ ÚPĚL ĎÁBELSKÉ Ó

import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;



/********************************************************************************
 * Třída {@code Direction8} slouží jako výčtový typ pro 8 hlavních a vedlejších
 * světových stran spolu se směrem {@link #NOWHERE} pro situace,
 * kdy není možno určit směr.
 */
public enum Direction8
{
//\CE== VALUES OF THE ENUMERATION TYPE =========================================

    /** Východ       = doprava.        */    EAST      ("E" ,  1,  0),
    /** Severovýchod = doprava nahoru. */    NORTH_EAST("NE",  1, -1),
    /** Sever        = nahoru.         */    NORTH     ("N" ,  0, -1),
    /** Severozápad  = doleva nahoru.  */    NORTH_WEST("NW", -1, -1),
    /** Západ        = doleva.         */    WEST      ("W" , -1,  0),
    /** Jihozápad    = doleva nahoru.  */    SOUTH_WEST("SW", -1,  1),
    /** Jih          = dolu.           */    SOUTH     ("S" ,  0,  1),
    /** Jihovýchod   = doprava dolu.   */    SOUTH_EAST("SE",  1,  1),
    /** Žádný        = nikam.          */    NOWHERE   ("0" ,  0,  0),
    ;



//##############################################################################
//\CC== REMAINING CLASS CONSTANTS (CONSTANT CLASS/STATIC ATTRIBUTES/FIELDS) ====

    /** Celkový počet definovaných směrů. */
    public static final int NUM_DIRS = values().length;

    /** Maska pro dělení modulo
     *  Výpočet masky předpokládá, že počet směrů je mocninou dvou
     *  plus jedna pro směr NOWHERE). */
    private static final int MASK = NUM_DIRS-2;

    /** Odmocnina z jedné poloviny pro výpočet šikmých vzdáleností. */
    private static final double SQR = Math.sqrt(0.5);

    /** Mapa konvertující názvy směrů (s diakritikou i bez ní) a jejich zkratky
     *  na příslušné směry. */
    private static final Map<String, Direction8> name2direction =
                                                 new HashMap<>(NUM_DIRS*3);

    /** Neměnný seznam hlavních světových stran. */
    private static final List<Direction8> cardinals =
            Collections.unmodifiableList(Arrays.asList(
                    new Direction8[]{EAST, NORTH, WEST, SOUTH}));


//\CV== CLASS VARIABLES (VARIABLE CLASS/STATIC ATTRIBUTES/FIELDS) ==============

    /** Příznak povolení operací se směrem {@link #NOWHERE}.
     *  Implicitně jsou zakázány. */
    private static boolean nowhereProhibited = false;



//##############################################################################
//\CI== CLASS (STATIC) INITIALIZER (CLASS CONSTRUCTOR) =========================

    //Inicializace statických atributů je nerealizovatelná před
    //definicí jednotlivých hodnot ==> je ji proto potřeba realizovat dodatečně
    static
    {
        if (NUM_DIRS != 9) {
            throw new RuntimeException(
                "\nNabouraný zdrojový kód - špatný počet směrů");
        }
        for (Direction8 dir : values())  {
            name2direction.put(dir.name(),   dir);
            name2direction.put(dir.shortcut, dir);
        }
    }



//\CF== CLASS (STATIC) FACTORY METHODS =========================================

    /***************************************************************************
     * Vrátí směr se zadaným názvem nebo zkratkou.
     * Bohužel není možno pojmenovat tuto metodu {@code valueOf()},
     * protože takto nazvanou metodu definuje překladač v této třídě
     * takže ji není možno přebít vlastní verzí.
     *
     * @param name Název požadovaného směru nebo jeho zkratka;
     *              při zadávání nezáleží na velikosti písmen
     * @return Požadovaný směr
     * @throws IllegalArgumentException  Neexistuje-li směr se zadaným
     *                                   názvem nebo zkratkou
     */
    public static Direction8 get(String name)
    {
        Direction8 dir = name2direction.get(name.toUpperCase());
        if (dir == null) {
            throw new IllegalArgumentException(
                  "\nNeznámý směr s daným názvem či zkratkou: " + name);
        }
        return dir;
    }


    /***************************************************************************
     * Vrátí směr se zadaným indexem.
     * Směry mají indexy přiřazené postupně podle výskytu směru
     * při otáčení proti směru hodinových ručiček.
     * {@link #EAST} má index 0, {@link #NORTH_EAST} 1 a tak dále
     * až po {@link #SOUTH_EAST} s indexem 7.
     * Za index je však možno dosadit jakékoliv celé číslo,
     * které se přepočte jako počet otoček o 45°.
     * Směr {@link #NOWHERE} je při tomto přepočtu přeskakován.
     *
     * @param index Index požadovaného směru
     * @return Požadovaný směr
     */
    public static Direction8 get(int index)
    {
        if (index < 0) {
            index = 0x7FFFFFF8 + index;
        }
        index = index & 7;  //index % 8;
        Direction8 dir = values()[index];
        return dir;
    }



//\CG== CLASS (STATIC) GETTERS AND SETTERS =====================================
//\CM== CLASS (STATIC) REMAINING NON-PRIVATE METHODS ===========================

    /***************************************************************************
     * Vrátí neměnný seznam se čtyřmi hlavními světovými stranami.
     *
     * @return  Požadovaný seznam
     */
    public static List<Direction8> cardinals()
    {
        return cardinals;
    }


    /***************************************************************************
     * Vrátí vektor se čtyřmi hlavními světovými stranami.
     *
     * @return  Požadovaný vektor
     */
    public static Direction8[] values4()
    {
        return new Direction8[] { EAST, NORTH, WEST, SOUTH };
    }


    /***************************************************************************
     * Nastaví, zda budou povoleny operace se směrem {@link #NOWHERE}.
     * Nejsou-li operace povoleny, vyhazují metody při použití tohoto směru
     * výjimku {@link java.lang.IllegalStateException}.
     * Jsou-li operace povoleny, pak objekt natočený do směru {@link #NOWHERE}
     * zůstává v tomto "směru" po jakémkoliv otočení
     * a při jakémkoliv přesunu zůstává na místě.
     *
     * @param prohibit {@code true} mají-li se operace zakázat,
     *                 {@code false} mají-li se povolit
     * @return Původní nastavení tohoto příznaku
     */
    public static boolean prohibitNowhere(boolean prohibit) {
        boolean original = nowhereProhibited;
        nowhereProhibited = prohibit;
        return original;
    }



//\CP== CLASS (STATIC) PRIVATE AND AUXILIARY METHODS ===========================



//##############################################################################
//\IC== INSTANCE CONSTANTS (CONSTANT INSTANCE ATTRIBUTES/FIELDS) ===============

    /** Velikost změny příslušné složky souřadnic po přesunu
     *  na sousední políčko v daném směru. */
    private final int dx, dy;

    /** Jedno- či dvoupísmenná zkratka úplného názvu daného směru. */
    private final String shortcut;



//\IV== INSTANCE VARIABLES (VARIABLE INSTANCE ATTRIBUTES/FIELDS) ===============



//##############################################################################
//\II== INSTANCE INITIALIZERS (CONSTRUCTORS) ===================================

    /***************************************************************************
     * Vytvoří nový směr a zapamatuje si zkratku jeho názvu
     * spolu se změnami souřadnic při pohybu v daném směru.
     *
     * @param shortcut  Jedno- či dvoj-písmenná zkratka označující daný směr
     * @param dx        Změna vodorovné souřadnice
     *                  při přesunu na sousední políčko v daném směru
     * @param dy        Změna svislé souřadnice
     *                  při přesunu na sousední políčko v daném směru
     */
    private Direction8(String shortcut, int dx, int dy)
    {
        this.shortcut = shortcut;
        this.dx = dx;
        this.dy = dy;
    }



//\IA== INSTANCE ABSTRACT METHODS ==============================================
//\IG== INSTANCE GETTERS AND SETTERS ===========================================

    /***************************************************************************
     * Vrátí zkratku názvu daného směru.
     *
     * @return  Požadovaná zkratka
     */
    public String getShortcut()
    {
        return shortcut;
    }


    /***************************************************************************
     * Vrátí informaci o tom, je-li daný směr jedním ze 4 hlavních směrů,
     * tj. jedná-li se o jeden ze směrů {@link #EAST}, {@link #NORTH},
     * {@link #WEST}, {@link #SOUTH}.
     *
     * @return Jedná-li se o hlavní směr, vrátí {@code true},
     *         jinak vrátí {@code false}
     */
    public
    boolean isCardinal()
    {
        return ((this.ordinal() & 1) == 0)  &&  (this != NOWHERE);
    }



//\IM== INSTANCE REMAINING NON-PRIVATE METHODS =================================

    /***************************************************************************
     * Vrátí index daného hlavního směru v rámci hlavních směrů.
     *
     * @return  Požadovaný index
     */
    public int ordinal4()
    {
        verifyCardinal();
        return ordinal() / 2;
    }


    /***************************************************************************
     * Vrátí pozici sousedního políčka v daném směru.
     *
     * @param position      Pozice stávajícího políčka
     * @return Pozice sousedního políčka v daném směru
     */
    public Position nextPosition(Position position)
    {
        verifyAllowed();
        return new Position(position.x() + dx,  position.y() + dy);
    }


    /***************************************************************************
     * Vrátí pozici políčka vzdáleného v daném směru o zadanou vzdálenost.
     *
     * @param position  Pozice stávajícího políčka
     * @param distance  Vzdálenost hledané pozice
     * @return Pozice políčka vzdáleného v daném směru o zadanou vzdálenost
     */
    public Position nextPosition(Position position, int distance)
    {
        verifyAllowed();
        if ((dx != 0)  &&  (dy != 0)) {
            int increment = (int)(SQR*distance + 0.5);
            return new Position(position.x() + increment,
                                position.y() + increment);
        } else {
            return new Position(position.x() + dx*distance,
                                position.y() + dy*distance);
        }
    }


    /***************************************************************************
     * Obdrží x-vou souřadnici políčka a vrátí x-vou souřadnici
     * sousedního políčka v daném směru.
     *
     * @param x Obdržená x-ová souřadnice
     * @return x-ová souřadnice políčka po přesunu o jedno pole v daném směru
     */
    public int nextX(int x)
    {
        verifyAllowed();
        return x + dx;
    }


    /***************************************************************************
     * Obdrží x-vou souřadnici políčka a vrátí x-vou souřadnici políčka
     * vzdáleného v daném směru o zadanou vzdálenost.
     *
     * @param x        Obdržená x-ová souřadnice
     * @param distance Vzdálenost políčka v daném směru
     * @return x-ová souřadnice vzdáleného políčka
     */
    public double nextX(int x, int distance)
    {
        verifyAllowed();
        if ((dx != 0)  &&  (dy != 0)) {
            return x + SQR*dx*distance;
        } else {
            return x + dx*distance;
        }
    }


    /***************************************************************************
     * Obdrží y-vou souřadnici políčka a vrátí y-vou souřadnici
     * sousedního políčka v daném směru.
     *
     * @param y Obdržená y-ová souřadnice
     * @return y-ová souřadnice sousedního políčka v daném směru
     */
    public int nextY(int y)
    {
        verifyAllowed();
        return y + dy;
    }


    /***************************************************************************
     * Obdrží x-vou souřadnici políčka a vrátí x-vou souřadnici políčka
     * vzdáleného v daném směru o zadanou vzdálenost.
     *
     * @param y        Obdržená y-ová souřadnice
     * @param distance Vzdálenost políčka v daném směru
     * @return y-ová souřadnice vzdáleného políčka
     */
    public double nextY(int y, int distance)
    {
        verifyAllowed();
        if ((dx != 0)  &&  (dy != 0)) {
            return y + SQR*dy*distance;
        } else {
            return y + dy*distance;
        }
    }


    /***************************************************************************
     * Vrátí změnu vodorovné souřadnice při přesunu
     * na sousední pole v daném směru.
     *
     * @return Změna x-ové souřadnice při přesunu o jedno pole v daném směru
     */
    public int dx()
    {
        verifyAllowed();
        return dx;
    }


    /***************************************************************************
     * Vrátí změnu svislé souřadnice při přesunu
     * na sousední pole v daném směru.
     *
     * @return Změna y-ové souřadnice při přesunu o jedno pole v daném směru
     */
    public int dy()
    {
        verifyAllowed();
        return dy;
    }


    /***************************************************************************
     * Vrátí směr otočený o 90° vlevo.
     *
     * @return Směr objektu po vyplnění příkazu vlevo v bok
     */
    public Direction8 leftTurn()
    {
        return turnBy(2);
    }


    /***************************************************************************
     * Vrátí směr otočený o 90° vpravo.
     *
     * @return Směr objektu po vyplnění příkazu vpravo v bok
     */
    public Direction8 rightTurn()
    {
        return turnBy(-2);
    }


    /***************************************************************************
     * Vrátí směr otočený o 180°.
     *
     * @return Směr objektu po vyplnění příkazu čelem vzad.
     */
    public Direction8 aboutTurn()
    {
        return turnBy(4);
    }


    /***************************************************************************
     * Vrátí směr otočený o 45° vlevo.
     *
     * @return Směr objektu po vyplnění příkazu nalevo vpříč.
     */
    public Direction8 halfLeft()
    {
        return turnBy(1);
    }


    /***************************************************************************
     * Vrátí směr otočený o 45° vpravo.
     *
     * @return Směr objektu po vyplnění příkazu napravo vpříč.
     */
    public Direction8 halfRight()
    {
        return turnBy(-1);
    }


    /***************************************************************************
     * Vrátí rozdíl pořadových čísel zadaných směrů, který po přičtení
     * k pořadovému číslu daného směru dá pořadové číslo zadaného parametru.
     *
     * @param direction Směr, vůči němuž číslo daného směru přepočítáváme
     * @return Rozdíl pořadových čísel zadaných směrů
     */
    public int ordinalDistanceTo(Direction8 direction)
    {
        verifyAllowed();
        int distance = ((this == NOWHERE)  ||  (direction == NOWHERE))
                     ? 0
                     : (direction.ordinal() - this.ordinal());
        return distance;
    }


    /***************************************************************************
     * Přepočítá rozměry zadané oblasti orientované na východ
     * a umístěné v referenční oblasti
     * na nové rozměry po otočení do tohoto směru.
     *
     * @param ref   Referenční oblast, v níž je otáčená oblast umístěna
     *              a vůči níž jsou udávány výchozí souřadnice.
     *              V této otočené oblasti má být oblast umístěna po otočení.
     *              Přitom se předpokládá, že souřadnice referenční oblasti
     *              se otočením nezmění.
     *              V tomto ohledu je ideální referenční oblast čtvercová.
     * @param inner Oblast, jejíž rozměry přepočítáváme a jejíž souřadnice
     *              jsou RELATIVNÍ vůči referenční oblasti
     * @return Oblast s novými parametry reprezentujícími původní oblast
     *         po otočení do zadaného směru;
     *         její souřadnice jsou udávány jako absolutní
     */
    public Area turnInArea(Area ref, Area inner)
    {
        return turnInArea(ref, inner, EAST);
    }


    /***************************************************************************
     * Přepočítá relativní pozici a absolutní rozměry zadané oblasti
     * a umístěné v referenční čtvercové oblasti orientované zadaným směrem
     * na absolutní rozměry po otočení referenční oblasti do tohoto směru.
     *
     * @param ref   Referenční oblast, v níž je otáčená oblast umístěna
     *              a vůči níž jsou udávány výchozí relativní souřadnice.
     *              V této otočené oblasti má být oblast umístěna po otočení.
     *              Přitom se předpokládá, že souřadnice referenční oblasti
     *              se otočením nezmění.
     * @param inner Oblast, jejíž rozměry přepočítáváme a jejíž souřadnice
     *              jsou RELATIVNÍ vůči referenční oblasti
     * @param fromDirection Směr, do nějž je objekt natočen nyní
     * @return Oblast s novými parametry reprezentujícími původní oblast
     *         po otočení referenční oblasti do zadaného směru;
     *         nové souřadnice jsou udávány jako absolutní
     */
    public Area turnInArea(Area ref, Area inner, Direction8 fromDirection)
    {
        verifyAllowed();
        if (ref.width() != ref.height()) {
            throw new IllegalArgumentException(
                    "\nReferenční oblast musí být čtvercová: " + ref);
        }
        int x, y, w, h;
        int distance = fromDirection.ordinalDistanceTo(this);

        switch(distance) //Přepočet závisí na cílovém směru
        {
            case 0:
                x = ref.x()  +  inner.x();
                y = ref.y()  +  inner.y();
                w = inner.width();
                h = inner.height();
                return new Area(x, y, w, h);

            case -6:
            case +2:
                x = ref.x()  +  inner.y();
                y = ref.y()  -  inner.x()  +  ref.width()  -  inner.width();
                w = inner.height();
                h = inner.width();
                return new Area(x, y, w, h);

            case -4:
            case +4:
                x = ref.x()  -  inner.x()  +  ref.width()   -  inner.width();
                y = ref.y()  -  inner.y()  +  ref.height()  -  inner.height();
                w = inner.width();
                h = inner.height();
                return new Area(x, y, w, h);

            case -2:
            case +6:
                x = ref.x()  -  inner.y()  +  ref.height()  -  inner.height();
                y = ref.y()  +  inner.x();
                w = inner.height();
                h = inner.width();
                return new Area(x, y, w, h);

            default:
                throw new RuntimeException(
                        "\nNení možné otočit oblast ze směru " +
                        fromDirection + " do směru " + this);
        }
    }


    /***************************************************************************
     * Přepočítá relativní pozici a absolutní rozměry zadané oblasti
     * a umístěné v referenční čtvercové oblasti orientované na východ
     * na absolutní rozměry po otočení referenční oblasti do tohoto směru.
     *
     * @param toDirection Směr, do nějž se má objekt natočit
     * @param ref   Referenční oblast, v níž je otáčená oblast umístěna
     *              a vůči níž jsou udávány výchozí relativní souřadnice.
     *              V této otočené oblasti má být oblast umístěna po otočení.
     *              Přitom se předpokládá, že souřadnice referenční oblasti
     *              se otočením nezmění.
     * @param inner Oblast, jejíž rozměry přepočítáváme a jejíž souřadnice
     *              jsou RELATIVNÍ vůči referenční oblasti
     * @return Oblast s novými parametry reprezentujícími původní oblast
     *         po otočení referenční oblasti do zadaného směru;
     *         nové souřadnice jsou udávány jako absolutní
     */
    public Area turnInAreaTo(Direction8 toDirection, Area ref, Area inner)
    {
        return toDirection.turnInArea(ref, inner, this);
    }


    /***************************************************************************
     * Zadaný otočný objekt natočený do některého hlavního směru
     * otočí do svého směru, který musí být také hlavním směrem,
     * tj. jedním ze směrů
     * {@link #EAST}, {@link #NORTH}, {@link #WEST}, {@link #SOUTH}.
     *
     * @param turnable  Otáčený objekt
     * @throws IllegalStateException Není-li zadaný objekt otočen do některé
     *         ze čtyř hlavních světových stran nebo jej nechceme otočit
     *         do některé ze čtyř hlavních světových stran
     */
    public void turnTo4(ITurnable4 turnable)
    {
        Direction8 direction = turnable.getDirection();
        this     .verifyCardinal();
        direction.verifyCardinal();
        this     .verifyAllowed();
        direction.verifyAllowed();
        if ((this == NOWHERE)  ||  (direction == NOWHERE)) {
            //Není otočen nikam nebo se nemá otáčet nikam
            return;
        }
        int amount = (direction.ordinal() - this.ordinal()) / 2;
        switch(amount)
        {
            case -1:
            case +3:
                turnable.turnLeft();
                break;

            case 0:
                break;

            case -3:
            case +1:
                turnable.turnRight();
                break;

            default:
                turnable.turnAbout();
                break;
        }
    }


    /***************************************************************************
     * Zadaný otočný objekt natočený do některého směru
     * otočí do svého směru.
     *
     * @param turnable  Otáčený objekt
     */
    public void turnTo8(ITurnable8 turnable)
    {
        Direction8 direction = turnable.getDirection();
        verifyAllowed();
        direction.verifyAllowed();
        if ((this == NOWHERE)  ||  (direction == NOWHERE)) {
            //Není otočen nikam nebo se nemá otáčet nikam
            return;
        }
        int amount = direction.ordinal() - this.ordinal();
        switch(amount)
        {
            case -3:
            case +5:
                turnable.turnLeft();
                turnable.halfLeft();
                return;                     //==========>

            case -2:
            case +6:
                turnable.turnLeft();
                return;                     //==========>

            case -1:
            case +7:
                turnable.halfLeft();
                return;                     //==========>

            case 0:
                return;                     //==========>

            case +1:
            case -7:
                turnable.halfRight();
                return;                     //==========>

            case +2:
            case -6:
                turnable.turnRight();
                return;                     //==========>

            case +3:
            case -5:
                turnable.turnRight();
                turnable.halfRight();
                return;                     //==========>

            case +4:
            case -4:
                turnable.turnAbout();
                return;                     //==========>

            default:
                throw new RuntimeException(
                    "\nNení možno se otočit ze směru " + direction +
                    " do směru " + this );
        }
    }



//\IP== INSTANCE PRIVATE AND AUXILIARY METHODS =================================

    /***************************************************************************
     * Vrátí směr otočený o zadaný počet osminek (45°) vlevo.
     *
     * @param eighths Počet osminek, o něž se má směr otočit,
     *                přičemž záporný počet označuje otočku vpravo
     * @return Směr objektu po vyplnění příkazu
     */
    public Direction8 turnBy(int eighths)
    {
        verifyAllowed();
        return (this == NOWHERE)
               ?  NOWHERE
               :  values()[MASK & (eighths + ordinal())];
    }


    /***************************************************************************
     * Ověří, že se nejedná o operaci zakázanou pro směr {@link #NOWHERE};
     * není-li tomu tak, vyhodí výjimku {@link IllegalArgumentException}.
     *
     * @throws IllegalStateException
     *         Pro směr {@link #NOWHERE} je tato operace zakázána
     */
    private void verifyAllowed()
    {
        if (nowhereProhibited  &&  (this == NOWHERE)) {
            Throwable t = new Throwable();
            StackTraceElement[] aste = t.getStackTrace();
            StackTraceElement   ste  = aste[1];
            String method = ste.getMethodName();

            throw new IllegalStateException("\nThis operation is prohibited "
                                   + "for the direction NOWHERE: " + method);
        }
    }


    /***************************************************************************
     * Ověří, že se daný směr je jedním ze čtyř hlavních směrů;
     * není-li, vyhodí výjimku {@link IllegalArgumentException}.
     *
     * @throws IllegalArgumentException
     *         Směr není jedním ze čtyř hlavních směrů
     */
    private void verifyCardinal()
            throws IllegalArgumentException
    {
        if (! isCardinal()) {
            String method = violatingMethod();
            throw new IllegalStateException(
                  "\nMetodu " + method + " nelze použít pro směr " + this +
                  "\nLze ji použít pouze pro čtyři hlavní světové strany");
        }
    }


    /***************************************************************************
     * Vrátí název metody, která nevyhověla testovaným podmínkám.
     *
     * @return Název metody
     */
    private String violatingMethod()
    {
        Throwable           t      = new Throwable();
        StackTraceElement[] aste   = t.getStackTrace();
        StackTraceElement   ste    = aste[2];
        String              method = ste.getMethodName();
        return method;
    }



//##############################################################################
//\NT== NESTED DATA TYPES ======================================================
}
