/* 

   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 DriverBoitierV3 : 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 = 500;

		// tampons Entrée/Sortie
		public byte[] inputbuffer = new byte[517]; 
		byte[] outputbuffer = new byte[514];

		//Thread de boucle
		Thread loopthread=null;

		public UniversDMX patch1=null;


		string portname = "";
		SerialPort serial = null;


		int brk = 150;
		int mab = 50;
		int nbc1 = 512;
		byte flags = 0;
		byte dmxinterval= 40;
		int usbrefresh = 40;
		bool sync=false;

		bool reinit=false ;

		public void ReInit () {
			reinit=true;
		}

		public int Break {
			set {
				brk = value;
				paramFlag = true;
			}
			get {
				return brk;
			}
		}

		public int Mab {
			set {
				mab = value;
				paramFlag = true;
			}
			get {
				return mab;
			}
		}

		public bool Merge {
			set {
				flags = (byte)( (flags & 0xFE) | (value?1:0));
				paramFlag = true;
			}
			get {
				return (flags&1)>0;
			}
		}

		public bool Sync {
			get {
				return sync;
			}
			set {
				flags = (byte)( (flags & 0xFD) | (value?2:0));
				sync = value;
			}
		}

		public int NbCircuits {
			get {
				return nbc1;
			}
			set {
				nbc1 = value;
				paramFlag= true;
			}
		}

		public byte DmxInterval {
			get {
				return dmxinterval;
			}
			set {
				dmxinterval = value;
				paramFlag= true;
			}
		}

		public int UsbRefresh {
			get {
				return usbrefresh;
			}
			set {
				usbrefresh = value;
			}
		}

		public DriverBoitierV3 (string serialport, string id): base(id)
		{
			patch1 = Conduite.Courante.Patches[0];
			portname = serialport;
			outputbuffer[0]=27;
			outputbuffer[1]=68;
			Start();

		}

		bool paramFlag = false;

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

		void Connection ()
		{
			Console.WriteLine ("DriverV3.Connection()");
			if (serial != null) {
				serial.Close ();
				serial.Dispose ();
				serial = null;
			}
			if (!System.IO.File.Exists (portname)) {
				Thread.Sleep (1500);
				return;
			}
			serial = new SerialPort (portname);
			Console.WriteLine (portname);

			try {
				serial.RtsEnable = true;
				serial.Open ();

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

			} catch (Exception ex) {
				etat = etatAutomate.Deconnecte;
				Console.WriteLine("DriverV3:Connection : {0}",ex);
				Thread.Sleep (1500);
			}
		}

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

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

			byte[] tmpBuffer = new byte[2];

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

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

			// 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;
		}

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

			byte[] tmpBuffer = new byte[2];

			tmpBuffer [0] = 27; // Esc
			tmpBuffer [1] = 65; // 'A'

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

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

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

		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 (usbrefresh);
						//Console.WriteLine(DateTime.Now-t);
						//t = DateTime.Now;
						if(sync)DmxSync();
						EnvoiTrame ();
						Reception ();
						Attente (finAttente);
						if (paramFlag)
							Parametrage ();
						if (reinit)
							EnvoieReInit();
						break;
					case etatAutomate.Erreur:
						Console.WriteLine("DriverV3 : etatAutomate.Erreur");
						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 DriverV3 : {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("DriverV3.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(!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("DriverV3.Reception : attente depassee");
					return ;
				}

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

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

		void Parametrage ()
		{
#if DEBUG
			Console.WriteLine ("DriverV3.Parametrage()");
#endif
			paramFlag = false;

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

			byte[] tmpBuffer = new byte[7];

			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) (flags );
			tmpBuffer [6] = (byte) (dmxinterval);

			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]!= lastVal[dmx]){
					eventsPending.Enqueue(new dmxState(dmx,inputbuffer[dmx],lastVal[dmx]));
					lastVal[dmx] = inputbuffer[dmx];
				}
			}
		}

		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 DriverBoitierV3UI(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(
			@"BV3-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("BV3-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("BV3-D{0}",dmxs.dmx );
				evd.value = dmxs.value;
				evd.prev_value = dmxs.lastvalue;
				callback(evd);
			}
		}

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

		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 ("DriverBoitierV3");

			parent.AppendChild (el);

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

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

			el.SetAttribute("circuits", nbc1.ToString());
			el.SetAttribute("mab",mab.ToString());
			el.SetAttribute("brk",brk.ToString());
			el.SetAttribute("merge1",((flags&1)>0).ToString());
			el.SetAttribute("sync",sync.ToString());
			el.SetAttribute("dmxinterval",dmxinterval.ToString());
			el.SetAttribute("usbrefresh",usbrefresh.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");

			DriverBoitierV3 drv = new DriverBoitierV3(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;
					}
				}
			}

			drv.NbCircuits = int.Parse(el.TryGetAttribute("circuits","512"));
			drv.Mab = int.Parse(el.TryGetAttribute("mab","150"));
			drv.Break = int.Parse(el.TryGetAttribute("brk","50"));
			drv.Merge = bool.Parse(el.TryGetAttribute("merge1","True"));
			drv.Sync = bool.Parse(el.TryGetAttribute("sync","True"));
			drv.DmxInterval = byte.Parse(el.TryGetAttribute("dmxinterval","22"));
			drv.UsbRefresh = int.Parse(el.TryGetAttribute("usbrefresh","22"));


			return drv;
		}

	}
}

