/*
 * Copyright (c) 2022, 2023, Oracle and/or its affiliates. All rights reserved.
 * Copyright (c) 2022, JetBrains s.r.o.. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

/*
 * @test
 * @bug 8294426
 * @summary The test verifies that a press {@link java.awt.event.MouseEvent}
 * contains correct modifiers although the according native mouse event is
 * accompanied by no mouse modifiers.
 * @author Nikita.Provotorov@jetbrains.com
 *
 * @key headful
 * @requires (os.family == "mac")
 *
 * @modules java.desktop/java.awt:open java.desktop/sun.lwawt:open java.desktop/sun.lwawt.macosx:+open
 * @run main/othervm MouseMacTouchPressEventModifiers
 */

import sun.lwawt.macosx.CocoaConstants;
import sun.lwawt.macosx.LWCToolkit;

import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;


/**
 * Sometimes native mouse events aren't accompanied by the correct mouse modifiers, i.e.
 *  {@link sun.lwawt.macosx.NSEvent#nsToJavaModifiers} returns 0 inside
 *  {@link sun.lwawt.macosx.CPlatformResponder#handleMouseEvent(int, int, int, int, int, int, int, int)}.
 * E.g. the situation above happens when a user taps (NOT clicks) on a trackpad on a M2 MacBooks while
 *  System Preferences -> Trackpad -> Tap to click is turned on.
 * The test emulates the situation via a direct invocation of
 *  {@link sun.lwawt.macosx.CPlatformResponder#handleMouseEvent(int, int, int, int, int, int, int, int)};
 *  unfortunately it's impossible to use {@link java.awt.Robot} because its mouse press events ARE accompanied
 *  by the correct modifiers ({@link sun.lwawt.macosx.NSEvent#nsToJavaModifiers} returns correct values).
 */
public class MouseMacTouchPressEventModifiers
{
    /**
     * How it works:
     * 1. Send a native mouse press to {@code frame} via
     *    {@link sun.lwawt.macosx.CPlatformResponder#handleMouseEvent(int, int, int, int, int, int, int, int)}
     *    (using reflection).
     * 2. Wait (via {@link Future#get()}) until it generates a usual java MouseEvent
     *    and dispatches it to the MouseListener of the {@code frame}.
     * 3. Verify the dispatched MouseEvent contains correct modifiers, modifiersEx and button number.
     * 4. Do all the steps above but for a corresponding mouse release.
     */
    public static void main(String[] args) throws Throwable {
        // TreeMap to preserve the testing order
        final var testCases = new TreeMap<>(Map.of(
            CocoaConstants.kCGMouseButtonLeft, new MouseEventFieldsToTest(MouseEvent.BUTTON1_MASK, MouseEvent.BUTTON1_DOWN_MASK, MouseEvent.BUTTON1),
            CocoaConstants.kCGMouseButtonRight, new MouseEventFieldsToTest(MouseEvent.BUTTON3_MASK, MouseEvent.BUTTON3_DOWN_MASK, MouseEvent.BUTTON3),
            CocoaConstants.kCGMouseButtonCenter, new MouseEventFieldsToTest(MouseEvent.BUTTON2_MASK, MouseEvent.BUTTON2_DOWN_MASK, MouseEvent.BUTTON2)
        ));

        SwingUtilities.invokeAndWait(MouseMacTouchPressEventModifiers::createAndShowGUI);

        try {
            for (var testCase : testCases.entrySet()) {
                final var fieldsToTest = testCase.getValue();

                final int mouseX = (frame.getWidth() - 1) / 2;
                final int mouseY = (frame.getHeight() - 1) / 2;

                // press

                MouseEvent event = frame.sendNativeMousePress(
                    0,
                    testCase.getKey(),
                    1,
                    mouseX,
                    mouseY
                ).get(500, TimeUnit.MILLISECONDS);
                System.out.println("A mouse press turned into: " + event);

                frame.checkInternalErrors();

                checkMouseEvent(event,
                    MouseEvent.MOUSE_PRESSED, fieldsToTest.modifiers, fieldsToTest.pressModifiersEx, fieldsToTest.button);

                // release

                event = frame.sendNativeMouseRelease(
                    0,
                    testCase.getKey(),
                    1,
                    mouseX,
                    mouseY
                ).get(500, TimeUnit.MILLISECONDS);
                System.out.println("A mouse release turned into: " + event);

                frame.checkInternalErrors();

                checkMouseEvent(event,
                    MouseEvent.MOUSE_RELEASED, fieldsToTest.modifiers, 0, fieldsToTest.button);

                System.out.println();
            }
        } finally {
            SwingUtilities.invokeAndWait(MouseMacTouchPressEventModifiers::disposeGUI);
            System.out.flush();
        }
    }


    private record MouseEventFieldsToTest(int modifiers, int pressModifiersEx, int button) {}

    private static MyFrame frame;

    private static void createAndShowGUI() {
        frame = new MyFrame();
        frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);

        frame.pack();
        frame.setSize(800, 500);

        frame.setLocationRelativeTo(null);
        frame.setAlwaysOnTop(true);

        frame.setVisible(true);
    }

    private static void disposeGUI() {
        if (frame != null) {
            frame.dispose();
        }
    }

    private static void checkMouseEvent(MouseEvent me,
        int expectedId, int expectedModifiers, int expectedModifiersEx, int expectedButton
    ) {
        boolean wrong = false;

        final var errMsg = new StringBuilder(1024);
        errMsg.append("Wrong MouseEvent ").append(me).append(':');

        if (me.getID() != expectedId) {
            errMsg.append("\n  eventId: expected <").append(expectedId).append(">, actual <").append(me.getID()).append('>');
            wrong = true;
        }
        if (me.getModifiers() != expectedModifiers) {
            errMsg.append("\n  modifiers: expected <").append(expectedModifiers).append(">, actual <").append(me.getModifiers()).append('>');
            wrong = true;
        }
        if (me.getModifiersEx() != expectedModifiersEx) {
            errMsg.append("\n  modifiersEx: expected <").append(expectedModifiersEx).append(">, actual <").append(me.getModifiersEx()).append('>');
            wrong = true;
        }
        if (me.getButton() != expectedButton) {
            errMsg.append("\n  button: expected <").append(expectedButton).append(">, actual <").append(me.getButton()).append('>');
            wrong = true;
        }

        if (wrong) {
            throw new IllegalArgumentException(errMsg.append('\n').toString());
        }
    }
}


class MyFrame extends JFrame {
    public MyFrame() {
        addMouseListener(new MouseAdapter() {
            @Override
            public void mousePressed(MouseEvent e) {
                System.out.println("MyFrame::mousePressed: " + e);
                keepPromiseVia(e);
            }

            @Override
            public void mouseReleased(MouseEvent e) {
                System.out.println("MyFrame::mouseReleased: " + e);
                keepPromiseVia(e);
            }
        });
    }

    public Future<MouseEvent> sendNativeMousePress(int modifierFlags, int buttonNumber, int clickCount, int x, int y) {
        final int eventType = (buttonNumber == CocoaConstants.kCGMouseButtonLeft) ? CocoaConstants.NSEventTypeLeftMouseDown
                              : (buttonNumber == CocoaConstants.kCGMouseButtonRight) ? CocoaConstants.NSEventTypeRightMouseDown
                              : CocoaConstants.NSEventTypeOtherMouseDown;

        return sendNativeMouseEvent(eventType, modifierFlags, buttonNumber, clickCount, x, y, getX() + x, getY() + y);
    }

    public Future<MouseEvent> sendNativeMouseRelease(int modifierFlags, int buttonNumber, int clickCount, int x, int y) {
        final int eventType = (buttonNumber == CocoaConstants.kCGMouseButtonLeft) ? CocoaConstants.NSEventTypeLeftMouseUp
                              : (buttonNumber == CocoaConstants.kCGMouseButtonRight) ? CocoaConstants.NSEventTypeRightMouseUp
                              : CocoaConstants.NSEventTypeOtherMouseUp;

        return sendNativeMouseEvent(eventType, modifierFlags, buttonNumber, clickCount, x, y, getX() + x, getY() + y);
    }

    public void checkInternalErrors() throws Throwable {
        final Throwable result = internalError.getAndSet(null);
        if (result != null) {
            throw result;
        }
    }


    private final AtomicReference<CompletableFuture<MouseEvent>> mouseEventPromise = new AtomicReference<>(null);

    private final AtomicReference<Throwable> internalError = new AtomicReference<>(null);

    private Future<MouseEvent> sendNativeMouseEvent(
        final int eventType,
        final int modifierFlags,
        final int buttonNumber,
        final int clickCount,
        final int x,
        final int y,
        final int absX,
        final int absY
    ) {
        assert !SwingUtilities.isEventDispatchThread();
        assert mouseEventPromise.get() == null : "Trying to send a mouse event while there is already a processing one";

        final CompletableFuture<MouseEvent> result = new CompletableFuture<>();

        SwingUtilities.invokeLater(() -> {
            try {
                LWCToolkit.invokeLater(() -> {
                    try {
                        final Object thisPlatformResponder = obtainFramePlatformResponder(this);
                        final Method thisPlatformResponderHandleMouseEventMethod = obtainHandleMouseEventMethod(thisPlatformResponder);

                        if (mouseEventPromise.compareAndExchange(null, result) != null) {
                            throw new IllegalStateException("Trying to send a mouse event while there is already a processing one");
                        }

                        thisPlatformResponderHandleMouseEventMethod.invoke(thisPlatformResponder,
                            eventType, modifierFlags, buttonNumber, clickCount, x, y, absX, absY);
                    } catch (Throwable err) {
                        // Remove the promise if thisPlatformResponderHandleMouseEventMethod.invoke(...) failed
                        mouseEventPromise.compareAndExchange(result, null);
                        failPromiseDueTo(result, err);
                    }
                }, this);
            } catch (Throwable err) {
                failPromiseDueTo(result, err);
            }
        });

        return result;
    }

    /** Wraps {@link CompletableFuture#complete(Object)} */
    private void keepPromiseVia(MouseEvent mouseEvent) {
        try {
            final CompletableFuture<MouseEvent> promise = mouseEventPromise.getAndSet(null);
            if (promise == null) {
                throw new IllegalStateException("The following unexpected MouseEvent has arrived: " + mouseEvent);
            }

            if (!promise.complete(mouseEvent)) {
                throw new IllegalStateException("The promise had already been completed when the following MouseEvent arrived: " + mouseEvent);
            }
        } catch (Throwable err) {
            setInternalError(err);
        }
    }

    /** Wraps {@link CompletableFuture#completeExceptionally(Throwable)} */
    private void failPromiseDueTo(CompletableFuture<MouseEvent> promise, Throwable cause) {
        try {
            if (!promise.completeExceptionally(cause)) {
                throw new IllegalStateException("The promise had already been completed when the following error arrived: " + cause);
            }
        } catch (Throwable err) {
            setInternalError(err);
        }
    }

    private void setInternalError(Throwable err) {
        if (internalError.compareAndExchange(null, err) != null) {
            System.err.println("Failed to set the following internal error because there is another one: " + err);
        }
    }

    /** Obtains {@code component.peer.platformWindow.responder} */
    private static Object obtainFramePlatformResponder(Component component) throws NoSuchFieldException, IllegalAccessException {
        final Object framePeer;
        {
            final var frameClass = Component.class;
            final var peerField = frameClass.getDeclaredField("peer");

            peerField.setAccessible(true);

            framePeer = peerField.get(component);
        }

        final Object peerPlatformWindow;
        {
            final var peerClass = framePeer.getClass();
            final var platformWindowField = peerClass.getDeclaredField("platformWindow");

            platformWindowField.setAccessible(true);

            peerPlatformWindow = platformWindowField.get(framePeer);
        }

        final Object platformWindowResponder;
        {
            final var peerPlatformWindowClass = peerPlatformWindow.getClass();
            final var platformWindowResponderField = peerPlatformWindowClass.getDeclaredField("responder");

            platformWindowResponderField.setAccessible(true);

            platformWindowResponder = platformWindowResponderField.get(peerPlatformWindow);
        }

        return platformWindowResponder;
    }

    /** Obtains {@link sun.lwawt.macosx.CPlatformResponder#handleMouseEvent(int, int, int, int, int, int, int, int)} */
    private static Method obtainHandleMouseEventMethod(final Object platformResponder) throws NoSuchMethodException {
        final var responderClass = platformResponder.getClass();
        final var handleMouseEventMethod = responderClass.getDeclaredMethod(
            "handleMouseEvent",
            int.class, int.class, int.class, int.class, int.class, int.class, int.class, int.class
        );

        handleMouseEventMethod.setAccessible(true);

        return handleMouseEventMethod;
    }
}