// Created with MonoDevelop
//
//    QuickStep: Acquire, view and identify spectra 
//    Copyright (C) 2018 Thomas Klose & Torsten Schache, 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.Collections.Generic;

using Hiperscan.Spectroscopy;
using Hiperscan.SGS;
using Hiperscan.Unix;
using Hiperscan.Spectroscopy.IdentModule;


namespace Hiperscan.Extensions.ApoIdent
{
	
	public partial class IdentServerClient
	{
        public event EventHandler DefaultConnectionFailed 	= null;
        public event EventHandler IdentModuleVersionRefused = null;
        public event EventHandler ConnectionRefused			= null;
        public event EventHandler OnNoReleaseVersion		= null;	
        public event EventHandler RegistrationFailed		= null;
		public event EventHandler RemindOfUpdate			= null;
		
		
        public IdentServerClient(Configurations config)
		{
            this.Config                         = config;
			this.Server 						= null;
			this.Authentification				= null;
		}

		public void Reset()
		{
			Console.WriteLine("Reset ident module client ...");
			this.Server 						= null;
			this.Address						= null;
			IdentServerClient.Info				= null;

			this.Result							= null;
			this.Server							= null;
			this.Authentification				= null;
		}


		
		#region start and stop ident module
		
		public ServerDeviceState Start(string server_address, int tcp_port, System.Version plugin_version, 
                                       bool is_release, bool has_second_chance)
		{
			this.Address 	= server_address;
			this.TcpPort	= tcp_port;

			try
			{
				this.StartIdentModule();
			}
			catch (System.ComponentModel.Win32Exception ex)
			{
				if (has_second_chance == false)
				{
					if (this.DefaultConnectionFailed != null)
                    {
                        DataEventArgs args = new DataEventArgs();
                        args.Data.Add(DataEventArgs.ERROR, ex.Message);
                        this.DefaultConnectionFailed(this, args);
                    }

					Console.WriteLine("Starting IdentModule failed: {0}", ex);
				}
				else
				{
					Console.WriteLine("IdentModule needed second chance: {0}", ex);
				}
				return ServerDeviceState.StartIdentModuleFailed;
			}


			DateTime then = DateTime.Now;
			TimeSpan span = new TimeSpan(0, 0, 10);

			for (;;)
			{
				System.Threading.Thread.Sleep(500);
				try
				{
					this.Server = IdentServer.GetRemoteObject(server_address, this.TcpPort);

					IdentServerClient.Info = this.Server.GetIdentModuleInfo();
                    this.Config.AcquisitionSettings.Locale = IdentServerClient.Info.Locale;
					Console.WriteLine(IdentServerClient.Info);
					string[] version_data = IdentServerClient.Info.ShortAssemblyVersion.Split(new char[]{'.'});
					
                    if (is_release == true)
					{
                        if (plugin_version.Major.ToString() != version_data[0] || 
                            plugin_version.Minor.ToString() != version_data[1])
                        {
                            if (has_second_chance == false)
                            {
                                string message = String.Format(Catalog.GetString("Incompatible Ident Module version: {0}"), 
                                                               IdentServerClient.Info.ShortAssemblyVersion);                              
                                if (this.IdentModuleVersionRefused != null)
                                {
                                    DataEventArgs args = new DataEventArgs();
                                    args.Data.Add(DataEventArgs.ERROR, message);
                                    this.IdentModuleVersionRefused(this, args);
                                }
                            }
                            return ServerDeviceState.StartIdentModuleFailed;
                        }
					}
				}
				catch (System.Runtime.Remoting.RemotingException ex)
				{
					if (DateTime.Now - then < span)
						continue;
					
					if (has_second_chance == false)
					{
						Console.WriteLine("Time out error while connecting to identmodule: {0}", ex);
						string message = String.Format(Catalog.GetString("Time out error while connecting to identmodule: {0}"), ex.Message);
						if (this.ConnectionRefused != null)
                        {
                            DataEventArgs args = new DataEventArgs();
                            args.Data.Add(DataEventArgs.ERROR, message);
                            this.ConnectionRefused(this, args);
                        }
					}
					else
					{
						Console.WriteLine("IdentModule needed second chance: {0}", ex);
					}
					return ServerDeviceState.StartIdentModuleFailed;
				}
				catch (Exception ex)
				{
					if (has_second_chance == false)
					{
						Console.WriteLine("Unknown error while connecting to identmodule: {0}", ex);
						if (this.ConnectionRefused != null)
                        {
                            DataEventArgs args = new DataEventArgs();
                            args.Data.Add(DataEventArgs.ERROR, ex.Message);
                            this.ConnectionRefused(this, args);
                        }    
					}
					else
					{
						Console.WriteLine("IdentModule needed second chance: {0}", ex);
					}

					return ServerDeviceState.StartIdentModuleFailed;	
				}

				break;
			}

			return ServerDeviceState.StartIdentModuleSuccess;
		}

		private void StartIdentModule()
		{
			Console.WriteLine("Starting IdentServer...");
            this.IsSecondProgramInstance = false;
            try 
            {
                this.IdentServerProcess.Kill();
                System.Threading.Thread.Sleep(500);
            }
            catch(Exception){}

            try
            {
                IIdentServer tmp = IdentServer.GetRemoteObject(this.Address, this.TcpPort);
                tmp.GetIdentModuleInfo();
                Console.WriteLine("Ident server is already running ...");
                this.IsSecondProgramInstance = true;
                return;
            }
            catch(Exception ex)
            {
                if (ApoIdentEnvs.DebugOn)
                    Console.WriteLine("Exception connecting to Ident Module: " + ex.ToString());
            }

            this.IdentServerProcess = this.CreateIdentServerProcess(false);
            this.IdentServerProcess.Start();
            Console.WriteLine("Identserver started.");
		}

		public void StopIdentModule()
		{
			try
			{
				if (this.IsSecondProgramInstance == true)
					return;

				Console.WriteLine("Stopping IdentServer...");
				try
                {
                    System.Diagnostics.Process proc = this.CreateIdentServerProcess(true);
                    proc.Start();
                }
                catch(Exception){}

				try 
				{
					System.Threading.Thread.Sleep(2500);
					this.IdentServerProcess.Kill();
					System.Threading.Thread.Sleep(500);
				}
                catch(Exception) {}
				
				Console.WriteLine("Ident server stopped successfully.");
			}
			catch(Exception ex)
			{
				Console.WriteLine("Error while stopping ident server: {0}", ex);
			}
		}



        private System.Diagnostics.Process CreateIdentServerProcess(bool stop)
		{
            System.Diagnostics.Process proc = new System.Diagnostics.Process();
            proc.StartInfo.Arguments = String.Empty;

			PlatformID osversion = Environment.OSVersion.Platform;
			if (osversion == PlatformID.Win32NT      ||
				osversion == PlatformID.Win32S       ||
				osversion == PlatformID.Win32Windows ||
				osversion == PlatformID.WinCE)
			{

                proc.StartInfo.FileName 		= FilesAndDirs.MonoExeFile;
                proc.StartInfo.Arguments		+= "\"" + FilesAndDirs.IdentServerExeFile + "\"";	
                proc.StartInfo.WorkingDirectory	= FilesAndDirs.IdentServerWorkingDir;
			}
			else
			{
				// linux
                proc.StartInfo.FileName 	= "identserver";
			}

            proc.StartInfo.Arguments += " --port " + this.TcpPort.ToString();

            if (stop == true)
                proc.StartInfo.Arguments += " --stop"; 
            else
                proc.StartInfo.Arguments += " --log \"" + FilesAndDirs.IdentServerLogFile + "\"";

            proc.StartInfo.CreateNoWindow  = true;
            proc.StartInfo.UseShellExecute = false;

            Console.WriteLine("Identserver proc - FileName :" + proc.StartInfo.FileName);
            Console.WriteLine("Identserver proc - Arguments:" + proc.StartInfo.Arguments);

            return proc;
		}

		#endregion start and stop ident module

		public ServerDeviceState RegisterDevice(Device device)
		{															
			try
			{
				byte[] challenge = this.Server.GetChallenge();
				device.WaitForIdle(30000);
				byte[] master_response = device.ReadMasterKey(challenge);
                this.Databases = this.Server.Initialize(device.Info.SerialNumber, master_response);
				device.WaitForIdle(20000);


                this.Authentification = new AuthentificationSet();
                this.Authentification.Serial = device.Info.SerialNumber;

                foreach (AccessDatabaseInfo db_info in this.Databases)
                {
                    if (db_info.IndividualKey == null)
                        db_info.IndividualKey = master_response;
                    else
                        db_info.IndividualKey = device.ReadIndividualKey(db_info.IndividualKey);

                    if (db_info == AccessDatabaseInfo.Master)
                        this.Authentification.AccessDatabase = db_info;
                }

				return ServerDeviceState.ConnectDeviceToIdentModuleSuccess;
			}
			catch(Exception ex)
			{
				Console.WriteLine(ex);

				if (this.RegistrationFailed != null)
                {
                    DataEventArgs args = new DataEventArgs();
                    args.Data.Add(DataEventArgs.ERROR, ex.Message);
                    this.RegistrationFailed(this, args);
                }

				return ServerDeviceState.ConnectDeviceToIdentModuleFailed;
			}
		}

		public ServerDeviceState CheckReleaseVersion()
		{
			if (IdentServerClient.Info.IsRelease == false)
			{
				string message = Catalog.GetString("the installed ident module is a development version. Please install a released ident module.");
				if (this.OnNoReleaseVersion != null)
                {
                    DataEventArgs args = new DataEventArgs();
                    args.Data.Add(DataEventArgs.ERROR, message);
                    this.OnNoReleaseVersion(this, args);
                }

				return ServerDeviceState.CheckingSubstanceDatabaseFailed;
			}

			return ServerDeviceState.CheckingSubstanceDatabaseSuccess;
		}

		public ServerDeviceState CheckForPendingUpdate()
		{
            if (this.Config.AcquisitionSettings.ReleaseExpirationTime < DateTime.Now)
			{
				if (this.RemindOfUpdate != null)
					this.RemindOfUpdate(this, EventArgs.Empty);

				return ServerDeviceState.CheckForUpdateFailed;
			}

			return ServerDeviceState.CheckForUpdateSuccess;
		}

		public void Classify(Spectrum spectrum)
		{
			try
			{
                this.Result = this.Server.Classify(this.Authentification, spectrum);	
				if (this.Result == null)
					throw new Exception(Catalog.GetString("Cannot classify spectrum."));
			}
			catch(Exception ex)
			{
				this.Result = null;
				throw ex;
			}
		}

        public Dictionary<string, ResultSet> ClassifiyBlindClassification(Dictionary<string, ModelClassInfo> class_names, Spectrum spectrum)
        {
            Dictionary<string, ResultSet> sets = new Dictionary<string, ResultSet>();

            foreach (string class_name in class_names.Keys)
            {
                ModelClassInfo class_info = class_names[class_name];
                AuthentificationSet authentification = new AuthentificationSet();
                authentification.AccessDatabase = class_info.DatabaseInfo;
                authentification.ModelClassName = class_name;
                authentification.Serial = this.Authentification.Serial;
                authentification.SubstanceId = AuthentificationSet.NO_SUBSTANCE;

                sets.Add(class_name, null);

                try
                {
                    sets[class_name] = this.Server.Classify(authentification, spectrum);
                }
                catch
                {
                    
                }
            }

            return sets;
        }

        public Dictionary<string, byte[]> GetAddtionalTestsPdfs()
        {
            return this.Server.GetAdditionalTestPdfs();
        }

		public byte[] GetSubstanceChangeLog()
		{
            return this.Server.GetUpdateChangesFile();
		}
		
		public void DiscardResult()
		{
			this.Result = null;
		}

		public bool HasValidResult
		{
			get
			{
				return this.Result != null;
			}
		}
		
		public bool IsTruePositive
		{
			get
			{
                return this.HasValidResult == true && this.Result.TruePositive;
			}
		}

        public byte[] GetLogfile()
        {
            return this.Server.GetLogfile();
        }

		private AuthentificationSet Authentification	{get; set;}
		public ResultSet Result							{get; private set;}

		private IIdentServer Server 					{get; set;}
		public string Address							{get; private set;}
		public int TcpPort								{get; private set;}

        private System.Diagnostics.Process IdentServerProcess {get; set;}
		private bool IsSecondProgramInstance			{get; set;}
        public List<AccessDatabaseInfo> Databases       {get; private set;}    

		public static IdentModuleInfo Info				{get; private set;}
        private Configurations Config                   {get; set;}
	}
}

