// Created with MonoDevelop
//
//    Copyright (C) 2014 Hiperscan GmbH Dresden, Germany
//
//    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 3 of the License, or
//    (at your option) any later version.
//
//    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 Hiperscan.Spectroscopy;
using Hiperscan.SGS;
using Hiperscan.Unix;


namespace Hiperscan.SpectralEngines
{
	public class ACM : IDisposable
	{
		private SerialPort serial_port;
		public const int DefaultReadTimeout = 1000;

		public ACM(string port)
		{
			try
			{
				this.serial_port = new SerialPort(port);//(port, 9600, Parity.Even, 7, StopBits.Two);
				this.serial_port.ReadTimeout = ACM.DefaultReadTimeout;
				this.serial_port.Open();
			}
			catch (System.IO.IOException ex)
			{
				// try again 1 s later
				Thread.Sleep(1000);
				this.serial_port = new SerialPort(port);//(port, 9600, Parity.Even, 7, StopBits.Two);
				this.serial_port.ReadTimeout = ACM.DefaultReadTimeout;
				this.serial_port.Open();
			}
		}
		
		~ACM()
		{
			if (this.serial_port.IsOpen)
			{
				this.serial_port.Close();
			}
		}
		
		public void Dispose()
		{
			if (this.serial_port.IsOpen)
			{
				this.serial_port.Close();
			}
			this.serial_port.Dispose();
		}
		
		public void WriteLine(string s)
		{
			Console.WriteLine("ACM.WriteLine(): " + s);
			this.serial_port.WriteLine(s);
		}

		public string ReadLine()
		{
			string line = this.serial_port.ReadLine();
			Console.WriteLine("ACM.ReadLine(): " + line);
			return line;
		}

		public byte[] Read(int count)
		{
			byte[] buf = new byte[count];

			int read_count = 0;

			Console.WriteLine("Going to read {0} bytes ...", count);
			while (count - read_count > 0)
			{
				byte[] tmp = new byte[count];
				int result = this.serial_port.Read(tmp, 0, count-read_count);

				if (result == 0)
					throw new Exception(Catalog.GetString("No data available."));

				Console.WriteLine("  bytes {0} to {1}", read_count, read_count+result);
				Array.Copy(tmp, 0, buf, read_count, result);
				read_count += result;
			}

			return buf;
		}

		public int ReadTimeout
		{
			get { return this.serial_port.ReadTimeout;  }
			set { this.serial_port.ReadTimeout = value; }
		}
	}

	public class NSeries : Hiperscan.SGS.Device
	{

        private string port;
		private string serial;
		private ACM acm;
		private Device control_pcb = null;

		public NSeries(DeviceInfo info, VersionInfo version) : base(info, version)
		{
			this.port = "/dev/" + info.SerialNumber;

			using (ACM acm = new ACM(this.port))
			{
				bool success = false;

				for (int ix=0; ix < 2; ++ix)
				{
					acm.WriteLine("!");

					while (acm.Read(1)[0] == '!') { }
					acm.ReadLine();

					acm.WriteLine("y");

					try
					{
						string serial = acm.ReadLine().Substring(14).Trim();
						string min_wavelength = acm.ReadLine().Substring(7).Trim();
						string max_wavelength = acm.ReadLine().Substring(7).Trim();
						acm.ReadLine();
						acm.ReadLine();
						acm.ReadLine();
						acm.ReadLine();
					
						acm.WriteLine("i");
						string firmware_info  = acm.ReadLine().Trim();
						string build_date     = acm.ReadLine();

						this.lut_timestamp = DateTime.MinValue;
						this.firmware_version = firmware_info;

						this.serial = serial;
						this.HardwareProperties.MinWavelength = double.Parse(min_wavelength);
						this.HardwareProperties.MaxWavelength = double.Parse(max_wavelength);
						this.HardwareProperties.SpectralResolution = 15;
						this.HardwareProperties.DiffractionAngle = 0;
						this.HardwareProperties.DynamicRange = 0;
						this.HardwareProperties.GridConstant = 0;
						this.HardwareProperties.IncidenceAngle = 0;
						this.HardwareProperties.MaxAverage = 2000;
						this.HardwareProperties.Sensitivity = 0;
						this.HardwareProperties.SignalNoiseRatio = 0;
						this.HardwareProperties.WavelengthDrift = 0.1;

						success = true;
						break;
					}
					catch (Exception ex)
					{
						Console.WriteLine("Exception while initializing device on port {0}: {1}", this.port, ex.Message);
						Console.WriteLine("Try again ...");
					}
				}

				if (success)
					this.SetState(State.Disconnected);
				else
					this.SetState(State.Unknown);
			}
		}

		public override void Open(bool auto_init)
		{
			this.acm = new ACM(this.port);

			if (auto_init == false)
				return;

			string control;

			this.SetState(State.Init);

			int ix = 0;
			for (int wl=(int)this.HardwareProperties.MinWavelength; wl <= (int)this.HardwareProperties.MaxWavelength; ++wl)
			{
				control = string.Format("W{0},{1}.0", ix++, wl);
				this.acm.WriteLine(control);
				this.acm.ReadLine();
			}

			control = string.Format("W{0},0.0", ix);
			this.acm.WriteLine(control);
			this.acm.ReadLine();

			this.current_config.Average = 15;
			this.current_config.QuickAverage = 1;

			try
			{
				DeviceList dev_list = new DeviceList(0x9c74);
				dev_list.Update();
				if (dev_list.Contains(this.Info.SerialNumber) && dev_list[this.Info.SerialNumber].Info.DeviceType != DeviceType.NSeries)
				{
					this.control_pcb = dev_list[this.Info.SerialNumber];
					this.control_pcb.StateChanged += delegate(Device device) {
						this.SetState(device.GetState());
					};
					this.control_pcb.Open();
					this.is_finder = control_pcb.IsFinder;
				}
			}
			catch (Exception ex)
			{
				Console.WriteLine("Error while initializing control PCB for device {0}: {1}", this.serial, ex.ToString());
			}

			this.SetState(State.Idle);
		}

		public override void Close()
		{
			try
			{
				if (this.control_pcb != null)
					this.control_pcb.Close();
			}
			catch {}

			try
			{
				this.acm.Dispose();
			}
			catch {}

			this.SetState(State.Disconnected);
		}

		internal override Config ReadConfig (bool throw_if_invalid)
		{
			throw new System.NotImplementedException ();
		}

		internal override void WriteConfig (Config config)
		{
			this.SetState(State.Idle);
//			throw new System.NotImplementedException ();
		}

		public override void WriteEEPROM ()
		{
			throw new System.NotImplementedException ();
		}

		public override void StartSingle (bool auto_finder_light)
		{
			Thread run_single = new Thread(new ParameterizedThreadStart(this.DoRunSingle));
			run_single.Start(auto_finder_light);
		}

		public override Spectrum Single (bool auto_finder_light, IdleTaskHandler idle_task)
		{
			try
			{
				Spectrum spectrum = null;
				
				if (this.GetState() == State.Continuous ||
				    this.GetState() == State.Stream     ||
				    this.GetState() == State.QuickStream)
				{
					while (spectrum == null)
					{
						lock (this.current_spectrum_mutex)
							spectrum = new Spectrum(this.current_spectrum);
						
						System.Threading.Thread.Sleep(50);
					}
				}
				else
				{
					if (auto_finder_light)
						this.SetFinderLight(true, idle_task);

					if (!this.SetState(State.Single))
						throw new Exception(Catalog.GetString("Device is not ready."));
					
					double[] lambda;
					double[] intensity;
					uint[] add_data;
					this.ReadSpectrum(this.current_config.Average, out add_data, out lambda, out intensity, idle_task);
				
					this.SetState(State.Idle);
					
					if ((lambda.Length < 2) || (lambda.Length != intensity.Length))
					{
						Console.WriteLine("Read spectrum is not valid");
						this.ExceptionHandler(Catalog.GetString("Read spectrum is not valid."), null);
						return null;
					}
					
					this.SpectrumCount = 1;

					WavelengthLimits limits = new WavelengthLimits(this.HardwareProperties.MinWavelength, this.HardwareProperties.MaxWavelength);
					spectrum = new Spectrum(new DataSet(lambda, intensity), limits);
					
					spectrum.AddData = add_data;

					if (auto_finder_light)
						this.SetFinderLight(false, idle_task);
				}
				
				DateTime now = DateTime.Now;
				
				spectrum.Serial             = this.info.SerialNumber;
				spectrum.FirmwareVersion    = this.FirmwareVersion;
				spectrum.LutTimestamp       = this.LutTimestamp.ToString();
				spectrum.SpectralResolution = this.HardwareProperties.SpectralResolution;
				spectrum.AverageCount       = this.CurrentConfig.Average;
				spectrum.SpectrumType       = SpectrumType.Single;

				if (this.IsFinder)
					spectrum.ProbeType = ProbeType.FinderStandard;

				spectrum.Timestamp           = now;
				spectrum.Id                 = "1_" + now.ToString("yyyy-MM-dd_HH-mm-ss_") + this.info.SerialNumber;
				spectrum.Label              = now.ToString() + " " + this.CurrentConfig.Name;
				spectrum.DefaultLabel       = true;
				spectrum.Comment            = this.CurrentConfig.DefaultComment;
				spectrum.IsFromFile         = false;
				spectrum.Number             = (long)this.SpectrumCount;
				spectrum.LimitBandwidth     = this.LimitBandwith;

				spectrum.HardwareInfo = this.FinderVersion.ToArray();

				if (this.CurrentConfig.HasAdcCorrection)
					spectrum.AdcCorrection = this.CurrentConfig.AdcCorrection;

				if (this.StateNotification)
					this.NewSingleNotify(spectrum.Clone());
				
				return spectrum;
			}
			catch (Exception ex)
			{
				this.ExceptionHandler(Catalog.GetString("Cannot read single spectrum:") +
				                      " " + ex.Message, ex);
				return null;
			}
		}

		public override void StartQuickStream ()
		{
			Thread run_quick = new Thread(new ThreadStart(this.DoRunQuick));
			run_quick.Start();
		}

		public override void StartStream ()
		{
			this.quick_stream_running = false;
			
			switch (this.CurrentConfig.StreamType)
			{
				
			case StreamType.Continuous:
				Thread run_continuous = new Thread(new ThreadStart(this.DoRunContinuous));
				run_continuous.Start();
				break;
				
			case StreamType.Timed:
				Thread run_timed = new Thread(new ThreadStart(this.DoRunTimed));
				run_timed.Start();
				break;
				
			default:
				throw new Exception("Unknown StreamType");
				
			}
		}

		public override void StopStream ()
		{
			if (this.quick_stream_running)
			{
				this.SetState(State.IdleRequest);
				return;
			}
			
			switch (this.CurrentConfig.StreamType)
			{
				
			case StreamType.Continuous:
				throw new NotImplementedException();
				break;
				
			case StreamType.Timed:
				this.SetState(State.IdleRequest);
				break;
								
			default:
				throw new Exception("Unknown StreamType");
				
			}
		}

		public override double ReadStatus()
		{
			this.acm.WriteLine("I0");
			string temperature = this.acm.ReadLine().Substring(7).Trim();
			return double.Parse(temperature, System.Globalization.CultureInfo.InvariantCulture);
		}

		public override void SetFinderLight(bool on, IdleTaskHandler idle_task)
		{
			if (this.control_pcb != null)
				this.control_pcb.SetFinderLight(on, idle_task);
			else
				throw new NotSupportedException("Not supported by hardware.");
		}

		public override void SetFinderWheel(Hiperscan.SGS.FinderWheelPosition position, IdleTaskHandler idle_task, bool no_wait=false)
		{
			if (this.control_pcb != null)
				this.control_pcb.SetFinderWheel(position, idle_task);
			else
				throw new NotSupportedException("Not supported by hardware.");
		}

		public override void SetFinderFanSpeed (bool on)
		{
			if (this.control_pcb != null)
				this.control_pcb.SetFinderFanSpeed(on);
			else
				throw new NotSupportedException("Not supported by hardware.");
		}

		public override byte ReadDimmerParameter()
		{
			if (this.control_pcb != null)
				return this.control_pcb.ReadDimmerParameter();
			throw new NotSupportedException("Not supported by hardware.");
		}

		public override bool FinderButtonPressed ()
		{
			if (this.control_pcb != null)
				return this.control_pcb.FinderButtonPressed();
			throw new NotSupportedException("Not supported by hardware.");
		}

		public override double ReadFinderTemperature()
		{
			if (this.control_pcb != null)
				return this.control_pcb.ReadFinderTemperature();
			throw new NotSupportedException("Not supported by hardware.");
		}

		public override byte[] ReadFinderHardwareVersion()
		{
			if (this.control_pcb != null)
				return this.control_pcb.ReadFinderHardwareVersion();
			else
				throw new NotSupportedException("Not supported by hardware.");
		}

		private void DoRunSingle(object o)
		{
			bool auto_finder_light = (bool)o;
			
			try
			{
				Spectrum spectrum = this.Single(auto_finder_light);
				this.spectrum_queue.Enqueue(spectrum);
			}
			catch (System.Exception ex)
			{
				this.ExceptionHandler(Catalog.GetString("Cannot read single spectrum:") +
				                      " " + ex.Message, ex);
			}
		}

		private void DoRunTimed()
		{
			try
			{
				if (this.GetState() == State.QuickStream)
				{
					this.StopStream();
					this.WaitForIdle(5*this.CurrentConfig.Timeout);
					this.StartStream();
					return;
				}
				
				this.SetFinderLight(true, null);

				if (!this.SetState(State.Stream))
					throw new Exception(Catalog.GetString("Device is not ready."));
				
				DateTime then = DateTime.Now;
				this.SpectrumCount = 0;
				
				while ( (this.GetState() == State.Stream)     &&
				       ((this.current_config.TimedCount == 0) ||
				        (this.SpectrumCount < this.CurrentConfig.TimedCount)) )
				{
					try
					{
						double[] lambda;
						double[] intensity;
						uint[] add_data;
						this.ReadSpectrum(this.current_config.Average, out add_data, out lambda, out intensity, null);

						WavelengthLimits limits = new WavelengthLimits(this.HardwareProperties.MinWavelength, this.HardwareProperties.MaxWavelength);
						Spectrum spectrum = new Spectrum(new DataSet(lambda, intensity), limits);
						
						spectrum.AddData            = add_data;
						spectrum.Timestamp           = DateTime.Now;
						spectrum.Serial             = this.Info.SerialNumber;
						spectrum.FirmwareVersion    = this.FirmwareVersion;
						spectrum.LutTimestamp       = this.LutTimestamp.ToString();
						spectrum.SpectralResolution = this.HardwareProperties.SpectralResolution;
						spectrum.AverageCount       = this.CurrentConfig.Average;
						spectrum.SpectrumType       = SpectrumType.Stream;

						if (this.IsFinder)
							spectrum.ProbeType = ProbeType.FinderStandard;

						spectrum.Id                 = "5_stream" + this.info.SerialNumber;
						spectrum.Label              = Enum.GetName(typeof(State), State.Stream) + " "  + this.CurrentConfig.Name;
						spectrum.DefaultLabel       = true;
						spectrum.Comment            = this.CurrentConfig.DefaultComment;
						spectrum.IsFromFile         = false;
						spectrum.Number             = (long)(++this.SpectrumCount);
						spectrum.LimitBandwidth     = this.LimitBandwith;
						
						spectrum.HardwareInfo = this.FinderVersion.ToArray();

						if (this.CurrentConfig.HasAdcCorrection)
							spectrum.AdcCorrection = this.CurrentConfig.AdcCorrection;
				
						if (this.StateNotification)
							this.NewStreamNotify(spectrum.Clone());
						
						this.spectrum_queue.Enqueue(spectrum);
						
						lock (this.current_spectrum_mutex)
							this.current_spectrum = spectrum;
						
					}
					catch (Exception ex)
					{
						this.ExceptionHandler(Catalog.GetString("Cannot read in timed mode:") + 
						                      " " + ex.Message, ex);
						break;
					}
					
					TimeSpan span;
					ulong mseconds = 0;
					ulong maxtime  = 0;
					bool  light_is_on = true;
					
					while (this.GetState() == State.Stream)
					{
						if (this.CurrentConfig.TimedSpan == 0)
							break;
						
						span     = DateTime.Now - then;
						mseconds = (ulong)span.TotalMilliseconds % (this.CurrentConfig.TimedSpan * 1000);
						
						if (mseconds < maxtime)
							break;
						else
							maxtime = mseconds;
						
						if ((this.CurrentConfig.TimedCount != 0) &&
						    (this.SpectrumCount >= this.CurrentConfig.TimedCount))
							break;
						
						// switch finder light off if there are more than 3.5 seconds to wait
						if (this.IsFinder && light_is_on && 
						    this.CurrentConfig.TimedSpan * 1000 - mseconds > 3500)
						{
							Console.WriteLine("Next acquisition in {0} ms.", 
							                  this.CurrentConfig.TimedSpan * 1000 - mseconds);
							lock (this)
							{
								this.SetState(State.Idle);
								this.SetFinderLight(false, null);
								this.SetState(State.Stream);
							}
							light_is_on = false;
						}

						Thread.Sleep(50);
					}
					
					if (!light_is_on && this.GetState() == State.Stream &&
					    (this.SpectrumCount < this.CurrentConfig.TimedCount || 
					     this.CurrentConfig.TimedCount == 0))
					{
						lock (this)
						{
							this.SetState(State.Idle);
							this.SetFinderLight(true, null);
							this.SetState(State.Stream);
						}
					}
				}
				Console.WriteLine("Queue contains still " + this.spectrum_queue.Count + " spectra.");
				
				State state = this.GetState();

				if (state == State.Unknown)
					return;

				if (state != State.Disconnected)
					this.SetState(State.Idle);
				
				this.SetFinderLight(false, null);
			}
			catch (Exception ex)
			{
				this.ExceptionHandler(Catalog.GetString("Cannot read in timed mode:") +
				                      " " + ex.Message, ex);
			}
		}

		private void DoRunQuick()
		{
			try
			{
				if (this.GetState() == State.Continuous ||
				    this.GetState() == State.Stream)
				{
					this.StopStream();
					this.WaitForIdle(this.CurrentConfig.Timeout);
					
					this.StartQuickStream();
					return;
				}
				
				Console.WriteLine("Write QuickStream config");
				this.old_config = new Config(this.CurrentConfig);
				this.CurrentConfig.Average = this.CurrentConfig.QuickAverage;
				this.CurrentConfig.Samples = this.CurrentConfig.QuickSamples;
				this.quick_stream_running  = true;
				this.WriteConfig(this.CurrentConfig);
				
				this.SetFinderLight(true, null);

				if (!this.SetState(State.QuickStream))
					throw new Exception(Catalog.GetString("Device is not ready."));
				
				while (this.GetState() == State.QuickStream)
				{
					try
					{
						double[] lambda;
						double[] intensity;
						uint[] add_data;
						this.ReadSpectrum(1, out add_data, out lambda, out intensity, null);

						WavelengthLimits limits = new WavelengthLimits(this.HardwareProperties.MinWavelength, this.HardwareProperties.MaxWavelength);
						Spectrum spectrum = new Spectrum(new DataSet(lambda, intensity), limits);
						
						spectrum.AddData            = add_data;
						spectrum.Timestamp           = DateTime.Now;
						spectrum.Serial             = this.Info.SerialNumber;
						spectrum.FirmwareVersion    = this.FirmwareVersion;
						spectrum.LutTimestamp       = this.LutTimestamp.ToString();
						spectrum.SpectralResolution = this.HardwareProperties.SpectralResolution;
						spectrum.AverageCount       = this.CurrentConfig.Average;
						spectrum.SpectrumType       = SpectrumType.QuickStream;

						spectrum.Id                 = "5_stream" + this.info.SerialNumber;
						spectrum.Label              = Enum.GetName(typeof(State), State.QuickStream) + " "  + this.CurrentConfig.Name;
						spectrum.DefaultLabel       = true;
						spectrum.Comment            = this.CurrentConfig.DefaultComment;
						spectrum.IsFromFile         = false;
						//spectrum.Number       = ++this.SpectrumCount;
						spectrum.LimitBandwidth     = this.LimitBandwith;

						spectrum.HardwareInfo = this.FinderVersion.ToArray();
						
						if (this.CurrentConfig.HasAdcCorrection)
							spectrum.AdcCorrection = this.CurrentConfig.AdcCorrection;

						if (this.StateNotification)
							this.NewQuickStreamNotify(spectrum.Clone());

						this.spectrum_queue.Enqueue(spectrum);
						
						lock (this.current_spectrum_mutex)
							this.current_spectrum = spectrum;
						
					}
					catch (Exception ex)
					{
						this.ExceptionHandler(Catalog.GetString("Cannot read in quick stream mode:") +
						                      " " + ex.Message, ex);
						break;
					}
				}
				
				Console.WriteLine("Queue contains still " + this.spectrum_queue.Count + " spectra.");
				
				State state = this.GetState();

				if (state == State.Unknown)
					return;

				if (state != State.Disconnected)
					this.SetState(State.Idle);

				this.SetFinderLight(false, null);
				
				this.CurrentConfig = this.old_config;
				this.old_config = null;
				this.WriteConfig(this.CurrentConfig);
			}
			catch (Exception ex)
			{
				this.ExceptionHandler(Catalog.GetString("Cannot read in quick stream mode:") + 
				                      " " + ex.ToString(), ex);
			}
		}

		private void DoRunContinuous()
		{
			throw new NotImplementedException();
		}

		private void ReadSpectrum(int average, out uint[] add_data, out double[] lambda, out double[] intensity, IdleTaskHandler idle_task)
		{
			double temperature = this.ReadStatus();

			string control = string.Format("N{0},0.1", average);
			this.acm.WriteLine(control);

			byte[] buf;

			try
			{
				this.acm.ReadTimeout = 60000;
				buf = this.acm.Read(2048);
			}
			catch (Exception ex)
			{
				throw ex;
			}
			finally
			{
				this.acm.ReadTimeout = ACM.DefaultReadTimeout;
			}

			this.acm.ReadLine();

			int count = buf.Length/sizeof(float);

			lambda = new double[count];
			intensity = new double[count];

			for (int ix = 0; ix < count; ++ix)
			{
				lambda[ix] = ix + this.HardwareProperties.MinWavelength;
				intensity[ix] = BitConverter.ToSingle(buf, ix*sizeof(float));
			}

			add_data = new uint[3];

			add_data[0] = 0;
			add_data[1] = (uint)(temperature*100.0);
			add_data[2] = 0;
		}

		public override FinderWheelPosition FinderWheelPosition
		{
			get { return this.control_pcb.FinderWheelPosition; }
		}

		public override bool FinderLampStatus
		{
			get { return this.control_pcb.FinderLampStatus; }
		}
	}
}

