package p.studio.compiler.backend.bytecode;

import org.junit.jupiter.api.Test;
import p.studio.utilities.structures.ReadOnlyList;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;

import static org.junit.jupiter.api.Assertions.*;

class BytecodeEmitterTest {

    @Test
    void emitMustDeduplicateSyscallsByCanonicalIdentityAndPreserveFirstOccurrenceOrder() {
        final var declA = new BytecodeModule.SyscallDecl("gfx", "draw_line", 1, 2, 0);
        final var declB = new BytecodeModule.SyscallDecl("audio", "play", 1, 1, 0);
        final var emitter = new BytecodeEmitter();

        final var module = emitter.emit(new BytecodeEmitter.EmissionPlan(
                0,
                ReadOnlyList.empty(),
                ReadOnlyList.empty(),
                ReadOnlyList.from(
                        new BytecodeEmitter.FunctionPlan(
                                "main",
                                0,
                                0,
                                0,
                                4,
                                ReadOnlyList.from(
                                        BytecodeEmitter.Operation.hostcall(declA, 2, 0),
                                        BytecodeEmitter.Operation.hostcall(declA, 2, 0),
                                        BytecodeEmitter.Operation.halt())),
                        new BytecodeEmitter.FunctionPlan(
                                "aux",
                                0,
                                0,
                                0,
                                2,
                                ReadOnlyList.from(
                                        BytecodeEmitter.Operation.hostcall(declB, 1, 0),
                                        BytecodeEmitter.Operation.ret())))));

        assertEquals(2, module.syscalls().size());
        assertEquals("gfx", module.syscalls().get(0).module());
        assertEquals("audio", module.syscalls().get(1).module());
        assertEquals(0x71, readU16(module.code(), 0));
        assertEquals(0, readU32(module.code(), 2));
        assertEquals(0x71, readU16(module.code(), 6));
        assertEquals(0, readU32(module.code(), 8));
        assertEquals(0x71, readU16(module.code(), 14));
        assertEquals(1, readU32(module.code(), 16));
    }

    @Test
    void emitMustRejectRawSyscallInPreloadPlan() {
        final var emitter = new BytecodeEmitter();
        final var thrown = assertThrows(BytecodeMarshalingException.class, () -> emitter.emit(new BytecodeEmitter.EmissionPlan(
                0,
                ReadOnlyList.empty(),
                ReadOnlyList.empty(),
                ReadOnlyList.from(
                        new BytecodeEmitter.FunctionPlan(
                                "main",
                                0,
                                0,
                                0,
                                1,
                                ReadOnlyList.from(BytecodeEmitter.Operation.rawSyscall(0x1001)))))));

        assertEquals(BytecodeMarshalingErrorCode.MARSHAL_LINKAGE_RAW_SYSCALL_IN_PRELOAD, thrown.code());
    }

    @Test
    void emitMustRejectAbiMismatch() {
        final var declA = new BytecodeModule.SyscallDecl("gfx", "draw_line", 1, 2, 0);
        final var emitter = new BytecodeEmitter();
        final var thrown = assertThrows(BytecodeMarshalingException.class, () -> emitter.emit(new BytecodeEmitter.EmissionPlan(
                0,
                ReadOnlyList.empty(),
                ReadOnlyList.empty(),
                ReadOnlyList.from(
                        new BytecodeEmitter.FunctionPlan(
                                "main",
                                0,
                                0,
                                0,
                                1,
                                ReadOnlyList.from(BytecodeEmitter.Operation.hostcall(declA, 1, 0)))))));

        assertEquals(BytecodeMarshalingErrorCode.MARSHAL_LINKAGE_HOST_ABI_MISMATCH, thrown.code());
    }

    @Test
    void emitIntrinsicMustNotCreateSyscalls() {
        final var emitter = new BytecodeEmitter();
        final var module = emitter.emit(new BytecodeEmitter.EmissionPlan(
                0,
                ReadOnlyList.empty(),
                ReadOnlyList.empty(),
                ReadOnlyList.from(
                        new BytecodeEmitter.FunctionPlan(
                                "main",
                                0,
                                0,
                                0,
                                1,
                                ReadOnlyList.from(
                                        BytecodeEmitter.Operation.intrinsic(0x2000),
                                        BytecodeEmitter.Operation.halt())))));

        assertTrue(module.syscalls().isEmpty());
        assertEquals(0x72, readU16(module.code(), 0));
        assertEquals(0x2000, readU32(module.code(), 2));
    }

    @Test
    void emitMustProducePcToSpanForEveryInstructionStartWhenSpansAreProvided() {
        final var emitter = new BytecodeEmitter();
        final var module = emitter.emit(new BytecodeEmitter.EmissionPlan(
                0,
                ReadOnlyList.empty(),
                ReadOnlyList.empty(),
                ReadOnlyList.from(
                        new BytecodeEmitter.FunctionPlan(
                                "main",
                                0,
                                0,
                                0,
                                4,
                                ReadOnlyList.from(
                                        BytecodeEmitter.Operation.halt(new BytecodeModule.SourceSpan(1, 0, 1)),
                                        BytecodeEmitter.Operation.callFunc(0, new BytecodeModule.SourceSpan(1, 2, 3)),
                                        BytecodeEmitter.Operation.intrinsic(0x2000, new BytecodeModule.SourceSpan(1, 4, 5)),
                                        BytecodeEmitter.Operation.ret(new BytecodeModule.SourceSpan(1, 6, 7)))))));

        assertEquals(4, module.debugInfo().pcToSpan().size());
        assertEquals(0, module.debugInfo().pcToSpan().get(0).pc());
        assertEquals(2, module.debugInfo().pcToSpan().get(1).pc());
        assertEquals(8, module.debugInfo().pcToSpan().get(2).pc());
        assertEquals(14, module.debugInfo().pcToSpan().get(3).pc());
    }

    @Test
    void emitMustEncodeJumpOpcodesWithU32Immediate() {
        final var emitter = new BytecodeEmitter();
        final var module = emitter.emit(new BytecodeEmitter.EmissionPlan(
                0,
                ReadOnlyList.empty(),
                ReadOnlyList.empty(),
                ReadOnlyList.from(new BytecodeEmitter.FunctionPlan(
                        "main",
                        0,
                        0,
                        0,
                        2,
                        ReadOnlyList.from(
                                BytecodeEmitter.Operation.jmp(12, new BytecodeModule.SourceSpan(1, 0, 1)),
                                BytecodeEmitter.Operation.jmpIfFalse(2, new BytecodeModule.SourceSpan(1, 2, 3)),
                                BytecodeEmitter.Operation.ret(new BytecodeModule.SourceSpan(1, 4, 5)))))));

        assertEquals(0x02, readU16(module.code(), 0));
        assertEquals(12, readU32(module.code(), 2));
        assertEquals(0x03, readU16(module.code(), 6));
        assertEquals(2, readU32(module.code(), 8));
    }

    @Test
    void emitMustDeduplicateConstPoolAndExportsByFirstOccurrence() {
        final var emitter = new BytecodeEmitter();
        final var module = emitter.emit(new BytecodeEmitter.EmissionPlan(
                0,
                ReadOnlyList.from(
                        new BytecodeModule.Int32Constant(7),
                        new BytecodeModule.Int32Constant(7),
                        new BytecodeModule.StringConstant("abc"),
                        new BytecodeModule.StringConstant("abc")),
                ReadOnlyList.from(
                        new BytecodeModule.Export("main", 0),
                        new BytecodeModule.Export("main", 0),
                        new BytecodeModule.Export("aux", 1)),
                ReadOnlyList.from(
                        new BytecodeEmitter.FunctionPlan(
                                "main",
                                0,
                                0,
                                0,
                                1,
                                ReadOnlyList.from(BytecodeEmitter.Operation.ret())))));

        assertEquals(2, module.constPool().size());
        assertEquals(2, module.exports().size());
        assertEquals("main", module.exports().get(0).symbol());
        assertEquals("aux", module.exports().get(1).symbol());
    }

    @Test
    void emitMustRemainDeterministicAfterInterning() {
        final var emitter = new BytecodeEmitter();
        final var plan = new BytecodeEmitter.EmissionPlan(
                0,
                ReadOnlyList.from(
                        new BytecodeModule.Int32Constant(1),
                        new BytecodeModule.Int32Constant(1)),
                ReadOnlyList.from(
                        new BytecodeModule.Export("main", 0),
                        new BytecodeModule.Export("main", 0)),
                ReadOnlyList.from(
                        new BytecodeEmitter.FunctionPlan(
                                "main",
                                0,
                                0,
                                0,
                                1,
                                ReadOnlyList.from(BytecodeEmitter.Operation.halt()))));

        final var first = emitter.emit(plan).serialize();
        final var second = emitter.emit(plan).serialize();
        assertArrayEquals(first, second);
    }

    private static int readU16(final byte[] bytes, final int offset) {
        return ByteBuffer.wrap(bytes, offset, 2).order(ByteOrder.LITTLE_ENDIAN).getShort() & 0xFFFF;
    }

    private static int readU32(final byte[] bytes, final int offset) {
        return ByteBuffer.wrap(bytes, offset, 4).order(ByteOrder.LITTLE_ENDIAN).getInt();
    }
}
