package com.brackeen.javagamebook.input;

import java.awt.*;
import java.awt.event.*;
import java.util.List;
import java.util.ArrayList;
import javax.swing.SwingUtilities;

/**
    InputManager zarządza wejściem z klawiatury i zdarzeń myszki.
    Zdarzenia są kojarzone z obiektami GameAction.
*/
public class InputManager implements KeyListener, MouseListener,
    MouseMotionListener, MouseWheelListener
{
    /**
        Niewidoczny kursor.
    */
    public static final Cursor INVISIBLE_CURSOR =
        Toolkit.getDefaultToolkit().createCustomCursor(
            Toolkit.getDefaultToolkit().getImage(""),
            new Point(0,0),
            "invisible");

    // kody myszy
    public static final int MOUSE_MOVE_LEFT = 0;
    public static final int MOUSE_MOVE_RIGHT = 1;
    public static final int MOUSE_MOVE_UP = 2;
    public static final int MOUSE_MOVE_DOWN = 3;
    public static final int MOUSE_WHEEL_UP = 4;
    public static final int MOUSE_WHEEL_DOWN = 5;
    public static final int MOUSE_BUTTON_1 = 6;
    public static final int MOUSE_BUTTON_2 = 7;
    public static final int MOUSE_BUTTON_3 = 8;

    private static final int NUM_MOUSE_CODES = 9;

    // kody klawiszy są zdefiniowane w pakiecie java.awt.KeyEvent.
    // większość kodów (poza niektórymi rzadko wykorzystywanych, 
    // na przykład "alt graph") są mniejsze niż 600;
    private static final int NUM_KEY_CODES = 600;

    private GameAction[] keyActions =
        new GameAction[NUM_KEY_CODES];
    private GameAction[] mouseActions =
        new GameAction[NUM_MOUSE_CODES];

    private Point mouseLocation;
    private Point centerLocation;
    private Component comp;
    private Robot robot;
    private boolean isRecentering;

    /**
        Tworzy nowy obiekt InputManager odbierający dane wejściowe
        dla określonego komponentu.
    */
    public InputManager(Component comp) {
        this.comp = comp;
        mouseLocation = new Point();
        centerLocation = new Point();

        // rejestracja klawiszy i nasłuchów dla myszy
        comp.addKeyListener(this);
        comp.addMouseListener(this);
        comp.addMouseMotionListener(this);
        comp.addMouseWheelListener(this);

        // umożliwia przechwytywanie klawisza TAB oraz pozostałych
        // i innych klawiszy wykorzystywanych normalnie do zmiany fokusu.
        comp.setFocusTraversalKeysEnabled(false);
    }

    /**
        Ustawia kursor w bieżącym komponencie wejściywym obiektu InputManager.
    */
    public void setCursor(Cursor cursor) {
        comp.setCursor(cursor);
    }

    /**
        Ustawia tryb względny myszy. W trybie względnym
        kursor mysz jest "zablokowany" w środku ekranu i jedynie mierzone są
        jej przesunięcia. w trybie normalnym kursor myszy może poruszać się
        swobodnie po ekranie.
    */
    public void setRelativeMouseMode(boolean mode) {
        if (mode == isRelativeMouseMode()) {
            return;
        }

        if (mode) {
            try {
                robot = new Robot();
                mouseLocation.x = comp.getWidth() / 2;
                mouseLocation.y = comp.getHeight() / 2;
                recenterMouse();
            }
            catch (AWTException ex) {
                // nie można utworzyć robota!
                robot = null;
            }
        }
        else {
            robot = null;
        }
    }

    /**
        Sprawdza, czy mysz działa w trybie względnym.
    */
    public boolean isRelativeMouseMode() {
        return (robot != null);
    }

    /**
        Kojarzy obiekt GameAction z określonym klawiszem. Kody
        klawiszy są zdefiniowane w java.awt.KeyEvent. Jeżeli klawisz
        ma już skojarzony GameAction, nowy obiekt nadpisuje go.
    */
    public void mapToKey(GameAction gameAction, int keyCode) {
        keyActions[keyCode] = gameAction;
    }

    /**
        Kojarzy obiekt GameAction z określonym zdarzeniem myszy. Kody zdarzeń
        są zdefiniowane w tutaj w InputManager (MOUSE_MOVE_LEFT,
        MOUSE_BUTTON_1, itd.). Jeżeli zdarzenie myszy ma już skojarzony obiekt
        GameAction, nowy obiekt go nadpisuje.
    */
    public void mapToMouse(GameAction gameAction,
        int mouseCode)
    {
        mouseActions[mouseCode] = gameAction;
    }

    /**
        Usuwa z bieżącego obiektu GameAction wszystkie skojarzone klawisze i 
        zdarzenia myszy.
    */
    public void clearMap(GameAction gameAction) {
        for (int i=0; i<keyActions.length; i++) {
            if (keyActions[i] == gameAction) {
                keyActions[i] = null;
            }
        }

        for (int i=0; i<mouseActions.length; i++) {
            if (mouseActions[i] == gameAction) {
                mouseActions[i] = null;
            }
        }

        gameAction.reset();
    }

    /**
        Zwraca listę nazw klawiszy i zdarzeń myszy skojarzonych z bieżącym
        obiektem GameAction. Każda pozycja w List jest typu String.
    */
    public List getMaps(GameAction gameCode) {
        ArrayList list = new ArrayList();

        for (int i=0; i<keyActions.length; i++) {
            if (keyActions[i] == gameCode) {
                list.add(getKeyName(i));
            }
        }

        for (int i=0; i<mouseActions.length; i++) {
            if (mouseActions[i] == gameCode) {
                list.add(getMouseName(i));
            }
        }
        return list;
    }

    /**
        Kasuje stan wszystkich obiektów GameAction, dzięki czemu
        działają jakby nic nie było naciśnięte.
    */
    public void resetAllGameActions() {
        for (int i=0; i<keyActions.length; i++) {
            if (keyActions[i] != null) {
                keyActions[i].reset();
            }
        }

        for (int i=0; i<mouseActions.length; i++) {
            if (mouseActions[i] != null) {
                mouseActions[i].reset();
            }
        }
    }

    /**
        Pobranie nazwy kodu zdarzenia klawisza.
    */
    public static String getKeyName(int keyCode) {
        return KeyEvent.getKeyText(keyCode);
    }

    /**
        Pobranie nazwy kodu zdarzenia myszy.
    */
    public static String getMouseName(int mouseCode) {
        switch (mouseCode) {
            case MOUSE_MOVE_LEFT: return "Mysz lewo";
            case MOUSE_MOVE_RIGHT: return "Mysz prawo";
            case MOUSE_MOVE_UP: return "Mysz góra";
            case MOUSE_MOVE_DOWN: return "Mysz dół";
            case MOUSE_WHEEL_UP: return "Kółko myszy góra";
            case MOUSE_WHEEL_DOWN: return "Kółko myszy dół";
            case MOUSE_BUTTON_1: return "Przycisk myszy 1";
            case MOUSE_BUTTON_2: return "Przycisk myszy 2";
            case MOUSE_BUTTON_3: return "Przycisk myszy 3";
            default: return "Nieznany kod zdarzenia myszy " + mouseCode;
        }
    }

    /**
        Zwraca współrzędną x kursora myszy.
    */
    public int getMouseX() {
        return mouseLocation.x;
    }


    /**
        Zwraca współrzędną y kursora myszy.
    */
    public int getMouseY() {
        return mouseLocation.y;
    }


    /**
        Korzysta z klasy Robot do ustawienia kursora myszy na środku ekranu.
        <p>Należy pamiętać, że ta funkcja klasy Robot może nie być dostępna
        na wszystkich platformach.
    */
    private synchronized void recenterMouse() {
        if (robot != null && comp.isShowing()) {
            centerLocation.x = comp.getWidth() / 2;
            centerLocation.y = comp.getHeight() / 2;
            SwingUtilities.convertPointToScreen(centerLocation,
                comp);
            isRecentering = true;
            robot.mouseMove(centerLocation.x, centerLocation.y);
        }
    }

    private GameAction getKeyAction(KeyEvent e) {
        int keyCode = e.getKeyCode();
        if (keyCode < keyActions.length) {
            return keyActions[keyCode];
        }
        else {
            return null;
        }
    }

    /**
        Zwraca kod zdarzenia myszy dla przycisku skojarzonego z bieżącym
        obiektem MouseEvent.
    */
    public static int getMouseButtonCode(MouseEvent e) {
         switch (e.getButton()) {
            case MouseEvent.BUTTON1:
                return MOUSE_BUTTON_1;
            case MouseEvent.BUTTON2:
                return MOUSE_BUTTON_2;
            case MouseEvent.BUTTON3:
                return MOUSE_BUTTON_3;
            default:
                return -1;
        }
    }

    private GameAction getMouseButtonAction(MouseEvent e) {
        int mouseCode = getMouseButtonCode(e);
        if (mouseCode != -1) {
             return mouseActions[mouseCode];
        }
        else {
             return null;
        }
    }

    // z interfejsu KeyListener
    public void keyPressed(KeyEvent e) {
        GameAction gameAction = getKeyAction(e);
        if (gameAction != null) {
            gameAction.press();
        }
        // upewnij się, że zdarzenie nie będzie dalej obsługiwane
        e.consume();
    }


    // z interfejsu KeyListener
    public void keyReleased(KeyEvent e) {
        GameAction gameAction = getKeyAction(e);
        if (gameAction != null) {
            gameAction.release();
        }
        // upewnij się, że zdarzenie nie będzie dalej obsługiwane
        e.consume();
    }

    // z interfejsu KeyListener
    public void keyTyped(KeyEvent e) {
        // upewnij się, że zdarzenie nie będzie dalej obsługiwane
        e.consume();
    }

    // z interfejsu MouseListener
    public void mousePressed(MouseEvent e) {
        GameAction gameAction = getMouseButtonAction(e);
        if (gameAction != null) {
            gameAction.press();
        }
    }

    // z interfejsu MouseListener
    public void mouseReleased(MouseEvent e) {
        GameAction gameAction = getMouseButtonAction(e);
        if (gameAction != null) {
            gameAction.release();
        }
    }

    // z interfejsu MouseListener
    public void mouseClicked(MouseEvent e) {
        // nic nie rób
    }

    // z interfejsu MouseListener
    public void mouseEntered(MouseEvent e) {
        mouseMoved(e);
    }

    // z interfejsu MouseListener
    public void mouseExited(MouseEvent e) {
        mouseMoved(e);
    }

    // z interfejsu MouseMotionListener
    public void mouseDragged(MouseEvent e) {
        mouseMoved(e);
    }

    // z interfejsu MouseMotionListener
    public synchronized void mouseMoved(MouseEvent e) {
        // to zdarzenie jest generowane przez centrowanie myszy - zignoruj
        if (isRecentering &&
            centerLocation.x == e.getX() &&
            centerLocation.y == e.getY())
        {
            isRecentering = false;
        }
        else {
            int dx = e.getX() - mouseLocation.x;
            int dy = e.getY() - mouseLocation.y;
            mouseHelper(MOUSE_MOVE_LEFT, MOUSE_MOVE_RIGHT, dx);
            mouseHelper(MOUSE_MOVE_UP, MOUSE_MOVE_DOWN, dy);

            if (isRelativeMouseMode()) {
                recenterMouse();
            }
        }

        mouseLocation.x = e.getX();
        mouseLocation.y = e.getY();

    }

    // z interfejsu MouseWheelListener
    public void mouseWheelMoved(MouseWheelEvent e) {
        mouseHelper(MOUSE_WHEEL_UP, MOUSE_WHEEL_DOWN,
            e.getWheelRotation());
    }

    private void mouseHelper(int codeNeg, int codePos,
        int amount)
    {
        GameAction gameAction;
        if (amount < 0) {
            gameAction = mouseActions[codeNeg];
        }
        else {
            gameAction = mouseActions[codePos];
        }
        if (gameAction != null) {
            gameAction.press(Math.abs(amount));
            gameAction.release();
        }
    }
}

