using Ryujinx.Graphics.Shader.IntermediateRepresentation;
using Ryujinx.Graphics.Shader.Translation;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Numerics;

namespace Ryujinx.Graphics.Shader.StructuredIr
{
    static class StructuredProgram
    {
        // TODO: Eventually it should be possible to specify the parameter types for the function instead of using S32 for everything.
        private const AggregateType FuncParameterType = AggregateType.S32;

        public static StructuredProgramInfo MakeStructuredProgram(
            IReadOnlyList<Function> functions,
            AttributeUsage attributeUsage,
            ShaderDefinitions definitions,
            ResourceManager resourceManager,
            TargetLanguage targetLanguage,
            bool debugMode)
        {
            StructuredProgramContext context = new(attributeUsage, definitions, resourceManager, debugMode);

            for (int funcIndex = 0; funcIndex < functions.Count; funcIndex++)
            {
                Function function = functions[funcIndex];

                BasicBlock[] blocks = function.Blocks;

                AggregateType returnType = function.ReturnsValue ? FuncParameterType : AggregateType.Void;

                AggregateType[] inArguments = new AggregateType[function.InArgumentsCount];
                AggregateType[] outArguments = new AggregateType[function.OutArgumentsCount];

                for (int i = 0; i < inArguments.Length; i++)
                {
                    inArguments[i] = FuncParameterType;
                }

                for (int i = 0; i < outArguments.Length; i++)
                {
                    outArguments[i] = FuncParameterType;
                }

                context.EnterFunction(blocks.Length, function.Name, returnType, inArguments, outArguments);

                PhiFunctions.Remove(blocks);

                for (int blkIndex = 0; blkIndex < blocks.Length; blkIndex++)
                {
                    BasicBlock block = blocks[blkIndex];

                    context.EnterBlock(block);

                    for (LinkedListNode<INode> opNode = block.Operations.First; opNode != null; opNode = opNode.Next)
                    {
                        Operation operation = (Operation)opNode.Value;

                        if (IsBranchInst(operation.Inst))
                        {
                            context.LeaveBlock(block, operation);
                        }
                        else
                        {
                            AddOperation(context, operation, targetLanguage, functions);
                        }
                    }
                }

                GotoElimination.Eliminate(context.GetGotos());

                AstOptimizer.Optimize(context);

                context.LeaveFunction();
            }

            return context.Info;
        }

        private static void AddOperation(StructuredProgramContext context, Operation operation, TargetLanguage targetLanguage, IReadOnlyList<Function> functions)
        {
            Instruction inst = operation.Inst;
            StorageKind storageKind = operation.StorageKind;

            if (inst is Instruction.Load or Instruction.Store)
            {
                if (storageKind.IsInputOrOutput())
                {
                    IoVariable ioVariable = (IoVariable)operation.GetSource(0).Value;
                    bool isOutput = storageKind.IsOutput();
                    int location = 0;
                    int component = 0;

                    if (context.Definitions.HasPerLocationInputOrOutput(ioVariable, isOutput))
                    {
                        location = operation.GetSource(1).Value;

                        if (operation.SourcesCount > 2 &&
                            operation.GetSource(2).Type == OperandType.Constant &&
                            context.Definitions.HasPerLocationInputOrOutputComponent(ioVariable, location, operation.GetSource(2).Value, isOutput))
                        {
                            component = operation.GetSource(2).Value;
                        }
                    }

                    context.Info.IoDefinitions.Add(new IoDefinition(storageKind, ioVariable, location, component));
                }
                else if (storageKind == StorageKind.ConstantBuffer && operation.GetSource(0).Type == OperandType.Constant)
                {
                    context.ResourceManager.SetUsedConstantBufferBinding(operation.GetSource(0).Value);
                }
            }

            bool vectorDest = IsVectorDestInst(inst);

            int sourcesCount = operation.SourcesCount;
            int outDestsCount = operation.DestsCount != 0 && !vectorDest ? operation.DestsCount - 1 : 0;

            IAstNode[] sources = new IAstNode[sourcesCount + outDestsCount];

            if (inst == Instruction.Call && targetLanguage == TargetLanguage.Spirv)
            {
                // SPIR-V requires that all function parameters are copied to a local variable before the call
                // (or at least that's what the Khronos compiler does).

                // First one is the function index.
                Operand funcIndexOperand = operation.GetSource(0);
                Debug.Assert(funcIndexOperand.Type == OperandType.Constant);
                int funcIndex = funcIndexOperand.Value;

                sources[0] = new AstOperand(OperandType.Constant, funcIndex);

                int inArgsCount = functions[funcIndex].InArgumentsCount;

                // Remaining ones are parameters, copy them to a temp local variable.
                for (int index = 1; index < operation.SourcesCount; index++)
                {
                    IAstNode source = context.GetOperandOrCbLoad(operation.GetSource(index));

                    if (index - 1 < inArgsCount)
                    {
                        AstOperand argTemp = context.NewTemp(FuncParameterType);
                        context.AddNode(new AstAssignment(argTemp, source));
                        sources[index] = argTemp;
                    }
                    else
                    {
                        sources[index] = source;
                    }
                }
            }
            else
            {
                for (int index = 0; index < operation.SourcesCount; index++)
                {
                    sources[index] = context.GetOperandOrCbLoad(operation.GetSource(index));
                }
            }

            for (int index = 0; index < outDestsCount; index++)
            {
                AstOperand oper = context.GetOperand(operation.GetDest(1 + index));

                oper.VarType = InstructionInfo.GetSrcVarType(inst, sourcesCount + index);

                sources[sourcesCount + index] = oper;
            }

            AstTextureOperation GetAstTextureOperation(TextureOperation texOp)
            {
                return new AstTextureOperation(
                    inst,
                    texOp.Type,
                    texOp.Format,
                    texOp.Flags,
                    texOp.Set,
                    texOp.Binding,
                    texOp.SamplerSet,
                    texOp.SamplerBinding,
                    texOp.Index,
                    sources);
            }

            int componentsCount = BitOperations.PopCount((uint)operation.Index);

            if (vectorDest && componentsCount > 1)
            {
                AggregateType destType = InstructionInfo.GetDestVarType(inst);

                IAstNode source;

                if (operation is TextureOperation texOp)
                {
                    if (texOp.Inst == Instruction.ImageLoad)
                    {
                        destType = texOp.Format.GetComponentType();
                    }

                    source = GetAstTextureOperation(texOp);
                }
                else
                {
                    source = new AstOperation(
                        inst,
                        operation.StorageKind,
                        operation.ForcePrecise,
                        operation.Index,
                        sources,
                        operation.SourcesCount);
                }

                AggregateType destElemType = destType;

                switch (componentsCount)
                {
                    case 2:
                        destType |= AggregateType.Vector2;
                        break;
                    case 3:
                        destType |= AggregateType.Vector3;
                        break;
                    case 4:
                        destType |= AggregateType.Vector4;
                        break;
                }

                AstOperand destVec = context.NewTemp(destType);

                context.AddNode(new AstAssignment(destVec, source));

                for (int i = 0; i < operation.DestsCount; i++)
                {
                    AstOperand dest = context.GetOperand(operation.GetDest(i));
                    AstOperand index = new(OperandType.Constant, i);

                    dest.VarType = destElemType;

                    context.AddNode(new AstAssignment(dest, new AstOperation(Instruction.VectorExtract, StorageKind.None, false, new[] { destVec, index }, 2)));
                }
            }
            else if (operation.Dest != null)
            {
                AstOperand dest = context.GetOperand(operation.Dest);

                // If all the sources are bool, it's better to use short-circuiting
                // logical operations, rather than forcing a cast to int and doing
                // a bitwise operation with the value, as it is likely to be used as
                // a bool in the end.
                if (IsBitwiseInst(inst) && AreAllSourceTypesEqual(sources, AggregateType.Bool))
                {
                    inst = GetLogicalFromBitwiseInst(inst);
                }

                bool isCondSel = inst == Instruction.ConditionalSelect;
                bool isCopy = inst == Instruction.Copy;

                if (isCondSel || isCopy)
                {
                    AggregateType type = GetVarTypeFromUses(operation.Dest);

                    if (isCondSel && type == AggregateType.FP32)
                    {
                        inst |= Instruction.FP32;
                    }

                    dest.VarType = type;
                }
                else
                {
                    dest.VarType = InstructionInfo.GetDestVarType(inst);
                }

                IAstNode source;

                if (operation is TextureOperation texOp)
                {
                    if (texOp.Inst == Instruction.ImageLoad)
                    {
                        dest.VarType = texOp.Format.GetComponentType();
                    }

                    source = GetAstTextureOperation(texOp);
                }
                else if (!isCopy)
                {
                    source = new AstOperation(
                        inst,
                        operation.StorageKind,
                        operation.ForcePrecise,
                        operation.Index,
                        sources,
                        operation.SourcesCount);
                }
                else
                {
                    source = sources[0];
                }

                context.AddNode(new AstAssignment(dest, source));
            }
            else if (operation.Inst == Instruction.Comment)
            {
                context.AddNode(new AstComment(((CommentNode)operation).Comment));
            }
            else if (operation is TextureOperation texOp)
            {
                AstTextureOperation astTexOp = GetAstTextureOperation(texOp);

                context.AddNode(astTexOp);
            }
            else
            {
                context.AddNode(new AstOperation(
                    inst,
                    operation.StorageKind,
                    operation.ForcePrecise,
                    operation.Index,
                    sources,
                    operation.SourcesCount));
            }

            // Those instructions needs to be emulated by using helper functions,
            // because they are NVIDIA specific. Those flags helps the backend to
            // decide which helper functions are needed on the final generated code.
            switch (operation.Inst)
            {
                case Instruction.MultiplyHighS32:
                    context.Info.HelperFunctionsMask |= HelperFunctionsMask.MultiplyHighS32;
                    break;
                case Instruction.MultiplyHighU32:
                    context.Info.HelperFunctionsMask |= HelperFunctionsMask.MultiplyHighU32;
                    break;
                case Instruction.SwizzleAdd:
                    context.Info.HelperFunctionsMask |= HelperFunctionsMask.SwizzleAdd;
                    break;
                case Instruction.FSIBegin:
                case Instruction.FSIEnd:
                    context.Info.HelperFunctionsMask |= HelperFunctionsMask.FSI;
                    break;
            }
        }

        private static AggregateType GetVarTypeFromUses(Operand dest)
        {
            HashSet<Operand> visited = [];

            Queue<Operand> pending = new();

            bool Enqueue(Operand operand)
            {
                if (visited.Add(operand))
                {
                    pending.Enqueue(operand);

                    return true;
                }

                return false;
            }

            Enqueue(dest);

            while (pending.TryDequeue(out Operand operand))
            {
                foreach (INode useNode in operand.UseOps)
                {
                    if (useNode is not Operation operation)
                    {
                        continue;
                    }

                    if (operation.Inst == Instruction.Copy)
                    {
                        if (operation.Dest.Type == OperandType.LocalVariable)
                        {
                            if (Enqueue(operation.Dest))
                            {
                                break;
                            }
                        }
                        else
                        {
                            return OperandInfo.GetVarType(operation.Dest.Type);
                        }
                    }
                    else
                    {
                        for (int index = 0; index < operation.SourcesCount; index++)
                        {
                            if (operation.GetSource(index) == operand)
                            {
                                return InstructionInfo.GetSrcVarType(operation.Inst, index);
                            }
                        }
                    }
                }
            }

            return AggregateType.S32;
        }

        private static bool AreAllSourceTypesEqual(IAstNode[] sources, AggregateType type)
        {
            foreach (IAstNode node in sources)
            {
                if (node is not AstOperand operand)
                {
                    return false;
                }

                if (operand.VarType != type)
                {
                    return false;
                }
            }

            return true;
        }

        private static bool IsVectorDestInst(Instruction inst)
        {
            return inst switch
            {
                Instruction.ImageLoad or
                Instruction.TextureSample => true,
                _ => false,
            };
        }

        private static bool IsBranchInst(Instruction inst)
        {
            return inst switch
            {
                Instruction.Branch or
                Instruction.BranchIfFalse or
                Instruction.BranchIfTrue => true,
                _ => false,
            };
        }

        private static bool IsBitwiseInst(Instruction inst)
        {
            return inst switch
            {
                Instruction.BitwiseAnd or
                Instruction.BitwiseExclusiveOr or
                Instruction.BitwiseNot or
                Instruction.BitwiseOr => true,
                _ => false,
            };
        }

        private static Instruction GetLogicalFromBitwiseInst(Instruction inst)
        {
            return inst switch
            {
                Instruction.BitwiseAnd => Instruction.LogicalAnd,
                Instruction.BitwiseExclusiveOr => Instruction.LogicalExclusiveOr,
                Instruction.BitwiseNot => Instruction.LogicalNot,
                Instruction.BitwiseOr => Instruction.LogicalOr,
                _ => throw new ArgumentException($"Unexpected instruction \"{inst}\"."),
            };
        }
    }
}
