/*
 * Copyright (c) 2004, 2018, 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.jvmti.RedefineClasses;

import java.io.*;
import java.util.*;
import nsk.share.*;
import nsk.share.jvmti.*;

/**
 * The test exercises that the JVMTI function RedefineClasses()
 * enable to redefine an inner class properly. Redefiniton
 * is performed in asynchronous manner from a separate
 * thread when the VM is provoked to switch into compiled mode.<br>
 * The test works as follows. Two threads are started: java thread
 * executing an inner class to be redefined, and native one executing
 * an agent code. Then the inner class method <code>redefclass028HotMethod()</code>
 * is provoked to be compiled (optimized), and thus the JVMTI event
 * <code>CompiledMethodLoad</code> should be sent. After that, the
 * agent redefines the inner class. Different kinds of outer fields
 * are accessed from executing methods in both versions of the
 * inner class. Upon the redefinition, the main test thread verifies
 * via the outer fields values that the inner method <code>run()</code>
 * having an active stack frame stays obsolete but the other inner
 * methods have been redefined. It also verifies that the outer class
 * is still can access the inner class fields after the redefinition.
 */
public class redefclass028 extends DebugeeClass {
    final static String REDEF_CLS_SIGNATURE =
        "Lnsk/jvmti/RedefineClasses/redefclass028$RedefClass;";

    static int status = Consts.TEST_PASSED;

    static Log log = null;

    // path to the directory with redefining class files
    static String clfBasePath = null;

    // dummy outer fields to be changed by the inner class
    private int prOuterFl[] = {0,0,0};
    int packOuterFl[] = {0,0,0};
    public int pubOuterFl[] = {0,0,0};
    private static int prStOuterFl[] = {0,0,0};
    static int packStOuterFl[] = {0,0,0};
    public static int pubStOuterFl[] = {0,0,0};

    /* a dummy outer private field to be accessed or not in
       the inner class and thus provoking compiler to add or not
       a synthetic access method into the outer class. */
    private char prOuterFieldToBeAccessed = 'a';

    native static void storeClassBytes(byte[] classBytes);
    native static void notifyNativeAgent(); /** notify native agent that "hot" method is entered */
    native static boolean isRedefinitionOccurred(); /** check whether class redefinition was already occurred */

    public static void main(String args[]) {
        args = nsk.share.jvmti.JVMTITest.commonInit(args);

        // produce JCK-like exit status.
        System.exit(run(args, System.out) + Consts.JCK_STATUS_BASE);
    }

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

    private int runIt(String args[], PrintStream out) {
        int iter;

        ArgumentHandler argHandler = new ArgumentHandler(args);
        log = new Log(out, argHandler);

        try {
            // number of iterations for a method to become 'hotspot' one
            iter = Integer.parseInt(args[0]);

            // directory to search a redefining class
            clfBasePath = args[1];
        } catch(Exception e) {
            throw new Failure("TEST BUG: Wrong test parameters, caught: "
                + e);
        }

        storeClassBytes(loadFromClassFile(REDEF_CLS_SIGNATURE));

        // testing sync
        log.display("waiting for the agent start ...\n");
        status = checkStatus(status);

        log.display("starting an auxiliary thread ...\n");
        RedefClass redefCls = new RedefClass(iter);
        redefCls.setDaemon(true);
        synchronized(redefCls) {
            redefCls.start();
            log.display("waiting for auxiliary thread readiness...\n");
            try {
                redefCls.wait(); // wait for the thread's readiness
            } catch (InterruptedException e) {
                redefCls.interrupt();
                throw new Failure("TEST FAILURE: waiting for auxiliary thread start, caught: "
                    + e);
            }
        }

        // testing sync
        log.display("auxiliary thread started\n"
            + "waiting for the agent finish ...\n");
        status = checkStatus(status);

        boolean isRedefinitionStarted = waitForRedefinitionStarted();
        boolean isRedefinitionCompleted = false;
        if (isRedefinitionStarted) {
            isRedefinitionCompleted = waitForRedefinitionCompleted();
        }

        log.display("waiting for auxiliary thread ...\n");
        redefCls.stopMe = true;
        try {
            redefCls.join();
        } catch (InterruptedException e) {
            redefCls.interrupt();
            throw new Failure("TEST FAILURE: waiting for auxiliary thread death, caught: "
                + e);
        }

        // CR 6604375: check whether class redefinition occurred
        if (isRedefinitionCompleted) {
            // verify results
            checkOuterFields(0, 1);
            checkOuterFields(1, 2);
            checkOuterFields(2, 2);
            checkInnerFields(redefCls, 1);
        }

        return status;
    }

    private boolean waitForRedefinitionStarted() {
        final int SLEEP_MS = 20;
        int iterationsLeft = 2000 / SLEEP_MS;
        while (iterationsLeft >= 0) {
            if (isRedefinitionOccurred()) {
                log.display("Redefinition started.");
                return true;
            }
            --iterationsLeft;
            safeSleep(SLEEP_MS);
        }
        log.complain("Redefinition not started. May need more time for -Xcomp.");
        status = Consts.TEST_FAILED;
        return false;
    }

    private boolean waitForRedefinitionCompleted() {
        final int SLEEP_MS = 20;
        int iterationsLeft = 10000 / SLEEP_MS;
        while (iterationsLeft >= 0) {
            // Check if new code has changed fields.
            if (prStOuterFl[1] == 2 && prStOuterFl[2] == 2) {
                log.display("Redefinition completed.");
                return true;
            }
            --iterationsLeft;
            safeSleep(SLEEP_MS);
        }
        log.complain("Redefinition not completed. May need more time for -Xcomp.");
        status = Consts.TEST_FAILED;
        return false;
    }

    private void checkOuterFields(int index, int expValue) {
        if (prOuterFl[index] != expValue
                || packOuterFl[index] != expValue
                || pubOuterFl[index] != expValue
                || prStOuterFl[index] != expValue
                || packStOuterFl[index] != expValue
                || pubStOuterFl[index] != expValue) {
            status = Consts.TEST_FAILED;
            log.complain("TEST FAILED: unexpected values of outer fields:"
                + "\n\tprOuterFl["+ index +"]: got: " + prOuterFl[index]
                + ", expected: " + expValue
                + "\n\tpackOuterFl["+ index +"]: got: " + packOuterFl[index]
                + ", expected: " + expValue
                + "\n\tpubOuterFl["+ index +"]: got: " + pubOuterFl[index]
                + ", expected: " + expValue

                + "\n\tprStOuterFl["+ index +"]: got: " + prStOuterFl[index]
                + ", expected: " + expValue
                + "\n\tpackStOuterFl["+ index +"]: got: " + packStOuterFl[index]
                + ", expected: " + expValue
                + "\n\tpubStOuterFl["+ index +"]: got: " + pubStOuterFl[index]
                + ", expected: " + expValue);
        }
    }

    private void checkInnerFields(RedefClass redefCls, int expValue) {
        if (redefCls.prInnerFl != expValue
                || redefCls.packInnerFl != expValue
                || redefCls.pubInnerFl != expValue) {
            status = Consts.TEST_FAILED;
            log.complain("TEST FAILED: unexpected values of inner fields:"
                + "\n\tprInnerFl: got: " + redefCls.prInnerFl
                + ", expected: " + expValue
                + "\n\tpackInnerFl: got: " + redefCls.packInnerFl
                + ", expected: " + expValue
                + "\n\tpubInnerFl: got: " + redefCls.pubInnerFl
                + ", expected: " + expValue);
        }
    }

    /**
     * Load bytes of a redefining class.
     */
    private static byte[] loadFromClassFile(String signature) {
        String testPath = clfBasePath + File.separator
            + "newclass" + File.separator
            + signature.substring(1, signature.length()-1).replace('/', File.separatorChar)
            + ".class";
        File classFile = null;

        log.display("looking for class file at\n\t"
            + testPath + " ...\n");

        try {
            classFile = new File(testPath);
        } catch (NullPointerException e) {
            throw new Failure("FAILURE: failed to open class file, caught: "
                + e);
        }

        log.display("loading " + classFile.length()
            + " bytes from class file "+ testPath + " ...\n");
        byte[] buf = new byte[(int) classFile.length()];
        try {
            InputStream in = new FileInputStream(classFile);
            in.read(buf);
            in.close();
        } catch (Exception e) {
            throw new Failure("FAILURE: failed to load bytes from class file, caught: "
                + e);
        }

        log.display(classFile.length()
            + " bytes of a redefining class loaded\n");

        return buf;
    }

    /**
     * Inner class to be redefined in the agent.
     */
    class RedefClass extends Thread {
        boolean stopMe = false;
        int iter;

        // dummy inner fields to be accessed by the outer class
        private int prInnerFl = 1;
        int packInnerFl = 1;
        public int pubInnerFl = 1;

        RedefClass(int iter) {
            super("RedefClass");
            this.iter = iter;
        }

        /**
         * This method will have an active stack frame during
         * inner class redinition, so this version should continue
         * execution and thus outer fields should have values equal 1.
         */
        public void run() {
            log.display(this.getName() + ": started");
            synchronized(this) {
                this.notify(); // notify the main thread

                prOuterFl[0] = 1;
                packOuterFl[0] = 1;
                pubOuterFl[0] = 1;
                prStOuterFl[0] = 1;
                packStOuterFl[0] = 1;
                pubStOuterFl[0] = 1;

                while(!stopMe) {
                    warmUpMethod();

                    // get the main thread chance to obtain CPU
                    try {
                        this.wait(100);
                    } catch(Exception e) {}
                }
                log.display(this.getName() + ": exiting");
            }
        }

        void warmUpMethod() {
            prOuterFl[1] = 1;
            packOuterFl[1] = 1;
            pubOuterFl[1] = 1;
            prStOuterFl[1] = 1;
            packStOuterFl[1] = 1;
            pubStOuterFl[1] = 1;

            for (int i=0; i<iter; i++)
                redefclass028HotMethod(i);

            redefclass028HotMethod(10);
        }

        /**
         * Hotspot method to be compiled.
         */
        void redefclass028HotMethod(int i) {
            prOuterFl[2] = 1;
            packOuterFl[2] = 1;
            pubOuterFl[2] = 1;
            prStOuterFl[2] = 1;
            packStOuterFl[2] = 1;
            pubStOuterFl[2] = 1;

            int j=0;

            j +=i;
            j--;
            if (j >10)
                j = 0;

            notifyNativeAgent();
        }

        /**
         * A dummy method accessing a private field of the outer class.
         * It provokes compiler to add a synthetic access method
         * into the outer class.
         */
         void methAccessingOuterPrivateField() {
            prOuterFieldToBeAccessed = 'b';
         }
    }

}
