package de.ladkau.questshock;

import android.app.Activity;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.os.SystemClock;
import android.view.MotionEvent;
import android.view.View;
import java.nio.ByteBuffer;

/**
 * Shared machinery for an off-screen, never-attached-to-a-window View tree
 * rendered into its own OpenXR quad (see android/app/src/main/cpp/
 * xr_overlay.c, which owns the quad's swapchain and drives whichever
 * concrete subclass via JNI - construction, touch input, and pixel
 * readback). Rendering to a Bitmap and dispatching synthetic MotionEvents
 * into an unattached hierarchy both work the same way they would for an
 * attached View - draw(Canvas)/dispatchTouchEvent() don't require a
 * ViewRootImpl/window, just a measured+laid-out tree.
 *
 * Subclasses (MenuOverlay, KeyboardOverlay) provide their own content via
 * buildContent() and, if they want translucency, contentAlpha() - and, since
 * Java has no static virtual dispatch and each overlay needs its own
 * singleton, each subclass still declares its own thin nativeInit()/
 * nativeTakePixelsIfDirty()/nativeDispatchTouch()/nativeUpdateCursor()
 * static methods (matching what xr_overlay.c's JNI glue resolves by class
 * name) that just delegate into the instance methods here
 * (takePixelsIfDirty()/dispatchTouch()/dispatchUpdateCursor()).
 */
abstract class OverlayPanel {
    private static final float CURSOR_RADIUS = 10f;

    // Button's own click handling (View.onTouchEvent()'s ACTION_UP case)
    // calls View.post(mPerformClick) rather than invoking performClick()
    // directly; post() queues the runnable to run once the view is attached
    // to a window and returns true immediately even when unattached, so on
    // this permanently-unattached tree that queued click silently never
    // fires - dispatchTouchEvent() still delivers the down/up events
    // correctly, only the click callback is swallowed. Registering this as
    // each button's OnTouchListener bypasses that path entirely: returning
    // true here skips View's internal onTouchEvent() (see
    // ViewGroup/View#dispatchTouchEvent), so performClick() (which still
    // runs any OnClickListener set via setOnClickListener()) is called
    // directly instead.
    protected static final View.OnTouchListener CLICK_ON_TOUCH_UP = (v, event) -> {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                v.setPressed(true);
                break;
            case MotionEvent.ACTION_UP:
                v.setPressed(false);
                v.performClick();
                break;
            case MotionEvent.ACTION_CANCEL:
                v.setPressed(false);
                break;
        }
        return true;
    };

    protected final Activity activity;
    private final int width;
    private final int height;
    private final View root;

    // contentBitmap holds just the panel's own rendered content (no cursor),
    // redrawn only when that content actually changes (redrawContent(), via
    // handleTouch() or a subclass's own key handling) via root.draw() - a
    // full off-screen View-tree traversal that gets noticeably more
    // expensive on a busier content tree. bitmap is what's actually
    // published to native (see takePixelsIfDirty()): compositeAndPublish()
    // cheaply blits contentBitmap plus the cursor circle into it. Splitting
    // these two apart matters because updateCursor() below fires every
    // single frame (~90Hz) while aiming at the panel - if it called the
    // expensive root.draw() path every time (as an earlier version did), a
    // busy content tree made each redraw slow enough that runOnUiThread()
    // posts piled up faster than the UI thread could drain them, so the
    // cursor visibly lagged behind the controller's actual aim instead of
    // tracking it.
    private final Bitmap contentBitmap;
    private final Canvas contentCanvas;
    private final Bitmap bitmap;
    private final Canvas canvas;
    private final Paint cursorPaint;
    // Draws contentBitmap at contentAlpha() - see compositeAndPublish().
    private final Paint contentAlphaPaint;

    private final Object pixelLock = new Object();
    private byte[] pendingPixels;

    // Only ever touched on the UI thread (both dispatchUpdateCursor() and
    // compositeAndPublish() run/are posted there) - no lock needed, unlike
    // pendingPixels above.
    private boolean cursorVisible = false;
    private float cursorX = 0f;
    private float cursorY = 0f;

    protected OverlayPanel(Activity activity, int width, int height) {
        this.activity = activity;
        this.width = width;
        this.height = height;

        root = buildContent();
        int widthSpec = View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY);
        int heightSpec = View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY);
        root.measure(widthSpec, heightSpec);
        root.layout(0, 0, width, height);

        contentBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        contentCanvas = new Canvas(contentBitmap);
        bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        canvas = new Canvas(bitmap);

        cursorPaint = new Paint();
        cursorPaint.setColor(0xFFFFFFFF);
        cursorPaint.setAntiAlias(true);

        // contentAlpha() must not depend on any subclass instance state -
        // it's called here, from the superclass constructor, before the
        // subclass's own field initializers/constructor body have run (the
        // usual Java construction order: this class's fields/constructor
        // first, then the subclass's). A static constant is the only thing
        // that's safe to return.
        contentAlphaPaint = new Paint();
        contentAlphaPaint.setAlpha(contentAlpha());

        redrawContent();
    }

    // Builds this overlay's content View tree - called once, from this
    // class's own constructor (see the note on contentAlpha() above: don't
    // rely on subclass instance fields being initialized yet here; if a
    // subclass needs its own mutable state available while building its
    // content, e.g. a list of buttons to later relabel, initialize that
    // field's value at the top of this method instead of via a field
    // initializer).
    protected abstract View buildContent();

    // 0-255; defaults to fully opaque. Override for translucency (see the
    // constructor's note on what's safe to depend on here).
    protected int contentAlpha() {
        return 255;
    }

    // Polled once per frame while this overlay's quad is visible (see
    // xr_overlay_render_and_build_layer()) - returns the current pixels
    // only once per redraw (null otherwise), so the native side knows when
    // it can skip re-uploading a texture that hasn't actually changed.
    protected final byte[] takePixelsIfDirty() {
        synchronized (pixelLock) {
            byte[] pixels = pendingPixels;
            pendingPixels = null;
            return pixels;
        }
    }

    // u/v are this quad's own hit-test coordinates (0..1, top-left origin)
    // - computed by xr_input.c's ray/quad intersection against xr_overlay.c's
    // reported quad geometry, forwarded here as a synthetic tap. Runs on the
    // UI thread since the View/Bitmap/Canvas objects here are otherwise only
    // ever touched from there.
    protected final void dispatchTouch(final float u, final float v, final boolean down) {
        activity.runOnUiThread(() -> handleTouch(u, v, down));
    }

    // u/v are the same hit-test coordinates dispatchTouch() uses, but
    // polled once per frame regardless of click state rather than only on
    // click edges - lets the cursor track the aim ray continuously instead
    // of only jumping when a trigger is pressed. visible=false (no hand's
    // ray currently on the quad) hides it.
    protected final void dispatchUpdateCursor(final float u, final float v,
                                               final boolean visible) {
        activity.runOnUiThread(() -> updateCursor(u, v, visible));
    }

    private void updateCursor(float u, float v, boolean visible) {
        float x = u * width;
        float y = v * height;
        if (visible == cursorVisible && x == cursorX && y == cursorY) {
            return;
        }
        cursorVisible = visible;
        cursorX = x;
        cursorY = y;
        compositeAndPublish();
    }

    private void handleTouch(float u, float v, boolean down) {
        float x = u * width;
        float y = v * height;
        long time = SystemClock.uptimeMillis();
        MotionEvent event = MotionEvent.obtain(
            time, time, down ? MotionEvent.ACTION_DOWN : MotionEvent.ACTION_UP, x, y, 0);
        try {
            root.dispatchTouchEvent(event);
        } finally {
            event.recycle();
        }
        redrawContent();
    }

    // Re-runs the full off-screen View-tree draw (expensive - see the
    // contentBitmap field comment above) - call only when the panel's
    // actual content changed, not for the cursor-only updates
    // compositeAndPublish() below handles on its own.
    protected final void redrawContent() {
        contentCanvas.drawColor(0xFF202020);
        root.draw(contentCanvas);
        compositeAndPublish();
    }

    // Cheap per-frame path: blits the last-rendered contentBitmap (no View
    // traversal) plus the cursor circle into bitmap and publishes it.
    // contentBitmap itself is fully opaque (redrawContent() draws an opaque
    // background first); contentAlpha() translucency is applied right here
    // instead, via contentAlphaPaint - which needs bitmap cleared to fully
    // transparent first, since otherwise each frame's alpha-blended draw
    // would blend against whatever was left over in bitmap from the
    // previous frame rather than against nothing, compounding into a
    // ghosting trail across frames.
    private void compositeAndPublish() {
        bitmap.eraseColor(0);
        canvas.drawBitmap(contentBitmap, 0, 0, contentAlphaPaint);
        if (cursorVisible) {
            canvas.drawCircle(cursorX, cursorY, CURSOR_RADIUS, cursorPaint);
        }

        byte[] pixels = new byte[width * height * 4];
        // ARGB_8888's actual in-memory byte order is R,G,B,A - matches
        // GL_RGBA/GL_UNSIGNED_BYTE on the native side with no swizzling.
        bitmap.copyPixelsToBuffer(ByteBuffer.wrap(pixels));
        synchronized (pixelLock) {
            pendingPixels = pixels;
        }
    }
}
