package invalid.lena.scrcpy;

import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.view.KeyEvent;
import android.view.MotionEvent;

import java.util.function.Consumer;

// Encodes UI events into scrcpy ControlMessage byte arrays and pushes
// them at ControlStream. Also mirrors the remote clipboard locally and
// echoes local clipboard changes the other way.
//
// Forwards multi-pointer touch and AOSP keycodes. Mouse buttons and
// scroll wheels are not forwarded yet.
public final class Controller implements ControlStream.InboundSink {

    private final Consumer<byte[]>  sender;
    private final ClipboardManager  clipboard;
    public  volatile String         lastEvent = "(idle)";
    // Held once so add/removePrimaryClipChangedListener see the same
    // listener reference. Method references generate fresh lambdas
    // each call site and the remove silently no-ops otherwise.
    private final ClipboardManager.OnPrimaryClipChangedListener clipListener =
            this::onLocalClipboardChanged;
    private volatile int targetW, targetH;
    private volatile int viewW,   viewH;

    // Suppress one local clipboard change after we set it from a remote update.
    private volatile boolean suppressNextClipChange;

    public Controller(Context ctx, Consumer<byte[]> sender) {
        this.sender = sender;
        this.clipboard = (ClipboardManager) ctx.getSystemService(Context.CLIPBOARD_SERVICE);
        if (clipboard != null) {
            clipboard.addPrimaryClipChangedListener(clipListener);
        }
    }

    public void release() {
        if (clipboard != null) {
            try { clipboard.removePrimaryClipChangedListener(clipListener); }
            catch (Exception ignored) {}
        }
    }

    public void setTargetSize(int w, int h) {
        targetW = w; targetH = h;
        Log.i("controller: target %dx%d", w, h);
    }

    public void setViewSize(int w, int h) {
        viewW = w; viewH = h;
    }

    // ---- inbound ----

    @Override
    public void onRemoteClipboard(String text) {
        Log.i("clipboard from target: %d chars", text.length());
        if (clipboard == null) return;
        suppressNextClipChange = true;
        try {
            clipboard.setPrimaryClip(ClipData.newPlainText("scrcpy-android", text));
        } catch (Exception e) {
            Log.w("clipboard set local failed: %s", e);
            suppressNextClipChange = false;
        }
    }

    private void onLocalClipboardChanged() {
        if (suppressNextClipChange) {
            suppressNextClipChange = false;
            return;
        }
        if (clipboard == null) return;
        ClipData data;
        try { data = clipboard.getPrimaryClip(); }
        catch (Exception e) { Log.w("clipboard get local failed: %s", e); return; }
        if (data == null || data.getItemCount() == 0) return;
        CharSequence cs = data.getItemAt(0).coerceToText(null);
        if (cs == null) return;
        sendSetClipboard(cs.toString(), false);
        Log.i("clipboard to target: %d chars", cs.length());
    }

    // ---- outbound ----

    public void onTouch(MotionEvent ev) {
        int tw = targetW, th = targetH, vw = viewW, vh = viewH;
        if (tw == 0 || th == 0 || vw == 0 || vh == 0) return;

        // scrcpy's wire protocol uses ACTION_DOWN/UP/MOVE/CANCEL with a
        // pointerId per message. The server tracks which pointers are
        // currently down. So we translate Android's masked actions:
        //   ACTION_POINTER_DOWN[i] -> ACTION_DOWN  (this pointer joins)
        //   ACTION_POINTER_UP[i]   -> ACTION_UP    (this pointer leaves)
        //   ACTION_MOVE            -> ACTION_MOVE for every current pointer
        //   ACTION_CANCEL          -> ACTION_CANCEL for every current pointer
        int action = ev.getActionMasked();
        int idx    = ev.getActionIndex();
        int n      = ev.getPointerCount();

        switch (action) {
            case MotionEvent.ACTION_DOWN:
                sendPointer(ev, 0, MotionEvent.ACTION_DOWN, tw, th, vw, vh);
                break;
            case MotionEvent.ACTION_POINTER_DOWN:
                sendPointer(ev, idx, MotionEvent.ACTION_DOWN, tw, th, vw, vh);
                break;
            case MotionEvent.ACTION_UP:
                sendPointer(ev, 0, MotionEvent.ACTION_UP, tw, th, vw, vh);
                break;
            case MotionEvent.ACTION_POINTER_UP:
                sendPointer(ev, idx, MotionEvent.ACTION_UP, tw, th, vw, vh);
                break;
            case MotionEvent.ACTION_MOVE:
                for (int i = 0; i < n; i++) sendPointer(ev, i, MotionEvent.ACTION_MOVE, tw, th, vw, vh);
                break;
            case MotionEvent.ACTION_CANCEL:
                for (int i = 0; i < n; i++) sendPointer(ev, i, MotionEvent.ACTION_CANCEL, tw, th, vw, vh);
                break;
            default:
                return;
        }
    }

    // Sizes come from onTouch's snapshot of the volatile fields, so a
    // concurrent resize cannot zero a divisor between check and use.
    private void sendPointer(MotionEvent ev, int index, int action,
                             int tw, int th, int vw, int vh) {
        long pointerId = ev.getPointerId(index);
        int x = (int) ev.getX(index);
        int y = (int) ev.getY(index);
        int tx = (int) ((long) x * tw / vw);
        int ty = (int) ((long) y * th / vh);
        // getPressure is calibrated around 1.0 but may exceed it on some
        // digitizers; clamp instead of masking so hard presses don't wrap
        // around to a light touch.
        int pressure = (action == MotionEvent.ACTION_UP) ? 0
                : Math.min((int)(ev.getPressure(index) * 0xffff), 0xffff);
        sender.accept(ControlMessages.touch(action, pointerId, tx, ty, tw, th,
                pressure, /* actionButton */ 0, /* buttons */ 0));
        lastEvent = "touch a=" + action + " (" + tx + "," + ty + ")";
    }

    public void onKey(KeyEvent ev) {
        int tw = targetW, th = targetH;
        if (tw == 0 || th == 0) return;
        int action = ev.getAction(); // ACTION_DOWN=0, ACTION_UP=1
        if (action != KeyEvent.ACTION_DOWN && action != KeyEvent.ACTION_UP) return;
        sendKeycode(action, ev.getKeyCode(), ev.getRepeatCount(), ev.getMetaState());
    }

    // Fire a tap of the target's Back. scrcpy server treats the
    // BACK_OR_SCREEN_ON message as Back when the target is on, screen-
    // on when it's off - useful for waking a locked target too.
    public void onBack() {
        sender.accept(ControlMessages.backOrScreenOn(ControlMessages.ACTION_DOWN));
        sender.accept(ControlMessages.backOrScreenOn(ControlMessages.ACTION_UP));
        lastEvent = "back";
    }

    // ---- encoders (delegate to pure-java ControlMessages) ----

    private void sendKeycode(int action, int keycode, int repeat, int metaState) {
        sender.accept(ControlMessages.keycode(action, keycode, repeat, metaState));
        lastEvent = "key  a=" + action + " kc=" + keycode;
    }

    private void sendSetClipboard(String text, boolean paste) {
        try {
            sender.accept(ControlMessages.setClipboard(/* sequence */ 0L, paste, text));
            lastEvent = "clip " + text.length() + " chars";
        } catch (IllegalArgumentException e) {
            Log.w("clipboard not sent: %s", e.getMessage());
        }
    }
}
