package p.studio.compiler.backend;

import org.junit.jupiter.api.Test;
import p.studio.compiler.backend.bytecode.BytecodeEmitter;
import p.studio.compiler.backend.irvm.LowerToIRVMService;
import p.studio.compiler.models.IRBackend;
import p.studio.compiler.models.IRBackendExecutableFunction;
import p.studio.compiler.source.Span;
import p.studio.compiler.source.identifiers.CallableId;
import p.studio.compiler.source.identifiers.FileId;
import p.studio.compiler.source.identifiers.IntrinsicId;
import p.studio.compiler.source.tables.IntrinsicReference;
import p.studio.utilities.structures.ReadOnlyList;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HexFormat;

import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;

class GoldenArtifactsTest {

    @Test
    void backendGoldenArtifactsMustRemainDeterministic() throws IOException {
        final var backend = fixtureBackend();
        final var lower = new LowerToIRVMService();

        final var firstProgram = lower.lower(backend, "core-v1");
        final var secondProgram = lower.lower(backend, "core-v1");
        final var firstSnapshot = renderIrvmSnapshot(firstProgram);
        final var secondSnapshot = renderIrvmSnapshot(secondProgram);
        assertEquals(firstSnapshot, secondSnapshot);

        final var firstBytecode = new BytecodeEmitter().emit(firstProgram.coherentEmissionPlan()).serialize();
        final var secondBytecode = new BytecodeEmitter().emit(secondProgram.coherentEmissionPlan()).serialize();
        assertArrayEquals(firstBytecode, secondBytecode);

        assertGolden("golden/irvm-program.txt", firstSnapshot);
        assertGolden("golden/bytecode-module.hex", HexFormat.of().formatHex(firstBytecode) + System.lineSeparator());
    }

    private IRBackend fixtureBackend() {
        return IRBackend.builder()
                .entryPointModuleId(new p.studio.compiler.source.identifiers.ModuleId(0))
                .executableFunctions(ReadOnlyList.from(new IRBackendExecutableFunction(
                        new FileId(1),
                        new p.studio.compiler.source.identifiers.ModuleId(0),
                        "main",
                        new CallableId(0),
                        0,
                        20,
                        0,
                        0,
                        0,
                        2,
                        ReadOnlyList.from(
                                new IRBackendExecutableFunction.Instruction(
                                        IRBackendExecutableFunction.InstructionKind.CALL_HOST,
                                        "",
                                        new IRBackendExecutableFunction.HostCallMetadata("gfx", "draw_pixel", 1, 0, 0),
                                        null,
                                        Span.none()),
                                new IRBackendExecutableFunction.Instruction(
                                        IRBackendExecutableFunction.InstructionKind.CALL_INTRINSIC,
                                        "",
                                        null,
                                        new IRBackendExecutableFunction.IntrinsicCallMetadata("input.pad", 1, new IntrinsicId(0)),
                                        0,
                                        0,
                                        Span.none()),
                                new IRBackendExecutableFunction.Instruction(
                                        IRBackendExecutableFunction.InstructionKind.RET,
                                        "",
                                        null,
                                        null,
                                        Span.none())),
                        Span.none())))
                .intrinsicPool(ReadOnlyList.from(new IntrinsicReference("input.pad", 1)))
                .build();
    }

    private String renderIrvmSnapshot(final p.studio.compiler.backend.irvm.IRVMProgram program) {
        final var out = new StringBuilder();
        out.append("profile=").append(program.module().vmProfile()).append('\n');
        for (var fnIndex = 0; fnIndex < program.module().functions().size(); fnIndex++) {
            final var fn = program.module().functions().get(fnIndex);
            out.append("fn#")
                    .append(fnIndex)
                    .append(':')
                    .append(fn.name())
                    .append(" param=").append(fn.paramSlots())
                    .append(" local=").append(fn.localSlots())
                    .append(" ret=").append(fn.returnSlots())
                    .append(" max=").append(fn.maxStackSlots())
                    .append('\n');
            for (final var instruction : fn.instructions()) {
                out.append("  ")
                        .append(instruction.op().name())
                        .append(" imm=")
                        .append(instruction.immediate() == null ? "-" : instruction.immediate())
                        .append('\n');
            }
        }
        return out.toString();
    }

    private void assertGolden(
            final String relativeResourcePath,
            final String actual) throws IOException {
        final var goldenPath = Path.of("src/test/resources").resolve(relativeResourcePath);
        final var updateGolden = Boolean.getBoolean("golden.update")
                || "true".equalsIgnoreCase(System.getenv("GOLDEN_UPDATE"));
        if (!Files.exists(goldenPath)) {
            Files.createDirectories(goldenPath.getParent());
            Files.writeString(goldenPath, actual);
            return;
        }
        if (updateGolden) {
            Files.createDirectories(goldenPath.getParent());
            Files.writeString(goldenPath, actual);
            return;
        }
        final var expected = Files.readString(goldenPath);
        assertEquals(expected, actual);
    }
}
