using Ryujinx.Graphics.Shader.Decoders;
using Ryujinx.Graphics.Shader.IntermediateRepresentation;
using System.Collections.Generic;

using static Ryujinx.Graphics.Shader.IntermediateRepresentation.OperandHelper;
using static Ryujinx.Graphics.Shader.Translation.Translator;

namespace Ryujinx.Graphics.Shader.Translation
{
    public class TranslatorContext
    {
        private readonly Block[][] _cfg;
        private readonly Block[][] _cfgA;
        private ShaderConfig _config;
        private ShaderConfig _configA;

        public ulong Address { get; }
        public ulong AddressA { get; }

        public ShaderStage Stage => _config.Stage;
        public int Size => _config.Size;
        public int SizeA => _configA != null ? _configA.Size : 0;

        public HashSet<int> TextureHandlesForCache => _config.TextureHandlesForCache;

        public IGpuAccessor GpuAccessor => _config.GpuAccessor;

        internal TranslatorContext(ulong address, Block[][] cfg, ShaderConfig config)
        {
            Address    = address;
            AddressA   = 0;
            _config    = config;
            _configA   = null;
            _cfg       = cfg;
            _cfgA      = null;
        }

        internal TranslatorContext(ulong addressA, ulong addressB, Block[][] cfgA, Block[][] cfgB, ShaderConfig configA, ShaderConfig configB)
        {
            Address  = addressB;
            AddressA = addressA;
            _config  = configB;
            _configA = configA;
            _cfg     = cfgB;
            _cfgA    = cfgA;
        }

        private static bool IsUserAttribute(Operand operand)
        {
            return operand != null &&
                   operand.Type == OperandType.Attribute &&
                   operand.Value >= AttributeConsts.UserAttributeBase &&
                   operand.Value < AttributeConsts.UserAttributeEnd;
        }

        private static FunctionCode[] Combine(FunctionCode[] a, FunctionCode[] b)
        {
            // Here we combine two shaders.
            // For shader A:
            // - All user attribute stores on shader A are turned into copies to a
            // temporary variable. It's assumed that shader B will consume them.
            // - All return instructions are turned into branch instructions, the
            // branch target being the start of the shader B code.
            // For shader B:
            // - All user attribute loads on shader B are turned into copies from a
            // temporary variable, as long that attribute is written by shader A.
            FunctionCode[] output = new FunctionCode[a.Length + b.Length - 1];

            List<Operation> ops = new List<Operation>(a.Length + b.Length);

            Operand[] temps = new Operand[AttributeConsts.UserAttributesCount * 4];

            Operand lblB = Label();

            for (int index = 0; index < a[0].Code.Length; index++)
            {
                Operation operation = a[0].Code[index];

                if (IsUserAttribute(operation.Dest))
                {
                    int tIndex = (operation.Dest.Value - AttributeConsts.UserAttributeBase) / 4;

                    Operand temp = temps[tIndex];

                    if (temp == null)
                    {
                        temp = Local();

                        temps[tIndex] = temp;
                    }

                    operation.Dest = temp;
                }

                if (operation.Inst == Instruction.Return)
                {
                    ops.Add(new Operation(Instruction.Branch, lblB));
                }
                else
                {
                    ops.Add(operation);
                }
            }

            ops.Add(new Operation(Instruction.MarkLabel, lblB));

            for (int index = 0; index < b[0].Code.Length; index++)
            {
                Operation operation = b[0].Code[index];

                for (int srcIndex = 0; srcIndex < operation.SourcesCount; srcIndex++)
                {
                    Operand src = operation.GetSource(srcIndex);

                    if (IsUserAttribute(src))
                    {
                        Operand temp = temps[(src.Value - AttributeConsts.UserAttributeBase) / 4];

                        if (temp != null)
                        {
                            operation.SetSource(srcIndex, temp);
                        }
                    }
                }

                ops.Add(operation);
            }

            output[0] = new FunctionCode(ops.ToArray());

            for (int i = 1; i < a.Length; i++)
            {
                output[i] = a[i];
            }

            for (int i = 1; i < b.Length; i++)
            {
                output[a.Length + i - 1] = b[i];
            }

            return output;
        }

        public ShaderProgram Translate(out ShaderProgramInfo shaderProgramInfo)
        {
            FunctionCode[] code = EmitShader(_cfg, _config);

            if (_configA != null)
            {
                FunctionCode[] codeA = EmitShader(_cfgA, _configA);

                _config.SetUsedFeature(_configA.UsedFeatures);

                code = Combine(codeA, code);
            }

            return Translator.Translate(code, _config, out shaderProgramInfo, SizeA);
        }
    }
}
