using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using Medivators.Common;
using Medivators.Common.Enums;
using Medivators.Common.Logging;
using Medivators.BusinessObjects;

namespace Medivators.Parser
{
	/// <summary>
	/// This class exists to keep track of the EndCycle 
	/// and StartCycle records coming from each Machine 
	/// Station. 
	/// 
	/// Records come in reverse order, so End really starts
	/// the next report while Start indicates the end of the
	/// current report
	/// 
	/// Only one cycle report may exist at one time for 
	/// any one machine/station pairing
	/// </summary>
	public class CycleReportAccumulator : IDisposable
	{
		private Hashtable _reports = null;
		private static object _lockToken = null;

		#region singleton constructor + dispose

		private static CycleReportAccumulator _instance = null;

		private CycleReportAccumulator()
		{
			_reports = new Hashtable();
			_lockToken = new object();
		}
		public static CycleReportAccumulator GetInstance()
		{
			if (_instance == null)
				_instance = new CycleReportAccumulator();
			return _instance;
		}
		public void Dispose()
		{
			_reports.Clear();
		}

		#endregion

		public void AddRecord(DeviceRecord Record)
		{
			try
			{
				Log.GetInstance().WriteLog(
					LogEntryType.DebugTrace,
					"CycleReportAccumulator received " + Record.ToString(),
					System.Diagnostics.EventLogEntryType.Information);

				//	only records with time stamps are saved off 
				//	to the Cache (and eventually, to database)
				if (IsTimestamped(Record))
					CacheManager.GetInstance().CacheDeviceEvent(Record);

				//	all new or changed records are processed as possible 
				//	CycleReport candidates 

				lock (_lockToken)
				{
					this.AnalyzeAsReportRecord(Record);
				}
			}
			catch (Exception ex)
			{
				ErrorHandler.GetInstance().LogException("CycleReportAccumulator.AddRecord error: " + ex.ToString());
			}
		}

		#region private helpers

		private void SaveReport(CycleReport Report)
		{
			//	DSDHeader records are not Timestamped, but anything
			//	written to the database MUST be timestamped. Solution:
			//	generate Timestamped versions of the DSDHeader records
			//	and add them to the internal Records collection.

			List<IDeviceRecord> addList = new List<IDeviceRecord>();
			List<IDeviceRecord> removeList = new List<IDeviceRecord>();
			foreach (IDeviceRecord record in Report.LogRecords)
			{
				//	apply a timestamp to records that do not have one
				if (! IsTimestamped(record))
				{
					TimestampedHeaderRecord datedRec = new TimestampedHeaderRecord((DSD201HeaderRecord)record, Report.StartTimestamp);
					addList.Add(datedRec);
					removeList.Add(record);
				}
			}
			foreach (IDeviceRecord removeRec in removeList)
				Report.LogRecords.Remove(removeRec);

			//	generated records still need to be sent to the database.
			//	Let the CacheManager simulate the way 
			//	it would behave if it had been sent by the device
			foreach (IDeviceRecord addRec in addList)
			{
				Report.AddRecord((DeviceRecord)addRec);
				CacheManager.GetInstance().CacheDeviceEvent(addRec);
			}

			// add cycle report to caching
			if ( CacheManager.GetInstance().CacheDeviceReport(Report) )
				//	trace event
				Log.GetInstance().WriteLog(LogEntryType.DebugTrace,
										   "Cycle report identified and added to collection: " + Report.ToString(),
										   System.Diagnostics.EventLogEntryType.Information);
			
			//	remove report from accumulator collection
			_reports.Remove(Report.ReportKey);
		}

		private bool IsTimestamped(IDeviceRecord Record)
		{
			//	method inspects passed record to see if it
			//  implements ITimestampedRecord
			return (Record.GetType().GetInterface(typeof(ITimestampedDeviceRecord).Name, true) != null);
		}

		private void AnalyzeAsReportRecord(DeviceRecord Record)
		{
			//	as far as this code block knows, there are only three
			//	kinds of records:
			//	
			//		EndCycle:	start a new cycle, dumping whatever may 
			//					have been in progress
			//		StartCycle:	If a cycle is in progress, save it off
			//		All others:	If a cycle is in progress, add record to it
			//
			//	It seems odd to begin a report on an EndCycle and end it
			//	on a StartCycle, but records are transmitted in reverse
			//	timestamp order. 
			try
			{
				string shortKey = Record.ReportKey;

				//	acquire the currently-in-progress report, if any
				CycleReport rpt = null;
				if (this._reports.Contains(shortKey))
					rpt = (CycleReport)this._reports[shortKey];

				switch (Record.RecordType)
				{
					case DeviceRecordTypes.CycleEnd:
						//	if no report in progress, start one
						if (rpt == null)
							StartReport(Record);

						//	if there is already a report, abort it and start fresh
						else
						{
							AbortReport(shortKey, "Cycle End received out of order: " + Record.ToString() + " Current Report: " + rpt.ToString());
							StartReport(Record);
						}
						break;

					case DeviceRecordTypes.CycleStart:
						//	if no report in progress, this is a warning-level exception
						if (rpt == null)
							AbortReport(shortKey, "Cycle Start received out of order (no report in progress): " + Record.ToString());

						//	if a report is in progress, this should complete it. 
						else
							if (!ApplyRecord(rpt, Record))
								AbortReport(shortKey, "Cycle Report Start record fails log number validation (most probably). Report: " + rpt.ToString());
							else if (!rpt.Validate())
								AbortReport(shortKey, "Cycle Report fails datatype validation. Report: " + rpt.ToString());
							else
								SaveReport(rpt);
						break;

					default:
						//	If a log record comes in out of sequence, don't
						//	abort the report, just don't add the log record
						//	to it
						if (rpt != null)
							if (!ApplyRecord(rpt, Record))
								Log.GetInstance().WriteLog(
									LogEntryType.DebugTrace,
									"Log record cannot be added to active report and is skipped: " + Record.ToString(),
									System.Diagnostics.EventLogEntryType.Information);
						break;
				}
			}
			catch (Exception ex)
			{
				ErrorHandler.GetInstance().LogException("CycleReportAccumulator.AnalyzeAsReportRecord", ex);
			}
		}

		private void StartReport(DeviceRecord Record)
		{
			Log.GetInstance().WriteLog(
				LogEntryType.DebugTrace, 
				"New cycle report started for record: " + Record.ToString(), 
				System.Diagnostics.EventLogEntryType.Information);
			CycleReport rpt = new CycleReport();
			rpt.AddRecord(Record);
			this._reports.Add(Record.ReportKey, rpt);
		}

		private void AbortReport(string ShortReportKey, string LogMessage)
		{
			Log.GetInstance().WriteLog(
				LogEntryType.DebugTrace,
				LogMessage,
				System.Diagnostics.EventLogEntryType.Warning);
			if ( ShortReportKey != null && ShortReportKey.Length > 0 )
				if ( this._reports.Contains(ShortReportKey) )
					// clear current report
					this._reports.Remove(ShortReportKey);
		}

		private bool ApplyRecord(CycleReport rpt, DeviceRecord rec)
		{
			try
			{
				Log.GetInstance().WriteLog(
					LogEntryType.DebugTrace,
					"Applying record " + rec.ToString() + " to Report " + rpt.ToString(),
					System.Diagnostics.EventLogEntryType.Information);
				rpt.AddRecord(rec);
				return true;
			}
			catch (ArgumentOutOfRangeException LogNumberException)
			{
				//	log numbers out of sequence represent a warning-level exception
				Log.GetInstance().WriteLog(
					LogEntryType.DebugTrace,
					LogNumberException.Message + "Record: " + rec.ToString() + " Report: " + rpt.ToString(),
					System.Diagnostics.EventLogEntryType.Warning);
				return false;
			}
			catch (IndexOutOfRangeException DuplicateRecord)
			{
				//	Sending the same record to the report multiple times is expected behavior. 
				//	ignore it. 
				Log.GetInstance().WriteLog(
					LogEntryType.DebugTrace,
					DuplicateRecord.Message,
					System.Diagnostics.EventLogEntryType.Information);
				return false;
			}
			catch (Exception ex)
			{
				ErrorHandler.GetInstance().LogException(
					"Unexpected error CycleReportAcumulator.ApplyRecord: " + rec.ToString() + " Report: " + rpt.ToString(),
					ex);
				return false;
			}
		}

		#endregion
	}
}
