/*
 * Copyright (c) 2006, 2024, 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.
 */
package nsk.share.jdi;

import java.io.*;
import java.util.*;
import com.sun.jdi.*;

import nsk.share.Consts;

/*
 *  Class is used as base debugger in tests for ThreadReference.ownedMonitorsAndFrames().
 *
 *  In all this test similar scenario is used:
 *      - debugger VM force debugge VM to create test thread which acquires different monitors
 *      - when test thread acquire all monitors debuggee save information about all acquired monitors in special array 'monitorsInfo'
 *      - debugger read data from 'monitorsInfo' and compare it with data returned by ThreadReference.ownedMonitorsAndFrames()
 */
public class OwnedMonitorsDebugger extends TestDebuggerType2 {

    /*
     * debug data about monitor acquired by debuggee test thread,
     * intended to compare with data returned by ThreadReference.ownedMonitorsAndFrames
     */
    public static class DebugMonitorInfo {
        // create DebugMonitorInfo using mirror of instance of nsk.share.locks.LockingThread.DebugMonitorInfo
        DebugMonitorInfo(ObjectReference debuggeeMirror) {
            monitor = (ObjectReference) debuggeeMirror.getValue(debuggeeMirror.referenceType().fieldByName("monitor"));
            stackDepth = ((IntegerValue) debuggeeMirror.getValue(debuggeeMirror.referenceType().fieldByName("stackDepth"))).intValue();
            thread = (ThreadReference) debuggeeMirror.getValue(debuggeeMirror.referenceType().fieldByName("thread"));
        }

        public DebugMonitorInfo(ObjectReference monitor, int stackDepth, ThreadReference thread) {
            this.monitor = monitor;
            this.stackDepth = stackDepth;
            this.thread = thread;
        }

        public ObjectReference monitor;

        public int stackDepth;

        public ThreadReference thread;
    }

    public static void main(String argv[]) {
        int result = run(argv,System.out);
        if (result != 0) {
            throw new RuntimeException("TEST FAILED with result " + result);
        }
    }

    public static int run(String argv[], PrintStream out) {
        return new OwnedMonitorsDebugger().runIt(argv, out);
    }

    protected boolean canRunTest() {
        if (!vm.canGetMonitorFrameInfo()) {
            log.display("TEST CANCELED due to: vm.canGetMonitorFrameInfo() = false");
            return false;
        } else
            return super.canRunTest();
    }

    protected String debuggeeClassName() {
        return OwnedMonitorsDebuggee.class.getName();
    }

    // read debuggee's array 'monitorsInfo' containing information about acquired monitors
    protected List<DebugMonitorInfo> getDebugMonitorsInfo() {
        List<DebugMonitorInfo> result = new ArrayList<DebugMonitorInfo>();

        ReferenceType referenceType = debuggee.classByName(debuggeeClassNameWithoutArgs());
        ArrayReference monitorsInfo = (ArrayReference) referenceType.getValue(referenceType.fieldByName("monitorsInfo"));

        for (int i = 0; i < monitorsInfo.length(); i++)
            result.add(new DebugMonitorInfo((ObjectReference) monitorsInfo.getValue(i)));

        return result;
    }

    int implStackDepth(ThreadReference threadReference, String className, String methodName) {
        try {
            int stackDepth = 0;
            for (int i = 0; i < threadReference.frames().size(); i++) {
                StackFrame sf = threadReference.frames().get(i);
                if (!sf.location().declaringType().name().equals(className)) {
                    return stackDepth;
                }
                if (sf.location().method().name().equals(methodName)) {
                    stackDepth = i;
                }
            }
        }  catch (IncompatibleThreadStateException e) {
            log.complain("Unexpected exception", e);
            return 0;
        }
        return 0;
    }

    private boolean compare(MonitorInfo actual, DebugMonitorInfo expected) {
        boolean success = true;

        int depth = actual.stackDepth();
        ThreadReference threadReference = actual.thread();
        if (expected.stackDepth != - 1) {
            // Exclude implementation of Thread.sleep()/Object.wait() from comparison
            depth -= implStackDepth(threadReference, "java.lang.Thread", "sleep");
            depth -= implStackDepth(threadReference, "java.lang.Object", "wait");
        }

        if (depth != expected.stackDepth) {
            setSuccess(false);
            success = false;
            log.complain("Expected and actual monitor(" + actual.monitor() + ") stack depth differs, expected: "
                    + expected.stackDepth + " actual (excluding wait/sleep impl): " + depth);
            log.complain("The stack:");
            try {
                for (int idx = 0; idx < threadReference.frames().size(); idx++) {
                    log.complain(idx + ": " + threadReference.frames().get(idx).toString());
                }
            } catch (IncompatibleThreadStateException e) {
                e.printStackTrace();
            }
        }

        if (!actual.thread().equals(expected.thread)) {
            setSuccess(false);
            success = false;
            log.complain("Expected and actual monitor(" + actual.monitor() + " thread differs, expected: " + expected.thread + " actual: "
                    + actual.thread());
        }

        return success;
    }

    protected void compare(List<MonitorInfo> actualData, List<DebugMonitorInfo> expectedData) {
        boolean success = true;

        // compare total amount of monitors
        if (actualData.size() != expectedData.size()) {
            setSuccess(false);
            success = false;
            log.complain("Number of expected monitors and actual ones differs");
            log.complain("Expected: " + expectedData.size() + ", actual: " + actualData.size());
        }

        // check that all expected monitors are contained in 'actualData'
        for (DebugMonitorInfo expectedMonitorInfo : expectedData) {
            boolean isMonitorFound = false;

            for (MonitorInfo actualMonitorInfo : actualData) {
                if (expectedMonitorInfo.monitor.equals(actualMonitorInfo.monitor())) {
                    isMonitorFound = true;

                    if (!compare(actualMonitorInfo, expectedMonitorInfo))
                        success = false;

                    break;
                }
            }

            if (!isMonitorFound) {
                setSuccess(false);
                success = false;
                log.complain("Expected monitor not found in result of ownedMonitorsAndFrames(): " + expectedMonitorInfo.monitor);
            }
        }

        // check that all monitors from 'actualData' are contained in
        // 'expectedData'
        for (MonitorInfo actualMonitorInfo : actualData) {
            boolean isMonitorFound = false;

            for (DebugMonitorInfo expectedMonitorInfo : expectedData) {
                if (actualMonitorInfo.monitor().equals(expectedMonitorInfo.monitor)) {
                    isMonitorFound = true;
                    break;
                }
            }

            if (!isMonitorFound) {
                setSuccess(false);
                success = false;
                log.complain("Unexpected monitor in result of ownedMonitorsAndFrames(): " + actualMonitorInfo.monitor() + " Depth: "
                        + actualMonitorInfo.stackDepth() + " Thread: " + actualMonitorInfo.thread());
            }
        }

        if (!success)
            logDebugInfo(actualData, expectedData);
    }

    private void logDebugInfo(List<MonitorInfo> actualData, List<DebugMonitorInfo> expectedData) {
        log.display("ACTUAL MONITORS (total " + actualData.size() + "):");

        ThreadReference thread = null;

        for (MonitorInfo monitorInfo : actualData) {
            log.display("Monitor: " + monitorInfo.monitor());
            log.display("Depth: " + monitorInfo.stackDepth());
            log.display("Thread: " + monitorInfo.thread());
            if (thread == null)
                thread =  monitorInfo.thread();
        }

        log.display("EXPECTED MONITORS (total " + expectedData.size() + "):");

        for (DebugMonitorInfo monitorInfo : expectedData) {
            log.display("Monitor: " + monitorInfo.monitor);
            log.display("Depth: " + monitorInfo.stackDepth);
            log.display("Thread: " + monitorInfo.thread);
            if (thread == null)
                thread =  monitorInfo.thread;
        }

        if (thread != null) {
            try {
                log.display("Thread frames:");
                for (StackFrame frame : thread.frames()) {
                    Location location = frame.location();
                    log.display(location.declaringType().name() + "." + location.method().name() + ", line: " + location.lineNumber());
                }
            } catch (Exception e) {
                unexpectedException(e);
            }
        }
    }

    /*
     * Check that ThreadReference.ownedMonitorsAndFrames() returns correct
     * data before calling this method debuggee should save information about
     * acquired monitors in special array 'monitorsInfo',
     * debugger forces debuggee to do it using command 'COMMAND_UPDATE_MONITOR_INFO'
     */
    protected void checkMonitorInfo(ThreadReference threadReference) {
        List<MonitorInfo> actualData = null;

        try {
            actualData = threadReference.ownedMonitorsAndFrames();
        } catch (Exception e) {
            setSuccess(false);
            log.complain("Unexpected exception: " + e);
            e.printStackTrace(log.getOutStream());
        }

        List<DebugMonitorInfo> expectedData = getDebugMonitorsInfo();

        compare(actualData, expectedData);
    }

}
