/*
 * Copyright (c) 2015, 2023, 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.
 */

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandleInfo;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.invoke.VarHandle;
import java.lang.invoke.WrongMethodTypeException;
import java.lang.reflect.Method;
import java.nio.ReadOnlyBufferException;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;

import static java.util.stream.Collectors.toList;
import static org.testng.Assert.*;

abstract class VarHandleBaseTest {
    static final int ITERS = Integer.getInteger("iters", 1);

    // More resilience for Weak* tests. These operations may spuriously
    // fail, and so we do several attempts with delay on failure.
    // Be mindful of worst-case total time on test, which would be at
    // roughly (delay*attempts) milliseconds.
    //
    static final int WEAK_ATTEMPTS = Integer.getInteger("weakAttempts", 100);
    static final int WEAK_DELAY_MS = Math.max(1, Integer.getInteger("weakDelay", 1));

    interface ThrowingRunnable {
        void run() throws Throwable;
    }

    static void checkUOE(ThrowingRunnable r) {
        checkWithThrowable(UnsupportedOperationException.class, null, r);
    }

    static void checkUOE(Object message, ThrowingRunnable r) {
        checkWithThrowable(UnsupportedOperationException.class, message, r);
    }

    static void checkROBE(ThrowingRunnable r) {
        checkWithThrowable(ReadOnlyBufferException.class, null, r);
    }

    static void checkROBE(Object message, ThrowingRunnable r) {
        checkWithThrowable(ReadOnlyBufferException.class, message, r);
    }

    static void checkIOOBE(ThrowingRunnable r) {
        checkWithThrowable(IndexOutOfBoundsException.class, null, r);
    }

    static void checkIOOBE(Object message, ThrowingRunnable r) {
        checkWithThrowable(IndexOutOfBoundsException.class, message, r);
    }

    static void checkAIOOBE(ThrowingRunnable r) {
        checkWithThrowable(ArrayIndexOutOfBoundsException.class, null, r);
    }

    static void checkAIOOBE(Object message, ThrowingRunnable r) {
        checkWithThrowable(ArrayIndexOutOfBoundsException.class, message, r);
    }

    static void checkASE(ThrowingRunnable r) {
        checkWithThrowable(ArrayStoreException.class, null, r);
    }

    static void checkASE(Object message, ThrowingRunnable r) {
        checkWithThrowable(ArrayStoreException.class, message, r);
    }

    static void checkISE(ThrowingRunnable r) {
        checkWithThrowable(IllegalStateException.class, null, r);
    }

    static void checkISE(Object message, ThrowingRunnable r) {
        checkWithThrowable(IllegalStateException.class, message, r);
    }

    static void checkIAE(ThrowingRunnable r) {
        checkWithThrowable(IllegalAccessException.class, null, r);
    }

    static void checkIAE(Object message, ThrowingRunnable r) {
        checkWithThrowable(IllegalAccessException.class, message, r);
    }

    static void checkWMTE(ThrowingRunnable r) {
        checkWithThrowable(WrongMethodTypeException.class, null, r);
    }

    static void checkWMTE(Object message, ThrowingRunnable r) {
        checkWithThrowable(WrongMethodTypeException.class, message, r);
    }

    static void checkCCE(ThrowingRunnable r) {
        checkWithThrowable(ClassCastException.class, null, r);
    }

    static void checkCCE(Object message, ThrowingRunnable r) {
        checkWithThrowable(ClassCastException.class, message, r);
    }

    static void checkNPE(ThrowingRunnable r) {
        checkWithThrowable(NullPointerException.class, null, r);
    }

    static void checkNPE(Object message, ThrowingRunnable r) {
        checkWithThrowable(NullPointerException.class, message, r);
    }

    static void checkWithThrowable(Class<? extends Throwable> re,
                                   Object message,
                                   ThrowingRunnable r) {
        Throwable _e = null;
        try {
            r.run();
        }
        catch (Throwable e) {
            _e = e;
        }
        message = message == null ? "" : message + ". ";
        assertNotNull(_e, String.format("%sNo throwable thrown. Expected %s", message, re));
        if (!re.isInstance(_e)) {
            fail(String.format("%sIncorrect throwable thrown, %s. Expected %s", message, _e, re), _e);
        }
    }


    enum TestAccessType {
        GET,
        SET,
        COMPARE_AND_SET,
        COMPARE_AND_EXCHANGE,
        GET_AND_SET,
        GET_AND_ADD,
        GET_AND_BITWISE;
    }

    enum TestAccessMode {
        GET(TestAccessType.GET),
        SET(TestAccessType.SET),
        GET_VOLATILE(TestAccessType.GET),
        SET_VOLATILE(TestAccessType.SET),
        GET_ACQUIRE(TestAccessType.GET),
        SET_RELEASE(TestAccessType.SET),
        GET_OPAQUE(TestAccessType.GET),
        SET_OPAQUE(TestAccessType.SET),
        COMPARE_AND_SET(TestAccessType.COMPARE_AND_SET),
        COMPARE_AND_EXCHANGE(TestAccessType.COMPARE_AND_EXCHANGE),
        COMPARE_AND_EXCHANGE_ACQUIRE(TestAccessType.COMPARE_AND_EXCHANGE),
        COMPARE_AND_EXCHANGE_RELEASE(TestAccessType.COMPARE_AND_EXCHANGE),
        WEAK_COMPARE_AND_SET_PLAIN(TestAccessType.COMPARE_AND_SET),
        WEAK_COMPARE_AND_SET(TestAccessType.COMPARE_AND_SET),
        WEAK_COMPARE_AND_SET_ACQUIRE(TestAccessType.COMPARE_AND_SET),
        WEAK_COMPARE_AND_SET_RELEASE(TestAccessType.COMPARE_AND_SET),
        GET_AND_SET(TestAccessType.GET_AND_SET),
        GET_AND_SET_ACQUIRE(TestAccessType.GET_AND_SET),
        GET_AND_SET_RELEASE(TestAccessType.GET_AND_SET),
        GET_AND_ADD(TestAccessType.GET_AND_ADD),
        GET_AND_ADD_ACQUIRE(TestAccessType.GET_AND_ADD),
        GET_AND_ADD_RELEASE(TestAccessType.GET_AND_ADD),
        GET_AND_BITWISE_OR(TestAccessType.GET_AND_BITWISE),
        GET_AND_BITWISE_OR_ACQUIRE(TestAccessType.GET_AND_BITWISE),
        GET_AND_BITWISE_OR_RELEASE(TestAccessType.GET_AND_BITWISE),
        GET_AND_BITWISE_AND(TestAccessType.GET_AND_BITWISE),
        GET_AND_BITWISE_AND_ACQUIRE(TestAccessType.GET_AND_BITWISE),
        GET_AND_BITWISE_AND_RELEASE(TestAccessType.GET_AND_BITWISE),
        GET_AND_BITWISE_XOR(TestAccessType.GET_AND_BITWISE),
        GET_AND_BITWISE_XOR_ACQUIRE(TestAccessType.GET_AND_BITWISE),
        GET_AND_BITWISE_XOR_RELEASE(TestAccessType.GET_AND_BITWISE),
        ;

        final TestAccessType at;
        final boolean isPolyMorphicInReturnType;
        final Class<?> returnType;

        TestAccessMode(TestAccessType at) {
            this.at = at;

            try {
                VarHandle.AccessMode vh_am = toAccessMode();
                Method m = VarHandle.class.getMethod(vh_am.methodName(), Object[].class);
                this.returnType = m.getReturnType();
                isPolyMorphicInReturnType = returnType != Object.class;
            }
            catch (Exception e) {
                throw new Error(e);
            }
        }

        boolean isOfType(TestAccessType at) {
            return this.at == at;
        }

        VarHandle.AccessMode toAccessMode() {
            return VarHandle.AccessMode.valueOf(name());
        }
    }

    static List<TestAccessMode> testAccessModes() {
        return Stream.of(TestAccessMode.values()).collect(toList());
    }

    static List<TestAccessMode> testAccessModesOfType(TestAccessType... ats) {
        Stream<TestAccessMode> s = Stream.of(TestAccessMode.values());
        return s.filter(e -> Stream.of(ats).anyMatch(e::isOfType))
                .collect(toList());
    }

    static List<VarHandle.AccessMode> accessModes() {
        return Stream.of(VarHandle.AccessMode.values()).collect(toList());
    }

    static List<VarHandle.AccessMode> accessModesOfType(TestAccessType... ats) {
        Stream<TestAccessMode> s = Stream.of(TestAccessMode.values());
        return s.filter(e -> Stream.of(ats).anyMatch(e::isOfType))
                .map(TestAccessMode::toAccessMode)
                .collect(toList());
    }

    static MethodHandle toMethodHandle(VarHandle vh, TestAccessMode tam, MethodType mt) {
        return vh.toMethodHandle(tam.toAccessMode());
    }

    static MethodHandle findVirtual(VarHandle vh, TestAccessMode tam, MethodType mt) {
        MethodHandle mh;
        try {
            mh = MethodHandles.publicLookup().
                    findVirtual(VarHandle.class,
                                tam.toAccessMode().methodName(),
                                mt);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return bind(vh, mh, mt);
    }

    static MethodHandle varHandleInvoker(VarHandle vh, TestAccessMode tam, MethodType mt) {
        MethodHandle mh = MethodHandles.varHandleInvoker(
                tam.toAccessMode(),
                mt);

        return bind(vh, mh, mt);
    }

    static MethodHandle varHandleExactInvoker(VarHandle vh, TestAccessMode tam, MethodType mt) {
        MethodHandle mh = MethodHandles.varHandleExactInvoker(
                tam.toAccessMode(),
                mt);

        return bind(vh, mh, mt);
    }

    private static MethodHandle bind(VarHandle vh, MethodHandle mh, MethodType emt) {
        assertEquals(mh.type(), emt.insertParameterTypes(0, VarHandle.class),
                     "MethodHandle type differs from access mode type");

        MethodHandleInfo info = MethodHandles.lookup().revealDirect(mh);
        assertEquals(info.getMethodType(), emt,
                     "MethodHandleInfo method type differs from access mode type");

        return mh.bindTo(vh);
    }

    private interface TriFunction<T, U, V, R> {
        R apply(T t, U u, V v);
    }

    enum VarHandleToMethodHandle {
        VAR_HANDLE_TO_METHOD_HANDLE(
                "VarHandle.toMethodHandle",
                true,
                VarHandleBaseTest::toMethodHandle),
        METHOD_HANDLES_LOOKUP_FIND_VIRTUAL(
                "Lookup.findVirtual",
                false,
                VarHandleBaseTest::findVirtual),
        METHOD_HANDLES_VAR_HANDLE_INVOKER(
                "MethodHandles.varHandleInvoker",
                false,
                VarHandleBaseTest::varHandleInvoker),
        METHOD_HANDLES_VAR_HANDLE_EXACT_INVOKER(
                "MethodHandles.varHandleExactInvoker",
                true,
                VarHandleBaseTest::varHandleExactInvoker);

        final String desc;
        final boolean isExact;
        final TriFunction<VarHandle, TestAccessMode, MethodType, MethodHandle> f;

        VarHandleToMethodHandle(String desc, boolean isExact,
                                TriFunction<VarHandle, TestAccessMode, MethodType, MethodHandle> f) {
            this.desc = desc;
            this.f = f;
            this.isExact = isExact;
        }

        MethodHandle apply(VarHandle vh, TestAccessMode am, MethodType mt) {
            return f.apply(vh, am, mt);
        }

        @Override
        public String toString() {
            return desc;
        }
    }

    static class Handles {
        static class AccessModeAndType {
            final TestAccessMode tam;
            final MethodType t;

            public AccessModeAndType(TestAccessMode tam, MethodType t) {
                this.tam = tam;
                this.t = t;
            }

            @Override
            public boolean equals(Object o) {
                if (this == o) return true;
                if (o == null || getClass() != o.getClass()) return false;

                AccessModeAndType x = (AccessModeAndType) o;

                if (tam != x.tam) return false;
                if (t != null ? !t.equals(x.t) : x.t != null) return false;

                return true;
            }

            @Override
            public int hashCode() {
                int result = tam != null ? tam.hashCode() : 0;
                result = 31 * result + (t != null ? t.hashCode() : 0);
                return result;
            }
        }

        final VarHandle vh;
        final VarHandleToMethodHandle f;
        final EnumMap<TestAccessMode, MethodType> amToType;
        final Map<AccessModeAndType, MethodHandle> amToHandle;

        Handles(VarHandle vh, VarHandleToMethodHandle f) throws Exception {
            this.vh = vh;
            this.f = f;
            this.amToHandle = new HashMap<>();

            amToType = new EnumMap<>(TestAccessMode.class);
            for (TestAccessMode am : testAccessModes()) {
                amToType.put(am, vh.accessModeType(am.toAccessMode()));
            }
        }

        MethodHandle get(TestAccessMode am) {
            return get(am, amToType.get(am));
        }

        MethodHandle get(TestAccessMode am, MethodType mt) {
            AccessModeAndType amt = new AccessModeAndType(am, mt);
            return amToHandle.computeIfAbsent(
                    amt, k -> f.apply(vh, am, mt));
        }

        Class<? extends Throwable> getWMTEOOrOther(Class<? extends Throwable> c) {
            return f.isExact ? WrongMethodTypeException.class : c;
        }

        void checkWMTEOrCCE(ThrowingRunnable r) {
            checkWithThrowable(getWMTEOOrOther(ClassCastException.class), null, r);
        }

    }

    interface AccessTestAction<T> {
        void action(T t) throws Throwable;
    }

    static abstract class AccessTestCase<T> {
        final String desc;
        final AccessTestAction<T> ata;
        final boolean loop;

        AccessTestCase(String desc, AccessTestAction<T> ata, boolean loop) {
            this.desc = desc;
            this.ata = ata;
            this.loop = loop;
        }

        boolean requiresLoop() {
            return loop;
        }

        abstract T get() throws Exception;

        void testAccess(T t) throws Throwable {
            ata.action(t);
        }

        @Override
        public String toString() {
            return desc;
        }
    }

    static class VarHandleAccessTestCase extends AccessTestCase<VarHandle> {
        final VarHandle vh;

        VarHandleAccessTestCase(String desc, VarHandle vh, AccessTestAction<VarHandle> ata) {
            this(desc, vh, ata, true);
        }

        VarHandleAccessTestCase(String desc, VarHandle vh, AccessTestAction<VarHandle> ata, boolean loop) {
            super("VarHandle -> " + desc, ata, loop);
            this.vh = vh;
        }

        @Override
        VarHandle get() {
            return vh;
        }
    }

    static class MethodHandleAccessTestCase extends AccessTestCase<Handles> {
        final VarHandle vh;
        final VarHandleToMethodHandle f;

        MethodHandleAccessTestCase(String desc, VarHandle vh, VarHandleToMethodHandle f, AccessTestAction<Handles> ata) {
            this(desc, vh, f, ata, true);
        }

        MethodHandleAccessTestCase(String desc, VarHandle vh, VarHandleToMethodHandle f, AccessTestAction<Handles> ata, boolean loop) {
            super("VarHandle -> " + f.toString() + " -> " + desc, ata, loop);
            this.vh = vh;
            this.f = f;
        }

        @Override
        Handles get() throws Exception {
            return new Handles(vh, f);
        }
    }

    static void testTypes(VarHandle vh) {
        List<Class<?>> pts = vh.coordinateTypes();

        for (TestAccessMode accessMode : testAccessModes()) {
            MethodType amt = vh.accessModeType(accessMode.toAccessMode());

            assertEquals(amt.parameterList().subList(0, pts.size()), pts);
        }

        for (TestAccessMode testAccessMode : testAccessModesOfType(TestAccessType.GET)) {
            MethodType mt = vh.accessModeType(testAccessMode.toAccessMode());
            assertEquals(mt.returnType(), vh.varType());
            assertEquals(mt.parameterList(), pts);
        }

        for (TestAccessMode testAccessMode : testAccessModesOfType(TestAccessType.SET)) {
            MethodType mt = vh.accessModeType(testAccessMode.toAccessMode());
            assertEquals(mt.returnType(), void.class);
            assertEquals(mt.parameterType(mt.parameterCount() - 1), vh.varType());
        }

        for (TestAccessMode testAccessMode : testAccessModesOfType(TestAccessType.COMPARE_AND_SET)) {
            MethodType mt = vh.accessModeType(testAccessMode.toAccessMode());
            assertEquals(mt.returnType(), boolean.class);
            assertEquals(mt.parameterType(mt.parameterCount() - 1), vh.varType());
            assertEquals(mt.parameterType(mt.parameterCount() - 2), vh.varType());
        }

        for (TestAccessMode testAccessMode : testAccessModesOfType(TestAccessType.COMPARE_AND_EXCHANGE)) {
            MethodType mt = vh.accessModeType(testAccessMode.toAccessMode());
            assertEquals(mt.returnType(), vh.varType());
            assertEquals(mt.parameterType(mt.parameterCount() - 1), vh.varType());
            assertEquals(mt.parameterType(mt.parameterCount() - 2), vh.varType());
        }

        for (TestAccessMode testAccessMode : testAccessModesOfType(TestAccessType.GET_AND_SET, TestAccessType.GET_AND_ADD)) {
            MethodType mt = vh.accessModeType(testAccessMode.toAccessMode());
            assertEquals(mt.returnType(), vh.varType());
            assertEquals(mt.parameterType(mt.parameterCount() - 1), vh.varType());
        }
    }

    static void weakDelay() {
        try {
            if (WEAK_DELAY_MS > 0) {
                Thread.sleep(WEAK_DELAY_MS);
            }
        } catch (InterruptedException ie) {
            // Do nothing.
        }
    }
}
