package dev.yog.nanite;

import dev.yog.bus.YogTypes;
import java.util.List;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.AnnotationNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.MethodNode;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;

/** A generated injection, and one fired, for the runtime's JVM test. */
public final class InjectProbe {
    private InjectProbe() {}

    static final String POINT = "test:point";

    private int counted(String s, int n) {
        return n;
    }

    static YogTypes.Inject inject() {
        return new YogTypes.Inject(POINT, "inject", InjectProbe.class.getName(), "counted", "(Ljava/lang/String;I)I",
                "HEAD", "", -1, "NONE", true, -1, "", "", false, List.of());
    }

    /** The mixin made of {@link #inject}: its parts, joined. */
    public static String generate() {
        ClassNode c = new ClassNode();
        new ClassReader(YogNaniteGen.mixin(0, inject()).bytes()).accept(c, 0);
        AnnotationNode mixin = c.invisibleAnnotations.get(0);
        MethodNode m = c.methods.get(0);
        AnnotationNode in = m.visibleAnnotations.get(0);
        return String.join("|", c.name, mixin.desc, String.valueOf(mixin.values), m.name, m.desc,
                in.desc, String.valueOf(in.values.get(1)));
    }

    /** {@link #inject} fired with ("x", 3): whether it was cancelled, and with what. */
    public static String fire() {
        YogInjections.points(List.of(POINT), List.of(new YogNaniteGen.Made(null, Type.INT_TYPE, null)));
        CallbackInfoReturnable<Integer> cir = new CallbackInfoReturnable<>("counted", true);
        YogInjections.fire(0, null, new Object[] {"x", 3}, cir, new Object[0]);
        return cir.isCancelled() + "," + cir.getReturnValue();
    }

    static final String MODIFY = "test:modify";
    static final String REDIRECT = "test:redirect";
    private static final String CALLEE = "Ldev/yog/nanite/InjectProbe;callee(I)I";

    static int base(int x) {
        return callee(x + 1);
    }

    public static int callee(int x) {
        return x * 10;
    }

    private static YogTypes.Inject at(String point, String kind, int index) {
        return new YogTypes.Inject(point, kind, InjectProbe.class.getName(), "base", "(I)I",
                "INVOKE", CALLEE, -1, "NONE", false, index, "", "", false, List.of());
    }

    /** A made mixin's handler {@code n} run on {@code x} (the class defined as a plain one). */
    private static Object run(YogNaniteGen.Made made, int n, int x) throws Exception {
        Class<?> c = new ClassLoader(InjectProbe.class.getClassLoader()) {
            Class<?> define(byte[] b) {
                return defineClass(null, b, 0, b.length);
            }
        }.define(made.bytes());
        java.lang.reflect.Method h = c.getDeclaredMethod("yog$nanite$" + n, int.class);
        h.setAccessible(true);
        return h.invoke(null, x);
    }

    /** A modify_arg and a redirect at {@link #base}'s call, made and run: their results, joined. */
    public static String modifyAndRedirect() throws Exception {
        YogNaniteGen.Made modify = YogNaniteGen.mixin(1, at(MODIFY, "modify_arg", 0));
        YogNaniteGen.Made redirect = YogNaniteGen.mixin(2, at(REDIRECT, "redirect", -1));
        YogInjections.points(List.of(POINT, MODIFY, REDIRECT),
                List.of(new YogNaniteGen.Made(null, Type.INT_TYPE, null), modify, redirect));
        return run(modify, 1, 5) + "," + run(redirect, 2, 5);
    }

    /** The redirect run again, its handlers gone: the call made as it was. */
    public static String redirectUnanswered() throws Exception {
        YogNaniteGen.Made redirect = YogNaniteGen.mixin(2, at("test:nobody", "redirect", -1));
        YogInjections.points(List.of(POINT, MODIFY, "test:nobody"),
                List.of(new YogNaniteGen.Made(null, Type.INT_TYPE, null), redirect, redirect));
        return String.valueOf(run(redirect, 2, 5));
    }

    static final String ADDED = "test:added";

    private static YogTypes.Inject adding(String point, String kind, String name, String desc, String target) {
        return new YogTypes.Inject(point, kind, InjectProbe.class.getName(), name, desc, "", target, -1, "", false, -1, "", "", false, List.of());
    }

    /** An added method called, an added field and an interface seen: joined. */
    public static String added() throws Exception {
        YogNaniteGen.Made method = YogNaniteGen.mixin(3, adding(ADDED, "add_method", "yogCharge", "()I", ""));
        YogNaniteGen.Made field = YogNaniteGen.mixin(4, adding("test:field", "add_field", "yog$probe$charge", "I", ""));
        YogNaniteGen.Made iface = YogNaniteGen.mixin(5, adding("test:iface", "implement", "", "", "java.lang.Runnable"));
        YogInjections.points(List.of(POINT, MODIFY, REDIRECT, ADDED, "test:field", "test:iface"),
                List.of(method, method, method, method, field, iface));
        Class<?> c = new ClassLoader(InjectProbe.class.getClassLoader()) {
            Class<?> define(byte[] b) {
                return defineClass(null, b, 0, b.length);
            }
        }.define(method.bytes());
        java.lang.reflect.Field unsafe = Class.forName("sun.misc.Unsafe").getDeclaredField("theUnsafe");
        unsafe.setAccessible(true);
        Object u = unsafe.get(null);
        Object instance = u.getClass().getMethod("allocateInstance", Class.class).invoke(u, c);
        Object charge = c.getMethod("yogCharge").invoke(instance);
        ClassNode f = new ClassNode();
        new ClassReader(field.bytes()).accept(f, 0);
        ClassNode i = new ClassNode();
        new ClassReader(iface.bytes()).accept(i, 0);
        return charge + "," + f.fields.get(0).name + f.fields.get(0).desc + "," + i.interfaces;
    }

    public static int stock = 5;

    static int readStock() {
        return stock;
    }

    static void store(int x) {
        stock = x;
    }

    private static final String STOCK = "Ldev/yog/nanite/InjectProbe;stock:I";

    private static YogTypes.Inject field(String point, String method, String desc, boolean write) {
        return new YogTypes.Inject(point, "redirect", InjectProbe.class.getName(), method, desc,
                "FIELD", STOCK, -1, "NONE", false, -1, "", "", write, List.of());
    }

    /** A field's read redirected (answered, then not) and its write (answered: not stored; then not: stored). */
    public static String fields() throws Exception {
        YogNaniteGen.Made read = YogNaniteGen.mixin(6, field("test:read", "readStock", "()I", false));
        YogNaniteGen.Made write = YogNaniteGen.mixin(7, field("test:write", "store", "(I)V", true));
        YogNaniteGen.Made unread = YogNaniteGen.mixin(8, field("test:nobody2", "readStock", "()I", false));
        YogNaniteGen.Made unwrite = YogNaniteGen.mixin(9, field("test:nobody3", "store", "(I)V", true));
        List<YogNaniteGen.Made> made = List.of(read, read, read, read, read, read, read, write, unread, unwrite);
        YogInjections.points(List.of(POINT, MODIFY, REDIRECT, ADDED, "f", "i", "test:read", "test:write", "test:nobody2", "test:nobody3"), made);
        Object answered = handler(read, 6).invoke(null);
        Object plain = handler(unread, 8).invoke(null);
        handler(write, 7).invoke(null, 99);
        int afterAnswered = stock;
        handler(unwrite, 9).invoke(null, 11);
        return answered + "," + plain + "," + afterAnswered + "," + stock;
    }

    private static java.lang.reflect.Method handler(YogNaniteGen.Made made, int n) throws Exception {
        Class<?> c = new ClassLoader(InjectProbe.class.getClassLoader()) {
            Class<?> define(byte[] b) {
                return defineClass(null, b, 0, b.length);
            }
        }.define(made.bytes());
        for (java.lang.reflect.Method m : c.getDeclaredMethods()) {
            if (m.getName().equals("yog$nanite$" + n)) {
                m.setAccessible(true);
                return m;
            }
        }
        throw new NoSuchMethodException("yog$nanite$" + n);
    }

    /** An inject asking for a local it writes (&I): the local doubled by a Rust handler, through its ref. */
    public static String locals() throws Exception {
        YogTypes.Inject asks = new YogTypes.Inject("test:locals", "inject", InjectProbe.class.getName(), "counted",
                "(Ljava/lang/String;I)I", "HEAD", "", -1, "NONE", true, -1, "", "", false, List.of("&I"));
        YogNaniteGen.Made made = YogNaniteGen.mixin(10, asks);
        List<String> keys = new java.util.ArrayList<>(java.util.Collections.nCopies(10, "unused"));
        keys.add("test:locals");
        YogInjections.points(keys, java.util.Collections.nCopies(11, made));
        int[] cell = {21};
        com.llamalad7.mixinextras.sugar.ref.LocalIntRef ref = new com.llamalad7.mixinextras.sugar.ref.LocalIntRef() {
            public int get() {
                return cell[0];
            }

            public void set(int v) {
                cell[0] = v;
            }
        };
        CallbackInfoReturnable<Integer> cir = new CallbackInfoReturnable<>("counted", true);
        java.lang.reflect.Method h = handler(made, 10);
        h.invoke(alloc(h.getDeclaringClass()), "x", 3, cir, ref);
        return String.valueOf(cell[0]);
    }

    /** An instance of {@code c} with no constructor run (a mixin class has none). */
    private static Object alloc(Class<?> c) throws Exception {
        java.lang.reflect.Field unsafe = Class.forName("sun.misc.Unsafe").getDeclaredField("theUnsafe");
        unsafe.setAccessible(true);
        Object u = unsafe.get(null);
        return u.getClass().getMethod("allocateInstance", Class.class).invoke(u, c);
    }
}
