/* 

   Copyright (C) Arnaud Houdelette		2012-2014
   Copyright (C) Emmanuel Langlois		2012-2014

   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; either version 2 of the License.
   
   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.
   
   You should have received a copy of the GNU General Public License
   along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

using System;
using System.Collections.Generic;
using System.Linq;

namespace DMX2
{
	/// <summary>
	/// Event data : Objet passé aux cibles d'evenements
	/// </summary>
	public struct EventData
	{
		public string id;
		public byte value;
		public byte prev_value ;
	}

	/// <summary>
	/// Interface pour les fournisseurs d'evenements
	/// </summary>
	public interface IEventProvider
	{
		/// <summary>
		/// Renvoie le nom du sous menu associé au fournisseur d'evenements.
		/// </summary>
		/// <value>
		/// Le nom du menu.
		/// </value>
		string MenuName{ get; }

		/// <summary>Demande au fournisseur de signaler l'evenement spécifié</summary>
		/// <param name='eventId'>L'id de l'evenement.</param>
		/// <returns><c>true</c> si le fournisseur reconnais l'id</returns>
		bool Bind (string eventId);

		/// <summary>Signale au fournisseur que l'evenement n'est plus attendu </summary>
		/// <param name='eventId'>L'id de l'evenement.</param>
		void Unbind (string eventId);

		/// <summary>Demande la construction du sous-menu evenement au fournisseur</summary>
		/// <returns>Le sous menu</returns>
		/// <param name='state'>Un objet qui sera devra etre stocké dans Data[EventManager.StateKey]</param>
		/// <param name='handler'>La delegate qui devra etre associée aux ButtonClickedEvent</param>
		Gtk.Menu GetProviderSubMenu( EventManager.EventMenuData state, Gtk.ButtonPressEventHandler handler );

		/// <summary>Appelé par le Timer de la conduite</summary>
		/// <param name='callback'>Fonction a appeler pour chaque evenement.</param>
		void ProcessEvents(EventManagerCallback callback);

		IFeedbackInfo GetFeedbackInfo(string eventId);
	}

	/// <summary>
	/// Interface pour les recepteurs d'evenements.
	/// </summary>
	public interface IEventTarget
	{
		/// <summary>Fonction appelée lors d'un evenement</summary>
		/// <returns>Vrai si l'evenement est traité</returns>
		/// <param name='data'>Evenement</param>
		bool FireEvent(EventData data);

		/// <summary>Indique a la cible qu'on lui associe un evenement</summary>
		/// <param name='id'>l'id de l'evenement</param>
		void Bind(string id);

		/// <summary>Indique a la cible qu'on lui des associe un evenement</summary>
		/// <param name='id'>l'id de l'evenement</param>
		void Unbind(string id);

		bool CanFeedback{ get; }
		void AddFeedback(IFeedbackInfo info);

	}

	public interface IFeedbackInfo {
		bool FeedBack(byte data);
	}

	/// <summary>
	/// Callback sur evenement.
	/// </summary>
	public delegate void EventManagerCallback(EventData data);


	/// <summary>
	/// Callback appele sur clic sur un bouton de menu de choix d'evenement
	/// </summary>
	public delegate void EventManagerMenuCallBack(object state, string eventId);

	/// <summary>
	/// Cible generique d'evenement.
	/// </summary>
	public class actionEventTarget : IEventTarget {
		public delegate bool EventAction (EventData data);
		EventAction action; 
		public actionEventTarget(EventAction _action)
		{
			action=_action;
		}		
		#region IEventTarget implementation
		bool IEventTarget.FireEvent (EventData data)
		{
			return action(data);
		}

		void IEventTarget.Bind (string id){}		// Rien a faire ici
		void IEventTarget.Unbind (string id){}
		bool IEventTarget.CanFeedback { get { return false; } }
		void IEventTarget.AddFeedback (IFeedbackInfo info)
		{
			throw new System.NotImplementedException ();
		}

		#endregion
	}

	/// <summary>
	/// Cible generique d'evenement.
	/// </summary>
	public class actionEventTargetEx : IEventTarget {
		public delegate bool EventAction (EventData data);
		public delegate int GetData();
		EventAction action; 
		GetData getdata;
		bool canfeedback=false , nofeedback=false;
		readonly List<IFeedbackInfo> fbInfos = new List<IFeedbackInfo>();

		public actionEventTargetEx(EventAction _action, GetData _getdata,bool feedback)
		{
			action=_action;
			getdata = _getdata;
			canfeedback = feedback;
		}		
		#region IEventTarget implementation
		bool IEventTarget.FireEvent (EventData data)
		{
			int val = getdata();

			if (
					(data.prev_value < val-5 && data.value < val-5) ||
					(data.prev_value > val+5 && data.value > val+5)
					) {
					return true;
			}

			nofeedback=true;
			bool res =action(data);
			nofeedback=false;
			return res;
		}

		public void FeedBack (byte data)
		{
			if (nofeedback)
				return;
			foreach (var info in fbInfos) {
				info.FeedBack(data);
			}
		}

		void IEventTarget.Bind (string id){}		// Rien a faire ici
		void IEventTarget.Unbind (string id){}
		bool IEventTarget.CanFeedback { get { return canfeedback; } }
		void IEventTarget.AddFeedback (IFeedbackInfo info)
		{
			fbInfos.Add(info);
		}
		#endregion
	}
	public class EventManager
	{
		Dictionary<string,eventBinding> bindings = new Dictionary<string,eventBinding>();
		List<IEventProvider> providers = new List<IEventProvider>();

		class eventBinding
		{
			List<IEventTarget> targets=new List<IEventTarget>();
			public void AddTarget(IEventTarget target)
			{
				if(!targets.Contains(target))
					targets.Add(target);
			}
			public void RemoveTarget(IEventTarget target)
			{
				targets.Remove(target);
			}
			public ICollection<IEventTarget> Targets {
				get {
					return targets;
				}
			}
		}

		public EventManager ()
		{

		}

		public void RegisterProvider (IEventProvider prov)
		{
			providers.Add (prov);
			foreach (var bind in bindings) {
				if (prov.Bind (bind.Key)) {
					IFeedbackInfo info = prov.GetFeedbackInfo(bind.Key);
					if(info!=null)
						foreach(IEventTarget target in bind.Value.Targets)
							if(target.CanFeedback) 
								target.AddFeedback(info);
				}
			}
		}

#region Menus
		// Cles pour association d'etat au elements de menu
		static public object StateKey = new object();	
		static public object EventIdKey = new object();

		// Objet associe aux menus afin de pourvoir retransmettre les infos au createur du menu
		public class EventMenuData {
			public EventManagerMenuCallBack CallBack;
			public object state;
		}

		///<summary> Appelé par l'interface. L'objet 'state' sera retransmis tel quel lors de l'appel au callback </summary>
		public Gtk.Menu GetMenu (object state, EventManagerMenuCallBack callback)
		{
			Gtk.Menu menu = new Gtk.Menu ();
			EventMenuData evd = new EventMenuData();
			evd.CallBack = callback;
			evd.state = state;

			Gtk.ButtonPressEventHandler handler = new Gtk.ButtonPressEventHandler (HandleButtonPressEvent);

			foreach (IEventProvider prov in providers) {
				Gtk.MenuItem provitem = new Gtk.MenuItem(prov.MenuName);
				provitem.Submenu = prov.GetProviderSubMenu(evd, handler);
				if(provitem.Submenu!=null)	menu.Add(provitem);
			}

			// Creation de l'element 'Aucun' retournant un ID vide, pour permettre la desassociation
			Gtk.MenuItem itemNone = new Gtk.MenuItem("Aucun");
			itemNone.Data[StateKey] = evd;
			itemNone.Data[EventIdKey] = "";
			itemNone.ButtonPressEvent += handler;
			menu.Add(itemNone);

			return menu;

		}

		///<summary> Traitement du clic sur les menus generes au dessus </summary>
		void HandleButtonPressEvent (object o, Gtk.ButtonPressEventArgs args)
		{
			Gtk.MenuItem item = o as Gtk.MenuItem;
			EventMenuData evd = item.Data[EventManager.StateKey] as EventMenuData;	// On récupere l'objet d'etat et la fonction a rappeler
			string id = item.Data[EventManager.EventIdKey] as string;				// ainsi que l'id evenement
			evd.CallBack(evd.state,id);			// Appel de la fonction 
		}
#endregion

		///<summary> Appelé par le Timer de la conduite ... appel le traitement pour l'ensemble des fournisseurs </summary>
		public void ProcessEvents ()
		{
			foreach (IEventProvider prov in providers) {
				prov.ProcessEvents(new EventManagerCallback(EventCallBack));
			}
		}

		public void UnregisterProvider (IEventProvider prov)
		{
			providers.Remove(prov);
		}

		///<summary> Enregistrement d'une association id => cible
		public bool Bind (string eventId, IEventTarget target)
		{
			if(!bindings.ContainsKey(eventId)) 
				bindings.Add (eventId,new eventBinding());
			bindings[eventId].AddTarget(target);
			target.Bind(eventId);
			foreach (IEventProvider prov in providers) {
				if(prov.Bind(eventId)) {
					if(target.CanFeedback){
						IFeedbackInfo info = prov.GetFeedbackInfo(eventId);
						if(info!=null) target.AddFeedback(info);
					}
					return true;
				}
			}
			return false;
		}

		///<summary> Desenregistrement de toutes les associations d'une cible</summary>
		public void Unbind (IEventTarget target)
		{
			var q = from bind in bindings where bind.Value.Targets.Contains(target) select bind.Key;
			foreach(string id in q.ToArray()) 
				Unbind(id,target);
		}

		///<summary> Desenregistrement d'une association id => cible</summary>
		public void Unbind (string eventId, IEventTarget target)
		{
			if (!bindings.ContainsKey (eventId)) return;
			bindings [eventId].RemoveTarget (target);
			if (bindings [eventId].Targets.Count == 0) {
				bindings.Remove(eventId);
				foreach (IEventProvider prov in providers) {
					prov.Unbind(eventId);
				}			
			}

			target.Unbind(eventId);
		}

		///<summary> Fonction appelee sur evenement</summary>
		public void EventCallBack (EventData data)
		{
#if DEBUG
			Console.WriteLine("Event {0} => {1} (last = {2})",data.id,data.value,data.prev_value);
#endif
			if (bindings.ContainsKey (data.id)) {
				foreach (IEventTarget target in bindings[data.id].Targets) {
					target.FireEvent(data);
				}
			}
		}

		public bool SaveBindings (System.Xml.XmlElement xmlParent, IEventTarget target)
		{
			bool ret=false;
			System.Xml.XmlElement xmlB;
			var q = from bind in bindings 
					where bind.Value.Targets.Contains(target) 
					select bind.Key;

			foreach (string id in q) {
				ret=true;
				xmlParent.AppendChild(xmlB = xmlParent.OwnerDocument.CreateElement("EventBinding"));
				xmlB.SetAttribute("id",id);
			}
			return ret;
		}

		public static string[] LoadBindings (System.Xml.XmlElement xmlParent)
		{
			var all=xmlParent.GetElementsByTagName("EventBinding");
			string[] ret = new string[all.Count]; int index=0;
			foreach (var xb in all) {
				ret[index++] = (xb as System.Xml.XmlElement).GetAttribute("id");
			}
			return ret;
		}

	}
}

