// Authors
//   Copyright (C) 2014 Stephan Sundermann <stephansundermann@gmail.com>

using System;
using Gst;
using Gtk;
using System.Runtime.InteropServices;
using Gst.Video;

namespace GstreamerSharp
{
    class Playback
    {
        static Element Playbin;
        static Range Slider;
        static TextView StreamsList;
        static ulong silderUpdateSignalID;

        static State State;
        static long Duration = -1;
        static int ignoreCount = 0;

        static void HandleValueChanged(object sender, EventArgs e)
        {
            var range = (Range)sender;
            var value = range.Value;
            Playbin.SeekSimple(Format.Time, SeekFlags.Flush | SeekFlags.KeyUnit, (long)(value * Gst.Constants.SECOND));
        }

        // This method is called when the STOP button is clicked
        static void HandleStop(object sender, EventArgs e)
        {
            Playbin.SetState(State.Ready);
        }

        // This method is called when the PAUSE button is clicked
        static void HandlePause(object sender, EventArgs e)
        {
            Playbin.SetState(State.Paused);
        }

        // This method is called when the PLAY button is clicked
        static void HandlePlay(object sender, EventArgs e)
        {
            Playbin.SetState(State.Playing);

        }

        static void HandleRealized(object sender, EventArgs e)
        {
            var widget = (Widget)sender;
            var window = widget.Window;
            IntPtr windowID = IntPtr.Zero;

            // Retrieve window handler from GDK
            switch (System.Environment.OSVersion.Platform)
            {
                case PlatformID.Unix:
                    windowID = gdk_x11_window_get_xid(window.Handle);
                    break;
                case PlatformID.Win32NT:
                case PlatformID.Win32S:
                case PlatformID.Win32Windows:
                case PlatformID.WinCE:
                    windowID = gdk_win32_window_get_handle(window.Handle);
                    break;
            }

            Element overlay = null;
            if (Playbin is Gst.Bin)
                overlay = ((Gst.Bin)Playbin).GetByInterface(VideoOverlayAdapter.GType);

            VideoOverlayAdapter adapter = new VideoOverlayAdapter(overlay.Handle);
// HERE
            adapter.SetRenderRectangle(50, 50, 214, 120);
            adapter.WindowHandle = windowID;
            adapter.HandleEvents(true);
        }

        // This function is called when the main window is closed
        static void HandleDelete(object o, DeleteEventArgs args)
        {
            HandleStop(null, null);
            Gtk.Application.Quit();
        }

        //This function is called everytime the video window needs to be redrawn (due to damage/exposure, rescaling, etc). GStreamer takes care of this in the PAUSED and PLAYING states, otherwise, we simply draw a black rectangle to avoid garbage showing up. */
        static void HandleDamage(object o, DamageEventArgs args)
        {
            var widget = (Widget)o;

            if (State != State.Paused && State != State.Playing)
            {
                var window = widget.Window;
                var allocation = widget.Allocation;

                var cr = Gdk.CairoHelper.Create(window);
                cr.SetSourceRGB(0, 0, 0);
                cr.Rectangle(0, 0, allocation.Width, allocation.Height);
                cr.Fill();
                cr.Dispose();
            }

            args.RetVal = false;
        }

        static void HandleDrawn(object o, DrawnArgs args)
        {
            var widget = o as Widget;
            Playbin.GetState(out State state, out State pending, 100);

            if (state != State.Paused && state != State.Playing)
            {
                var window = widget.Window;
                var allocation = widget.Allocation;

                var cr = Gdk.CairoHelper.Create(window);
                cr.SetSourceRGB(255, 0, 0);
                cr.Rectangle(0, 0, allocation.Width, allocation.Height);
                cr.Fill();
                cr.Dispose();
            }
            args.RetVal = false;
        }

        static void CreateUI()
        {
            var mainWindow = new Window(WindowType.Toplevel);
            mainWindow.DeleteEvent += HandleDelete;

            var videoWindow = new DrawingArea();
            videoWindow.DoubleBuffered = false;
            videoWindow.Realized += HandleRealized;
            videoWindow.Drawn += HandleDrawn;
            videoWindow.DamageEvent += HandleDamage;

            var playButton = new Button(Stock.MediaPlay);
            playButton.Clicked += HandlePlay;

            var pauseButton = new Button(Stock.MediaPause);
            pauseButton.Clicked += HandlePause;

            var stopButton = new Button(Stock.MediaStop);
            stopButton.Clicked += HandleStop;

            Slider = new HScale(0, 100, 1);
            ((Scale)Slider).DrawValue = false;
            Slider.ValueChanged += HandleValueChanged;

            StreamsList = new TextView();
            StreamsList.Editable = false;

            var controls = new HBox(false, 0);
            controls.PackStart(playButton, false, false, 2);
            controls.PackStart(pauseButton, false, false, 2);
            controls.PackStart(stopButton, false, false, 2);
            controls.PackStart(Slider, true, true, 2);

            var mainHBox = new HBox(false, 0);
            mainHBox.PackStart(videoWindow, true, true, 0);
            mainHBox.PackStart(StreamsList, false, false, 2);

            var mainBox = new VBox(false, 0);
            mainBox.PackStart(mainHBox, true, true, 0);
            mainBox.PackStart(controls, false, false, 0);
            mainWindow.Add(mainBox);
            mainWindow.SetDefaultSize(640, 480);

            mainWindow.ShowAll();
        }

        // This function is called periodically to refresh the GUI
        static bool RefreshUI()
        {
            var fmt = Format.Time;
            long current = 0;

            // We do not want to update anything nless we are in the PAUSED or PLAYING states
            if (State != State.Playing && State != State.Paused)
                return true;

            // If we didn't know it yet, query the stream duration
            if (Duration < 0)
            {
                if (!Playbin.QueryDuration(fmt, out Duration))
                    Console.WriteLine("Could not query the current duration.");
                else
                {
                    // Set the range of the silder to the clip duration, in SECONDS
                    Slider.SetRange(0, Duration / (double)Gst.Constants.SECOND);
                }
            }

            if (Playbin.QueryPosition(fmt, out current))
            {
                // Block the "value-changed" signal, so the HandleSlider function is not called (which would trigger a seek the user has not requested)
                ignoreCount++;
                Slider.ValueChanged -= HandleValueChanged;
                // Set the position of the slider to the current pipeline position, in SECONDS
                Slider.Value = current / (double)Gst.Constants.SECOND;
                Slider.ValueChanged += HandleValueChanged;

            }
            return true;
        }



        // This function is called when an error message is posted on the bus
        static void HandleTags(object sender, GLib.SignalArgs args)
        {
            // We are possibly in the Gstreamer working thread, so we notify the main thread of this event through a message in the bus
            var s = new Structure("tags-changed");
            Playbin.PostMessage(Message.NewApplication(Playbin, s));
        }

        // This function is called when an error message is posted on the bus
        static void HandleError(object sender, GLib.SignalArgs args)
        {
            var msg = (Message)args.Args[0];
            string debug;
            GLib.GException exc;
            msg.ParseError(out exc, out debug);
            Console.WriteLine(string.Format("Error received from element {0}: {1}", msg.Src.Name, exc.Message));
            Console.WriteLine("Debugging information: {0}", debug);
            // Set the pipeline to READY (which stops playback)
            Playbin.SetState(State.Ready);
        }

        // This function is called when an End-Of-Stream message is posted on the bus. We just set the pipelien to READY (which stops playback)
        static void HandleEos(object sender, GLib.SignalArgs args)
        {
            Console.WriteLine("End-Of-Stream reached.");
            Playbin.SetState(State.Ready);
        }

        // This function is called when the pipeline changes states. We use it to keep track of the current state.
        static void HandleStateChanged(object sender, GLib.SignalArgs args)
        {
            var msg = (Message)args.Args[0];
            State oldState, newState, pendingState;
            msg.ParseStateChanged(out oldState, out newState, out pendingState);
            if (msg.Src == Playbin)
            {
                State = newState;
                Console.WriteLine("State set to {0}", Element.StateGetName(newState));
                if (oldState == State.Ready && newState == State.Paused)
                {
                    // For extra responsiveness, we refresh the GUI as soon as we reach the PAUSED state
                    RefreshUI();
                }
            }

        }

        // Extract metadata from all the streams and write it to the text widget in the GUI
        static void AnalyzeStreams()
        {
            TagList tags;
            String str, totalStr;
            uint rate;

            // Clean current contents of the widget
            var text = StreamsList.Buffer;
            text.Text = String.Empty;

            // Read some properties
            var nVideo = (int)Playbin["n-video"];
            var nAudio = (int)Playbin["n-audio"];
            var nText = (int)Playbin["n-text"];

            for (int i = 0; i < nVideo; i++)
            {
                // Retrieve the stream's video tags
                tags = (TagList)Playbin.Emit("get-video-tags", i);

                if (tags != null)
                {
                    totalStr = string.Format("video stream {0}:\n", i);
                    text.InsertAtCursor(totalStr);
                    tags.GetString(Gst.Constants.TAG_VIDEO_CODEC, out str);
                    totalStr = string.Format("  codec: {0}\n", str != null ? str : "unknown");
                    text.InsertAtCursor(totalStr);
                }
            }

            for (int i = 0; i < nAudio; i++)
            {
                // Retrieve the stream's audio tags
                tags = (TagList)Playbin.Emit("get-audio-tags", i);

                if (tags != null)
                {
                    totalStr = string.Format("audio stream {0}:\n", i);
                    text.InsertAtCursor(totalStr);

                    str = String.Empty;
                    if (tags.GetString(Gst.Constants.TAG_AUDIO_CODEC, out str))
                    {
                        totalStr = string.Format("  codec: {0}\n", str);
                        text.InsertAtCursor(totalStr);
                    }
                    str = String.Empty;

                    if (tags.GetString(Gst.Constants.TAG_LANGUAGE_CODE + "dr", out str))
                    {
                        totalStr = string.Format("  language: {0}\n", str);
                        text.InsertAtCursor(totalStr);
                    }
                    str = String.Empty;

                    if (tags.GetUint(Gst.Constants.TAG_BITRATE, out rate))
                    {
                        totalStr = string.Format("  bitrate: {0}\n", rate);
                        text.InsertAtCursor(totalStr);
                    }
                }
            }

            for (int i = 0; i < nText; i++)
            {
                // Retrieve the stream's text tags
                tags = (TagList)Playbin.Emit("get-text-tags", i);

                if (tags != null)
                {
                    totalStr = string.Format("subtitle stream {0}:\n", i);
                    text.InsertAtCursor(totalStr);

                    if (tags.GetString(Gst.Constants.TAG_LANGUAGE_CODE, out str))
                    {
                        totalStr = string.Format("  language: {0}\n", str);
                        text.InsertAtCursor(totalStr);
                    }
                }
            }
        }

        // This function is called when an "application" message is posted on the bus. Here we retrieve the message posted by the HandleTags callback
        static void HandleApplication(object sender, GLib.SignalArgs args)
        {
            var msg = (Message)args.Args[0];

            if (msg.Structure.Name.Equals("tags-changed"))
            {
                // If the message is the "tags-changed" (only one we are currently issuing), update the stream info GUI
                AnalyzeStreams();
            }
        }

        public static void Main(string[] args)
        {
            // Initialize GTK
            Gtk.Application.Init();

            // Initialize Gstreamer
            Gst.Application.Init(ref args);

            // Create the elements
            Playbin = ElementFactory.Make("playbin", "playbin");

            if (Playbin == null)
            {
                Console.WriteLine("Not all elements could be created");
                return;
            }

            // Set the URI to play.
            Playbin["uri"] = "http://download.blender.org/durian/trailer/sintel_trailer-1080p.mp4";

            // Connect to interesting signals in playbin
            Playbin.Connect("video-tags-changed", HandleTags);
            Playbin.Connect("audio-tags-changed", HandleTags);
            Playbin.Connect("text-tags-changed", HandleTags);


            // Create the GUI
            CreateUI();

            // Instruct the bus to emit signals for each received message, and connect to the interesting signals
            var bus = Playbin.Bus;
            bus.AddSignalWatch();
            bus.Connect("message::error", HandleError);
            bus.Connect("message::eos", HandleEos);
            bus.Connect("message::state-changed", HandleStateChanged);
            bus.Connect("message::application", HandleApplication);


            // Start playing
            var ret = Playbin.SetState(State.Playing);
            if (ret == StateChangeReturn.Failure)
            {
                Console.WriteLine("Unable to set the pipeline to the playing state.");
                return;
            }

            // Register a function that GLib will call every second
            GLib.Timeout.Add(1, RefreshUI);

            // Start the GTK main loop- We will not regain control until gtk_main_quit is called
            Gtk.Application.Run();

            // Free resources
            Playbin.SetState(State.Null);

        }

        [DllImport("libgdk-3.so.0")]
        static extern IntPtr gdk_x11_window_get_xid(IntPtr handle);

        //[DllImport("libgdk-win32-3.0-0.dll")]
        //static extern IntPtr gdk_win32_drawable_get_handle(IntPtr handle);
        [DllImport("libgdk-3-0.dll", CallingConvention = CallingConvention.Cdecl)]
        public static extern IntPtr gdk_win32_window_get_handle(IntPtr handle);

        [DllImport("libX11.so.6")]
        static extern int XInitThreads();
    }
}
