package p.studio.compiler.models;

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.identifiers.ModuleId;
import p.studio.utilities.structures.ReadOnlyList;

import java.util.Objects;

public record IRBackendExecutableFunction(
        FileId fileId,
        ModuleId moduleId,
        String callableName,
        CallableId callableId,
        int sourceStart,
        int sourceEnd,
        int paramSlots,
        int localSlots,
        int returnSlots,
        int maxStackSlots,
        ReadOnlyList<Instruction> instructions,
        Span span) {

    public IRBackendExecutableFunction(
            final FileId fileId,
            final String callableName,
            final CallableId callableId,
            final int sourceStart,
            final int sourceEnd,
            final int paramSlots,
            final int localSlots,
            final int returnSlots,
            final int maxStackSlots,
            final ReadOnlyList<Instruction> instructions,
            final Span span) {
        this(
                fileId,
                ModuleId.none(),
                callableName,
                callableId,
                sourceStart,
                sourceEnd,
                paramSlots,
                localSlots,
                returnSlots,
                maxStackSlots,
                instructions,
                span);
    }

    public IRBackendExecutableFunction {
        fileId = Objects.requireNonNull(fileId, "fileId");
        moduleId = moduleId == null ? ModuleId.none() : moduleId;
        if (!moduleId.isNone()) {
            moduleId.getIndex();
        }
        callableName = Objects.requireNonNull(callableName, "callableName");
        if (callableName.isBlank()) {
            throw new IllegalArgumentException("callableName must not be blank");
        }
        callableId = Objects.requireNonNull(callableId, "callableId");
        if (callableId.isNone()) {
            throw new IllegalArgumentException("callableId must not be none");
        }
        callableId.getIndex();
        if (sourceStart < 0 || sourceEnd < 0 || sourceEnd < sourceStart) {
            throw new IllegalArgumentException("invalid source span bounds");
        }
        if (paramSlots < 0 || localSlots < 0 || returnSlots < 0 || maxStackSlots < 0) {
            throw new IllegalArgumentException("slots must be non-negative");
        }
        if (maxStackSlots < returnSlots) {
            throw new IllegalArgumentException("maxStackSlots must be >= returnSlots");
        }
        instructions = instructions == null ? ReadOnlyList.empty() : instructions;
        for (final var instruction : instructions) {
            Objects.requireNonNull(instruction, "instruction");
        }
        span = span == null ? Span.none() : span;
    }

    public record Instruction(
            InstructionKind kind,
            ModuleId calleeModuleId,
            String calleeCallableName,
            CallableId calleeCallableId,
            HostCallMetadata hostCall,
            IntrinsicCallMetadata intrinsicCall,
            String label,
            String targetLabel,
            Integer expectedArgSlots,
            Integer expectedRetSlots,
            Span span) {
        public Instruction(
                final InstructionKind kind,
                final ModuleId calleeModuleId,
                final String calleeCallableName,
                final CallableId calleeCallableId,
                final HostCallMetadata hostCall,
                final IntrinsicCallMetadata intrinsicCall,
                final Span span) {
            this(kind, calleeModuleId, calleeCallableName, calleeCallableId, hostCall, intrinsicCall, null, null, null, null, span);
        }

        public Instruction(
                final InstructionKind kind,
                final ModuleId calleeModuleId,
                final String calleeCallableName,
                final CallableId calleeCallableId,
                final HostCallMetadata hostCall,
                final IntrinsicCallMetadata intrinsicCall,
                final Integer expectedArgSlots,
                final Integer expectedRetSlots,
                final Span span) {
            this(kind, calleeModuleId, calleeCallableName, calleeCallableId, hostCall, intrinsicCall, null, null, expectedArgSlots, expectedRetSlots, span);
        }

        public Instruction(
                final InstructionKind kind,
                final ModuleId calleeModuleId,
                final String calleeCallableName,
                final HostCallMetadata hostCall,
                final IntrinsicCallMetadata intrinsicCall,
                final Span span) {
            this(kind, calleeModuleId, calleeCallableName, null, hostCall, intrinsicCall, null, null, null, null, span);
        }

        public Instruction(
                final InstructionKind kind,
                final ModuleId calleeModuleId,
                final String calleeCallableName,
                final HostCallMetadata hostCall,
                final IntrinsicCallMetadata intrinsicCall,
                final Integer expectedArgSlots,
                final Integer expectedRetSlots,
                final Span span) {
            this(kind, calleeModuleId, calleeCallableName, null, hostCall, intrinsicCall, null, null, expectedArgSlots, expectedRetSlots, span);
        }

        public Instruction(
                final InstructionKind kind,
                final String calleeCallableName,
                final CallableId calleeCallableId,
                final HostCallMetadata hostCall,
                final IntrinsicCallMetadata intrinsicCall,
                final String label,
                final String targetLabel,
                final Integer expectedArgSlots,
                final Integer expectedRetSlots,
                final Span span) {
            this(kind, ModuleId.none(), calleeCallableName, calleeCallableId, hostCall, intrinsicCall, label, targetLabel, expectedArgSlots, expectedRetSlots, span);
        }

        public Instruction(
                final InstructionKind kind,
                final String calleeCallableName,
                final CallableId calleeCallableId,
                final HostCallMetadata hostCall,
                final IntrinsicCallMetadata intrinsicCall,
                final Span span) {
            this(kind, ModuleId.none(), calleeCallableName, calleeCallableId, hostCall, intrinsicCall, null, null, null, null, span);
        }

        public Instruction(
                final InstructionKind kind,
                final String calleeCallableName,
                final CallableId calleeCallableId,
                final HostCallMetadata hostCall,
                final IntrinsicCallMetadata intrinsicCall,
                final Integer expectedArgSlots,
                final Integer expectedRetSlots,
                final Span span) {
            this(kind, ModuleId.none(), calleeCallableName, calleeCallableId, hostCall, intrinsicCall, null, null, expectedArgSlots, expectedRetSlots, span);
        }

        public Instruction(
                final InstructionKind kind,
                final String calleeCallableName,
                final HostCallMetadata hostCall,
                final IntrinsicCallMetadata intrinsicCall,
                final Span span) {
            this(kind, ModuleId.none(), calleeCallableName, null, hostCall, intrinsicCall, null, null, null, null, span);
        }

        public Instruction(
                final InstructionKind kind,
                final String calleeCallableName,
                final HostCallMetadata hostCall,
                final IntrinsicCallMetadata intrinsicCall,
                final Integer expectedArgSlots,
                final Integer expectedRetSlots,
                final Span span) {
            this(kind, ModuleId.none(), calleeCallableName, null, hostCall, intrinsicCall, null, null, expectedArgSlots, expectedRetSlots, span);
        }

        public Instruction {
            Objects.requireNonNull(kind, "kind");
            span = span == null ? Span.none() : span;
            calleeModuleId = calleeModuleId == null ? ModuleId.none() : calleeModuleId;
            if (!calleeModuleId.isNone()) {
                calleeModuleId.getIndex();
            }
            calleeCallableName = calleeCallableName == null ? "" : calleeCallableName;
            label = label == null ? "" : label;
            targetLabel = targetLabel == null ? "" : targetLabel;
            if (calleeCallableId != null && calleeCallableId.isNone()) {
                throw new IllegalArgumentException("calleeCallableId must not be none");
            }
            if (calleeCallableId != null) {
                calleeCallableId.getIndex();
            }
            if (expectedArgSlots != null && expectedArgSlots < 0) {
                throw new IllegalArgumentException("expectedArgSlots must be non-negative");
            }
            if (expectedRetSlots != null && expectedRetSlots < 0) {
                throw new IllegalArgumentException("expectedRetSlots must be non-negative");
            }
            switch (kind) {
                case PUSH_I32 -> {
                    if (label.isBlank()) {
                        throw new IllegalArgumentException("PUSH_I32 requires immediate label payload");
                    }
                    if (!targetLabel.isBlank()
                            || !calleeCallableName.isBlank()
                            || calleeCallableId != null
                            || hostCall != null
                            || intrinsicCall != null
                            || expectedArgSlots != null
                            || expectedRetSlots != null) {
                        throw new IllegalArgumentException("PUSH_I32 must not carry call metadata");
                    }
                }
                case PUSH_BOOL -> {
                    if (label.isBlank()) {
                        throw new IllegalArgumentException("PUSH_BOOL requires immediate label payload");
                    }
                    if (!targetLabel.isBlank()
                            || !calleeCallableName.isBlank()
                            || calleeCallableId != null
                            || hostCall != null
                            || intrinsicCall != null
                            || expectedArgSlots != null
                            || expectedRetSlots != null) {
                        throw new IllegalArgumentException("PUSH_BOOL must not carry call metadata");
                    }
                }
                case PUSH_CONST -> {
                    if (!targetLabel.isBlank()
                            || !calleeCallableName.isBlank()
                            || calleeCallableId != null
                            || hostCall != null
                            || intrinsicCall != null
                            || expectedArgSlots != null
                            || expectedRetSlots != null) {
                        throw new IllegalArgumentException("PUSH_CONST must not carry call metadata");
                    }
                }
                case GET_LOCAL -> {
                    if (expectedArgSlots == null) {
                        throw new IllegalArgumentException("GET_LOCAL requires local slot in expectedArgSlots");
                    }
                    if (!label.isBlank()
                            || !targetLabel.isBlank()
                            || !calleeCallableName.isBlank()
                            || calleeCallableId != null
                            || hostCall != null
                            || intrinsicCall != null
                            || expectedRetSlots != null) {
                        throw new IllegalArgumentException("GET_LOCAL must not carry call metadata");
                    }
                }
                case GET_GLOBAL -> {
                    if (expectedArgSlots == null) {
                        throw new IllegalArgumentException("GET_GLOBAL requires global slot in expectedArgSlots");
                    }
                    if (!label.isBlank()
                            || !targetLabel.isBlank()
                            || !calleeCallableName.isBlank()
                            || calleeCallableId != null
                            || hostCall != null
                            || intrinsicCall != null
                            || expectedRetSlots != null) {
                        throw new IllegalArgumentException("GET_GLOBAL must not carry call metadata");
                    }
                }
                case SET_LOCAL -> {
                    if (expectedArgSlots == null) {
                        throw new IllegalArgumentException("SET_LOCAL requires local slot in expectedArgSlots");
                    }
                    if (!label.isBlank()
                            || !targetLabel.isBlank()
                            || !calleeCallableName.isBlank()
                            || calleeCallableId != null
                            || hostCall != null
                            || intrinsicCall != null
                            || expectedRetSlots != null) {
                        throw new IllegalArgumentException("SET_LOCAL must not carry call metadata");
                    }
                }
                case SET_GLOBAL -> {
                    if (expectedArgSlots == null) {
                        throw new IllegalArgumentException("SET_GLOBAL requires global slot in expectedArgSlots");
                    }
                    if (!label.isBlank()
                            || !targetLabel.isBlank()
                            || !calleeCallableName.isBlank()
                            || calleeCallableId != null
                            || hostCall != null
                            || intrinsicCall != null
                            || expectedRetSlots != null) {
                        throw new IllegalArgumentException("SET_GLOBAL must not carry call metadata");
                    }
                }
                case CALL_FUNC -> {
                    if (calleeCallableId == null) {
                        throw new IllegalArgumentException("CALL_FUNC requires calleeCallableId");
                    }
                    if (hostCall != null || intrinsicCall != null) {
                        throw new IllegalArgumentException("CALL_FUNC must not carry host or intrinsic metadata");
                    }
                }
                case CALL_HOST -> {
                    if (hostCall == null) {
                        throw new IllegalArgumentException("CALL_HOST requires hostCall metadata");
                    }
                    if (calleeCallableId != null) {
                        throw new IllegalArgumentException("CALL_HOST must not carry calleeCallableId");
                    }
                    if (intrinsicCall != null) {
                        throw new IllegalArgumentException("CALL_HOST must not carry intrinsic metadata");
                    }
                }
                case CALL_INTRINSIC -> {
                    if (intrinsicCall == null) {
                        throw new IllegalArgumentException("CALL_INTRINSIC requires intrinsic metadata");
                    }
                    if (calleeCallableId != null) {
                        throw new IllegalArgumentException("CALL_INTRINSIC must not carry calleeCallableId");
                    }
                    if (hostCall != null) {
                        throw new IllegalArgumentException("CALL_INTRINSIC must not carry host metadata");
                    }
                }
                case HALT, RET, POP, ADD, SUB, MUL, DIV, MOD, EQ, NEQ, LT, LTE, GT, GTE, AND, OR, NOT, NEG -> {
                    if (!calleeCallableName.isBlank() || calleeCallableId != null || hostCall != null || intrinsicCall != null) {
                        throw new IllegalArgumentException(kind + " must not carry callsite metadata");
                    }
                    if (!label.isBlank() || !targetLabel.isBlank()) {
                        throw new IllegalArgumentException(kind + " must not carry label metadata");
                    }
                    if (expectedArgSlots != null || expectedRetSlots != null) {
                        throw new IllegalArgumentException(kind + " must not carry expected slot metadata");
                    }
                }
                case LABEL -> {
                    if (label.isBlank()) {
                        throw new IllegalArgumentException("LABEL requires label");
                    }
                    if (!targetLabel.isBlank()
                            || !calleeCallableName.isBlank()
                            || calleeCallableId != null
                            || hostCall != null
                            || intrinsicCall != null
                            || expectedArgSlots != null
                            || expectedRetSlots != null) {
                        throw new IllegalArgumentException("LABEL must not carry call metadata");
                    }
                }
                case JMP, JMP_IF_TRUE, JMP_IF_FALSE -> {
                    if (targetLabel.isBlank()) {
                        throw new IllegalArgumentException(kind + " requires targetLabel");
                    }
                    if (!label.isBlank()
                            || !calleeCallableName.isBlank()
                            || calleeCallableId != null
                            || hostCall != null
                            || intrinsicCall != null
                            || expectedArgSlots != null
                            || expectedRetSlots != null) {
                        throw new IllegalArgumentException(kind + " must not carry call metadata");
                    }
                }
            }
        }
    }

    public enum InstructionKind {
        PUSH_I32,
        PUSH_BOOL,
        PUSH_CONST,
        GET_LOCAL,
        GET_GLOBAL,
        SET_LOCAL,
        SET_GLOBAL,
        POP,
        ADD,
        SUB,
        MUL,
        DIV,
        MOD,
        EQ,
        NEQ,
        LT,
        LTE,
        GT,
        GTE,
        AND,
        OR,
        NOT,
        NEG,
        HALT,
        RET,
        CALL_FUNC,
        CALL_HOST,
        CALL_INTRINSIC,
        LABEL,
        JMP,
        JMP_IF_TRUE,
        JMP_IF_FALSE,
    }

    public record HostCallMetadata(
            String module,
            String name,
            long version,
            int argSlots,
            int retSlots) {
        public HostCallMetadata {
            module = Objects.requireNonNull(module, "module");
            name = Objects.requireNonNull(name, "name");
            if (module.isBlank() || name.isBlank()) {
                throw new IllegalArgumentException("module and name must not be blank");
            }
            if (version < 0 || argSlots < 0 || retSlots < 0) {
                throw new IllegalArgumentException("host metadata values must be non-negative");
            }
        }
    }

    public record IntrinsicCallMetadata(
            String canonicalName,
            long canonicalVersion,
            IntrinsicId intrinsicId) {
        public IntrinsicCallMetadata {
            canonicalName = Objects.requireNonNull(canonicalName, "canonicalName");
            if (canonicalName.isBlank()) {
                throw new IllegalArgumentException("canonicalName must not be blank");
            }
            if (canonicalVersion < 0) {
                throw new IllegalArgumentException("canonicalVersion must be non-negative");
            }
            intrinsicId = Objects.requireNonNull(intrinsicId, "intrinsicId");
            if (intrinsicId.isNone()) {
                throw new IllegalArgumentException("intrinsicId must not be none");
            }
            intrinsicId.getIndex();
        }
    }
}
