package de.ladkau.questshock;

import android.app.Activity;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.List;
import org.libsdl.app.SDLActivity;

/**
 * The on-screen keyboard quad (see OverlayPanel for the shared off-screen-
 * render/touch/cursor plumbing this builds on) - a hand-built, full
 * US-layout key grid (there's no system IME to borrow once immersive) plus
 * a text field echoing what's been sent. Each key press is forwarded
 * straight to the game as real input (see handleKeyPress()/
 * handleCharKeyPress()) - non-printable keys as an
 * SDLActivity.onNativeKeyDown()/onNativeKeyUp() pair, the same one a
 * physical Bluetooth keyboard's presses already drive, and printable keys
 * via a synthesized SDL_TEXTINPUT event pushed from native code (see
 * commitPrintableChar()/nativeSendPrintableChar() in questshock_native.c) -
 * except while a Ctrl/Alt "armed" modifier is active, when they go through
 * the same onNativeKeyDown()/onNativeKeyUp() path instead, so the modifier
 * carries across (see toggleModifier()). Shift is a purely local layer
 * toggle (uppercase/symbols vs lowercase/numbers), never itself sent to the
 * game.
 *
 * This is a fully independent overlay quad from MenuOverlay's small
 * launcher - opened via that launcher's "Keyboard" button
 * (nativeShowKeyboard()) but otherwise self-contained: once open, the
 * controller's menu button (which only ever affects the menu launcher, see
 * xr_input.c) doesn't hide it, so it stays up as a standing input panel
 * while actually playing rather than a modal you open and close. The only
 * way to hide it is its own title bar's Close button (buildTitleBar()),
 * which also doubles as a drag handle for repositioning it (handled
 * entirely on the native side - see xr_input.c - since it never reaches
 * this class's own touch dispatch). contentAlpha() renders the whole panel
 * semi-transparent so whatever's behind it stays visible while it's up.
 */
public class KeyboardOverlay extends OverlayPanel {
    // Matches xr_session.c's KEYBOARD_WIDTH/KEYBOARD_HEIGHT swapchain size -
    // fixed, not tied to any real display metric, since this tree is never
    // shown in a real window.
    private static final int WIDTH = 1024;
    private static final int HEIGHT = 768;

    private static final int TYPED_TEXT_HEIGHT = 100;

    // Height of the title bar (buildTitleBar()) and width of its Close
    // button - xr_input.c's TITLE_BAR_V_FRACTION/CLOSE_BUTTON_U_FRACTION
    // must be kept in sync with these (as fractions of WIDTH/HEIGHT above),
    // since the drag-handle-vs-Close-button-vs-rest-of-the-keyboard
    // decision happens entirely on the native side, before a hit ever
    // reaches this class's own touch dispatch.
    private static final int TITLE_BAR_HEIGHT = 90;
    private static final int CLOSE_BUTTON_WIDTH = 160;

    // ~70% opaque - readable but lets whatever's behind the panel (the game
    // quad, or empty space) show through while it's left up during play.
    private static final int PANEL_ALPHA = 180;

    private static KeyboardOverlay instance;

    private TextView typedTextView;
    private final StringBuilder typedText = new StringBuilder();

    // Shift is a local-only layer toggle (never itself sent to the game -
    // see updateShiftVisual()); Ctrl/Alt are one-shot "armed" modifiers,
    // sent to the game the moment they're pressed and released again as
    // soon as the next key consumes them (see toggleModifier()/
    // releaseArmedModifiers()) - there's no press-and-hold gesture with a
    // laser pointer + trigger click, so armed-then-consumed stands in for
    // holding the key down.
    private boolean shiftActive = false;
    private boolean ctrlArmed = false;
    private boolean altArmed = false;
    private Button shiftButton;
    private Button ctrlButton;
    private Button altButton;

    // Every CharKey created by addRow(), so updateShiftVisual() can relabel
    // all of them at once when Shift toggles. Assigned (not a field
    // initializer) at the top of buildContent() - see OverlayPanel's
    // constructor note on why a field a subclass's buildContent() depends
    // on can't rely on normal field-initializer timing.
    private List<CharKey> charKeys;

    // A key whose printed character (and, while unmodified, whose game
    // input) depends on the Shift layer - covers letters, digits, and
    // punctuation uniformly (e.g. 'q'/'Q', '1'/'!', '-'/'_'). androidKeyCode
    // is only used for the Ctrl/Alt-combo path (see handleCharKeyPress()) -
    // the unmodified path goes through commitPrintableChar() instead, which
    // doesn't need an Android keycode at all.
    private static final class CharKey {
        final char base;
        final char shifted;
        final int androidKeyCode;
        Button button;

        CharKey(char base, char shifted, int androidKeyCode) {
            this.base = base;
            this.shifted = shifted;
            this.androidKeyCode = androidKeyCode;
        }
    }

    private static CharKey charKey(char base, char shifted, int androidKeyCode) {
        return new CharKey(base, shifted, androidKeyCode);
    }

    // Space is a CharKey like any other (see addRow()), but a literal
    // space character makes for an unreadable, blank-looking button - keep
    // showing the word "Space" instead, same as before it became a CharKey.
    private static String charKeyLabel(CharKey key, boolean shiftActive) {
        if (key.base == ' ') {
            return "Space";
        }
        return String.valueOf(shiftActive ? key.shifted : key.base);
    }

    private KeyboardOverlay(Activity activity) {
        super(activity, WIDTH, HEIGHT);
    }

    @Override
    protected int contentAlpha() {
        return PANEL_ALPHA;
    }

    @Override
    protected View buildContent() {
        charKeys = new ArrayList<>();

        LinearLayout panel = new LinearLayout(activity);
        panel.setOrientation(LinearLayout.VERTICAL);
        panel.setBackgroundColor(0xFF202020);

        panel.addView(buildTitleBar(),
            new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, TITLE_BAR_HEIGHT));

        typedTextView = new TextView(activity);
        typedTextView.setTextColor(0xFFFFFFFF);
        typedTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 28);
        typedTextView.setBackgroundColor(0xFF303030);
        typedTextView.setPadding(20, 20, 20, 20);
        panel.addView(typedTextView, new LinearLayout.LayoutParams(
            ViewGroup.LayoutParams.MATCH_PARENT, TYPED_TEXT_HEIGHT));

        // Rows follow real keyboard geometry where it helps (Tab starting
        // the QWERTY row, Enter ending the home row, Shift starting the
        // bottom-letter row, a compact arrow cluster bottom-right) so the
        // layout reads as a familiar keyboard rather than an arbitrary grid.
        addRow(panel, "Esc", "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10",
            "F11", "F12", "Back");
        addRow(panel,
            charKey('`', '~', KeyEvent.KEYCODE_GRAVE),
            charKey('1', '!', KeyEvent.KEYCODE_1),
            charKey('2', '@', KeyEvent.KEYCODE_2),
            charKey('3', '#', KeyEvent.KEYCODE_3),
            charKey('4', '$', KeyEvent.KEYCODE_4),
            charKey('5', '%', KeyEvent.KEYCODE_5),
            charKey('6', '^', KeyEvent.KEYCODE_6),
            charKey('7', '&', KeyEvent.KEYCODE_7),
            charKey('8', '*', KeyEvent.KEYCODE_8),
            charKey('9', '(', KeyEvent.KEYCODE_9),
            charKey('0', ')', KeyEvent.KEYCODE_0),
            charKey('-', '_', KeyEvent.KEYCODE_MINUS),
            charKey('=', '+', KeyEvent.KEYCODE_EQUALS));
        addRow(panel, "Tab",
            charKey('q', 'Q', KeyEvent.KEYCODE_Q), charKey('w', 'W', KeyEvent.KEYCODE_W),
            charKey('e', 'E', KeyEvent.KEYCODE_E), charKey('r', 'R', KeyEvent.KEYCODE_R),
            charKey('t', 'T', KeyEvent.KEYCODE_T), charKey('y', 'Y', KeyEvent.KEYCODE_Y),
            charKey('u', 'U', KeyEvent.KEYCODE_U), charKey('i', 'I', KeyEvent.KEYCODE_I),
            charKey('o', 'O', KeyEvent.KEYCODE_O), charKey('p', 'P', KeyEvent.KEYCODE_P),
            charKey('[', '{', KeyEvent.KEYCODE_LEFT_BRACKET),
            charKey(']', '}', KeyEvent.KEYCODE_RIGHT_BRACKET),
            charKey('\\', '|', KeyEvent.KEYCODE_BACKSLASH));
        addRow(panel, "Ctrl",
            charKey('a', 'A', KeyEvent.KEYCODE_A), charKey('s', 'S', KeyEvent.KEYCODE_S),
            charKey('d', 'D', KeyEvent.KEYCODE_D), charKey('f', 'F', KeyEvent.KEYCODE_F),
            charKey('g', 'G', KeyEvent.KEYCODE_G), charKey('h', 'H', KeyEvent.KEYCODE_H),
            charKey('j', 'J', KeyEvent.KEYCODE_J), charKey('k', 'K', KeyEvent.KEYCODE_K),
            charKey('l', 'L', KeyEvent.KEYCODE_L),
            charKey(';', ':', KeyEvent.KEYCODE_SEMICOLON),
            charKey('\'', '"', KeyEvent.KEYCODE_APOSTROPHE),
            "Enter");
        addRow(panel, "Shift",
            charKey('z', 'Z', KeyEvent.KEYCODE_Z), charKey('x', 'X', KeyEvent.KEYCODE_X),
            charKey('c', 'C', KeyEvent.KEYCODE_C), charKey('v', 'V', KeyEvent.KEYCODE_V),
            charKey('b', 'B', KeyEvent.KEYCODE_B), charKey('n', 'N', KeyEvent.KEYCODE_N),
            charKey('m', 'M', KeyEvent.KEYCODE_M),
            charKey(',', '<', KeyEvent.KEYCODE_COMMA),
            charKey('.', '>', KeyEvent.KEYCODE_PERIOD),
            charKey('/', '?', KeyEvent.KEYCODE_SLASH),
            "Up");
        addRow(panel, "Alt", charKey(' ', ' ', KeyEvent.KEYCODE_SPACE), "Left", "Down", "Right");
        return panel;
    }

    // The title bar: a decorative "drag here" label (not itself
    // interactive - see the class doc, dragging is handled entirely by
    // xr_input.c before a hit ever reaches here) plus a real Close button,
    // which is dispatched as an ordinary click like any other key. Its
    // height and the Close button's width must stay in sync with
    // xr_input.c's TITLE_BAR_V_FRACTION/CLOSE_BUTTON_U_FRACTION.
    private LinearLayout buildTitleBar() {
        LinearLayout bar = new LinearLayout(activity);
        bar.setOrientation(LinearLayout.HORIZONTAL);
        bar.setBackgroundColor(0xFF3A3A3A);

        TextView label = new TextView(activity);
        label.setText("Keyboard  -  drag here to move");
        label.setTextColor(0xFFFFFFFF);
        label.setTextSize(TypedValue.COMPLEX_UNIT_SP, 22);
        label.setGravity(Gravity.CENTER_VERTICAL);
        label.setPadding(24, 0, 0, 0);
        bar.addView(label, new LinearLayout.LayoutParams(
            0, ViewGroup.LayoutParams.MATCH_PARENT, 1f));

        Button close = new Button(activity);
        close.setText("Close");
        close.setOnClickListener(v -> handleKeyPress("Close"));
        close.setOnTouchListener(CLICK_ON_TOUCH_UP);
        bar.addView(close,
            new LinearLayout.LayoutParams(CLOSE_BUTTON_WIDTH, ViewGroup.LayoutParams.MATCH_PARENT));

        return bar;
    }

    // Each row fills the panel's remaining height evenly (weight 1 on the
    // row itself). Accepts a mix of String (a fixed-action control key,
    // handled by handleKeyPress()) and CharKey (a Shift-sensitive character
    // key, handled by handleCharKeyPress()) per item, so a row can match
    // real keyboard geometry (e.g. "Tab" followed by a run of CharKeys).
    // Within a row, CharKeys and most control keys share width equally
    // except Space (wider, like a real keyboard) and Esc/Back/Enter/Tab/
    // Shift/Ctrl/Alt (narrower, see keyWeight()).
    private void addRow(LinearLayout panel, Object... items) {
        LinearLayout row = new LinearLayout(activity);
        row.setOrientation(LinearLayout.HORIZONTAL);
        for (Object item : items) {
            Button key = new Button(activity);
            key.setOnTouchListener(CLICK_ON_TOUCH_UP);
            float weight;
            if (item instanceof CharKey) {
                CharKey charKey = (CharKey) item;
                charKey.button = key;
                key.setText(charKeyLabel(charKey, shiftActive));
                key.setOnClickListener(v -> handleCharKeyPress(charKey));
                charKeys.add(charKey);
                // Space is a CharKey too (base == shifted == ' '), but keeps
                // its traditional wide key like the other control keys do.
                weight = (charKey.base == ' ') ? 4f : 1f;
            } else {
                String label = (String) item;
                key.setText(label);
                key.setOnClickListener(v -> handleKeyPress(label));
                weight = keyWeight(label);
                if (label.equals("Shift")) {
                    shiftButton = key;
                } else if (label.equals("Ctrl")) {
                    ctrlButton = key;
                } else if (label.equals("Alt")) {
                    altButton = key;
                }
            }
            row.addView(key, new LinearLayout.LayoutParams(
                0, ViewGroup.LayoutParams.MATCH_PARENT, weight));
        }
        panel.addView(row, new LinearLayout.LayoutParams(
            ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f));
    }

    private static float keyWeight(String label) {
        if (label.equals("Back") || label.equals("Esc") || label.equals("Enter")
                || label.equals("Tab") || label.equals("Shift") || label.equals("Ctrl")
                || label.equals("Alt")) {
            return 1.5f;
        }
        return 1f;
    }

    // Maps a control key's label to the Android keycode forwarded via
    // SDLActivity.onNativeKeyDown()/onNativeKeyUp() - the same pair a
    // physical Bluetooth keyboard's key events already drive (see
    // SDLActivity.handleKeyEvent()). F-keys are contiguous in
    // android.view.KeyEvent (KEYCODE_F1..KEYCODE_F12), so "F1".."F12" are
    // computed rather than listed individually.
    private static int controlKeyCode(String label) {
        switch (label) {
            case "Back": return KeyEvent.KEYCODE_DEL;
            case "Esc": return KeyEvent.KEYCODE_ESCAPE;
            case "Enter": return KeyEvent.KEYCODE_ENTER;
            case "Tab": return KeyEvent.KEYCODE_TAB;
            case "Up": return KeyEvent.KEYCODE_DPAD_UP;
            case "Down": return KeyEvent.KEYCODE_DPAD_DOWN;
            case "Left": return KeyEvent.KEYCODE_DPAD_LEFT;
            case "Right": return KeyEvent.KEYCODE_DPAD_RIGHT;
            default:
                if (label.charAt(0) == 'F') {
                    int n = Integer.parseInt(label.substring(1));
                    return KeyEvent.KEYCODE_F1 + (n - 1);
                }
                throw new IllegalArgumentException("no keycode for " + label);
        }
    }

    // "Close" (the title bar's button - see buildTitleBar()) hides this
    // overlay's quad entirely (see nativeRequestClose()) and never reaches
    // the game. Shift/Ctrl/Alt are handled separately below (Shift is a
    // local layer toggle; Ctrl/Alt are one-shot armed modifiers) since
    // neither sends a plain key event of its own the way every other
    // control key here does. Everything else (Back/Esc/Enter/Tab/arrows/
    // F1-F12) is non-printable, so it's forwarded as a real Android key
    // event via controlKeyCode() above, then releases any armed Ctrl/Alt
    // modifier (this key just consumed it). This all matters beyond just
    // the visible typedTextView below: the engine's cutscene skip handler
    // (cutsloop.c's cutscene_key_handler()) only reacts to Esc/Enter/Space,
    // so those need to reach the game as actual game input, not just be
    // echoed locally.
    private void handleKeyPress(String label) {
        if (label.equals("Close")) {
            nativeRequestClose();
            return;
        }
        if (label.equals("Shift")) {
            shiftActive = !shiftActive;
            updateShiftVisual();
            return;
        }
        if (label.equals("Ctrl") || label.equals("Alt")) {
            toggleModifier(label);
            return;
        }

        int keyCode = controlKeyCode(label);
        SDLActivity.onNativeKeyDown(keyCode);
        SDLActivity.onNativeKeyUp(keyCode);
        releaseArmedModifiers();

        if (label.equals("Back")) {
            if (typedText.length() > 0) {
                typedText.setLength(typedText.length() - 1);
            }
        } else {
            typedText.append('[').append(label).append(']');
        }
        typedTextView.setText(typedText.toString());
    }

    // A CharKey's printed character depends only on the Shift layer
    // (updateShiftVisual() keeps its button's label in sync). Which game
    // input it produces depends on whether a modifier is armed: unmodified,
    // it goes through commitPrintableChar() below like a normal typed
    // character; with Ctrl/Alt armed, it instead goes through the same
    // onNativeKeyDown()/onNativeKeyUp() path handleKeyPress() uses for
    // control keys, since that's the only path that carries
    // ev.key.keysym.mod through to the engine (sdl_events.c's pump_events()
    // reads Ctrl/Alt only off that path, never off SDL_TEXTINPUT).
    private void handleCharKeyPress(CharKey key) {
        char c = shiftActive ? key.shifted : key.base;
        if (ctrlArmed || altArmed) {
            SDLActivity.onNativeKeyDown(key.androidKeyCode);
            SDLActivity.onNativeKeyUp(key.androidKeyCode);
            releaseArmedModifiers();
        } else {
            commitPrintableChar(c);
        }
        typedText.append(c);
        typedTextView.setText(typedText.toString());
    }

    // Ctrl/Alt are "armed" rather than held: pressing one immediately sends
    // its keydown (so SDL's own modifier tracking picks it up for whatever
    // key comes next - see handleCharKeyPress()/handleKeyPress()) and
    // brackets its label for feedback; pressing the same key again before
    // it's been used cancels it (sends the matching keyup, un-brackets).
    // The normal case - actually being consumed by the next key press - is
    // handled by releaseArmedModifiers() below, not here.
    private void toggleModifier(String label) {
        boolean ctrl = label.equals("Ctrl");
        Button button = ctrl ? ctrlButton : altButton;
        int keyCode = ctrl ? KeyEvent.KEYCODE_CTRL_LEFT : KeyEvent.KEYCODE_ALT_LEFT;
        boolean nowArmed = ctrl ? !ctrlArmed : !altArmed;
        if (ctrl) {
            ctrlArmed = nowArmed;
        } else {
            altArmed = nowArmed;
        }
        if (nowArmed) {
            SDLActivity.onNativeKeyDown(keyCode);
        } else {
            SDLActivity.onNativeKeyUp(keyCode);
        }
        button.setText(nowArmed ? "[" + label + "]" : label);
    }

    // Called after any key actually reaches the game (a control key in
    // handleKeyPress(), or a CharKey in handleCharKeyPress()) so an armed
    // Ctrl/Alt only ever applies to the very next key, then releases -
    // matching a real Ctrl/Alt+key combo's keyup once the combo is done.
    private void releaseArmedModifiers() {
        if (ctrlArmed) {
            SDLActivity.onNativeKeyUp(KeyEvent.KEYCODE_CTRL_LEFT);
            ctrlArmed = false;
            ctrlButton.setText("Ctrl");
        }
        if (altArmed) {
            SDLActivity.onNativeKeyUp(KeyEvent.KEYCODE_ALT_LEFT);
            altArmed = false;
            altButton.setText("Alt");
        }
    }

    // Shift never itself reaches the game (see class doc) - it only flips
    // which character each CharKey shows/sends, including its own label.
    // Note this means a Ctrl+Shift+key chord won't carry Shift to the
    // engine (Ctrl/Alt are real forwarded modifier keys, Shift here isn't) -
    // not worth solving unless it actually comes up.
    private void updateShiftVisual() {
        shiftButton.setText(shiftActive ? "[Shift]" : "Shift");
        for (CharKey key : charKeys) {
            key.button.setText(charKeyLabel(key, shiftActive));
        }
    }

    // Synthesizes the SDL_TEXTINPUT event pump_events() (sdl_events.c)
    // requires for printable characters (see handleCharKeyPress() above),
    // by building and pushing it directly in native code (see
    // nativeSendPrintableChar() in questshock_native.c) rather than through
    // Android's IME (there's no real IME session behind this off-screen,
    // never-attached grid for that plumbing to hook into).
    private static void commitPrintableChar(char c) {
        nativeSendPrintableChar(c);
    }

    private static native void nativeSendPrintableChar(char c);

    // Hides this overlay's quad (xr_overlay_set_visible(..., false)) - the
    // only way to do so, since the controller's menu button doesn't affect
    // the keyboard (see this class's doc comment and xr_input.c).
    private static native void nativeRequestClose();

    // Called from xr_overlay.c's xr_overlay_init_jni(), on the render
    // thread, right after it resolves this class via the activity's own
    // ClassLoader (plain FindClass() can't see app classes from a thread
    // that was attached to the JVM rather than spawned from Java).
    public static void nativeInit(Activity activity) {
        if (instance == null) {
            instance = new KeyboardOverlay(activity);
        }
    }

    public static byte[] nativeTakePixelsIfDirty() {
        return instance == null ? null : instance.takePixelsIfDirty();
    }

    public static void nativeDispatchTouch(final float u, final float v, final boolean down) {
        if (instance != null) {
            instance.dispatchTouch(u, v, down);
        }
    }

    public static void nativeUpdateCursor(final float u, final float v, final boolean visible) {
        if (instance != null) {
            instance.dispatchUpdateCursor(u, v, visible);
        }
    }
}
