import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.InsnNode;
import org.objectweb.asm.tree.InsnList;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.Opcodes;

/**
 * Patches zombie.vehicles.BaseVehicle so addPointConstraint() no longer force-breaks
 * both vehicles before creating a new constraint.
 */
public final class BaseVehicleConstraintPatch {
    private static final String TARGET_NAME = "addPointConstraint";
    private static final String BREAK_DESC_OBJECT_BOOL = "(ZLjava/lang/Boolean;)V";
    private static final String BREAK_DESC_PRIMITIVE_BOOL = "(ZZ)V";
    private static final String BASE_VEHICLE_OWNER = "zombie/vehicles/BaseVehicle";

    private BaseVehicleConstraintPatch() {
    }

    public static void main(String[] args) throws Exception {
        if (args.length != 2) {
            System.err.println("Usage: BaseVehicleConstraintPatch <input BaseVehicle.class> <output BaseVehicle.class>");
            System.exit(2);
        }

        Path input = Paths.get(args[0]);
        Path output = Paths.get(args[1]);
        byte[] original = Files.readAllBytes(input);

        ClassNode classNode = new ClassNode();
        new ClassReader(original).accept(classNode, 0);

        int removedCalls = 0;
        int inspectedAddPointMethods = 0;
        for (MethodNode method : classNode.methods) {
            if (!TARGET_NAME.equals(method.name) || !isTargetAddPointConstraint(method.desc)) {
                continue;
            }
            inspectedAddPointMethods++;
            removedCalls += patchAddPointConstraint(method);
        }

        if (removedCalls < 2) {
            throw new IllegalStateException(
                    "Expected to remove 2 breakConstraint calls, removed "
                            + removedCalls
                            + " (inspected addPoint methods: "
                            + inspectedAddPointMethods
                            + ")");
        }

        ClassWriter writer = new ClassWriter(0);
        classNode.accept(writer);

        Files.createDirectories(output.getParent());
        Files.write(output, writer.toByteArray());
        System.out.println("Patched BaseVehicle.class; removed breakConstraint calls: " + removedCalls);
    }

    private static boolean isTargetAddPointConstraint(String methodDesc) {
        // We only want the 5-arg overload:
        // (IsoPlayer, BaseVehicle, String, String, boolean|Boolean) -> void
        return "(Lzombie/characters/IsoPlayer;Lzombie/vehicles/BaseVehicle;Ljava/lang/String;Ljava/lang/String;Z)V"
                        .equals(methodDesc)
                || "(Lzombie/characters/IsoPlayer;Lzombie/vehicles/BaseVehicle;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;)V"
                        .equals(methodDesc);
    }

    private static int patchAddPointConstraint(MethodNode method) {
        int patched = 0;
        InsnList insns = method.instructions;

        for (AbstractInsnNode node = insns.getFirst(); node != null; ) {
            AbstractInsnNode next = node.getNext();
            if (node instanceof MethodInsnNode call
                    && BASE_VEHICLE_OWNER.equals(call.owner)
                    && "breakConstraint".equals(call.name)) {
                if (!(BREAK_DESC_OBJECT_BOOL.equals(call.desc)
                        || BREAK_DESC_PRIMITIVE_BOOL.equals(call.desc))) {
                    node = next;
                    continue;
                }
                // Keep stack-map frames valid by preserving stack effect:
                // breakConstraint(...) consumes objectref + 2 args and returns void.
                // Replace invoke with POP2 + POP (consume 3 category-1 stack slots).
                InsnList replacement = new InsnList();
                replacement.add(new InsnNode(Opcodes.POP2));
                replacement.add(new InsnNode(Opcodes.POP));
                insns.insert(node, replacement);
                insns.remove(node);
                patched++;
            }
            node = next;
        }

        return patched;
    }
}
