using ARMeilleure.Common;
using ARMeilleure.Memory;
using Ryujinx.Cpu.LightningJit.CodeGen;
using Ryujinx.Cpu.LightningJit.CodeGen.Arm64;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Numerics;

namespace Ryujinx.Cpu.LightningJit.Arm32.Target.Arm64
{
    static class Compiler
    {
        public const uint UsableGprsMask = 0x7fff;
        public const uint UsableFpSimdMask = 0xffff;
        public const uint UsablePStateMask = 0xf0000000;

        private const int Encodable26BitsOffsetLimit = 0x2000000;

        private readonly struct Context
        {
            public readonly CodeWriter Writer;
            public readonly RegisterAllocator RegisterAllocator;
            public readonly MemoryManagerType MemoryManagerType;
            public readonly TailMerger TailMerger;
            public readonly AddressTable<ulong> FuncTable;
            public readonly IntPtr DispatchStubPointer;

            private readonly RegisterSaveRestore _registerSaveRestore;
            private readonly IntPtr _pageTablePointer;

            public Context(
                CodeWriter writer,
                RegisterAllocator registerAllocator,
                MemoryManagerType mmType,
                TailMerger tailMerger,
                AddressTable<ulong> funcTable,
                RegisterSaveRestore registerSaveRestore,
                IntPtr dispatchStubPointer,
                IntPtr pageTablePointer)
            {
                Writer = writer;
                RegisterAllocator = registerAllocator;
                MemoryManagerType = mmType;
                TailMerger = tailMerger;
                FuncTable = funcTable;
                _registerSaveRestore = registerSaveRestore;
                DispatchStubPointer = dispatchStubPointer;
                _pageTablePointer = pageTablePointer;
            }

            public readonly int GetReservedStackOffset()
            {
                return _registerSaveRestore.GetReservedStackOffset();
            }

            public readonly void WritePrologueAt(int instructionPointer)
            {
                CodeWriter writer = new();
                Assembler asm = new(writer);

                _registerSaveRestore.WritePrologue(ref asm);

                // If needed, set up the fixed registers with the pointers we will use.
                // First one is the context pointer (passed as first argument),
                // second one is the page table or address space base, it is at a fixed memory location and considered constant.

                if (RegisterAllocator.FixedContextRegister != 0)
                {
                    asm.Mov(Register(RegisterAllocator.FixedContextRegister), Register(0));
                }

                asm.Mov(Register(RegisterAllocator.FixedPageTableRegister), (ulong)_pageTablePointer);

                LoadFromContext(ref asm);

                // Write the prologue at the specified position in our writer.
                Writer.WriteInstructionsAt(instructionPointer, writer);
            }

            public readonly void WriteEpilogueWithoutContext()
            {
                Assembler asm = new(Writer);

                _registerSaveRestore.WriteEpilogue(ref asm);
            }

            public void LoadFromContext()
            {
                Assembler asm = new(Writer);

                LoadFromContext(ref asm);
            }

            private void LoadFromContext(ref Assembler asm)
            {
                LoadGprFromContext(ref asm, RegisterAllocator.UsedGprsMask & UsableGprsMask, NativeContextOffsets.GprBaseOffset);
                LoadFpSimdFromContext(ref asm, RegisterAllocator.UsedFpSimdMask & UsableFpSimdMask, NativeContextOffsets.FpSimdBaseOffset);
                LoadPStateFromContext(ref asm, UsablePStateMask, NativeContextOffsets.FlagsBaseOffset);
            }

            public void StoreToContext()
            {
                Assembler asm = new(Writer);

                StoreToContext(ref asm);
            }

            private void StoreToContext(ref Assembler asm)
            {
                StoreGprToContext(ref asm, RegisterAllocator.UsedGprsMask & UsableGprsMask, NativeContextOffsets.GprBaseOffset);
                StoreFpSimdToContext(ref asm, RegisterAllocator.UsedFpSimdMask & UsableFpSimdMask, NativeContextOffsets.FpSimdBaseOffset);
                StorePStateToContext(ref asm, UsablePStateMask, NativeContextOffsets.FlagsBaseOffset);
            }

            private void LoadGprFromContext(ref Assembler asm, uint mask, int baseOffset)
            {
                Operand contextPtr = Register(RegisterAllocator.FixedContextRegister);

                while (mask != 0)
                {
                    int reg = BitOperations.TrailingZeroCount(mask);
                    int offset = baseOffset + reg * 8;

                    if (reg < 31 && (mask & (2u << reg)) != 0 && offset < RegisterSaveRestore.Encodable9BitsOffsetLimit)
                    {
                        mask &= ~(3u << reg);

                        asm.LdpRiUn(Register(reg), Register(reg + 1), contextPtr, offset);
                    }
                    else
                    {
                        mask &= ~(1u << reg);

                        asm.LdrRiUn(Register(reg), contextPtr, offset);
                    }
                }
            }

            private void LoadFpSimdFromContext(ref Assembler asm, uint mask, int baseOffset)
            {
                Operand contextPtr = Register(RegisterAllocator.FixedContextRegister);

                while (mask != 0)
                {
                    int reg = BitOperations.TrailingZeroCount(mask);
                    int offset = baseOffset + reg * 16;

                    mask &= ~(1u << reg);

                    asm.LdrRiUn(Register(reg, OperandType.V128), contextPtr, offset);
                }
            }

            private void LoadPStateFromContext(ref Assembler asm, uint mask, int baseOffset)
            {
                if (mask == 0)
                {
                    return;
                }

                Operand contextPtr = Register(RegisterAllocator.FixedContextRegister);

                using ScopedRegister tempRegister = RegisterAllocator.AllocateTempGprRegisterScoped();

                asm.LdrRiUn(tempRegister.Operand, contextPtr, baseOffset);
                asm.MsrNzcv(tempRegister.Operand);
            }

            private void StoreGprToContext(ref Assembler asm, uint mask, int baseOffset)
            {
                Operand contextPtr = Register(RegisterAllocator.FixedContextRegister);

                while (mask != 0)
                {
                    int reg = BitOperations.TrailingZeroCount(mask);
                    int offset = baseOffset + reg * 8;

                    if (reg < 31 && (mask & (2u << reg)) != 0 && offset < RegisterSaveRestore.Encodable9BitsOffsetLimit)
                    {
                        mask &= ~(3u << reg);

                        asm.StpRiUn(Register(reg), Register(reg + 1), contextPtr, offset);
                    }
                    else
                    {
                        mask &= ~(1u << reg);

                        asm.StrRiUn(Register(reg), contextPtr, offset);
                    }
                }
            }

            private void StoreFpSimdToContext(ref Assembler asm, uint mask, int baseOffset)
            {
                Operand contextPtr = Register(RegisterAllocator.FixedContextRegister);

                while (mask != 0)
                {
                    int reg = BitOperations.TrailingZeroCount(mask);
                    int offset = baseOffset + reg * 16;

                    mask &= ~(1u << reg);

                    asm.StrRiUn(Register(reg, OperandType.V128), contextPtr, offset);
                }
            }

            private void StorePStateToContext(ref Assembler asm, uint mask, int baseOffset)
            {
                if (mask == 0)
                {
                    return;
                }

                Operand contextPtr = Register(RegisterAllocator.FixedContextRegister);

                using ScopedRegister tempRegister = RegisterAllocator.AllocateTempGprRegisterScoped();
                using ScopedRegister tempRegister2 = RegisterAllocator.AllocateTempGprRegisterScoped();

                asm.LdrRiUn(tempRegister.Operand, contextPtr, baseOffset);
                asm.MrsNzcv(tempRegister2.Operand);
                asm.And(tempRegister.Operand, tempRegister.Operand, InstEmitCommon.Const(0xfffffff));
                asm.Orr(tempRegister.Operand, tempRegister.Operand, tempRegister2.Operand);
                asm.StrRiUn(tempRegister.Operand, contextPtr, baseOffset);
            }
        }

        public static CompiledFunction Compile(CpuPreset cpuPreset, IMemoryManager memoryManager, ulong address, AddressTable<ulong> funcTable, IntPtr dispatchStubPtr, bool isThumb)
        {
            MultiBlock multiBlock = Decoder<InstEmit>.DecodeMulti(cpuPreset, memoryManager, address, isThumb);

            Dictionary<ulong, int> targets = new();

            CodeWriter writer = new();
            RegisterAllocator regAlloc = new();
            Assembler asm = new(writer);
            CodeGenContext cgContext = new(writer, asm, regAlloc, memoryManager.Type, isThumb);
            ArmCondition lastCondition = ArmCondition.Al;
            int lastConditionIp = 0;

            // Required for load/store to context.
            regAlloc.EnsureTempGprRegisters(2);

            ulong pc = address;

            for (int blockIndex = 0; blockIndex < multiBlock.Blocks.Count; blockIndex++)
            {
                Block block = multiBlock.Blocks[blockIndex];

                Debug.Assert(block.Address == pc);

                targets.Add(pc, writer.InstructionPointer);

                for (int index = 0; index < block.Instructions.Count; index++)
                {
                    InstInfo instInfo = block.Instructions[index];

                    if (index < block.Instructions.Count - 1)
                    {
                        cgContext.SetNextInstruction(block.Instructions[index + 1]);
                    }
                    else
                    {
                        cgContext.SetNextInstruction(default);
                    }

                    SetConditionalStart(cgContext, ref lastCondition, ref lastConditionIp, instInfo.Name, instInfo.Flags, instInfo.Encoding);

                    if (block.IsLoopEnd && index == block.Instructions.Count - 1)
                    {
                        // If this is a loop, the code might run for a long time uninterrupted.
                        // We insert a "sync point" here to ensure the loop can be interrupted if needed.

                        cgContext.AddPendingSyncPoint();

                        asm.B(0);
                    }

                    cgContext.SetPc((uint)pc);

                    instInfo.EmitFunc(cgContext, instInfo.Encoding);

                    if (cgContext.ConsumeNzcvModified())
                    {
                        ForceConditionalEnd(cgContext, ref lastCondition, lastConditionIp);
                    }

                    cgContext.UpdateItState();

                    pc += instInfo.Flags.HasFlag(InstFlags.Thumb16) ? 2UL : 4UL;
                }

                if (Decoder<InstEmit>.WritesToPC(block.Instructions[^1].Encoding, block.Instructions[^1].Name, block.Instructions[^1].Flags, block.IsThumb))
                {
                    // If the block ends with a PC register write, then we have a branch from register.

                    InstEmitCommon.SetThumbFlag(cgContext, regAlloc.RemapGprRegister(RegisterUtils.PcRegister));

                    cgContext.AddPendingIndirectBranch(block.Instructions[^1].Name, RegisterUtils.PcRegister);

                    asm.B(0);
                }

                ForceConditionalEnd(cgContext, ref lastCondition, lastConditionIp);
            }

            RegisterSaveRestore rsr = new(
                regAlloc.UsedGprsMask & AbiConstants.GprCalleeSavedRegsMask,
                regAlloc.UsedFpSimdMask & AbiConstants.FpSimdCalleeSavedRegsMask,
                OperandType.FP64,
                multiBlock.HasHostCall,
                multiBlock.HasHostCall ? CalculateStackSizeForCallSpill(regAlloc.UsedGprsMask, regAlloc.UsedFpSimdMask, UsablePStateMask) : 0);

            TailMerger tailMerger = new();

            Context context = new(writer, regAlloc, memoryManager.Type, tailMerger, funcTable, rsr, dispatchStubPtr, memoryManager.PageTablePointer);

            InstInfo lastInstruction = multiBlock.Blocks[^1].Instructions[^1];
            bool lastInstIsConditional = GetCondition(lastInstruction, isThumb) != ArmCondition.Al;

            if (multiBlock.IsTruncated || lastInstIsConditional || lastInstruction.Name.IsCall() || IsConditionalBranch(lastInstruction))
            {
                WriteTailCallConstant(context, ref asm, (uint)pc);
            }

            IEnumerable<PendingBranch> pendingBranches = cgContext.GetPendingBranches();

            foreach (PendingBranch pendingBranch in pendingBranches)
            {
                RewriteBranchInstructionWithTarget(context, pendingBranch, targets);
            }

            tailMerger.WriteReturn(writer, context.WriteEpilogueWithoutContext);

            context.WritePrologueAt(0);

            return new(writer.AsByteSpan(), (int)(pc - address));
        }

        private static int CalculateStackSizeForCallSpill(uint gprUseMask, uint fpSimdUseMask, uint pStateUseMask)
        {
            // Note that we don't discard callee saved FP/SIMD register because only the lower 64 bits is callee saved,
            // so if the function is using the full register, that won't be enough.
            // We could do better, but it's likely not worth it since this case happens very rarely in practice.

            return BitOperations.PopCount(gprUseMask & ~AbiConstants.GprCalleeSavedRegsMask) * 8 +
                   BitOperations.PopCount(fpSimdUseMask) * 16 +
                   (pStateUseMask != 0 ? 8 : 0);
        }

        private static void SetConditionalStart(
            CodeGenContext context,
            ref ArmCondition condition,
            ref int instructionPointer,
            InstName name,
            InstFlags flags,
            uint encoding)
        {
            if (!context.ConsumeItCondition(out ArmCondition currentCond))
            {
                currentCond = GetCondition(name, flags, encoding, context.IsThumb);
            }

            if (currentCond != condition)
            {
                WriteConditionalEnd(context, condition, instructionPointer);

                condition = currentCond;

                if (currentCond != ArmCondition.Al)
                {
                    instructionPointer = context.CodeWriter.InstructionPointer;
                    context.Arm64Assembler.B(currentCond.Invert(), 0);
                }
            }
        }

        private static bool IsConditionalBranch(in InstInfo instInfo)
        {
            return instInfo.Name == InstName.B && (ArmCondition)(instInfo.Encoding >> 28) != ArmCondition.Al;
        }

        private static ArmCondition GetCondition(in InstInfo instInfo, bool isThumb)
        {
            return GetCondition(instInfo.Name, instInfo.Flags, instInfo.Encoding, isThumb);
        }

        private static ArmCondition GetCondition(InstName name, InstFlags flags, uint encoding, bool isThumb)
        {
            // For branch, we handle conditional execution on the instruction itself.
            bool hasCond = flags.HasFlag(InstFlags.Cond) && !CanHandleConditionalInstruction(name, encoding, isThumb);

            return hasCond ? (ArmCondition)(encoding >> 28) : ArmCondition.Al;
        }

        private static bool CanHandleConditionalInstruction(InstName name, uint encoding, bool isThumb)
        {
            if (name == InstName.B)
            {
                return true;
            }

            // We can use CSEL for conditional MOV from registers, as long the instruction is not setting flags.
            // We don't handle thumb right now because the condition comes from the IT block which would be more complicated to handle.
            if (name == InstName.MovR && !isThumb && (encoding & (1u << 20)) == 0)
            {
                return true;
            }

            return false;
        }

        private static void ForceConditionalEnd(CodeGenContext context, ref ArmCondition condition, int instructionPointer)
        {
            WriteConditionalEnd(context, condition, instructionPointer);

            condition = ArmCondition.Al;
        }

        private static void WriteConditionalEnd(CodeGenContext context, ArmCondition condition, int instructionPointer)
        {
            if (condition != ArmCondition.Al)
            {
                int delta = context.CodeWriter.InstructionPointer - instructionPointer;
                uint branchInst = context.CodeWriter.ReadInstructionAt(instructionPointer) | (((uint)delta & 0x7ffff) << 5);
                Debug.Assert((int)((branchInst & ~0x1fu) << 8) >> 11 == delta * 4);

                context.CodeWriter.WriteInstructionAt(instructionPointer, branchInst);
            }
        }

        private static void RewriteBranchInstructionWithTarget(in Context context, in PendingBranch pendingBranch, Dictionary<ulong, int> targets)
        {
            switch (pendingBranch.BranchType)
            {
                case BranchType.Branch:
                    RewriteBranchInstructionWithTarget(context, pendingBranch.Name, pendingBranch.TargetAddress, pendingBranch.WriterPointer, targets);
                    break;
                case BranchType.Call:
                    RewriteCallInstructionWithTarget(context, pendingBranch.TargetAddress, pendingBranch.NextAddress, pendingBranch.WriterPointer);
                    break;
                case BranchType.IndirectBranch:
                    RewriteIndirectBranchInstructionWithTarget(context, pendingBranch.Name, pendingBranch.TargetAddress, pendingBranch.WriterPointer);
                    break;
                case BranchType.TableBranchByte:
                case BranchType.TableBranchHalfword:
                    RewriteTableBranchInstructionWithTarget(
                        context,
                        pendingBranch.BranchType == BranchType.TableBranchHalfword,
                        pendingBranch.TargetAddress,
                        pendingBranch.NextAddress,
                        pendingBranch.WriterPointer);
                    break;
                case BranchType.IndirectCall:
                    RewriteIndirectCallInstructionWithTarget(context, pendingBranch.TargetAddress, pendingBranch.NextAddress, pendingBranch.WriterPointer);
                    break;
                case BranchType.SyncPoint:
                case BranchType.SoftwareInterrupt:
                case BranchType.ReadCntpct:
                    RewriteHostCall(context, pendingBranch.Name, pendingBranch.BranchType, pendingBranch.TargetAddress, pendingBranch.NextAddress, pendingBranch.WriterPointer);
                    break;
                default:
                    Debug.Fail($"Invalid branch type '{pendingBranch.BranchType}'");
                    break;
            }
        }

        private static void RewriteBranchInstructionWithTarget(in Context context, InstName name, uint targetAddress, int branchIndex, Dictionary<ulong, int> targets)
        {
            CodeWriter writer = context.Writer;
            Assembler asm = new(writer);

            int delta;
            int targetIndex;
            uint encoding = writer.ReadInstructionAt(branchIndex);

            if (encoding == 0x14000000)
            {
                // Unconditional branch.

                if (targets.TryGetValue(targetAddress, out targetIndex))
                {
                    delta = targetIndex - branchIndex;

                    if (delta >= -Encodable26BitsOffsetLimit && delta < Encodable26BitsOffsetLimit)
                    {
                        writer.WriteInstructionAt(branchIndex, encoding | (uint)(delta & 0x3ffffff));

                        return;
                    }
                }

                targetIndex = writer.InstructionPointer;
                delta = targetIndex - branchIndex;

                writer.WriteInstructionAt(branchIndex, encoding | (uint)(delta & 0x3ffffff));
                WriteTailCallConstant(context, ref asm, targetAddress);
            }
            else
            {
                // Conditional branch.

                uint branchMask = 0x7ffff;
                int branchMax = (int)(branchMask + 1) / 2;

                if (targets.TryGetValue(targetAddress, out targetIndex))
                {
                    delta = targetIndex - branchIndex;

                    if (delta >= -branchMax && delta < branchMax)
                    {
                        writer.WriteInstructionAt(branchIndex, encoding | (uint)((delta & branchMask) << 5));

                        return;
                    }
                }

                targetIndex = writer.InstructionPointer;
                delta = targetIndex - branchIndex;

                if (delta >= -branchMax && delta < branchMax)
                {
                    writer.WriteInstructionAt(branchIndex, encoding | (uint)((delta & branchMask) << 5));
                    WriteTailCallConstant(context, ref asm, targetAddress);
                }
                else
                {
                    // If the branch target is too far away, we use a regular unconditional branch
                    // instruction instead which has a much higher range.
                    // We branch directly to the end of the function, where we put the conditional branch,
                    // and then branch back to the next instruction or return the branch target depending
                    // on the branch being taken or not.

                    uint branchInst = 0x14000000u | ((uint)delta & 0x3ffffff);
                    Debug.Assert((int)(branchInst << 6) >> 4 == delta * 4);

                    writer.WriteInstructionAt(branchIndex, branchInst);

                    int movedBranchIndex = writer.InstructionPointer;

                    writer.WriteInstruction(0u); // Placeholder
                    asm.B((branchIndex + 1 - writer.InstructionPointer) * 4);

                    delta = writer.InstructionPointer - movedBranchIndex;

                    writer.WriteInstructionAt(movedBranchIndex, encoding | (uint)((delta & branchMask) << 5));
                    WriteTailCallConstant(context, ref asm, targetAddress);
                }
            }

            Debug.Assert(name == InstName.B || name == InstName.Cbnz, $"Unknown branch instruction \"{name}\".");
        }

        private static void RewriteCallInstructionWithTarget(in Context context, uint targetAddress, uint nextAddress, int branchIndex)
        {
            CodeWriter writer = context.Writer;
            Assembler asm = new(writer);

            WriteBranchToCurrentPosition(context, branchIndex);

            asm.Mov(context.RegisterAllocator.RemapGprRegister(RegisterUtils.LrRegister), nextAddress);

            context.StoreToContext();
            InstEmitFlow.WriteCallWithGuestAddress(
                writer,
                ref asm,
                context.RegisterAllocator,
                context.TailMerger,
                context.WriteEpilogueWithoutContext,
                context.FuncTable,
                context.DispatchStubPointer,
                context.GetReservedStackOffset(),
                nextAddress,
                InstEmitCommon.Const((int)targetAddress));
            context.LoadFromContext();

            // Branch back to the next instruction (after the call).
            asm.B((branchIndex + 1 - writer.InstructionPointer) * 4);
        }

        private static void RewriteIndirectBranchInstructionWithTarget(in Context context, InstName name, uint targetRegister, int branchIndex)
        {
            CodeWriter writer = context.Writer;
            Assembler asm = new(writer);

            WriteBranchToCurrentPosition(context, branchIndex);

            using ScopedRegister target = context.RegisterAllocator.AllocateTempGprRegisterScoped();

            asm.And(target.Operand, context.RegisterAllocator.RemapGprRegister((int)targetRegister), InstEmitCommon.Const(~1));

            context.StoreToContext();

            if ((name == InstName.Bx && targetRegister == RegisterUtils.LrRegister) ||
                name == InstName.Ldm ||
                name == InstName.Ldmda ||
                name == InstName.Ldmdb ||
                name == InstName.Ldmib)
            {
                // Arm32 does not have a return instruction, instead returns are implemented
                // either using BX LR (for leaf functions), or POP { ... PC }.

                asm.Mov(Register(0), target.Operand);

                context.TailMerger.AddUnconditionalReturn(writer, asm);
            }
            else
            {
                InstEmitFlow.WriteCallWithGuestAddress(
                    writer,
                    ref asm,
                    context.RegisterAllocator,
                    context.TailMerger,
                    context.WriteEpilogueWithoutContext,
                    context.FuncTable,
                    context.DispatchStubPointer,
                    context.GetReservedStackOffset(),
                    0u,
                    target.Operand,
                    isTail: true);
            }
        }

        private static void RewriteTableBranchInstructionWithTarget(in Context context, bool halfword, uint rn, uint rm, int branchIndex)
        {
            CodeWriter writer = context.Writer;
            Assembler asm = new(writer);

            WriteBranchToCurrentPosition(context, branchIndex);

            using ScopedRegister target = context.RegisterAllocator.AllocateTempGprRegisterScoped();

            asm.Add(
                target.Operand,
                context.RegisterAllocator.RemapGprRegister((int)rn),
                context.RegisterAllocator.RemapGprRegister((int)rm),
                ArmShiftType.Lsl,
                halfword ? 1 : 0);

            InstEmitMemory.WriteAddressTranslation(context.MemoryManagerType, context.RegisterAllocator, asm, target.Operand, target.Operand);

            if (halfword)
            {
                asm.LdrhRiUn(target.Operand, target.Operand, 0);
            }
            else
            {
                asm.LdrbRiUn(target.Operand, target.Operand, 0);
            }

            asm.Add(target.Operand, context.RegisterAllocator.RemapGprRegister(RegisterUtils.PcRegister), target.Operand, ArmShiftType.Lsl, 1);

            context.StoreToContext();

            InstEmitFlow.WriteCallWithGuestAddress(
                writer,
                ref asm,
                context.RegisterAllocator,
                context.TailMerger,
                context.WriteEpilogueWithoutContext,
                context.FuncTable,
                context.DispatchStubPointer,
                context.GetReservedStackOffset(),
                0u,
                target.Operand,
                isTail: true);
        }

        private static void RewriteIndirectCallInstructionWithTarget(in Context context, uint targetRegister, uint nextAddress, int branchIndex)
        {
            CodeWriter writer = context.Writer;
            Assembler asm = new(writer);

            WriteBranchToCurrentPosition(context, branchIndex);

            using ScopedRegister target = context.RegisterAllocator.AllocateTempGprRegisterScoped();

            asm.And(target.Operand, context.RegisterAllocator.RemapGprRegister((int)targetRegister), InstEmitCommon.Const(~1));
            asm.Mov(context.RegisterAllocator.RemapGprRegister(RegisterUtils.LrRegister), nextAddress);

            context.StoreToContext();
            InstEmitFlow.WriteCallWithGuestAddress(
                writer,
                ref asm,
                context.RegisterAllocator,
                context.TailMerger,
                context.WriteEpilogueWithoutContext,
                context.FuncTable,
                context.DispatchStubPointer,
                context.GetReservedStackOffset(),
                nextAddress & ~1u,
                target.Operand);
            context.LoadFromContext();

            // Branch back to the next instruction (after the call).
            asm.B((branchIndex + 1 - writer.InstructionPointer) * 4);
        }

        private static void RewriteHostCall(in Context context, InstName name, BranchType type, uint imm, uint pc, int branchIndex)
        {
            CodeWriter writer = context.Writer;
            Assembler asm = new(writer);

            uint encoding = writer.ReadInstructionAt(branchIndex);
            int targetIndex = writer.InstructionPointer;
            int delta = targetIndex - branchIndex;

            writer.WriteInstructionAt(branchIndex, encoding | (uint)(delta & 0x3ffffff));

            switch (type)
            {
                case BranchType.SyncPoint:
                    InstEmitSystem.WriteSyncPoint(context.Writer, context.RegisterAllocator, context.TailMerger, context.GetReservedStackOffset());
                    break;
                case BranchType.SoftwareInterrupt:
                    context.StoreToContext();
                    switch (name)
                    {
                        case InstName.Bkpt:
                            InstEmitSystem.WriteBkpt(context.Writer, context.RegisterAllocator, context.TailMerger, context.GetReservedStackOffset(), pc, imm);
                            break;
                        case InstName.Svc:
                            InstEmitSystem.WriteSvc(context.Writer, context.RegisterAllocator, context.TailMerger, context.GetReservedStackOffset(), pc, imm);
                            break;
                        case InstName.Udf:
                            InstEmitSystem.WriteUdf(context.Writer, context.RegisterAllocator, context.TailMerger, context.GetReservedStackOffset(), pc, imm);
                            break;
                    }
                    context.LoadFromContext();
                    break;
                case BranchType.ReadCntpct:
                    InstEmitSystem.WriteReadCntpct(context.Writer, context.RegisterAllocator, context.GetReservedStackOffset(), (int)imm, (int)pc);
                    break;
                default:
                    Debug.Fail($"Invalid branch type '{type}'");
                    break;
            }

            // Branch back to the next instruction.
            asm.B((branchIndex + 1 - writer.InstructionPointer) * 4);
        }

        private static void WriteBranchToCurrentPosition(in Context context, int branchIndex)
        {
            CodeWriter writer = context.Writer;

            int targetIndex = writer.InstructionPointer;

            if (branchIndex + 1 == targetIndex)
            {
                writer.RemoveLastInstruction();
            }
            else
            {
                uint encoding = writer.ReadInstructionAt(branchIndex);
                int delta = targetIndex - branchIndex;

                writer.WriteInstructionAt(branchIndex, encoding | (uint)(delta & 0x3ffffff));
            }
        }

        private static void WriteTailCallConstant(in Context context, ref Assembler asm, uint address)
        {
            context.StoreToContext();
            InstEmitFlow.WriteCallWithGuestAddress(
                context.Writer,
                ref asm,
                context.RegisterAllocator,
                context.TailMerger,
                context.WriteEpilogueWithoutContext,
                context.FuncTable,
                context.DispatchStubPointer,
                context.GetReservedStackOffset(),
                0u,
                InstEmitCommon.Const((int)address),
                isTail: true);
        }

        private static Operand Register(int register, OperandType type = OperandType.I64)
        {
            return new Operand(register, RegisterType.Integer, type);
        }

        public static void PrintStats()
        {
        }
    }
}