/* 

   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.Runtime.InteropServices;
using System.Collections.Generic;
using System.Linq;

namespace DMX2
{
	public class MidiEventProvider : IEventProvider, IDisposable
	{

		class internalEvent {

			readonly string internalName;
			bool bound=false;
			readonly uint page;
			readonly int midiEvCode;

			public bool Bound {
				get {
					return bound;
				}
				set {
					bound = value;
				}
			}

			public string InternalName {
				get {
					return internalName;
				}
			}

			public uint Page {
				get {
					return page;
				}
			}

			public int MidiEvCode {
				get {
					return midiEvCode;
				}
			}


			public internalEvent(string _id, uint _page, int _evHCode)
			{
				internalName=_id;
				page=_page;
				midiEvCode = _evHCode;
			}

			int lastknownvalue=-1;

			public int LastKnownValue {
				get {
					return lastknownvalue;
				}
				set {
					lastknownvalue = value;
				}
			}

			AlsaSeqLib.snd_seq_event_t storedevent;
			public AlsaSeqLib.snd_seq_event_t StoredEvent {
				get {
					return storedevent;
				}
				set {
					storedevent = value;
				}
			}
		}
		abstract class feedbackinfo : IFeedbackInfo {
			MidiEventProvider prov;
			readonly internalEvent iev;
			protected AlsaSeqLib.snd_seq_event_t ev;


			public feedbackinfo(MidiEventProvider _prov, internalEvent _iev){
				prov = _prov;
				iev=_iev;
			}

			protected abstract bool UpdateEvent(byte data);

			#region IFeedbackInfo implementation
			bool IFeedbackInfo.FeedBack (byte data)
			{

				bool update = UpdateEvent (data);
				iev.StoredEvent = ev;
				if (!update) return true;

				iev.LastKnownValue = data;

				if (prov.CurrentPage == iev.Page || iev.Page == 0) {
					prov.SendEvent (ev);
					foreach (int srcid in prov.feedbacksources) {
						prov.lastValueOfSrc[CombineHash( srcid , iev.MidiEvCode)] = data;
					}
				}

				return true;
			}
			#endregion
		}
		class ctrlfeedbackinfo : feedbackinfo {
			public ctrlfeedbackinfo(MidiEventProvider _prov, internalEvent _iev, byte channel,uint param)
				:base(_prov,_iev)
			{
				ev = new AlsaSeqLib.snd_seq_event_t(); 
				ev.data_ev_ctrl.channel = channel;
				ev.data_ev_ctrl.param= param;
				ev.type = AlsaSeqLib.snd_seq_event_type_t.SND_SEQ_EVENT_CONTROLLER;
			}

			protected override bool UpdateEvent (byte data)
			{
				int newvalue= (int)data * 127 / 255;
				if(newvalue==ev.data_ev_ctrl.value) return false;
				ev.data_ev_ctrl.value = newvalue;
				return true;
			}
		}
		class pitchbendfeedbackinfo : feedbackinfo {
			public pitchbendfeedbackinfo(MidiEventProvider _prov, internalEvent _iev, byte channel)
				:base(_prov,_iev)
			{
				ev = new AlsaSeqLib.snd_seq_event_t(); 
				ev.data_ev_ctrl.channel = channel;
				ev.type = AlsaSeqLib.snd_seq_event_type_t.SND_SEQ_EVENT_PITCHBEND;
			}

			protected override bool UpdateEvent (byte data)
			{
				int newvalue = (int)data * 16000 / 255-8000;
				if(newvalue==ev.data_ev_ctrl.value) return false;
				ev.data_ev_ctrl.value = newvalue;
				return true;
			}
		}

		public class MidiDev{
			string name;
			public string Name {get{return name;} }
			public bool HasFeedback { get; set; }

			readonly List<int> connected = new List<int>();
			public List<int> ConnectedPorts {
				get{return connected;}
			}

			public MidiDev(string _name)
			{
				name=_name;
			}

		}

		/// <summary>
		/// Etat interne des evenements midi paginés.
		/// </summary>
		readonly Dictionary<string,internalEvent> eventlist = new Dictionary<string, internalEvent>();

		/// <summary>
		/// Liste des peripheriques connus (presents ou non)
		/// </summary>
		readonly Dictionary<string,MidiDev> knowndevices = new Dictionary<string,MidiDev>();		

		/// <summary>
		/// Liste des ports connectés avec feedback
		/// </summary>
		readonly List<int> feedbacksources = new List<int>();

		//static readonly Dictionary<int,MidiDev> srcidToDev = new Dictionary<int, MidiDev>();
		readonly List<byte> unpaginatedchannels = new List<byte>();

		/// <summary>
		/// Derniere valeur connue pour une evenement sur source donnée : 
		/// Soit recue, soit envoyée (feedback / changement de page)
		/// </summary>
		readonly Dictionary<int,byte> lastValueOfSrc = new Dictionary<int, byte>();


		EventData last; 
		internalEvent levent=null;
		bool connected=false;

		bool guirefreshflag=false;

		uint page=1;
		uint maxpage=8;

		public uint CurrentPage {
			get {
				return page;
			}
			set {
				if(value<1 || value > maxpage) return;
				page = value;
				Refresh();
			}
		}

		public uint Maxpage {
			get {
				return maxpage;
			}
			set {
				maxpage = value;
			}
		}
		public List<byte> UnpaginatedChannels {
			get {
				return unpaginatedchannels;
			}
		}

		public bool GuiRefreshFlag {
			get {
				if(guirefreshflag){
					guirefreshflag=false;
					return true;
				}
				return false;
			}
		}


		public void ConnectDevice (string name)
		{
			knowndevices.Add(name,new MidiDev(name));
			AutoConnect();			
		}

		public void RefreshFeedback (string name)
		{
			foreach (int port in knowndevices[name].ConnectedPorts) {
				if(knowndevices[name].HasFeedback){
					if(!feedbacksources.Contains(port))
						feedbacksources.Add(port);
				}else{
					if(feedbacksources.Contains(port))
						feedbacksources.Remove(port);
				}
			}
		}

		public void DisconnectDevice (MidiEventProvider.MidiDev dev)
		{
			if (!knowndevices.ContainsKey (dev.Name))
				return;
			knowndevices.Remove (dev.Name);
			// TODO: Deconnecter ici

			foreach (int connectedport in dev.ConnectedPorts) {
				int client = connectedport >> 8;
				int port = connectedport & 0xFF;
				AlsaSeqLib.Deconnecte(client,port);
			}
		}

		public bool IsKnownDevice(string name){
			return knowndevices.ContainsKey(name);
		}

		public IEnumerable<MidiDev> KnownDevices {
			get {
				return knowndevices.Values;
			}
		}


		public MidiEventProvider (EventManager manager)
		{
			/*MidiDev dev = new MidiDev("VMPK Input:VMPK Input");
			dev.HasFeedback = true;
			knowndevices.Add(dev.Name,dev);
			dev = new MidiDev("VMPK Output:VMPK Output");
			dev.HasFeedback = true;
			knowndevices.Add(dev.Name,dev);*/

			manager.RegisterProvider (this);
			AlsaSeqLib.Init ();
	
			AlsaSeqLib.ConnectFrom(AlsaSeqLib.SND_SEQ_CLIENT_SYSTEM, AlsaSeqLib.SND_SEQ_PORT_SYSTEM_ANNOUNCE);

			AutoConnect();
		}

		void AutoConnect ()
		{
			foreach (var cli in AlsaSeqLib.EnumClients()) {
				foreach(var p in cli.Ports){
					PortDetected(cli,p);
				}
			}
		}

		void  PortDetected (AlsaSeqLib.Client cli, AlsaSeqLib.Port p)
		{
			// Execute a chaque 'apparition' d'un port midi

			// teste si connection auto au port et connecte si besoin

			guirefreshflag=true;

			string fullportname = cli.Name + ':' + p.Name;
			if(knowndevices.ContainsKey(fullportname)){
				int srcid = p.ClientId <<8 + p.PortId;
				if(knowndevices[fullportname].ConnectedPorts.Contains(srcid))
					return;

				AlsaSeqLib.Connect(p);
			}
		}

		void PortConnect (AlsaSeqLib.snd_seq_connect_t cn, bool connect)
		{
			int clientId,portId;
			if (cn.dest.client == AlsaSeqLib.ClientId) {
				clientId = cn.sender.client;
				portId = cn.sender.port;
			} else {
				clientId = cn.dest.client;
				portId = cn.dest.port;
			}

			int srcid = clientId << 8 + portId;
			if (connect) {
				string fpname= AlsaSeqLib.GetClientByID(clientId).Name + ":"+ AlsaSeqLib.GetPortByIDs (clientId, portId).Name;
				if (!knowndevices.ContainsKey(fpname)) return;
				if (knowndevices [fpname].ConnectedPorts.Contains (srcid))
					return;
				knowndevices [fpname].ConnectedPorts.Add (srcid);
				if(knowndevices [fpname].HasFeedback)
					feedbacksources.Add (srcid);
				guirefreshflag=true;

				//srcidToDev[srcid] = knowndevices [fpname];

				return;
			}

			foreach (var dev in knowndevices.Values) {
				if(dev.ConnectedPorts.Contains (srcid))
				{
					/*if(srcidToDev.ContainsKey(srcid))
						srcidToDev.Remove(srcid);*/
					dev.ConnectedPorts.Remove(srcid);
					guirefreshflag=true;
					return;
				}
			}

		}

		static int CombineHash (int hash1, int hash2)
		{
			unchecked {
				return hash1 * 33 + hash2;
			}
		}

		protected bool HasFeedback (int source)
		{
			return feedbacksources.Contains(source);
		}

		public void SendEvent (AlsaSeqLib.snd_seq_event_t ev)
		{
			AlsaSeqLib.SendEventToSubscribers(ev);
		}

		public void Refresh ()
		{
			foreach (var ievent in eventlist.Values) {
				if (ievent.Page == page){
					SendEvent(ievent.StoredEvent);
					foreach(int src in feedbacksources){
						int lnvk = CombineHash(src, ievent.MidiEvCode);
						if(ievent.LastKnownValue!=-1)
							lastValueOfSrc[lnvk] = (byte)ievent.LastKnownValue;
					}
				}
			}
		}

		#region IEventProvider implementation


		static System.Text.RegularExpressions.Regex regexEventID = new System.Text.RegularExpressions.Regex(
			@"MIDI-PAGE(?<page>\d+)-(?<id>.+)",
			System.Text.RegularExpressions.RegexOptions.Compiled);

		bool IEventProvider.Bind (string eventId)
		{
			// On indique a l'EventManager qu'on traite, si l'ID commence par 'MIDI-'
			if (! eventId.StartsWith ("MIDI-"))
				return false;

			if (! eventlist.ContainsKey (eventId)) {
				var res = regexEventID.Match (eventId);
				if (!res.Success) return false;
				uint _page = uint.Parse (res.Groups ["page"].Value);
				int _evHC = res.Groups["id"].Value.GetHashCode();
				eventlist.Add (eventId, new internalEvent (eventId,_page,_evHC ));
			}

			eventlist[eventId].Bound = true;
			return true;
		}

		void IEventProvider.Unbind (string eventId)
		{
			if(! eventlist.ContainsKey(eventId)) return;
			eventlist[eventId].Bound = false;
			return;
		}

		Gtk.Menu IEventProvider.GetProviderSubMenu (EventManager.EventMenuData state, Gtk.ButtonPressEventHandler handler)
		{
			if(!connected) return null;											// Si pas encore recu d'evenements => pas de menu
			Gtk.Menu retmenu = new Gtk.Menu ();

			if (levent != null) {												// Creation du sous menu "Dernier"
				/*Gtk.MenuItem lmenuitem = new Gtk.MenuItem ("Dernier");
				retmenu.Add (lmenuitem);
				Gtk.Menu lmenu = new Gtk.Menu ();
				lmenuitem.Submenu = lmenu;*/
				Gtk.MenuItem item = new Gtk.MenuItem(GetDescription(levent.InternalName));
				item.Data[EventManager.EventIdKey] = levent.InternalName;
				item.Data[EventManager.StateKey] = state;
				item.ButtonPressEvent += handler;
				retmenu.Add (item);
			}

			Gtk.MenuItem evmenuitem = new Gtk.MenuItem ("Events");				// Creation du sous menu "Events"
			retmenu.Add (evmenuitem);
			Gtk.Menu evmenu = new Gtk.Menu ();
			evmenuitem.Submenu = evmenu;

			List<string> sortedKeys = eventlist.Keys.ToList();		// On recupere des IDs 
			sortedKeys.Sort();										// et on les trie

			foreach ( string key in sortedKeys ) {
				internalEvent evt= eventlist[key];
				Gtk.MenuItem item = new Gtk.MenuItem(GetDescription(evt.InternalName));
				item.Data[EventManager.EventIdKey] = evt.InternalName;
				item.Data[EventManager.StateKey] = state;
				item.ButtonPressEvent += handler;
				evmenu.Add (item);
			}

			return retmenu;

		}

		void IEventProvider.ProcessEvents (EventManagerCallback callback)
		{
			AlsaSeqLib.snd_seq_event_t evS;
			uint evpage;

				// Tant qu'il y des evenements midi en attente
			while (AlsaSeqLib.GetEvent(out evS)) {

				string id = null; 
				int value=0;
				byte channel = 255;

				switch (evS.type) {
				case AlsaSeqLib.snd_seq_event_type_t.SND_SEQ_EVENT_PORT_SUBSCRIBED:	// Connection d'un périph midi
				case AlsaSeqLib.snd_seq_event_type_t.SND_SEQ_EVENT_PORT_UNSUBSCRIBED:
					PortConnect(
						evS.data_connect,
						evS.type==AlsaSeqLib.snd_seq_event_type_t.SND_SEQ_EVENT_PORT_SUBSCRIBED
						);
					continue;
				case AlsaSeqLib.snd_seq_event_type_t.SND_SEQ_EVENT_CONTROLLER:
					if(evS.data_ev_ctrl.param==127 && evS.data_ev_ctrl.value >0){
						CurrentPage++; continue;
					}

					if(evS.data_ev_ctrl.param==126 && evS.data_ev_ctrl.value>0){
						CurrentPage--; continue;
					}

					id = string.Format("CTRL-C{0}P{1}",evS.data_ev_ctrl.channel,evS.data_ev_ctrl.param);
					channel = evS.data_ev_ctrl.channel;
					value = 255 * evS.data_ev_ctrl.value / 127;				// Conversion {0,127} => {0,255}
					break;
				case AlsaSeqLib.snd_seq_event_type_t.SND_SEQ_EVENT_NOTEON:
					id = string.Format("NOTE-C{0}N{1}",evS.data_ev_note.channel, evS.data_ev_note.note );
					channel = evS.data_ev_note.channel;
					value = 255 ;
					break;
				case AlsaSeqLib.snd_seq_event_type_t.SND_SEQ_EVENT_NOTEOFF:
					id = string.Format("NOTE-C{0}N{1}",evS.data_ev_note.channel, evS.data_ev_note.note );
					channel = evS.data_ev_note.channel;
					value = 0; 
					break;
				case AlsaSeqLib.snd_seq_event_type_t.SND_SEQ_EVENT_PITCHBEND:
					id = string.Format("PB-C{0}",evS.data_ev_ctrl.channel);
					channel = evS.data_ev_ctrl.channel;
					value = ((evS.data_ev_ctrl.value + 8000) *255 / 16000);
					if(value<0) value =0; if(value>255) value = 255;
					break;
				case AlsaSeqLib.snd_seq_event_type_t.SND_SEQ_EVENT_CLOCK:
				case AlsaSeqLib.snd_seq_event_type_t.SND_SEQ_EVENT_SENSING:
					continue;
				case AlsaSeqLib.snd_seq_event_type_t.SND_SEQ_EVENT_PGMCHANGE:
					CurrentPage = (uint)evS.data_ev_ctrl.value;
					continue;
				case AlsaSeqLib.snd_seq_event_type_t.SND_SEQ_EVENT_PORT_START:
					PortDetected(
						AlsaSeqLib.GetClientByID(evS.data_addr.client),
						AlsaSeqLib.GetPortByIDs(evS.data_addr.client,evS.data_addr.port)
						);
					continue;


				//TODO : Regarder si d'autres controles interessants.
				default:
						id= null;
#if DEBUG
						Console.WriteLine(string.Format ("event {0}", evS.type) );	
						Info.Publish(string.Format ("event {0}", evS.type) );	// On affiche les evenements inconnus
#endif
					continue;
				}

				connected=true;



				if(id!=null)
				{
					// Hashcode de l'ev Midi, non pagine
					int evHC = id.GetHashCode();
					int srcid = evS.source.client <<8 + evS.source.port;
					int lnvk = CombineHash( srcid , evHC);

					if(channel == 255 || unpaginatedchannels.Contains(channel))
						evpage= 0;
					else
						evpage= page;

					// Construction de l'ID evenement
					id = string.Format("MIDI-PAGE{0}-{1}",evpage,id);
			
					// Creation de l'objet interne si innexistant
					if(!eventlist.ContainsKey(id))
						eventlist.Add(id,new internalEvent(id,page,evHC));
					levent= eventlist[id];					//Dernier Evenement recu conserve pour menu

					if(!lastValueOfSrc.ContainsKey(lnvk))
						lastValueOfSrc[lnvk]= (byte)value;


					EventData evData = new EventData();		
					evData.id = id;
					evData.value = (byte)value;
					evData.prev_value = lastValueOfSrc[lnvk];
					if(evData.Equals(last)) continue;		
					last = evData;

					if(eventlist[id].Bound)	{
						callback(evData);
					}

					lastValueOfSrc[lnvk] = (byte)value;

					eventlist[id].StoredEvent = evS;
					eventlist[id].LastKnownValue = (byte)value;

				}
			}
		}

		string IEventProvider.MenuName {
			get {
				return "Midi";
			}
		}

		//TODO gerer pages et feeddback

		static System.Text.RegularExpressions.Regex regexCtrlEventID = new System.Text.RegularExpressions.Regex(
			@"MIDI-PAGE(?<page>\d+)-CTRL-C(?<chan>\d+)P(?<param>\d+)",
			System.Text.RegularExpressions.RegexOptions.Compiled);

		static System.Text.RegularExpressions.Regex regexPbEventID = new System.Text.RegularExpressions.Regex(
			@"MIDI-PAGE(?<page>\d+)-PB-C(?<chan>\d+)",
			System.Text.RegularExpressions.RegexOptions.Compiled);

		static System.Text.RegularExpressions.Regex regexNoteEventID = new System.Text.RegularExpressions.Regex(
			@"MIDI-PAGE(?<page>\d+)-NOTE-C(?<chan>\d+)N(?<note>\d+)",
			System.Text.RegularExpressions.RegexOptions.Compiled);

		string GetDescription (string eventId)
		{
			if(!eventlist.ContainsKey(eventId)) return null;

			var res = regexCtrlEventID.Match (eventId);
			if (res.Success) {
				uint page = uint.Parse(res.Groups ["page"].Value);
				byte chan = byte.Parse (res.Groups ["chan"].Value);
				uint param = uint.Parse (res.Groups ["param"].Value);
				return string.Format("Page {2} => Control-Change C({0}) Param-{1}",chan+1,param,page);
			}
			res = regexPbEventID.Match (eventId);
			if (res.Success) {
				uint page = uint.Parse(res.Groups ["page"].Value);
				byte chan = byte.Parse (res.Groups ["chan"].Value);
				return string.Format("Page {1} => PitchBend C({0})",chan+1,page);
			}
			res = regexNoteEventID.Match (eventId);
			if (res.Success) {
				uint page = uint.Parse(res.Groups ["page"].Value);
				byte chan = byte.Parse (res.Groups ["chan"].Value);
				byte note = byte.Parse (res.Groups ["note"].Value);
				return string.Format("Page {2} => Note C({0}) Note-{1}",chan+1,note,page);
			}
			return eventId;
		}

		IFeedbackInfo IEventProvider.GetFeedbackInfo (string eventId)
		{
			if(!eventlist.ContainsKey(eventId)) return null;

			var res = regexCtrlEventID.Match (eventId);
			if (res.Success) {
				byte chan = byte.Parse (res.Groups ["chan"].Value);
				uint param = uint.Parse (res.Groups ["param"].Value);
				return new ctrlfeedbackinfo (this, eventlist[eventId],chan, param);
			}
			res = regexPbEventID.Match (eventId);
			if (res.Success) {
				byte chan = byte.Parse (res.Groups ["chan"].Value);
				return new pitchbendfeedbackinfo (this, eventlist[eventId], chan);
			}
			return null;

		}

		#endregion		


		#region IDisposable implementation

		bool disposed=false;

		~MidiEventProvider(){
			Dispose();
		}
		public void Dispose ()
		{
			if(disposed)return;
			disposed=true;
			AlsaSeqLib.Close();
		}
		#endregion

		public void Save (System.Xml.XmlElement parent)
		{
			System.Xml.XmlElement el = parent.OwnerDocument.CreateElement ("Midi");
			parent.AppendChild (el);
			el.SetAttribute ("maxpage", maxpage.ToString ());

			System.Xml.XmlElement xmlEl;
			foreach (MidiDev dev in knowndevices.Values) {
				el.AppendChild (xmlEl = parent.OwnerDocument.CreateElement ("MidiDev"));
				xmlEl.SetAttribute ("name", dev.Name);
				xmlEl.SetAttribute ("feedback", dev.HasFeedback.ToString ());
			}

			foreach (byte ch in unpaginatedchannels) {
				el.AppendChild (xmlEl = parent.OwnerDocument.CreateElement ("UPC"));
				xmlEl.SetAttribute ("ch", ch.ToString());
			}

		}

		public void Load (System.Xml.XmlElement el)
		{
			if(el==null) return;
			maxpage = uint.Parse (el.TryGetAttribute ("maxpage", "8"));

			foreach (var xd in el.GetElementsByTagName("MidiDev")) {
				System.Xml.XmlElement xdev = xd as System.Xml.XmlElement;
				string name = xdev.GetAttribute ("name");
				if (!knowndevices.ContainsKey (name))
					knowndevices.Add (name, new MidiDev (name));
				knowndevices [name].HasFeedback = bool.Parse (xdev.TryGetAttribute ("feedback", "false"));
			}
			unpaginatedchannels.Clear();
			foreach (var xu in el.GetElementsByTagName("UPC")) {
				System.Xml.XmlElement xupc = xu as System.Xml.XmlElement;
				unpaginatedchannels.Add(byte.Parse(xupc.GetAttribute("ch")));
			}

			AutoConnect();
		}

	}
}
