using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.IO.Pipes;
using System.Reflection;
using System.Text;
using System.Windows;
using System.Windows.Documents;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Media.Media3D;
using Soletude.Stags.Images.Helpers;

namespace Soletude.Stagsi.Plugins
{
    public class FFMpeg
    {
        static string ffprobe;
        static string ffmpeg;

        public static void StagsiPlugin(IPluginService service)
        {
            // Path of Stagsi.exe.
            var exePath = Path.GetDirectoryName(Assembly.GetCallingAssembly().Location);
            ffprobe = string.Format("{0}\\Plugins\\ffprobe.exe", exePath);
            ffmpeg = string.Format("{0}\\Plugins\\ffmpeg.exe", exePath);

            if (!File.Exists(ffprobe) || !File.Exists(ffmpeg))
            {
                // Not registering the plugin in absense of ffprobe or ffmpeg.
                return;
            }

            // ffmpeg supports many more but these are the most common/useful.
            var supportedExtensions = new[] {
                "avi", "flv", "mkv", "mp4", "m4v", "mpg", "vob", "webm", "wmv",
                "3gp"
            };

            service.RegisterImageLoaderV1(RenderVideo,
                new ImageLoaderV1 {FileExtensions = supportedExtensions});
        }

        public static BitmapSource RenderVideo(Stream input, IImageLoadingV1 parameters)
        {
            // Match binary signatures because ffprobe doesn't provide canonical
            // container extension and we can't trust original file extension
            // that can be messed up.
            var format = DetectFormat(input);

            if (format == null)
            {
                return null;
            }

            var filePath = parameters.FilePath;

            // Stagsi's run-time plugin compiler doesn't support $"" so using Format().
            var ffprobeArgs = string.Format("-v quiet -select_streams v:0 -show_entries stream=width,height:format=duration -of default=nw=1 \"{0}\"", filePath);

            var process = new Process();
            process.StartInfo.FileName = ffprobe;
            process.StartInfo.Arguments = ffprobeArgs;
            process.StartInfo.RedirectStandardOutput = true;
            process.StartInfo.UseShellExecute = false;
            process.StartInfo.CreateNoWindow = true;
            process.StartInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden;
            process.Start();

            var sb = new StringBuilder();
            while (!process.StandardOutput.EndOfStream)
            {
                sb.AppendLine(process.StandardOutput.ReadLine());
            }

            var str = sb.ToString().Split(new[] { "\r\n" }, StringSplitOptions.None);
            var videoWidth = int.Parse(str[0].Replace("width=", ""));
            var videoHeight = int.Parse(str[1].Replace("height=",""));
            // Set parameters.Width/Height/Format for Stagsi to know the file's features.
            FileMagic.FillOutValues(parameters, videoWidth, videoHeight, format);

            var width = (int)parameters.Width;
            var height = (int)parameters.Height;
            // Does Stagsi want original video dimensions or a smaller box is fine?
            if (width == 0 || height == 0)
            {
                width = videoWidth;
                height = videoHeight;
            }

            // If a smaller box is desired, fit the video's frames within that box.
            if (videoWidth > width / 2)
            {
                var originalProp = (double)videoWidth / videoHeight;
                videoWidth = width / 2;     // 2x2 grid thus dividing by 2
                videoHeight = (int)(videoWidth / originalProp);
            }
            else if (videoHeight > height / 2)
            {
                var originalProp = (double)videoWidth / videoHeight;
                videoHeight = height / 2;
                videoWidth = (int)(videoHeight * originalProp);
            }

            // Video's duration in seconds (may be fractional).
            var duration = double.Parse(str[2].Replace("duration=", ""),
                CultureInfo.InvariantCulture);

            RenderTargetBitmap[] images = new RenderTargetBitmap[4];
            for (var i = 0; i < 4; i++)
            {
                // For the top-left thumbnail, take the frame of the 5th second
                // (if video is shorter than 10 seconds, take the middle frame).
                // For others, use frames at 1/6, 3/6, 5/6 video length.
                var seconds = i == 0
                    ? (duration > 10 ? 5 : duration / 2)
                    : (duration / 3 * (i - 1) + duration / 6);

                var ffmpegArgs = string.Format("-ss {0} -i \"{1}\" -s {2}*{3} -y -f rawvideo -c:v bmp -vframes 1 -",
                    TimeSpan.FromMilliseconds(1000.0 * seconds), filePath, videoWidth, videoHeight);

                process = new Process();
                process.StartInfo.FileName = ffmpeg;
                process.StartInfo.Arguments = ffmpegArgs;
                process.StartInfo.RedirectStandardOutput = true;
                process.StartInfo.UseShellExecute = false;
                process.StartInfo.CreateNoWindow = true;
                process.StartInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden;
                process.Start();

                MemoryStream stream = new MemoryStream();
                process.StandardOutput.BaseStream.CopyTo(stream);
                images[i] = (RenderTargetBitmap)FileMagic.GetBitmapImageFromStream(stream,
                    width / 2, videoWidth, videoHeight);
            }

            return GetRenderTarget4(images[0], images[1], images[2], images[3], width, height);

        }

        public static string DetectFormat(Stream input)
        {
            var length = 16;
            var buf = new byte[length];

            if (input.Read(buf, 0, length) == length)
            {
                // Credits: https://www.garykessler.net/library/file_sigs.html
                if (buf[0]  == 0x52 && buf[1]  == 0x49 && buf[2]  == 0x46 && buf[3] == 0x46 &&
                    buf[8]  == 0x41 && buf[9]  == 0x56 && buf[10] == 0x49 && buf[11] == 0x20 &&
                    buf[12] == 0x4C && buf[13] == 0x49 && buf[14] == 0x53 && buf[15] == 0x54)
                {
                    return "avi";
                }
                else if (buf[0] == 0x46 && buf[1] == 0x4C && buf[2] == 0x56 && buf[3] == 0x01)
                {
                    return "flv";
                }
                else if (buf[0] == 0x1A && buf[1] == 0x45 && buf[2] == 0xDF && buf[3] == 0xA3)
                {
                    return "mkv"; // + webm (same container format, same signature)
                }
                else if (buf[4] == 0x66 && buf[5] == 0x74 && buf[6] == 0x79 && buf[7] == 0x70)
                {
                    if ((buf[8] == 0x4D && buf[9] == 0x34 && buf[10] == 0x56 && buf[11] == 0x20) ||
                        (buf[8] == 0x6D && buf[9] == 0x70 && buf[10] == 0x34 && buf[11] == 0x32))
                    {
                        return "m4v";
                    }
                    else if ((buf[8] == 0x4D && buf[9] == 0x53 && buf[10] == 0x4E && buf[11] == 0x56) ||
                             (buf[8] == 0x69 && buf[9] == 0x73 && buf[10] == 0x6F && buf[11] == 0x6D))
                    {
                        return "mp4";
                    }
                    else if (buf[8] == 0x33 && buf[9] == 0x67 && buf[10] == 0x70)
                    {
                        return "3gp";
                    }
                }
                else if (buf[0] == 0x00 && buf[1] == 0x00 && buf[2] == 0x01 && buf[3] >= 0xB0 && buf[3] <= 0xBF)
                {
                    return buf[3] == 0xBA ? "vob" : "mpg";
                }
                else if (buf[0]  == 0x30 && buf[1]  == 0x26 && buf[2]  == 0xB2 && buf[3]  == 0x75 &&
                         buf[4]  == 0x8E && buf[5]  == 0x66 && buf[6]  == 0xCF && buf[7]  == 0x11 &&
                         buf[8]  == 0xA6 && buf[9]  == 0xD9 && buf[10] == 0x00 && buf[11] == 0xAA &&
                         buf[12] == 0x00 && buf[13] == 0x62 && buf[14] == 0xCE && buf[15] == 0x6C)
                {
                    return "wmv";
                }
            }
            return null;
        }

        public static RenderTargetBitmap GetRenderTarget4(RenderTargetBitmap result1,
            RenderTargetBitmap result2, RenderTargetBitmap result3,
            RenderTargetBitmap result4, int width, int height)
        {
            // Try PixelFormats.Default or PixelFormats.Bgra32 or PixelFormats.Rgb24 instead of PixelFormats.Pbgra32.
            // The P stands for pre-multiplied - the assumption is that each channel is pre-multiplied by alpha.
            var visual = new DrawingVisual();
            var dc = visual.RenderOpen();
            var rect = new Rect(0, 0, width, height);
            dc.DrawRectangle(Brushes.White, null, rect);
            RenderResult(dc, result1, 0, 0, width / 2, height / 2);
            RenderResult(dc, result2, width / 2, 0, width / 2, height / 2);
            RenderResult(dc, result3, 0, height / 2, width / 2, height / 2);
            RenderResult(dc, result4, width / 2, height / 2, width / 2, height / 2);
            dc.Close();

            var dpiXProperty = typeof(SystemParameters).GetProperty("DpiX",
                BindingFlags.NonPublic | BindingFlags.Static);
            var dpiYProperty = typeof(SystemParameters).GetProperty("Dpi",
                BindingFlags.NonPublic | BindingFlags.Static);

            var dpiX = (int)dpiXProperty.GetValue(null, null);
            var dpiY = (int)dpiYProperty.GetValue(null, null);

            var pixWid = (int)(width  * dpiX / 96.0);
            var pixHei = (int)(height * dpiY / 96.0);

            var bmp = new RenderTargetBitmap(pixWid, pixHei, dpiX, dpiY,
                PixelFormats.Default);
            bmp.Render(visual);
            bmp.Freeze();
            return bmp;
        }

        private static void RenderResult(DrawingContext dc, RenderTargetBitmap result,
            double startX, double startY, double wid, double hei)
        {
            var prop = (double)result.PixelWidth / result.PixelHeight;
            double finalWidth  = wid;
            double finalHeight = hei;
            double offsetX = 0;
            double offsetY = 0;
            if (prop > 1)
            {
                finalHeight = finalWidth / prop;
                offsetY = (hei - finalHeight) / 2;
            }
            else
            {
                finalWidth = finalHeight * prop;
                offsetX = (wid - finalWidth) / 2;
            }
            dc.DrawImage(result, new Rect(startX + offsetX, startY + offsetY, finalWidth, finalHeight));
        }
    }
}
