using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Runtime.InteropServices;
using SharpDX;
using SharpDX.Direct3D11;
using SharpDX.DXGI;
using TurboJpegWrapper;
using Device = SharpDX.Direct3D11.Device;
using MapFlags = SharpDX.Direct3D11.MapFlags;
using Resource = SharpDX.DXGI.Resource;

namespace Crysome.Client.Handlers
{
    internal sealed class DxgiCapture : IDisposable
    {
        private Device _device;
        private OutputDuplication _duplication;
        private Texture2D _staging;
        private int _width;
        private int _height;
        private bool _disposed;

        private byte[] _bgrBuffer;
        private TJCompressor _turboJpeg;
        private bool _turboAvailable;

        public int Width => _width;
        public int Height => _height;
        public bool IsInitialized => _duplication != null;

        public bool Init(int screenIndex)
        {
            Cleanup();
            try
            {
                using (var factory = new Factory1())
                {
                    Adapter1 adapter = factory.GetAdapter1(0);
                    _device = new Device(adapter, DeviceCreationFlags.BgraSupport);

                    int outputIdx = Math.Max(0, screenIndex);
                    Output output = adapter.GetOutput(outputIdx);
                    using (var output1 = output.QueryInterface<Output1>())
                    {
                        _width = output.Description.DesktopBounds.Right - output.Description.DesktopBounds.Left;
                        _height = output.Description.DesktopBounds.Bottom - output.Description.DesktopBounds.Top;

                        _staging = new Texture2D(_device, new Texture2DDescription
                        {
                            CpuAccessFlags = CpuAccessFlags.Read,
                            BindFlags = BindFlags.None,
                            Format = Format.B8G8R8A8_UNorm,
                            Width = _width,
                            Height = _height,
                            MipLevels = 1,
                            ArraySize = 1,
                            SampleDescription = new SampleDescription(1, 0),
                            Usage = ResourceUsage.Staging,
                            OptionFlags = ResourceOptionFlags.None
                        });

                        _duplication = output1.DuplicateOutput(_device);
                    }
                    output.Dispose();
                    adapter.Dispose();
                }

                _bgrBuffer = new byte[_width * _height * 3];

                try
                {
                    _turboJpeg = new TJCompressor();
                    _turboAvailable = true;
                }
                catch
                {
                    _turboAvailable = false;
                }

                return true;
            }
            catch
            {
                Cleanup();
                return false;
            }
        }

        public byte[] CaptureFrameToJpeg(int quality, int timeoutMs = 8)
        {
            if (_duplication == null)
                return null;

            Resource desktopResource = null;
            try
            {
                var result = _duplication.TryAcquireNextFrame(timeoutMs, out _, out desktopResource);
                if (result.Failure || desktopResource == null)
                    return null;

                using (var texture = desktopResource.QueryInterface<Texture2D>())
                {
                    _device.ImmediateContext.CopyResource(texture, _staging);
                }

                var mapSource = _device.ImmediateContext.MapSubresource(_staging, 0, MapMode.Read, MapFlags.None);
                try
                {
                    ConvertBgraToRgb(mapSource.DataPointer, mapSource.RowPitch);
                }
                finally
                {
                    _device.ImmediateContext.UnmapSubresource(_staging, 0);
                }

                if (_turboAvailable)
                    return TurboEncode(quality);
                else
                    return GdiEncode(quality);
            }
            catch (SharpDXException ex) when (ex.ResultCode.Code == SharpDX.DXGI.ResultCode.AccessLost.Result.Code)
            {
                Cleanup();
                return null;
            }
            catch
            {
                return null;
            }
            finally
            {
                try { desktopResource?.Dispose(); } catch { }
                try { _duplication?.ReleaseFrame(); } catch { }
            }
        }

        private unsafe void ConvertBgraToRgb(IntPtr srcPtr, int srcStride)
        {
            fixed (byte* dstBase = _bgrBuffer)
            {
                byte* src = (byte*)srcPtr;
                byte* dst = dstBase;
                int dstStride = _width * 3;

                for (int y = 0; y < _height; y++)
                {
                    byte* srcRow = src + y * srcStride;
                    byte* dstRow = dst + y * dstStride;
                    int x = 0;

                    for (; x < _width; x++)
                    {
                        dstRow[0] = srcRow[2]; // R (turbo wants RGB)
                        dstRow[1] = srcRow[1]; // G
                        dstRow[2] = srcRow[0]; // B
                        srcRow += 4;
                        dstRow += 3;
                    }
                }
            }
        }

        private byte[] TurboEncode(int quality)
        {
            try
            {
                return _turboJpeg.Compress(
                    _bgrBuffer,
                    _width * 3,
                    _width,
                    _height,
                    TJPixelFormats.TJPF_RGB,
                    TJSubsamplingOptions.TJSAMP_420,
                    quality,
                    TJFlags.FASTDCT);
            }
            catch
            {
                _turboAvailable = false;
                return GdiEncode(quality);
            }
        }

        private byte[] GdiEncode(int quality)
        {
            using (var bmp = new Bitmap(_width, _height, PixelFormat.Format24bppRgb))
            {
                var bmpData = bmp.LockBits(
                    new Rectangle(0, 0, _width, _height),
                    ImageLockMode.WriteOnly,
                    PixelFormat.Format24bppRgb);

                int bgrStride = _width * 3;
                int dstStride = bmpData.Stride;

                unsafe
                {
                    fixed (byte* srcBase = _bgrBuffer)
                    {
                        byte* dst = (byte*)bmpData.Scan0;
                        for (int y = 0; y < _height; y++)
                        {
                            // BGR buffer has RGB order, but Bitmap Format24bppRgb expects BGR
                            byte* srcRow = srcBase + y * bgrStride;
                            byte* dstRow = dst + y * dstStride;
                            for (int x = 0; x < _width; x++)
                            {
                                dstRow[0] = srcRow[2]; // B
                                dstRow[1] = srcRow[1]; // G
                                dstRow[2] = srcRow[0]; // R
                                srcRow += 3;
                                dstRow += 3;
                            }
                        }
                    }
                }

                bmp.UnlockBits(bmpData);

                var codec = GetJpegCodec();
                using (var ms = new MemoryStream(131072))
                {
                    if (codec != null)
                    {
                        using (var ep = new EncoderParameters(1))
                        {
                            ep.Param[0] = new EncoderParameter(Encoder.Quality, quality);
                            bmp.Save(ms, codec, ep);
                        }
                    }
                    else
                    {
                        bmp.Save(ms, ImageFormat.Jpeg);
                    }
                    return ms.ToArray();
                }
            }
        }

        private static ImageCodecInfo _jpegCodec;
        private static readonly object _codecLock = new object();

        private static ImageCodecInfo GetJpegCodec()
        {
            if (_jpegCodec != null) return _jpegCodec;
            lock (_codecLock)
            {
                if (_jpegCodec != null) return _jpegCodec;
                foreach (var info in ImageCodecInfo.GetImageEncoders())
                    if (info.FormatID == ImageFormat.Jpeg.Guid)
                    { _jpegCodec = info; break; }
            }
            return _jpegCodec;
        }

        private void Cleanup()
        {
            try { _turboJpeg?.Dispose(); } catch { }
            try { _duplication?.Dispose(); } catch { }
            try { _staging?.Dispose(); } catch { }
            try { _device?.Dispose(); } catch { }
            _turboJpeg = null;
            _duplication = null;
            _staging = null;
            _device = null;
            _bgrBuffer = null;
        }

        public void Dispose()
        {
            if (!_disposed)
            {
                _disposed = true;
                Cleanup();
            }
        }
    }
}
