using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Timers;
using System.Text;
using Medivators.BusinessObjects;
using Medivators.Common;
using Medivators.Common.Enums;
using Medivators.Common.Logging;
using Medivators.Common.Settings;
using Medivators.Data;
using Medivators.Data.Queries;

namespace Medivators.Parser
{
	public class CacheManager
	{
		private DateTime _lastHeartbeatMessage = DateTime.Now;
		private DateTime _lastChunkGrab = DateTime.Now;

		//	stores query references when launched, including ID, 
		//	so that they can be unwired and disposed when
		//	they are complete
		private Hashtable _queries = new Hashtable();

		private Timer _timer = null;
		private int _commitInterval = 0;
		private int _commitGranularity = 0;


		public bool IsRunning
		{
			get { return _timer != null && _timer.Enabled == true; }
		}

		#region singleton constructor

		private static CacheManager _instance = null;

		private CacheManager()
		{
			_commitInterval = Convert.ToInt32(TextSettings.GetInstance()[SettingTypes.Application, "CommitInterval"]);
			_commitGranularity = Convert.ToInt32(TextSettings.GetInstance()[SettingTypes.Application, "CommitGranularity"]);
		}
		public static CacheManager GetInstance()
		{
			if (_instance == null)
				_instance = new CacheManager();
			return _instance;
		}

		#endregion

		#region adding records and reports from the device to the cache

		public bool CacheDeviceEvent(IDeviceRecord Record)
		{
			//	method examines committed and uncommitted caches 
			//	to see if this record is new, changed from the 
			//	previous version, or identical to previous version.
			//
			//	when new or changed, record is added to the 
			//	uncommitted cache for later insertion into the
			//  database. When identical, the record is discarded
			bool itemAdded = false;

			try
			{
				Log.GetInstance().WriteLog(LogEntryType.DebugTrace, "Caching record " + Record.ToString());
				LogRecordCacheItem item = new LogRecordCacheItem(Record);
				itemAdded = CacheAdd(
									(ICacheItem)item,
									UncommittedLogRecordCache.GetInstance(),
									CommittedLogRecordCache.GetInstance());
			}
			catch (Exception ex)
			{
				ErrorHandler.GetInstance().LogException(LogEntryType.Critical, "CacheManager.CacheDeviceEvent error: " + ex.ToString());
			}
			return itemAdded;
		}
		public bool CacheDeviceReport(CycleReport Report)
		{
			bool itemAdded = false;
			try
			{
				CycleReportCacheItem item = new CycleReportCacheItem(Report);
				itemAdded = CacheAdd(
									(ICacheItem)item,
									UncommittedCycleReportCache.GetInstance(),
									CommittedCycleReportCache.GetInstance());
			}
			catch (Exception ex)
			{
				ErrorHandler.GetInstance().LogException(LogEntryType.Critical, "CacheManager.CacheDeviceReport error: " + ex.ToString());
				itemAdded = false;
			}
			return itemAdded;
		}

		private bool CacheAdd(ICacheItem Item, Cache UncommittedCache, Cache CommittedCache)
		{
			// search caches
			SearchResult searchOld = CommittedCache.Search(Item);
			SearchResult searchNew = UncommittedCache.Search(Item);

			switch (searchOld.Result)
			{
				case SearchResult.Results.Exists:
					//	if the record is already in the database 
					//	unchanged, we're done
					Log.GetInstance().WriteLog(LogEntryType.DebugTrace, "Record exists in Committed queue. Discarding: " + Item.ToString());
					return false;

				case SearchResult.Results.Changed:
					//	if the committed version of the record is 
					//	different, delete it and allow it to 
					//	be overwritten with the new version

					Log.GetInstance().WriteLog(LogEntryType.Normal,
						string.Format("Record changed since committed. New record ({0}): Old record ({1})",
							Item.ToString(),
							CommittedCache.Values[searchOld.Index].ToString()),
							System.Diagnostics.EventLogEntryType.Warning);

					CommittedCache.RemoveAt(searchOld.Index);
					break;
			}

			//	if the record is already queued up to be committed, we're done
			if (searchNew.Result == SearchResult.Results.Exists)
			{
				Log.GetInstance().WriteLog(LogEntryType.DebugTrace, "Record exists in Uncommitted queue. Discarding: " + Item.ToString());
				return false;
			}
			//	add or overwrite record to the uncommitted cache
			Log.GetInstance().WriteLog(LogEntryType.DebugTrace, "New record. Adding to Uncommitted queue: " + Item.ToString());
			UncommittedCache[Item.Key] = Item;
			return true;
		}
		#endregion

		#region database query events

		private void query_SelectComplete(object sender, SelectCompleteEventArgs e)
		{
			//	a select statement has returned data. We need to compare
			//	these values to the records in cache and possibly, update the 
			//	database version to match the cached version. 
			try
			{
				Log.GetInstance().WriteLog(
					LogEntryType.DebugTrace,
					"CacheManager.SelectComplete reports " +
					sender.GetType().Name +
					"(" + e.DataOperationID.ToString() + ")" +
					" reports select complete. " +
					"Rows=(" + DataMapCaster.Cast(e.Data).Count + ")",
					EventLogEntryType.Information);

				//	step 1: acquire all records expecting this Select 
				Cache cachedData = GetCacheByQuery((AsynchronousExecute)sender);
				List<ICacheItem> items = cachedData.GetByPendingOperationID(e.DataOperationID);

				//  step 2: clear this pending operation from all Cached data 
				cachedData.ResetPendingOperationID(e.DataOperationID);

				//	step 3: see if the cached records also existed in the data
				//			just received from the database

				if (items != null)
				{
					foreach (ICacheItem item in items)
					{
						if (item == null)
							Log.GetInstance().WriteLog(
								"Unable to locate Item by pending operation ID " +
								"(" + e.DataOperationID + ")" +
								" in query_SelectComplete ! )",
								System.Diagnostics.EventLogEntryType.Warning);
						else
						{
							bool found = false;
							bool equal = false;
							foreach (IBusinessObject bObj in DataMapCaster.Cast(e.Data))
							{
								if (bObj != null)
								{
									if (item.Key.Equals(bObj.Key))
									{
										found = true;
										//	compare records to see if they are the same
										equal = Compare(item, bObj);
										break;
									}
								}
							}
							//	step 4: insert or update into the database, or if record
							//			matches db version, set DBVerified flag on the
							//			the cache item so we don't go get it from the db 
							//			again later
							if (!found)
							{
								//	if cached version does not exist in the database,
								//	insert it
								IInsertQuery qry = GetInsertQuery(cachedData[item.Key]);
								cachedData[item.Key].PendingOperationID = qry.DataOperationID;
								qry.Insert();
							}
							else if (!equal)
							{
								//	per requirements, newly read records
								//	always overwrite persisted records
								IUpdateQuery qry = GetUpdateQuery(cachedData[item.Key]);
								cachedData[item.Key].PendingOperationID = qry.DataOperationID;
								qry.Update();
							}
							else
								//  record exists and matches new db version.
								//	set db verified flat
								cachedData[item.Key].DBVerified = true;
						}
					}
				}
			}
			catch (Exception ex)
			{
				ErrorHandler.GetInstance().LogException("CacheManager.query_SelectComplete error: ", ex);
			}
			finally
			{
				DisposeQuery(e.DataOperationID);
			}
		}
		private void query_DataException(object sender, DataExceptionEventArgs e)
		{
			//	clear any records that were expecting this 
			//	operation to succeed. Hopefully we'll get
			//	it right the next time the device transmits 
			//	this record. 

			ErrorHandler.GetInstance().LogException(
				"CacheManager.DataException reported by " +
				sender.GetType().Name +
				" Operation: " + e.DataOperationID.ToString() +
				" SQL: " + e.SQL +
				" Error: " + e.Ex.ToString());
			try
			{
				GetCacheByQuery((AsynchronousExecute)sender).RemoveByPendingOperationID(e.DataOperationID);
			}
			catch (Exception ex)
			{
				ErrorHandler.GetInstance().LogException("CacheManager.query_DataException error:", ex);
			}
			DisposeQuery(e.DataOperationID);
		}
		private void query_InsertComplete(object sender, InsertCompleteEventArgs e)
		{
			try
			{
				Log.GetInstance().WriteLog(
					LogEntryType.DebugTrace,
					"CacheManager.InsertComplete reports " +
					sender.GetType().Name +
					"(" + e.DataOperationID.ToString() + ")" +
					" reports insert/update complete",
					EventLogEntryType.Information);

				Cache cachedData = GetCacheByQuery((AsynchronousExecute)sender);
				if (e.RowsAffected > 0)
					cachedData.ResetPendingOperationID(e.DataOperationID, true);
				else
				{
					//	inserts/updates returning zero rows represent an exception
					ErrorHandler.GetInstance().LogException(
						"CacheManager.InsertComplete returned no rows. " +
						" Operation: " + e.DataOperationID.ToString() +
						" SQL: " + e.SQL);
					//	clear any records that were expecting this 
					//	operation to succeed
					cachedData.RemoveByPendingOperationID(e.DataOperationID);
				}
			}
			catch (Exception ex)
			{
				ErrorHandler.GetInstance().LogException("CacheManager.query_InsertComplete error:", ex);
			}
			finally
			{
				DisposeQuery(e.DataOperationID);
			}
		}

		#endregion

		#region cache timer and timer control

		public void Start()
		{
			Log.GetInstance().WriteLog(
				LogEntryType.DebugTrace,
				"Cache Timer Start",
				EventLogEntryType.Information);

			if (_timer == null)
			{
				_timer = new Timer();
				_timer.Interval = this._commitInterval;
				_timer.Elapsed += new ElapsedEventHandler(CacheTimer_Elapsed);
			}
			_timer.Enabled = true;
		}
		public void Stop()
		{
			Log.GetInstance().WriteLog(
				LogEntryType.DebugTrace,
				"Cache Timer Stop",
				EventLogEntryType.Information);
			if (_timer != null)
				_timer.Enabled = false;
		}
		private void CacheTimer_Elapsed(object sender, ElapsedEventArgs e)
		{
			//	only log a heartbeat pulse every 30 seconds or so
			if (DateTime.Compare(DateTime.Now, _lastHeartbeatMessage.AddSeconds(30.0)) >= 0)
			{
				Log.GetInstance().WriteLog(
					LogEntryType.DebugTrace,
					"Cache Timer Elapsed",
					EventLogEntryType.Information);
				_lastHeartbeatMessage = DateTime.Now;
			}
			try
			{
				ProcessCache();
			}
			catch (Exception ex)
			{
				ErrorHandler.GetInstance().LogException("CacheManager_TimerElapsed", ex);
					
			}
		}

		#endregion

		public void ProcessCache()
		{

			// **
			// **	grab and process N logrecords (N = setting)
			// **	from uncommitted caches and move to committed
			// **

			List<ICacheItem> listRecords = null;

			// try to grab in chunks of (N)
			if (UncommittedLogRecordCache.GetInstance().Count >= this._commitGranularity)
				listRecords = UncommittedLogRecordCache.GetInstance().GetHeadItems(this._commitGranularity);

			// but even if there are not (N) records, can't leave orphans
			// so enforce clean up every two minutes or so
			else if (_lastChunkGrab.AddMinutes(2).CompareTo(DateTime.Now) <= 0 )
				listRecords = UncommittedLogRecordCache.GetInstance().GetHeadItems(UncommittedLogRecordCache.GetInstance().Count);


			if (listRecords != null)
			{
				_lastChunkGrab = DateTime.Now;
				foreach (LogRecordCacheItem item in listRecords)
					//	move into committed list
					CommittedCacheAdd(item);
			}

			//	 grab and process 1 Cycle Report (and only 1!)
			if (UncommittedCycleReportCache.GetInstance().Count > 0)
			{
				//	move into committed list
				List<ICacheItem> listReports = UncommittedCycleReportCache.GetInstance().GetHeadItems(1);
				foreach (CycleReportCacheItem item in listReports)
					//	move into committed list
					CommittedCacheAdd(item);
			}
			
			// **
			// **	now look for any required database activity in committed caches
			// **

			// search log record cache for unverified records
			List<ICacheItem> commitList = CommittedLogRecordCache.GetInstance().GetUnverifiedItems(this._commitGranularity);
			
			//	wait until we have several records before attempting to synch
			if (commitList != null && commitList.Count >= this._commitGranularity)
			{
				// init and launch select query to acquire database version
				// of returned unverified records
				SelectLogRecords qry = new SelectLogRecords();
				WireQueryEvents(qry);
				this._queries.Add(qry.DataOperationID, qry);

				string[] keys = new string[commitList.Count];
				int i = 0;
				foreach (ICacheItem item in commitList)
				{
					CommittedLogRecordCache.GetInstance()[item.Key].PendingOperationID = qry.DataOperationID;
					keys[i++] = item.Key;
				}
				qry.SelectByKeys(keys);
			}

			// search cycle report cache for unverified reports ( but only one )
			commitList = CommittedCycleReportCache.GetInstance().GetUnverifiedItems(1);
			if (commitList != null && commitList.Count > 0)
			{
				// init and launch select query to acquire database version
				// of returned unverified records

				SelectCycleReports qry = new SelectCycleReports();
				WireQueryEvents(qry);
				this._queries.Add(qry.DataOperationID, qry);
				CommittedCycleReportCache.GetInstance()[commitList[0].Key].PendingOperationID = qry.DataOperationID;
				qry.SelectByKey(commitList[0].Key);
			}
		}

		#region private helpers

		private void CommittedCacheAdd(ICacheItem Item)
		{
			//	Method searches the commmitted cache for the passed
			//	item, and then adds the item to the cache, depending
			//	on the results of the search.
			//
			//	Note that no db activity is initiated here. 
			//  the Timer loop initiates database activity 
			//  automatically 


			try
			{
				Cache committed = null;
				Cache uncommitted = null;
				IBusinessObject bObj = null;
				string itemType = string.Empty;

				if (Item.GetType().Equals(typeof(LogRecordCacheItem)))
				{
					committed = CommittedLogRecordCache.GetInstance();
					uncommitted = UncommittedLogRecordCache.GetInstance();
					bObj = (IBusinessObject)((LogRecordCacheItem)Item).Record;
					itemType = "LogRecord";
				}
				else if (Item.GetType().Equals(typeof(CycleReportCacheItem)))
				{
					committed = CommittedCycleReportCache.GetInstance();
					uncommitted = UncommittedCycleReportCache.GetInstance();
					bObj = (IBusinessObject)((CycleReportCacheItem)Item).Report;
					itemType = "CycleReport";
				}

				SearchResult result = committed.Search(Item);
				switch (result.Result)
				{
					case SearchResult.Results.Changed:
						//	if the new value is different from the
						//	old value, overwrite the old with
						//	the new in the cache 
						Log.GetInstance().WriteLog(LogEntryType.Normal,
							string.Format(itemType + " Changed: Old value ({0}), new value ({1})",
							committed[Item.Key].ToString(),
							bObj.ToString()));
						committed[Item.Key] = Item;
						committed[Item.Key].DBVerified = false;
						break;
					case SearchResult.Results.DbOperationPending:
						// put back into uncommitted to await a time when
						// there is not already a db update underway for 
						// this record
						uncommitted.Add(Item.Key, Item);
						break;
					case SearchResult.Results.Exists:
						// nothing to do if the old record already exists.
						// and is equal to the new record
						break;
					case SearchResult.Results.New:
						Log.GetInstance().WriteLog(LogEntryType.DebugTrace,
							string.Format(itemType + "New detected: value ({0})",
							bObj.ToString()));
						committed[Item.Key] = Item;
						committed[Item.Key].DBVerified = false;
						break;
				}
			}
			catch (Exception ex)
			{
				ErrorHandler.GetInstance().LogException("CacheManager.CommittedCacheAddRecord error: ", ex);
			}
		}
		private bool Compare(ICacheItem Item, IBusinessObject BusinessObject)
		{
			bool rtnVal = false;
			try
			{
				if (Item == null || BusinessObject == null)
					rtnVal = false;
				else
					if (Item.GetType().Equals(typeof(CycleReportCacheItem)))

						rtnVal = ((CycleReportCacheItem)Item).Report.Equals(BusinessObject);
					else
						rtnVal = Item.ToString().Equals(BusinessObject.ToString());
			}
			catch
			{
				rtnVal = false;
			}
			return rtnVal;
		}
		private Cache GetCacheByQuery(AsynchronousExecute query)
		{
			//TODO: augment as new queries are added
			if (query.GetType().Equals(typeof(InsertCycleReport)) ||
				 query.GetType().Equals(typeof(InsertCycleReportLogRecord)) ||
				 query.GetType().Equals(typeof(UpdateCycleReport)) ||
				 query.GetType().Equals(typeof(SelectCycleReports)))
			{
				return CommittedCycleReportCache.GetInstance();
			}
			else
				return CommittedLogRecordCache.GetInstance();
		}
		private IInsertQuery GetInsertQuery(ICacheItem Item)
		{
			IInsertQuery qry = null;
			if (Item.GetType().Equals(typeof(LogRecordCacheItem)))
			{
				IDeviceRecord data = ((LogRecordCacheItem)Item).Record;
				qry = new InsertLogRecord(data);
			}
			else if (Item.GetType().Equals(typeof(CycleReportCacheItem)))
			{
				CycleReport data = ((CycleReportCacheItem)Item).Report;
				qry = new InsertCycleReport(data);
				ProcessCycleReportSupportRecords(data);
			}

			WireQueryEvents((AsynchronousExecute)qry);
			this._queries.Add(qry.DataOperationID, qry);
			return qry;
		}
		private IUpdateQuery GetUpdateQuery(ICacheItem Item)
		{
			IUpdateQuery qry = null;
			if (Item.GetType().Equals(typeof(LogRecordCacheItem)))
			{
				IDeviceRecord data = ((LogRecordCacheItem)Item).Record;
				qry = new UpdateLogRecord(data);
			}
			else if (Item.GetType().Equals(typeof(CycleReportCacheItem)))
			{
				CycleReport data = ((CycleReportCacheItem)Item).Report;
				qry = new UpdateCycleReport(data);
				ProcessCycleReportSupportRecords(data);
			}

			WireQueryEvents((AsynchronousExecute)qry);
			this._queries.Add(qry.DataOperationID, qry);
			return qry;
		}
		private void WireQueryEvents(AsynchronousExecute Query)
		{
			Query.DataException += new DataExceptionEventHandler(query_DataException);
			Query.InsertComplete += new InsertCompleteEventHandler(query_InsertComplete);
			Query.SelectComplete += new SelectCompleteEventHandler(query_SelectComplete);
		}
		private void DisposeQuery(long ID)
		{
			lock (_queries)
			{
				((AsynchronousExecute)_queries[ID]).DataException -= this.query_DataException;
				((AsynchronousExecute)_queries[ID]).InsertComplete -= this.query_InsertComplete;
				((AsynchronousExecute)_queries[ID]).SelectComplete -= this.query_SelectComplete;
				((AsynchronousExecute)_queries[ID]).Dispose();
				_queries.Remove(ID);
			}
		}
		private void ProcessCycleReportSupportRecords(CycleReport data)
		{
			//	Cycle reports contain references to DeviceEvent records,
			//	and those relationship records are inserted here.
			//
			//  Note that CycleReportLogRecord reference records
			//	cannot be updated. Instead, we delete any that are no 
			//	longer needed.
			//
			//	last note: we cannot know when the supporting log records
			//	will actually be inserted into the database. So the 
			//	database does not require a relationship between CRLR 
			//	records and LogRecord records. CRLR records are only 
			//	insert synchronously, also in this method. 
			try
			{
				List<string> newKeys = new List<string>();
				List<string> oldKeys = new List<string>();
				List<string> delKeys = new List<string>();
				List<string> insKeys = new List<string>();

				//	step 1: acquire keys from passed report
				foreach (IDeviceRecord record in data.LogRecords)
					newKeys.Add(record.Key);

				//	step 2: acquire keys from database version of report
				using (SelectCycleReportLogRecords qrySelect = new SelectCycleReportLogRecords())
				{
					List<IBusinessObject> dbKeys = qrySelect.Select(data.Key);
					if (dbKeys != null)
						foreach (IBusinessObject keys in dbKeys)
						{
							oldKeys.Add(((CycleReportLogRecord)keys).LogRecordKey);
						}
				}

				//	step 3: find any that are in the old list but not in the new 
				//			list. They should be deleted
				foreach (string key in oldKeys)
					if (!newKeys.Contains(key))
						delKeys.Add(key);

				//	step 4: find any that are in the new list but not in the old
				//			list. They should be inserted
				foreach (string key in newKeys)
					if (!oldKeys.Contains(key))
						insKeys.Add(key);

				//	step 5: delete any found in step 3
				if (delKeys.Count > 0)
					using (DeleteCycleReportLogRecords qryDelete = new DeleteCycleReportLogRecords())
					{
						if (qryDelete.Delete(delKeys) == 0)
							Log.GetInstance().WriteLog(
								LogEntryType.DebugTrace,
								"Delete Join Records found no records to delete! Report key=" + delKeys.ToString(),
								EventLogEntryType.Warning);
					}

				//	step 6: insert new keys found in step 4
				if (insKeys.Count > 0)
					foreach (string logRecordKey in insKeys)
						using (InsertCycleReportLogRecord qry = new InsertCycleReportLogRecord(data.Key, logRecordKey))
						{
							if (qry.Insert() == 0)
								Log.GetInstance().WriteLog(
									LogEntryType.Normal,
									"Insert Join Records failed to insert! Report key=" + data.Key + " Record Key=" + logRecordKey,
									EventLogEntryType.Warning);

						}
			}
			catch (Exception ex)
			{
				ErrorHandler.GetInstance().LogException("CacheManager.ProcessCycleReportSupportRecords error: ", ex);
			}
		}

		#endregion
	}
}
