using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Collections;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Reflection;
using System.Text;

using miew.ReadOnly;
using miew.Binding;
using miew.Reflection;
using agree;
using System.Collections.ObjectModel;

namespace agree.itsdb
{
	using Type = System.Type;

	///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
	/// <summary>
	/// 
	/// </summary>
	///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
	public interface IitsdbTable : IBindingList, ISysObj
	{
		Type ItsdbType { get; }

		void Load(String s_dir, IList<String> schema);

		void ValidateExternalKeys();
	};

	///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
	/// <summary>
	/// 
	/// </summary>
	///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
	public class ItsdbDatabase : List<IitsdbTable>, ISysObj
	{
		readonly static public Type[] ttypes = 
		{
			typeof(ItsdbItemTable),
			typeof(ItsdbAnalysisTable),
			typeof(ItsdbPhenomenonTable),
			typeof(ItsdbParameterTable),
			typeof(ItsdbSetTable),
			typeof(ItsdbItem_PhenomenonTable),
			typeof(ItsdbItem_SetTable),
			typeof(ItsdbRunTable),
			typeof(ItsdbParseTable),
			typeof(ItsdbResultTable),
			typeof(ItsdbRuleTable),
			typeof(ItsdbOutputTable),
			typeof(ItsdbEdgeTable),
			typeof(ItsdbTreeTable),
			typeof(ItsdbDecisionTable),
			typeof(ItsdbPreferenceTable),
			typeof(ItsdbUpdateTable),
			typeof(ItsdbFoldTable),
			typeof(ItsdbScoreTable),
		};

		public Dictionary<String, IitsdbTable> table_name_lookup = new Dictionary<String, IitsdbTable>(StringComparer.OrdinalIgnoreCase);

		public IList<IitsdbTable> Tables { get { return this; } }

		public ItsdbItemTable ItemTable { get { return (ItsdbItemTable)table_name_lookup["item"]; } }
		public ItsdbAnalysisTable AnalysisTable { get { return (ItsdbAnalysisTable)table_name_lookup["analysis"]; } }
		public ItsdbParseTable ParseTable { get { return (ItsdbParseTable)table_name_lookup["parse"]; } }
		public ItsdbResultTable ResultsTable { get { return (ItsdbResultTable)table_name_lookup["result"]; } }
		public ItsdbRunTable RunTable { get { return (ItsdbRunTable)table_name_lookup["run"]; } }

		public String SourceDirectory { get { return s_dir; } }

		String s_dir;
		ISysObj so;

		///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
		/// <summary>
		/// 
		/// </summary>
		///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
		public IitsdbTable TableFromAnonymousItems(String name, String description, IEnumerable data)
		{
			Object prototype = data.OfType<Object>().First();

			var pi = AnonymousTypePromoter.GetPromotionInfo(
						name, 
						prototype,
						new String[] { "System.ComponentModel", "agree.itsdb" },
						new String[] { "System.dll", Assembly.GetAssembly(typeof(ItsdbItemType)).Location },
						new Type[] { typeof(ItsdbItemType) });

			Type T_tab = typeof(ItsdbTable<>).MakeGenericType(new Type[] { pi.Type });

			IitsdbTable t = (IitsdbTable)Activator.CreateInstance(T_tab, this, name, description);

			foreach (Object o in data)
				t.Add(pi.Promote(o));

			this.Add(t);
			return t;
		}


		///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
		/// <summary>
		/// Construct an Itsdb database over the specified directory
		/// </summary>
		///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
		public ItsdbDatabase(ISysObj so, String s_dir)
			:base(ttypes.Length)
		{
			this.so = so;
			this.s_dir = s_dir;

			Object[] ctor_args = new Object[] { this };
			for (int i = 0; i < ttypes.Length; i++)
			{
				IitsdbTable t = (IitsdbTable)Activator.CreateInstance(ttypes[i], ctor_args);
				this.Add(t);
				table_name_lookup.Add(t.SysObjName, t);
			}

			String relations_file = Path.Combine(s_dir, "relations");
			if (!File.Exists(relations_file))
			{
				String msg = String.Format("The filename 'relations' could not be found in the directory '{0}'", s_dir);
				throw new FileNotFoundException(msg, "relations");
			}

			List<String> schema = new List<String>();
			IitsdbTable cur = null;

			foreach (String _l in File.ReadAllLines(relations_file))
			{
				String l = _l;
				int ix = l.IndexOf('#');
				if (ix != -1)
					l = l.Remove(ix);
				l = l.Trim();
				if (l == String.Empty)
					continue;

				String[] rgs = l.Split(default(Char[]), StringSplitOptions.RemoveEmptyEntries);
				if (rgs.Length == 1)
				{
					if (cur != null)
					{
						cur.Load(s_dir, schema);
						schema.Clear();
					}

					String s_tab = rgs[0].Trim(':');
					if (!table_name_lookup.TryGetValue(s_tab, out cur))
					{
						String msg = String.Format("The relations file refers to an unknown table type '{0}'", s_tab);
						throw new Exception(msg);
					}
				}
				else
					schema.Add(rgs[0]);
			}

			/// do the last table
			if (cur != null)
				cur.Load(s_dir, schema);

			/// now that all tables are loaded, validate external keys
			//foreach (IitsdbTable t in tables)
			//    t.ValidateExternalKeys();
		}

		public IitsdbTable GetTableFromItemType(Type t)
		{
			return this.First(tt => tt.ItsdbType == t);
		}

		public string SysObjName
		{
			get { return s_dir; }
		}

		public string SysObjDescription
		{
			get { return String.Format("[incr tsdb()] database : {0}", s_dir); }
		}

		public IReadOnlyDictionary<String, ISysObj> SysObjChildren
		{
			get { return new CovariantDictionaryWrapper<String, ISysObj, IitsdbTable>(table_name_lookup); }
		}

		public ISysObj SysObjParent
		{
			get { return so; }
		}
	};

	/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
	///// <summary>
	///// 
	///// </summary>
	/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
	//public class ItsdbJoinedTable<T> : ListItemBindHelper<T>, IitsdbJoinedTable
	//{
	//    ItsdbDatabase db;
	//    String name;
	//    String description;
	//    Type T_item;

	//    public ItsdbJoinedTable(ItsdbDatabase db, String name, String description, IEnumerable<T> data)
	//        :base(data)
	//    {
	//        //data.OfType<Object>().ToList()
	//        this.db = db;
	//        this.name = name;
	//        this.description = description;
	//        //this.T_item = this.Count > 0 ? this[0].GetType() : typeof(Object);
	//    }

	//    public string SysObjName
	//    {
	//        get { return name; }
	//    }

	//    public string SysObjDescription
	//    {
	//        get { return description; }
	//    }

	//    public IReadOnlyDictionary<String, ISysObj> SysObjChildren
	//    {
	//        get { return SysObjHelper<ISysObj>.Empty; }
	//    }

	//    public ISysObj SysObjParent
	//    {
	//        get { return db; }
	//    }

	//    public Type ItsdbType
	//    {
	//        get { return T_item; }
	//    }
	//};

	///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
	/// <summary>
	/// Template class for table of Itsb items of some strong-type
	/// </summary>
	///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
	public class ItsdbTable<T> : BindingList<T>, IitsdbTable where T : ItsdbItemType
	{
		public ItsdbDatabase db;
		String name;
		String description;

		public ItsdbTable(ItsdbDatabase db)
		{
			this.db = db;
		}

		public ItsdbTable(ItsdbDatabase db, String name, String description)
			:this(db)
		{
			this.name = name;
			this.description = description;
		}

		public Type ItsdbType { get { return typeof(T); } }

		/// <summary>
		/// Using the ordered list of fields specified in 'schema,' load the table with data from a plaintext or gzip 
		/// file with the table's matching name, if any is found in the specified directory.
		/// </summary>
		public void Load(String s_dir, IList<String> schema)
		{
			var fields = schema.Select(s =>
			{
				FieldInfo fi = typeof(T).GetField("_" + s.Replace('-', '_'), BindingFlags.Instance | BindingFlags.NonPublic);
				if (fi == null)
				{
					String msg = String.Format("The field '{0}' in the schema for '{1}' is not recognized.", s, SysObjName);
					throw new Exception(msg);
				}
				return fi;
			}).ToArray();

			/// See if there's a data file for this table
			String db_file = Path.Combine(s_dir, SysObjName);
			Stream str;
			if (!File.Exists(db_file))
			{
				db_file += ".gz";
				if (!File.Exists(db_file))
					return;
			}

			/// If the data file is compressed, switch the stream to a gzip decoder
			str = File.Open(db_file, FileMode.Open, FileAccess.Read, FileShare.Read);
			if (db_file.EndsWith(".gz"))
				str = new GZipStream(str, CompressionMode.Decompress);

			/// Read lines from the data file into the table
			bool f_gave_warning = false;
			using (StreamReader sr = new StreamReader(str, Encoding.UTF8))
			{
				int i_l = 0;
				String line;
				while ((line = sr.ReadLine()) != null)
				{
					i_l++;
					/// Split the line of raw data into parts
					String[] data = line.Replace(@"\\", @"\").Split('@');
					if (data.Length > fields.Length)
					{
						String msg = String.Format("Number of data items does not match schema in file '{0}', line {1}", db_file, i_l);
						throw new Exception(msg);
					}
					else if (!f_gave_warning && data.Length < fields.Length)
					{
						//Console.WriteLine("warning: Number of data fields ({0}) is less than the number of fields in the schema ({1}) in file '{2}'", data.Length, fields.Length, db_file);
						f_gave_warning = true;
					}

					/// Create an item of the appropriate type and load it with the data from each field
					T o_item = Activator.CreateInstance<T>();
					for (int i = 0; i < data.Length; i++)
					{
						FieldInfo fi = fields[i];
						String d = data[i];

						/// Convert the string data into a strongly typed object of the appropriate type
						Object o_data;
						if (fi.FieldType == typeof(long))
						{
							long il;
							if (d == String.Empty)
								il = 0;
							else if (!long.TryParse(d, out il))
							{
								String msg = String.Format("Invalid long integer value '{0}' in file '{1}', line {2}", d, db_file, i_l);
								throw new Exception(msg);
							}
							o_data = il;
						}
						else if (fi.FieldType == typeof(int))
						{
							int ii;
							if (d == String.Empty)
								ii = 0;
							else if (!int.TryParse(d, out ii))
							{
								uint ui;
								if (!uint.TryParse(d, out ui))
								{
									String msg = String.Format("Invalid integer value '{0}' in file '{1}', line {2}", d, db_file, i_l);
									throw new Exception(msg);
								}
								ii = (int)ui;
							}
							o_data = ii;
						}
						else if (fi.FieldType == typeof(string))
							o_data = d;
						else if (fi.FieldType == typeof(DateTime))
						{
							String sd = new String(d.Where(ch => ch != '(' && ch != ')').ToArray());
							DateTime dt;
							if (sd == String.Empty)
								dt = default(DateTime);
							else if (!DateTime.TryParse(sd, CultureInfo.GetCultureInfo("de-DE"), DateTimeStyles.None, out dt))
							{
								String msg = String.Format("Unrecognized date/time format '{0}' in file '{1}', line {2}", d, db_file, i_l);
								throw new Exception(msg);
							}
							o_data = dt;
						}
						else
							throw new Exception();

						/// Set the field's value into the item
						fi.SetValue(o_item, o_data);
					}
					/// Add the item to the table
					this.Add(o_item);
				}
			}
			str.Dispose();

			/// setup primary keys, if any
			foreach (FieldInfo pkf in typeof(T).GetFields(BindingFlags.Instance | BindingFlags.NonPublic)
											.Where(fi => fi.GetCustomAttributes(true).Any(o => o is PrimaryKeyAttribute)))
			{
				if (pkf.FieldType != typeof(int))
					throw new Exception("primary key must be an integer");

				FieldInfo map_field = GetType().GetField(pkf.Name + "_map");
				Dictionary<int, T> map = (Dictionary<int, T>)map_field.GetValue(this);

#if DUPLICATE_KEY_INFO
				Dictionary<int, int> repeated_keys = new Dictionary<int, int>();
#endif
				foreach (T t in this)
				{
					int id = (int)pkf.GetValue(t);
					if (map.ContainsKey(id))
					{
#if DUPLICATE_KEY_INFO
						if (repeated_keys.ContainsKey(id))
						    repeated_keys[id]++;
						else
						    repeated_keys.Add(id, 2);
#endif
					}
					else
						map.Add(id, t);
				}
#if DUPLICATE_KEY_INFO
				if (repeated_keys.Count > 0)
				{
				    Console.WriteLine("warning: field '{0}' in table '{1}' has duplicate primary key value(s):", pkf.Name, db_file);
				    foreach (var kvp in repeated_keys.OrderBy(k => k.Key))
				        Console.WriteLine("\tvalue: {0} ({1} instances)", kvp.Key, kvp.Value);
				}
#endif
			}
		}

		///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
		/// <summary>
		/// validate external keys via reflection
		/// </summary>
		///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
		public void ValidateExternalKeys()
		{
			/// Process all fields in this table's item type which are marked with the 'ExternalKey' attribute
			foreach (var fa in typeof(T)
								.GetFields(BindingFlags.Instance | BindingFlags.NonPublic)
								.SelectMany(fi_this => fi_this.GetCustomAttributes(true)
															.OfType<ExternalKeyAttribute>()
															.Select(attr => new { fi_this, attr })))
			{
				/// Common name of the shared key
				String s_key = fa.fi_this.Name;

				/// type of external item which introduces the primary key
				Type ext_item_type = fa.attr.ti_ext;

				/// find a matching field name in the external item
				FieldInfo fi_ext = ext_item_type.GetField(s_key, BindingFlags.Instance | BindingFlags.NonPublic);
				if (fi_ext == null || fi_ext.GetCustomAttributes(typeof(PrimaryKeyAttribute), true).Length == 0)
				{
					String msg = String.Format("Couldn't resolve primary key '{0}' in table '{1}' referenced by external key in table '{2}'",
						s_key, ext_item_type, SysObjName);
					throw new Exception(msg);
				}

				/// get external table
				IitsdbTable ext_table = db.GetTableFromItemType(ext_item_type) as IitsdbTable;
				if (ext_table == null)
					continue;

				/// get external table type
				Type ext_table_type = ext_table.GetType();

				/// Get the map out of the external table
				Object map = ext_table_type.GetField(s_key + "_map", BindingFlags.Instance | BindingFlags.NonPublic)
												.GetValue(ext_table);

				/// find the 'ContainsKey' method for a generic dictionary containing the external item as a value
				MethodInfo mi_ContainsKey = typeof(Dictionary<,>)
												.MakeGenericType(new Type[] { typeof(int), ext_item_type })
												.GetMethod("ContainsKey");

				/// Check each item in this table
				HashSet<int> bad_ids = new HashSet<int>();
				foreach (T t in this)
				{
					/// get the secondary key value specified by an item
					int id = (int)fa.fi_this.GetValue(t);

					/// See if the primary contains the value
					if (!(bool)mi_ContainsKey.Invoke(map, new Object[] { id }))
						bad_ids.Add(id);
				}

				if (bad_ids.Count > 0)
				{
					Console.WriteLine();
					String msg = String.Format("The following value(s) specified for field '{0}' in table '{1}' do not match any value of that field in primary key table '{2}':",
						s_key.Substring(1).Replace('_', '-'),
						SysObjName,
						ext_table.SysObjName);
					Console.WriteLine(msg);
					Console.WriteLine(String.Join(", ", bad_ids.OrderBy(_i => _i)));
				}
			}
		}

		public String SysObjName
		{
			get { return typeof(T).Name.Replace("Itsdb", String.Empty).Replace('_', '-').ToLower(); }
		}

		public string SysObjDescription
		{
			get { return String.Format("{0} - {1}", db.SysObjName, SysObjName); }
		}

		public IReadOnlyDictionary<string, ISysObj> SysObjChildren
		{
			get { return SysObjHelper<ISysObj>.Empty; }
		}

		public ISysObj SysObjParent
		{
			get { return db; }
		}
	};

	///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
	///
	/// Itsdb table types follow 
	///
	///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

	public class ItsdbItemTable : ItsdbTable<ItsdbItem>
	{
		public ItsdbItemTable(ItsdbDatabase db) : base(db) { }

		public Dictionary<int, ItsdbItem> _i_id_map = new Dictionary<int, ItsdbItem>();
	};

	public class ItsdbAnalysisTable : ItsdbTable<ItsdbAnalysis>
	{
		public ItsdbAnalysisTable(ItsdbDatabase db) : base(db) { }

	};

	public class ItsdbPhenomenonTable : ItsdbTable<ItsdbPhenomenon>
	{
		public ItsdbPhenomenonTable(ItsdbDatabase db) : base(db) { }

		public Dictionary<int, ItsdbPhenomenon> _p_id_map = new Dictionary<int, ItsdbPhenomenon>();
	};

	public class ItsdbParameterTable : ItsdbTable<ItsdbParameter>
	{
		public ItsdbParameterTable(ItsdbDatabase db) : base(db) { }

	};

	public class ItsdbSetTable : ItsdbTable<ItsdbSet>
	{
		public ItsdbSetTable(ItsdbDatabase db) : base(db) { }

		public Dictionary<int, ItsdbSet> _s_id_map = new Dictionary<int, ItsdbSet>();
	};

	public class ItsdbItem_PhenomenonTable : ItsdbTable<ItsdbItem_Phenomenon>
	{
		public ItsdbItem_PhenomenonTable(ItsdbDatabase db) : base(db) { }

		public Dictionary<int, ItsdbItem_Phenomenon> _ip_id_map = new Dictionary<int, ItsdbItem_Phenomenon>();
	};

	public class ItsdbItem_SetTable : ItsdbTable<ItsdbItem_Set>
	{
		public ItsdbItem_SetTable(ItsdbDatabase db) : base(db) { }

	};

	public class ItsdbRunTable : ItsdbTable<ItsdbRun>
	{
		public ItsdbRunTable(ItsdbDatabase db) : base(db) { }

		public Dictionary<int, ItsdbRun> _run_id_map = new Dictionary<int, ItsdbRun>();
	};

	public class ItsdbParseTable : ItsdbTable<ItsdbParse>
	{
		public ItsdbParseTable(ItsdbDatabase db) : base(db) { }

		public Dictionary<int, ItsdbParse> _parse_id_map = new Dictionary<int, ItsdbParse>();
	};

	public class ItsdbResultTable : ItsdbTable<ItsdbResult>
	{
		public ItsdbResultTable(ItsdbDatabase db) : base(db) { }

		public Dictionary<int, ItsdbResult> _result_id_map = new Dictionary<int, ItsdbResult>();
	};

	public class ItsdbRuleTable : ItsdbTable<ItsdbRule>
	{
		public ItsdbRuleTable(ItsdbDatabase db) : base(db) { }

	};

	public class ItsdbOutputTable : ItsdbTable<ItsdbOutput>
	{
		public ItsdbOutputTable(ItsdbDatabase db) : base(db) { }

	};

	public class ItsdbEdgeTable : ItsdbTable<ItsdbEdge>
	{
		public ItsdbEdgeTable(ItsdbDatabase db) : base(db) { }

		public Dictionary<int, ItsdbEdge> _e_id_map = new Dictionary<int, ItsdbEdge>();
	};

	public class ItsdbTreeTable : ItsdbTable<ItsdbTree>
	{
		public ItsdbTreeTable(ItsdbDatabase db) : base(db) { }

	};

	public class ItsdbDecisionTable : ItsdbTable<ItsdbDecision>
	{
		public ItsdbDecisionTable(ItsdbDatabase db) : base(db) { }

	};

	public class ItsdbPreferenceTable : ItsdbTable<ItsdbPreference>
	{
		public ItsdbPreferenceTable(ItsdbDatabase db) : base(db) { }

	};

	public class ItsdbUpdateTable : ItsdbTable<ItsdbUpdate>
	{
		public ItsdbUpdateTable(ItsdbDatabase db) : base(db) { }

	};

	public class ItsdbFoldTable : ItsdbTable<ItsdbFold>
	{
		public ItsdbFoldTable(ItsdbDatabase db) : base(db) { }

		public Dictionary<int, ItsdbFold> _f_id_map = new Dictionary<int, ItsdbFold>();
	};

	public class ItsdbScoreTable : ItsdbTable<ItsdbScore>
	{
		public ItsdbScoreTable(ItsdbDatabase db) : base(db) { }

		public Dictionary<int, ItsdbScore> _score_id_map = new Dictionary<int, ItsdbScore>();
	};

}