// 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_drawable_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);
			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 CreateUI () {
			var mainWindow = new Window (WindowType.Toplevel);
			mainWindow.DeleteEvent += HandleDelete;

			var videoWindow = new DrawingArea ();
			videoWindow.DoubleBuffered = false;
			videoWindow.Realized += HandleRealized;
			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 ("libX11.so.6")]
		static extern int XInitThreads ();
	}
}
