/*
 * Copyright (c) 2015, 2017, Oracle and/or its affiliates. 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
 * @summary  Call Object.wait() method. Check that monitor information
 *           presented in the stack is correct. Call notifyAll method
 *           monitor info have to disappear from the stack.
 *           Repeats the same scenario calling interrupt() method
 * @modules java.base/jdk.internal.misc
 * @library /test/lib
 * @library ../share
 * @run main/othervm -XX:+UsePerfData WaitNotifyThreadTest
 */
import common.ToolResults;
import java.util.Iterator;
import utils.*;

public class WaitNotifyThreadTest {

    private Object monitor = new Object();
    private final String OBJECT = "a java.lang.Object";
    private final String OBJECT_WAIT = "java.lang.Object.wait";

    interface Action {

        void doAction(Thread thread);
    }

    class ActionNotify implements Action {

        @Override
        public void doAction(Thread thread) {
            //Notify the waiting thread, so it stops waiting and sleeps
            synchronized (monitor) {
                monitor.notifyAll();
            }
        }
    }

    class ActionInterrupt implements Action {

        @Override
        public void doAction(Thread thread) {
            // Interrupt the thread
            thread.interrupt();
        }
    }

    class WaitThread extends Thread {

        @Override
        public void run() {
            try {
                synchronized (monitor) {
                    monitor.wait();
                }
            } catch (InterruptedException x) {

            }
            Utils.sleep();
        }
    }

    public static void main(String[] args) throws Exception {
        new WaitNotifyThreadTest().doTest();
    }

    private void doTest() throws Exception {

        // Verify stack trace consistency when notifying the thread
        doTest(new ActionNotify());

        // Verify stack trace consistency when interrupting the thread
        doTest(new ActionInterrupt());
    }

    private void doTest(Action action) throws Exception {

        final String WAITING_THREAD_NAME = "MyWaitingThread";

        // Start athread that just waits
        WaitThread waitThread = new WaitThread();
        waitThread.setName(WAITING_THREAD_NAME);
        waitThread.start();

        // Collect output from the jstack tool
        JstackTool jstackTool = new JstackTool(ProcessHandle.current().pid());
        ToolResults results = jstackTool.measure();

        // Analyze the jstack output for the patterns needed
        JStack jstack1 = new DefaultFormat().parse(results.getStdoutString());
        ThreadStack ti1 = jstack1.getThreadStack(WAITING_THREAD_NAME);
        analyzeThreadStackWaiting(ti1);

        action.doAction(waitThread);

        // Collect output from the jstack tool again
        results = jstackTool.measure();

        // Analyze the output again
        JStack jstack2 = new DefaultFormat().parse(results.getStdoutString());
        ThreadStack ti2 = jstack2.getThreadStack(WAITING_THREAD_NAME);
        analyzeThreadStackNoWaiting(ti2);

    }

    private void analyzeThreadStackWaiting(ThreadStack ti1) {
        Iterator<MethodInfo> it = ti1.getStack().iterator();

        String monitorAddress = null;
        while (it.hasNext()) {
            MethodInfo mi = it.next();
            if (mi.getName().startsWith(OBJECT_WAIT) && mi.getCompilationUnit() == null /*native method*/) {
                if (mi.getLocks().size() == 1) {
                    MonitorInfo monInfo = mi.getLocks().getFirst();
                    if (monInfo.getType().equals("waiting on") && compareMonitorClass(monInfo)) {
                        monitorAddress = monInfo.getMonitorAddress();
                    } else {
                        System.err.println("Error: incorrect monitor info: " + monInfo.getType() + ", " + monInfo.getMonitorClass());
                        throw new RuntimeException("Incorrect lock record in "
                                + OBJECT_WAIT + " method");
                    }

                } else {
                    throw new RuntimeException(OBJECT_WAIT
                            + " method has to contain one lock record bu it contains " + mi.getLocks().size());
                }
            }

            if (mi.getName().startsWith("WaitThread.run")) {
                if (monitorAddress == null) {
                    throw new RuntimeException("Cannot found monitor info associated with " + OBJECT_WAIT + " method");
                }

                int numLocks = mi.getLocks().size();
                for (int i = 0; i < numLocks - 1; ++i) {
                    assertMonitorInfo("waiting to re-lock in wait()", mi.getLocks().get(i), monitorAddress);
                }
                assertMonitorInfo("locked", mi.getLocks().getLast(), monitorAddress);
            }
        }

    }

    private void assertMonitorInfo(String expectedMessage, MonitorInfo monInfo, String monitorAddress) {
        if (monInfo.getType().equals(expectedMessage)
                && compareMonitorClass(monInfo)
                && monInfo.getMonitorAddress().equals(
                        monitorAddress)) {
            System.out.println("Correct monitor info found");
        } else {
            System.err.println("Error: incorrect monitor info: " + monInfo.getType() + ", " + monInfo.getMonitorClass() + ", " + monInfo.getMonitorAddress());
            System.err.println("Expected: " + expectedMessage + ", a java.lang.Object, " + monitorAddress);
            throw new RuntimeException("Incorrect lock record in 'run' method");
        }
    }

    private boolean compareMonitorClass(MonitorInfo monInfo) {
        // If monitor class info is present in the jstack output
        // then compare it with the class of the actual monitor object
        // If there is no monitor class info available then return true
        return OBJECT.equals(monInfo.getMonitorClass()) || (monInfo.getMonitorClass() == null);
    }

    private void analyzeThreadStackNoWaiting(ThreadStack ti2) {
        Iterator<MethodInfo> it = ti2.getStack().iterator();

        while (it.hasNext()) {
            MethodInfo mi = it.next();
            if (mi.getLocks().size() != 0) {
                throw new RuntimeException("Unexpected lock record in "
                        + mi.getName() + " method");
            }
        }
    }

}
