/*
 * 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 a local 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. One is a java
 * thread creating and executing a local class in an instance
 * invoker method of the outer class. Second is a native thread
 * executing an agent code. Then the local class method
 * <code>redefclass030HotMethod()</code> is provoked to be compiled
 * (optimized), and thus the JVMTI event <code>CompiledMethodLoad</code>
 * should be sent. After that, the agent redefines the local class.
 * Different kinds of outer fields and a local variable of the outer
 * invoker method are accessed from executing methods in both versions
 * of the local class. Upon the redefinition, the main test thread verifies
 * via the outer fields/local variable values that the inner methods
 * have been redefined. It also verifies that the outer class
 * is still can access the local class fields after the redefinition.
 */
public class redefclass030 extends DebugeeClass {
    final static String REDEF_CLS_SIGNATURE =
        "Lnsk/jvmti/RedefineClasses/redefclass030$RedefClassWrapper$1RedefClass;";

    static int status = Consts.TEST_PASSED;

    static Log log = null;

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

    // dummy "outer-outer" fields to be changed by the local class
    private int prOuterOuterFl[] = {0,0};
    int packOuterOuterFl[] = {0,0};
    public int pubOuterOuterFl[] = {0,0};
    private static int prStOuterOuterFl[] = {0,0};
    static int packStOuterOuterFl[] = {0,0};
    public static int pubStOuterOuterFl[] = {0,0};

    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 redefclass030().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");
        RedefClassWrapper redefClsWrapper = new RedefClassWrapper(iter);
        redefClsWrapper.setDaemon(true);
        synchronized(redefClsWrapper) {
            redefClsWrapper.start();
            log.display("waiting for auxiliary thread readiness...\n");
            try {
                redefClsWrapper.wait(); // wait for the thread's readiness
            } catch (InterruptedException e) {
                redefClsWrapper.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(redefClsWrapper);
        }

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

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

        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(RedefClassWrapper redefClsWrapper) {
        final int SLEEP_MS = 20;
        int iterationsLeft = 10000 / SLEEP_MS;
        while (iterationsLeft >= 0) {
            // Check if new code has changed fields.
            if (prStOuterOuterFl[0] == 2 && prStOuterOuterFl[1] == 2 && redefClsWrapper.prOuterFl[1] == 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 checkOuterOuterFields(int index, int expValue) {
        if (prOuterOuterFl[index] != expValue
                || packOuterOuterFl[index] != expValue
                || pubOuterOuterFl[index] != expValue
                || prStOuterOuterFl[index] != expValue
                || packStOuterOuterFl[index] != expValue
                || pubStOuterOuterFl[index] != expValue) {
            status = Consts.TEST_FAILED;
            log.complain("TEST FAILED: unexpected values of outer fields of the class"
                + "\n\t\"" + this.toString() + "\":"
                + "\n\t\tprOuterOuterFl["+ index +"]: got: " + prOuterOuterFl[index]
                + ", expected: " + expValue
                + "\n\t\tpackOuterOuterFl["+ index +"]: got: " + packOuterOuterFl[index]
                + ", expected: " + expValue
                + "\n\t\tpubOuterOuterFl["+ index +"]: got: " + pubOuterOuterFl[index]
                + ", expected: " + expValue

                + "\n\t\tprStOuterOuterFl["+ index +"]: got: " + prStOuterOuterFl[index]
                + ", expected: " + expValue
                + "\n\t\tpackStOuterOuterFl["+ index +"]: got: " + packStOuterOuterFl[index]
                + ", expected: " + expValue
                + "\n\t\tpubStOuterOuterFl["+ index +"]: got: " + pubStOuterOuterFl[index]
                + ", expected: " + expValue);
        }
    }

    private void checkOuterFields(RedefClassWrapper redefClsWrapper, int index, int expValue) {
        if (redefClsWrapper.prOuterFl[index] != expValue
                || redefClsWrapper.packOuterFl[index] != expValue
                || redefClsWrapper.pubOuterFl[index] != expValue) {
            status = Consts.TEST_FAILED;
            log.complain("TEST FAILED: unexpected values of outer fields of the class"
                + "\n\t\"" + redefClsWrapper.toString() + "\":"
                + "\n\t\tprOuterFl["+ index +"]: got: " + redefClsWrapper.prOuterFl[index]
                + ", expected: " + expValue
                + "\n\t\tpackOuterFl["+ index +"]: got: " + redefClsWrapper.packOuterFl[index]
                + ", expected: " + expValue
                + "\n\t\tpubOuterFl["+ index +"]: got: " + redefClsWrapper.pubOuterFl[index]
                + ", 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;
    }

    /**
     * Class executing the local inner class to be redefined.
     */
    class RedefClassWrapper extends Thread {
        boolean stopMe = false;
        int iter;

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

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

        public void run() {
            // dummy local vars to be changed by the local class
            final int outerLocalVar[] = {0,0};

            /**
             * Local inner class to be redefined in an agent.
             */
            class RedefClass {
                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) {
                    this.iter = iter;
                }

                void warmUpMethod() {
                    prOuterOuterFl[0] = 1;
                    packOuterOuterFl[0] = 1;
                    pubOuterOuterFl[0] = 1;
                    prStOuterOuterFl[0] = 1;
                    packStOuterOuterFl[0] = 1;
                    pubStOuterOuterFl[0] = 1;

                    prOuterFl[0] = 1;
                    packOuterFl[0] = 1;
                    pubOuterFl[0] = 1;

                    outerLocalVar[0] = 1;

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

                    redefclass030HotMethod(10);
                }

                /**
                 * Hotspot method to be compiled.
                 */
                void redefclass030HotMethod(int i) {
                    prOuterOuterFl[1] = 1;
                    packOuterOuterFl[1] = 1;
                    pubOuterOuterFl[1] = 1;
                    prStOuterOuterFl[1] = 1;
                    packStOuterOuterFl[1] = 1;
                    pubStOuterOuterFl[1] = 1;

                    prOuterFl[1] = 1;
                    packOuterFl[1] = 1;
                    pubOuterFl[1] = 1;

                    outerLocalVar[1] = 1;

                    int j=0;

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

                    notifyNativeAgent();
                }
            }
            /*************************************************************/

            log.display(this.getName() + ": started");

            RedefClass redefCls = new RedefClass(iter);

            synchronized(this) {
                this.notify(); // notify the main thread

                while(!stopMe) {
                    redefCls.warmUpMethod();

                    // get the main thread chance to obtain CPU
                    try {
                        this.wait(100);
                    } catch(Exception e) {}
                }

                if (redefCls.prInnerFl != 1
                        || redefCls.packInnerFl != 1
                        || redefCls.pubInnerFl != 1) {
                    status = Consts.TEST_FAILED;
                    log.complain("TEST FAILED: unexpected values of inner fields of the local class"
                        + "\n\t\"" + this.toString() + "\":"
                        + "\n\t\tprInnerFl: got: " + redefCls.prInnerFl
                        + ", expected: 1"
                        + "\n\t\tpackInnerFl: got: " + redefCls.packInnerFl
                        + ", expected: 1"
                        + "\n\t\tpubInnerFl: got: " + redefCls.pubInnerFl
                        + ", expected: 1");
                }

                log.display(this.getName() + ": exiting");
            }
        }
    }

}