/* 

   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.Threading;
using System.IO.Ports;
using System.Xml;
using System.Collections.Generic;

namespace DMX2
{
	public class DriverBoitierV2 : DriverDMX, IEventProvider 
	{

		struct dmxState {
			public dmxState(int _dmx, byte _value, byte _lastvalue){
				value=_value; dmx=_dmx; lastvalue = _lastvalue;
			}
			public int dmx;
			public byte value;
			public byte lastvalue;
		} 

		enum etatAutomate {
			Deconnecte,
			Transmission,
			Erreur,
			Fin
		}

		const int timeout = 200;

		// tampons Entrée/Sortie
		public byte[] inputbuffer = new byte[532]; 
		byte[] outputbuffer = new byte[1026];

		//Thread de boucle
		Thread loopthread=null;

		public UniversDMX patch1=null;
		public UniversDMX patch2=null;

		string portname = "";
		SerialPort serial = null;


		int brk = 150;
		int mab = 50;
		int nbc1 = 512;
		byte flag_merge1 = 1;
		byte flag_merge2 = 1;

		bool reinit=false ;



		public void ReInit () {
			reinit=true;
		}

		public int Break {
			get {
				return brk;
			}
		}

		public int Mab {
			get {
				return mab;
			}
		}

		public byte Flag_merge1 {
			get {
				return flag_merge1;
			}
		}

		public byte Flag_merge2 {
			get {
				return flag_merge2;
			}
		}

		public DriverBoitierV2 (string serialport, string id): base(id)
		{
			portname = serialport;
			outputbuffer[0]=27;
			outputbuffer[1]=68;
			Start();

		}

		bool paramFlag = false;

		public void SetBreak( int _brk, int _mab, byte _merge1, byte _merge2)
		{
			brk = _brk;
			mab = _mab;
			flag_merge1 = _merge1;
			flag_merge2 = _merge2;
			paramFlag = true;
		}

		void Start ()
		{
			if (loopthread == null) {
				loopthread = new Thread(new ThreadStart(MainLoop));
				loopthread.Start();
			}
		}

		void Connection ()
		{
#if DEBUG
			Console.WriteLine ("DriverV2.Connection()");
#endif
			if (serial != null) {
				serial.Close ();
				serial.Dispose ();
				serial = null;
			}
			if (!System.IO.File.Exists (portname)) {
				Thread.Sleep (200);
				return;
			}
			serial = new SerialPort (portname, 460800, Parity.None, 8, StopBits.One);
			try {
				serial.Open ();
				Attente(DateTime.Now.AddMilliseconds(2000));

				if(Synchronisation())
					etat = etatAutomate.Transmission;
				else {
					serial.Close();
					etat = etatAutomate.Deconnecte;
				}

			} 
#if DEBUG
			catch (Exception ex) {
				Console.WriteLine("DriverV2:Connection : {0}",ex);
#else
			catch {
#endif
				etat = etatAutomate.Deconnecte;
				Thread.Sleep (500);
			}
		}

		/// <summary>
		/// Synchronise le pilote et le boitier ...
		/// Après connexion ou erreur
		/// </summary>
		bool Synchronisation ()
		{
			//return true;
#if DEBUG
			Console.WriteLine ("DriverV2.Synchronisation()");
#endif

			if (serial == null)
				return false;
			if (!serial.IsOpen)
				return false;

			// Au cas ou le boitier attends une fin de commande : envoi 1030 octets a 0 (le boitier ignorera tout seul la suite)
			byte[] tmpBuffer = new byte[1030];
			serial.Write (tmpBuffer, 0, 1030);

			// On attends un peu
			Thread.Sleep (300);

			// Vide le buffer d'entree
			if (serial.BytesToRead > 0)
				serial.ReadExisting ();

			if(serial.BytesToWrite > 0) 
				Console.WriteLine("Les infos partent pas ...");

			// on envoie Esc 'A'
			tmpBuffer [0] = 27;
			tmpBuffer [1] = 65;
			serial.Write (tmpBuffer, 0, 2);

			// On attends un peu
			if(!WaitForData (1)) {
				return false;
			}
			paramFlag = true;
			serial.Read(tmpBuffer,0,1);
			if(tmpBuffer[0] == 65) return true;
			return false;
		}

		volatile etatAutomate etat = etatAutomate.Deconnecte;
		DateTime finAttente = DateTime.Now;
		int compteErreur = 0;

		void MainLoop ()
		{
			//DateTime t = DateTime.Now;
			while (!disposed && etat != etatAutomate.Fin) {
				try {
					switch (etat) {
					case etatAutomate.Deconnecte:
						Connection ();
						compteErreur = 0;
						break;
					case etatAutomate.Transmission:
						finAttente = DateTime.Now.AddMilliseconds (22);
						//Console.WriteLine(DateTime.Now-t);
						//t = DateTime.Now;
						EnvoiTrame ();
						Reception ();
						Attente (finAttente);
						if (paramFlag)
							Parametrage ();
						if (reinit)
							EnvoieReInit();
						break;
					case etatAutomate.Erreur:
#if DEBUG
						Console.WriteLine("DriverV2 : etatAutomate.Erreur");
#endif
						compteErreur ++;
						Deconnecte ();
						Attente (DateTime.Now.AddSeconds (2));
						break;
//				case etatAutomate.Parametrage:
//					EnvoiParam();
//					break;
//				case etatAutomate.Reset:
//					EnvoiReset();
//					break;
					}
				} catch (Exception ex) {
					Console.WriteLine("Exception dans DriverV2 : {0}",ex);
					if(etat != etatAutomate.Fin) etat = etatAutomate.Erreur;
				}
			}
			Deconnecte();
		}

		void Attente (DateTime date)
		{
			int sleeptime = (int) (date - DateTime.Now).TotalMilliseconds-1;
			if(sleeptime>2)
				Thread.Sleep(sleeptime);

			while (DateTime.Now<date) Thread.Sleep(1);				       
		}

		void Deconnecte ()
		{
#if DEBUG
			Console.WriteLine("DriverV2.Deconnection");
#endif
			etat = etatAutomate.Deconnecte;
			if(serial == null)	return;

			serial.Close();
			serial.Dispose();
		}

		void EnvoiTrame ()
		{
			try {

				if(patch1!=null)	patch1.CalculUnivers(outputbuffer,2,512);
				if(patch2!=null)	patch2.CalculUnivers(outputbuffer,514,512);
				if(!serial.IsOpen) {
					etat = etatAutomate.Erreur; 
					return;
				}
				while (serial.BytesToWrite > 0) 
					Thread.Sleep(1);

				serial.Write(outputbuffer,0,outputbuffer.Length);
			} catch (Exception ex) {
				Console.WriteLine("Exception Envoi {0}",ex);
				etat = etatAutomate.Erreur;
			}
		}

		bool WaitForData (int len)
		{
			int wcnt =0 ;

			while (serial.BytesToRead < len) {
				Thread.Sleep (1);
				if (++wcnt > timeout) 
					return false;
			}
			return true;
		}
//		void EnvoiParam ()
//		{
//			throw new NotImplementedException ();
//		}
//
//		void EnvoiReset ()
//		{
//			throw new NotImplementedException ();
//		}
		byte flag_input;
		void Reception ()
		{
			try {


				if(!serial.IsOpen || etat == etatAutomate.Erreur) {
					etat = etatAutomate.Erreur;
					return;
				}

				if(!WaitForData (inputbuffer.Length)) {
					etat = etatAutomate.Erreur;
					Console.WriteLine("DriverV2.Reception : attente depassee");
					return ;
				}

				serial.Read(inputbuffer,0,inputbuffer.Length);
				if (flag_input != inputbuffer[0]) {
					flag_input = inputbuffer[0];
					Console.WriteLine(flag_input );
				}
				if(serial.BytesToRead>0)
					serial.ReadExisting ();

				ProcessData();

				compteErreur= 0;
			} catch (Exception ex) {
				Console.WriteLine(serial.BytesToRead);
				Console.WriteLine("Exception Reception {0}",ex);
				etat = etatAutomate.Erreur;
			}
		}

		void Parametrage ()
		{
			paramFlag = false;

			if (!serial.IsOpen) {
				etat = etatAutomate.Erreur; 
				return;
			}

			byte[] tmpBuffer = new byte[6];

			tmpBuffer [0] = 27; // Esc
			tmpBuffer [1] = 66; // 'B'

			tmpBuffer [2] = // nb circuits
				(byte)(nbc1 / 2 - 1);

			tmpBuffer [3] = (byte)brk;
			tmpBuffer [4] = (byte)mab;

			tmpBuffer [5] = (byte) (flag_merge1 + flag_merge2 * 2);

			serial.Write (tmpBuffer, 0, tmpBuffer.Length);

			if(!WaitForData (1)) {
				etat = etatAutomate.Erreur;
				return ;
			}

			serial.Read(tmpBuffer,0,1);
			if(tmpBuffer[0] != 66)
				etat = etatAutomate.Erreur;

		}

		void EnvoieReInit()
		{
			reinit = false;

			if (!serial.IsOpen) {
				etat = etatAutomate.Erreur; 
				return;
			}

			byte[] tmpBuffer = new byte[2];

			tmpBuffer [0] = 27; // Esc
			tmpBuffer [1] = 67; // 'B'

			serial.Write (tmpBuffer, 0, tmpBuffer.Length);

			if(!WaitForData (1)) {
				etat = etatAutomate.Erreur;
				return ;
			}

			serial.Read(tmpBuffer,0,1);
			if(tmpBuffer[0] != 67)
				etat = etatAutomate.Erreur;
		}

		void ProcessData ()
		{
			lock(watchdmx)
			foreach (int dmx in watchdmx) {
				if( inputbuffer[dmx-1]!= lastVal[dmx]){
					eventsPending.Enqueue(new dmxState(dmx,inputbuffer[dmx-1],lastVal[dmx]));
					lastVal[dmx] = inputbuffer[dmx-1];
				}
			}
		}

		public override void Dispose ()
		{
			disposed = true;
			etat = etatAutomate.Fin;
			if (loopthread != null) {
				loopthread.Join ();
				loopthread = null;
			}
			if(serial != null)
				serial.Dispose();

		}	
		#region implemented abstract members of DMX2.DriverDMX
		public override Gtk.Widget GetUI ()
		{
			return new DriverBoitierV2UI(this);
		}
		#endregion

	

		#region IEventProvider implementation

		List<int> watchdmx = new List<int>();
		Dictionary<int,byte> lastVal = new Dictionary<int, byte>();

		static System.Text.RegularExpressions.Regex regexEventID = new System.Text.RegularExpressions.Regex(
			@"BV2-D(?<dmx>\d+)?",
			System.Text.RegularExpressions.RegexOptions.Compiled);

		System.Collections.Concurrent.ConcurrentQueue<dmxState> eventsPending = 
			new System.Collections.Concurrent.ConcurrentQueue<dmxState>();

		bool IEventProvider.Bind (string eventId)
		{
			var res = regexEventID.Match (eventId);
			if (res.Success) {
				int dmx = int.Parse (res.Groups ["dmx"].Value);
				if(dmx<0||dmx>511) return false;
				lock(watchdmx){
					if(!watchdmx.Contains(dmx)) 
					{
						watchdmx.Add(dmx);
						lastVal[dmx] = 0;
					}
				}
				return true;
			}
			return false;
		}

		void IEventProvider.Unbind (string eventId)
		{
			var res = regexEventID.Match (eventId);
			if (res.Success) {
				int dmx = int.Parse (res.Groups ["dmx"].Value);
				if(dmx<1||dmx>512) return;
				lock(watchdmx) watchdmx.Remove (dmx);
				return;
			}
			return;
		}

		Gtk.Menu IEventProvider.GetProviderSubMenu (EventManager.EventMenuData state, Gtk.ButtonPressEventHandler handler)
		{
			Gtk.Menu retmenu = new Gtk.Menu ();

			Gtk.MenuItem evmenuitem = new Gtk.MenuItem ("Entrée DMX");
			retmenu.Add (evmenuitem);
			Gtk.Menu evmenu = new Gtk.Menu ();
			evmenuitem.Submenu = evmenu;

			for (int c = 0; c<512; c+=100) {
				Gtk.MenuItem citem = new Gtk.MenuItem(string.Format("De {0} à {1}", c==0?1:c, c==500?512:c+99 )); 
				Gtk.Menu cmenu = new Gtk.Menu();
				citem.Submenu = cmenu;
				evmenu.Add(citem);

				for(int d=0 ; d<100;d+=10){
					if(c+d>512) break;
					Gtk.MenuItem ditem = new Gtk.MenuItem(string.Format("De {0} à {1}", c+d==0?1:c+d, c+d==510?512:c+d+9 )); 
					Gtk.Menu dmenu = new Gtk.Menu();
					ditem.Submenu = dmenu;
					cmenu.Add(ditem);

					for(int u=0; u<10;u++){
						if(c+d+u==0) continue;
						if(c+d+u>512) break;
						Gtk.MenuItem uitem = new Gtk.MenuItem(string.Format("Entrée DMX {0}",c+d+u));
						uitem.Data[EventManager.EventIdKey] = string.Format("BV2-D{0}",c+d+u);
						uitem.Data[EventManager.StateKey] = state;
						uitem.ButtonPressEvent += handler;
						dmenu.Add (uitem);

					}
				}
			}

			return retmenu;
		}

		void IEventProvider.ProcessEvents (EventManagerCallback callback)
		{
			dmxState dmxs; 
			EventData evd;
			while (eventsPending.TryDequeue(out dmxs)) {
				evd.id= string.Format("BV2-D{0}",dmxs.dmx );
				evd.value = dmxs.value;
				evd.prev_value = dmxs.lastvalue;
				callback(evd);
			}
		}

		string IEventProvider.MenuName {
			get {
				return "Boitier V2";
			}
		}

		IFeedbackInfo IEventProvider.GetFeedbackInfo (string eventId)
		{
			return null;
		}

		#endregion

		#region implemented abstract members of DMX2.DriverDMX
		public override void Save (System.Xml.XmlElement parent)
		{
			System.Xml.XmlElement el = parent.OwnerDocument.CreateElement ("DriverBoitierV2");

			parent.AppendChild (el);

			el.SetAttribute ("portname", portname.ToString ());
			el.SetAttribute ("id", ID);

			if(patch1!=null)	el.SetAttribute ("univers1", patch1.Nom);
			if(patch2!=null)	el.SetAttribute ("univers2", patch2.Nom);

			el.SetAttribute("mab",mab.ToString());
			el.SetAttribute("brk",brk.ToString());
			el.SetAttribute("merge1",(flag_merge1!=0).ToString());
			el.SetAttribute("merge2",(flag_merge2!=0).ToString());

		}
		#endregion

		public static new DriverDMX Load(Conduite conduite, XmlElement el) {
			//System.Xml.XmlElement xmlE;

			string port = el.GetAttribute("portname");

			if(! System.IO.File.Exists(port)) return null;

			string id = el.GetAttribute("id");

			DriverBoitierV2 drv = new DriverBoitierV2(port,id);

			if(el.HasAttribute("univers1"))
			{
				string univ = el.GetAttribute("univers1");
				foreach (UniversDMX u in conduite.Patches){
					if(u.Nom== univ){
						drv.patch1 = u;
						break;
					}
				}
			}
			if(el.HasAttribute("univers2"))
			{
				string univ = el.GetAttribute("univers2");
				foreach (UniversDMX u in conduite.Patches){
					if(u.Nom== univ){
						drv.patch2 = u;
						break;
					}
				}
			}

			int mab,brk;
			byte merge1,merge2;

			mab = int.Parse(el.TryGetAttribute("mab","150"));
			brk = int.Parse(el.TryGetAttribute("brk","50"));
			merge1 = (byte)( bool.Parse(el.TryGetAttribute("merge1","True"))?1:0 );
			merge2 = (byte)( bool.Parse(el.TryGetAttribute("merge2","True"))?1:0 );

			drv.SetBreak(brk,mab,merge1,merge2);

			return drv;
		}

	}
}

