using System.IO;

namespace Ryujinx.Graphics.VDec
{
    class H264Decoder
    {
        private int    _log2MaxPicOrderCntLsbMinus4;
        private bool   _deltaPicOrderAlwaysZeroFlag;
        private bool   _frameMbsOnlyFlag;
        private int    _picWidthInMbs;
        private int    _picHeightInMapUnits;
        private bool   _entropyCodingModeFlag;
        private bool   _bottomFieldPicOrderInFramePresentFlag;
        private int    _numRefIdxL0DefaultActiveMinus1;
        private int    _numRefIdxL1DefaultActiveMinus1;
        private bool   _deblockingFilterControlPresentFlag;
        private bool   _redundantPicCntPresentFlag;
        private bool   _transform8x8ModeFlag;
        private bool   _mbAdaptiveFrameFieldFlag;
        private bool   _direct8x8InferenceFlag;
        private bool   _weightedPredFlag;
        private bool   _constrainedIntraPredFlag;
        private bool   _fieldPicFlag;
        private bool   _bottomFieldFlag;
        private int    _log2MaxFrameNumMinus4;
        private int    _chromaFormatIdc;
        private int    _picOrderCntType;
        private int    _picInitQpMinus26;
        private int    _chromaQpIndexOffset;
        private int    _chromaQpIndexOffset2;
        private int    _weightedBipredIdc;
        private int    _frameNumber;
        private byte[] _scalingMatrix4;
        private byte[] _scalingMatrix8;

        public void Decode(H264ParameterSets Params, H264Matrices matrices, byte[] frameData)
        {
            _log2MaxPicOrderCntLsbMinus4           = Params.Log2MaxPicOrderCntLsbMinus4;
            _deltaPicOrderAlwaysZeroFlag           = Params.DeltaPicOrderAlwaysZeroFlag;
            _frameMbsOnlyFlag                      = Params.FrameMbsOnlyFlag;
            _picWidthInMbs                         = Params.PicWidthInMbs;
            _picHeightInMapUnits                   = Params.PicHeightInMapUnits;
            _entropyCodingModeFlag                 = Params.EntropyCodingModeFlag;
            _bottomFieldPicOrderInFramePresentFlag = Params.BottomFieldPicOrderInFramePresentFlag;
            _numRefIdxL0DefaultActiveMinus1        = Params.NumRefIdxL0DefaultActiveMinus1;
            _numRefIdxL1DefaultActiveMinus1        = Params.NumRefIdxL1DefaultActiveMinus1;
            _deblockingFilterControlPresentFlag    = Params.DeblockingFilterControlPresentFlag;
            _redundantPicCntPresentFlag            = Params.RedundantPicCntPresentFlag;
            _transform8x8ModeFlag                  = Params.Transform8x8ModeFlag;

            _mbAdaptiveFrameFieldFlag = ((Params.Flags >> 0) & 1) != 0;
            _direct8x8InferenceFlag   = ((Params.Flags >> 1) & 1) != 0;
            _weightedPredFlag         = ((Params.Flags >> 2) & 1) != 0;
            _constrainedIntraPredFlag = ((Params.Flags >> 3) & 1) != 0;
            _fieldPicFlag             = ((Params.Flags >> 5) & 1) != 0;
            _bottomFieldFlag          = ((Params.Flags >> 6) & 1) != 0;

            _log2MaxFrameNumMinus4  = (int)(Params.Flags >> 8)  & 0xf;
            _chromaFormatIdc        = (int)(Params.Flags >> 12) & 0x3;
            _picOrderCntType        = (int)(Params.Flags >> 14) & 0x3;
            _picInitQpMinus26       = (int)(Params.Flags >> 16) & 0x3f;
            _chromaQpIndexOffset    = (int)(Params.Flags >> 22) & 0x1f;
            _chromaQpIndexOffset2   = (int)(Params.Flags >> 27) & 0x1f;
            _weightedBipredIdc      = (int)(Params.Flags >> 32) & 0x3;
            _frameNumber            = (int)(Params.Flags >> 46) & 0x1ffff;

            _picInitQpMinus26     = (_picInitQpMinus26     << 26) >> 26;
            _chromaQpIndexOffset  = (_chromaQpIndexOffset  << 27) >> 27;
            _chromaQpIndexOffset2 = (_chromaQpIndexOffset2 << 27) >> 27;

            _scalingMatrix4 = matrices.ScalingMatrix4;
            _scalingMatrix8 = matrices.ScalingMatrix8;

            if (FFmpegWrapper.IsInitialized)
            {
                FFmpegWrapper.DecodeFrame(frameData);
            }
            else
            {
                FFmpegWrapper.H264Initialize();

                FFmpegWrapper.DecodeFrame(DecoderHelper.Combine(EncodeHeader(), frameData));
            }
        }

        private byte[] EncodeHeader()
        {
            using (MemoryStream data = new MemoryStream())
            {
                H264BitStreamWriter writer = new H264BitStreamWriter(data);

                // Sequence Parameter Set.
                writer.WriteU(1, 24);
                writer.WriteU(0, 1);
                writer.WriteU(3, 2);
                writer.WriteU(7, 5);
                writer.WriteU(100, 8);
                writer.WriteU(0, 8);
                writer.WriteU(31, 8);
                writer.WriteUe(0);
                writer.WriteUe(_chromaFormatIdc);

                if (_chromaFormatIdc == 3)
                {
                    writer.WriteBit(false);
                }

                writer.WriteUe(0);
                writer.WriteUe(0);
                writer.WriteBit(false);
                writer.WriteBit(false); //Scaling matrix present flag

                writer.WriteUe(_log2MaxFrameNumMinus4);
                writer.WriteUe(_picOrderCntType);

                if (_picOrderCntType == 0)
                {
                    writer.WriteUe(_log2MaxPicOrderCntLsbMinus4);
                }
                else if (_picOrderCntType == 1)
                {
                    writer.WriteBit(_deltaPicOrderAlwaysZeroFlag);

                    writer.WriteSe(0);
                    writer.WriteSe(0);
                    writer.WriteUe(0);
                }

                int picHeightInMbs = _picHeightInMapUnits / (_frameMbsOnlyFlag ? 1 : 2);

                writer.WriteUe(16);
                writer.WriteBit(false);
                writer.WriteUe(_picWidthInMbs - 1);
                writer.WriteUe(picHeightInMbs - 1);
                writer.WriteBit(_frameMbsOnlyFlag);

                if (!_frameMbsOnlyFlag)
                {
                    writer.WriteBit(_mbAdaptiveFrameFieldFlag);
                }

                writer.WriteBit(_direct8x8InferenceFlag);
                writer.WriteBit(false); //Frame cropping flag
                writer.WriteBit(false); //VUI parameter present flag

                writer.End();

                // Picture Parameter Set.
                writer.WriteU(1, 24);
                writer.WriteU(0, 1);
                writer.WriteU(3, 2);
                writer.WriteU(8, 5);

                writer.WriteUe(0);
                writer.WriteUe(0);

                writer.WriteBit(_entropyCodingModeFlag);
                writer.WriteBit(false);
                writer.WriteUe(0);
                writer.WriteUe(_numRefIdxL0DefaultActiveMinus1);
                writer.WriteUe(_numRefIdxL1DefaultActiveMinus1);
                writer.WriteBit(_weightedPredFlag);
                writer.WriteU(_weightedBipredIdc, 2);
                writer.WriteSe(_picInitQpMinus26);
                writer.WriteSe(0);
                writer.WriteSe(_chromaQpIndexOffset);
                writer.WriteBit(_deblockingFilterControlPresentFlag);
                writer.WriteBit(_constrainedIntraPredFlag);
                writer.WriteBit(_redundantPicCntPresentFlag);
                writer.WriteBit(_transform8x8ModeFlag);

                writer.WriteBit(true);

                for (int index = 0; index < 6; index++)
                {
                    writer.WriteBit(true);

                    WriteScalingList(writer, _scalingMatrix4, index * 16, 16);
                }

                if (_transform8x8ModeFlag)
                {
                    for (int index = 0; index < 2; index++)
                    {
                        writer.WriteBit(true);

                        WriteScalingList(writer, _scalingMatrix8, index * 64, 64);
                    }
                }

                writer.WriteSe(_chromaQpIndexOffset2);

                writer.End();

                return data.ToArray();
            }
        }

        // ZigZag LUTs from libavcodec.
        private static readonly byte[] ZigZagDirect = new byte[]
        {
            0,   1,  8, 16,  9,  2,  3, 10,
            17, 24, 32, 25, 18, 11,  4,  5,
            12, 19, 26, 33, 40, 48, 41, 34,
            27, 20, 13,  6,  7, 14, 21, 28,
            35, 42, 49, 56, 57, 50, 43, 36,
            29, 22, 15, 23, 30, 37, 44, 51,
            58, 59, 52, 45, 38, 31, 39, 46,
            53, 60, 61, 54, 47, 55, 62, 63
        };

        private static readonly byte[] ZigZagScan = new byte[]
        {
            0 + 0 * 4, 1 + 0 * 4, 0 + 1 * 4, 0 + 2 * 4,
            1 + 1 * 4, 2 + 0 * 4, 3 + 0 * 4, 2 + 1 * 4,
            1 + 2 * 4, 0 + 3 * 4, 1 + 3 * 4, 2 + 2 * 4,
            3 + 1 * 4, 3 + 2 * 4, 2 + 3 * 4, 3 + 3 * 4
        };

        private static void WriteScalingList(H264BitStreamWriter writer, byte[] list, int start, int count)
        {
            byte[] scan = count == 16 ? ZigZagScan : ZigZagDirect;

            int lastScale = 8;

            for (int index = 0; index < count; index++)
            {
                byte value = list[start + scan[index]];

                int deltaScale = value - lastScale;

                writer.WriteSe(deltaScale);

                lastScale = value;
            }
        }
    }
}