// Created with MonoDevelop
//
//    Copyright (C) 2017 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.IO;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Globalization;
using System.Text;

using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;

using Hiperscan.Unix;


namespace Hiperscan.FileIO
{
	
	public class SubstanceDatabase
	{
		
		public class Lines : List<List<string>> {};
		public class WorkingData : Dictionary<int, List <string>>{}
		public class AliasData : Dictionary<int, List<int>> {}
		public class Errors : List<string>{}
		public class NotMeasurableData : Dictionary<string, List<string>>{}
		
		public class NameTable
		{
			public WorkingData DefaultNames {get;set;}
			public WorkingData LatinNames 	{get;set;}
			public AliasData Aliase			{get;set;}
			
			public NameTable()
			{
				this.DefaultNames 	= new SubstanceDatabase.WorkingData();
				this.LatinNames 	= new SubstanceDatabase.WorkingData();
				this.Aliase			= new SubstanceDatabase.AliasData();
			}
		}

		
		public const string Version = "2.0";
		private struct Parser
		{
			public const string UTF7Separator    = ";";
			public const string UnicodeSeparator = "\t";
		}
		

		private Dictionary<string,List<string>> substances;
		private Dictionary<string,string> names;
		private List<string> subset;
		private Dictionary<string,List<string>>.KeyCollection subset_keys;
		
		public SubstanceDatabase()
		{
			this.substances = new Dictionary<string,List<string>>();
			this.names      = new Dictionary<string,string>();
		}
		
		private void UpdateNames()
		{
			this.names.Clear();
			Dictionary<string,List<string>> dict = new Dictionary<string,List<string>>();
			
			foreach (string key in this.substances.Keys)
			{
				if (this.subset != null && !this.subset.Contains(key))
					continue;
				
				dict.Add(key, null);
				
				List<string> substance = this.substances[key];
				foreach (string name in substance)
				{
					if (!this.names.ContainsKey(name))
						this.names.Add(name, key);
				}
			}
			
			this.subset_keys = dict.Keys;
		}
		
		public string GetName(string key)
		{
			if (this.substances.ContainsKey(key) && this.substances[key].Count > 0)
				return this.substances[key][0];
			else
				return key;
		}
		
		public List<string> GetNames(string key)
		{
			if (this.substances.ContainsKey(key))
				return this.substances[key];
			else
				return new List<string>(new string[] {key});
		}
		
		public bool ContainsKey(string key)
		{
			return this.substances.ContainsKey(key);
		}
		
		public string[] GetKeysByName(string resolved_name)
		{
			List<string> keys = new List<string>();

			foreach (string id in this.substances.Keys)
			{
				if (this.subset != null && !this.subset.Contains(id))
					continue;
				
				foreach (string name in this.substances[id])
				{
					if (resolved_name == name && !keys.Contains(id))
						keys.Add(id);
				}
			}
			
			return keys.ToArray();
		}
		
		public static SubstanceDatabase Read(string fname)
		{
			using (FileStream fstream = File.OpenRead(fname))
			{
				System.Text.Encoding enc;
				Regex regex;
				
				if (fname.ToLower().EndsWith(".csv"))
				{
					// UTF-7 encoded
					enc = System.Text.Encoding.UTF7;
					regex = new Regex(SubstanceDatabase.Parser.UTF7Separator);
				}
				else
				{
					// Unicode encoded
					enc = System.Text.Encoding.Default;
					regex = new Regex(SubstanceDatabase.Parser.UnicodeSeparator);
				}
				
				using (StreamReader stream = new StreamReader(fstream, enc, true))
				{
					SubstanceDatabase substancedb = new SubstanceDatabase();
					string line;
					int ix = 0;
					
					while ((line = stream.ReadLine()) != null)
					{
						if (line.StartsWith("#"))
						    continue;
						
						++ix;
						string[] s = regex.Split(line);
						
						if (s.Length < 2)
							continue;
							//throw new Exception(Catalog.GetString("Cannot read substance name."));

						if (substancedb.substances.ContainsKey(s[0]))
						{
							Console.WriteLine("{0} ({1}): Entry already exists in dictionary: {2},{3}", fname, ix, s[0], s[1]);
							continue;
						}
						
						List<string> names = new List<string>();
						for (int jx=1; jx < s.Length; ++jx)
						{
							string n = s[jx].Trim();
							if (!string.IsNullOrEmpty(n))
								names.Add(n);
						}
						
						substancedb.substances.Add(s[0], names);
					}
					stream.Close();
					
					substancedb.UpdateNames();
					return substancedb;
				}
			}
		}
		
		public bool IsAlias(string name1, string name2)
		{			
			if (!this.names.ContainsKey(name1) ||
			    !this.names.ContainsKey(name2))
				return false;
			return (this.names[name1] == this.names[name2]);
		}
		
		public Dictionary<string,List<string>> Substances
		{
			get { return this.substances; }
		}
		
		public List<string> Names
		{
			get { return new List<string>(this.names.Keys); }
		}
		
		public Dictionary<string,List<string>>.KeyCollection Keys
		{
			get 
			{ 
				if (this.subset_keys != null)
					return this.subset_keys;
				else
					return this.substances.Keys; 
			}
		}
		
		public List<string> Subset
		{
			get { return this.subset; }
			set
			{ 
				this.subset = value;
				this.UpdateNames();
			}
		}
		



		public static Lines ReadSubstanceFile(string file_name)
		{
			Lines values = new Lines();
			string text = null;
			using (StreamReader sr = new StreamReader(file_name, Encoding.UTF8))
			{
				text = sr.ReadToEnd();
			}
			text = text.Replace("\"", String.Empty);
			text = text.Replace("\r", String.Empty);
			using (StreamWriter sw = new StreamWriter(file_name, false, Encoding.UTF8))
			{
				sw.Write(text);
			}
			
			string[] all_lines = text.Split(new char[]{'\n'}, StringSplitOptions.RemoveEmptyEntries);
			foreach(string line in all_lines)
			{
				if (line.StartsWith("#") == true)
					continue;
				string[] line_data = line.Split(new char[]{'\t'}, StringSplitOptions.RemoveEmptyEntries);
				if (line_data.Length < 2)
					continue;
				for (int i = 0; i < line_data.Length; i++)
					line_data[i] = line_data[i].Trim();
				values.Add(new List<string>(line_data));  
			}
			return values;
		}
		
		public static WorkingData CheckLineData(Lines lines, Errors errors)
		{
			WorkingData values = new WorkingData();
			foreach (List<string> entry in lines)
			{
				int id;
				try
				{
					id = Int32.Parse(entry[0]);
				}
				catch(Exception)
				{
					errors.Add(String.Format("Ungueltige Substanz-ID: {0}", entry[0]));
					continue;
				}
				
				if (values.ContainsKey(id) == true)
				{
					errors.Add(String.Format("Substanz-ID {0} ist mehrmals vergeben", entry[0]));
					continue;
				}
				
				bool success = true;
				for (int i = 1; i < entry.Count; i++)
				{
					if (entry[i].Length > 75)
					{
						errors.Add(String.Format("Substanzname {0} ist zu lang (max 75 Zeichen).", entry[i]));
						success = false;
					}
				}
				
				if (success == false)
					continue;
				
				values.Add(id, new List<string>(entry.GetRange(1, entry.Count - 1)));
			}
			
			return values;
		}
		
		public static void CrossCheck(WorkingData default_data, WorkingData latin_data, Errors errors)
		{
			foreach(int id in default_data.Keys)
			{
				if (latin_data.ContainsKey(id) == false)
					errors.Add(String.Format("Latin-Namenslist enthält keinen Substanz-Schlüssel {0}.", id));
			}
			
			foreach(int id in latin_data.Keys)
			{
				if (default_data.ContainsKey(id) == false)
					errors.Add(String.Format("Dafault-Namensliste enthält keinen Substanz-Schlüssel {0}.", id));
			}
		}
		
		public static NameTable CreateNameTable(WorkingData default_names, WorkingData latin_names, Errors errors)
		{
			NameTable name_table = new NameTable();
			foreach (int default_id in default_names.Keys)
			{
				int main_id = -1;
				
				foreach(string default_name in default_names[default_id])
				{
					foreach(int compare_id in name_table.DefaultNames.Keys)
					{
						if (name_table.DefaultNames[compare_id].Contains(default_name) == true)
						{
							main_id = compare_id;
							break;	
						}
					}
				}
				
				if (main_id == -1)
				{
					name_table.DefaultNames.Add(default_id, new List<string>());
					foreach( string default_name_1 in default_names[default_id])
					{
						bool found = false;
						foreach( string default_name_2 in name_table.DefaultNames[default_id])
						{
							if (default_name_1.ToLower() == default_name_2.ToLower())
								found = true; 
						}
						if (found == false)
							name_table.DefaultNames[default_id].Add(default_name_1);
					}
					
					name_table.LatinNames.Add(default_id, new List<string>());
					foreach( string latin_name_1 in latin_names[default_id])
					{
						bool found = false;
						foreach( string latin_name_2 in name_table.LatinNames[default_id])
						{
							if (latin_name_1.ToLower() == latin_name_2.ToLower())
								found = true; 
						}
						if (found == false)
							name_table.LatinNames[default_id].Add(latin_name_1);
					}
					
					name_table.Aliase.Add(default_id, new List<int>());
					name_table.Aliase[default_id].Add(default_id);
				}
				else
				{
					name_table.Aliase[main_id].Add(default_id);
					foreach( string default_name_1 in default_names[default_id])
					{
						bool found = false;
						foreach( string default_name_2 in name_table.DefaultNames[main_id])
						{
							if (default_name_1.ToLower() == default_name_2.ToLower())
								found = true; 
						}
						if (found == false)
							name_table.DefaultNames[main_id].Add(default_name_1);
					}
					
					foreach( string latin_name_1 in latin_names[default_id])
					{
						bool found = false;
						foreach( string latin_name_2 in name_table.LatinNames[main_id])
						{
							if (latin_name_1.ToLower() == latin_name_2.ToLower())
								found = true; 
						}
						if (found == false)
							name_table.LatinNames[main_id].Add(latin_name_1);
					}
				}
			}
			
			foreach (int latin_id1 in name_table.LatinNames.Keys)
			{
				foreach (int latin_id2 in name_table.LatinNames.Keys)
				{
					if (latin_id1 == latin_id2)
						continue;
					
					foreach (string latin_name in name_table.LatinNames[latin_id1])
					{
						if (name_table.LatinNames[latin_id2].Contains(latin_name) == true)
						    errors.Add(String.Format("Latin-Name {0}: {1} <-> {2}.",
						               latin_name, latin_id1, latin_id2));
					}
				}
			}
			
			return name_table;
		}
		
		public static NotMeasurableData GetNotMeausrableSubstances(string file_name, Errors errors)
		{
			NotMeasurableData values = new NotMeasurableData();
			if (File.Exists(file_name) == false)
			{
				Console.WriteLine("Keine Datei {0} gefunden.", file_name);
				return values;
			}
			
			try
			{
				using(StreamReader sr = new StreamReader(file_name, Encoding.UTF8))
				{
					int line_counter = 1;
					string group_name = String.Empty;
					string line;
					while((line = sr.ReadLine()) != null)
					{
						line = line.Trim();
						line = line.Trim(new char[]{'\t'});
						if (line == string.Empty)
						{
							line_counter++;
							continue;
						}
						
						if (line.StartsWith("Substance-Group") == true)
						{
							string[] group_data = line.Split(new string[]{"::"},StringSplitOptions.RemoveEmptyEntries);
							if (group_data.Length != 2)
							{
								errors.Add(String.Format("Kann Substanzeintrag in Zeile {0} nicht auflösen", line_counter));
								group_name = String.Empty;
							}
							else
							{
								group_name = group_data[1];
								if (values.ContainsKey(group_name) == true)
								{
									errors.Add(String.Format("Gruppe {0} ist bereits definiert worden", group_name));
									group_name = String.Empty;
								}
								else
								{
									values.Add(group_name, new List<String>());
								}
							}
						}
						else
						{
							if (group_name == String.Empty)
							{
								errors.Add(String.Format("Eintrag in Zeile {0} kann keiner Gruppe zugeordent werden.", line_counter));
							}
							else
							{
								string[] substance_data = line.Split(new string[]{"::"},StringSplitOptions.RemoveEmptyEntries);
								if (substance_data.Length != 2)
								{
									errors.Add(String.Format("Ungültiger Substanzeintrag in Zeile {0}", line_counter));
								}
								else
								{
									values[group_name].Add(line);
								}
							}
						}
						line_counter++;
					}
				}
			}
			catch(Exception ex)
			{
				errors.Add(ex.Message);
				values = new NotMeasurableData();
			}
			
			List<string> remove_list = new List<string>();
			foreach(string entry in values.Keys)
			{
				if (values[entry].Count == 0)
					remove_list.Add(entry);
			}
			
			foreach (string entry in remove_list)
				values.Remove(entry);
			
			return values;	
		}
		
		public static void CheckNotMeasurableData(NotMeasurableData data, Errors errors)
		{
			foreach(string group_name in data.Keys)
			{
				Dictionary<string, int> substance_self_control_latin = new Dictionary<string, int>();
				Dictionary<string, int> substance_self_control_default = new Dictionary<string, int>();
				foreach(string entry in data[group_name])
				{
					string[] substance_data = entry.Split(new string[]{"::"},StringSplitOptions.RemoveEmptyEntries);
					string[] default_names = substance_data[0].Split(new string[]{"\t"},StringSplitOptions.RemoveEmptyEntries);
					string[] latin_names   = substance_data[1].Split(new string[]{"\t"},StringSplitOptions.RemoveEmptyEntries);
					foreach (string default_name in default_names)
					{
						if (substance_self_control_default.ContainsKey(default_name.ToLower()) == true)
						{
							errors.Add(String.Format("Der Default-Name {0} ist für die Gruppe {1} mehrmals definiert",
								                         default_name, group_name));
						}
						else
						{
							substance_self_control_default.Add(default_name.ToLower(),0);
						}
					}
					foreach (string latin_name in latin_names)
					{
						if (substance_self_control_latin.ContainsKey(latin_name.ToLower()) == true)
						{
							errors.Add(String.Format("Der Latin-Name {0} ist für die Gruppe {1} mehrmals definiert",
								                         latin_name, group_name));
						}
						else
						{
							substance_self_control_latin.Add(latin_name.ToLower(),0);
						}
					}
				}
			}
		}
	}
}
