using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Xml;
using System.Runtime.InteropServices;
using System.Threading;

namespace StarTrinity
{
	
	public enum LogLevel
	{
		Error = 0,
		Warning = 1,
		Info = 2,
		Debug = 3,
		Trace = 4,
	}
	interface ILog
	{
		void Write(string module, string objectName, string text, LogLevel level);
		void Write(string module, string objectName, string format, params object[] args);
		void Write(string module, string objectName, string text);
		void WriteError(string module, string objectName, string format, params object[] args);
		void WriteError(string module, string objectName, string text);
        void WriteWarning(string module, string objectName, string format, params object[] args);
        void WriteWarning(string module, string objectName, string text);
		void WriteException(string module, Exception exc, string operationName);
		void WriteException(string module, Exception exc, string relatedObject, string relatedActionName, bool assertionExceptionsAreCritical);
		
	}
	/// <summary>
	/// writes messages to log files. has warning and error additional logs
	/// </summary>
	public class Log : IDisposable, ILog
	{
		/// <summary>
		/// was copied to here from common.cs in order to make Log.cs independent from other source files
		/// </summary>
		static class XmlHelperInternal
		{
			public static string GetSubNodeInnerTextNullable(XmlNode node, string name) // returns null if not found
			{
				foreach (XmlNode subNode in node.ChildNodes)
					if (String.Compare(subNode.Name, name, true) == 0)
						return subNode.InnerText;
				return null;
			}
			public static string GetSubNodeInnerText(XmlNode node, string name)
			{
				foreach (XmlNode subNode in node.ChildNodes)
					if (String.Compare(subNode.Name, name, true) == 0)
						return subNode.InnerText;
				throw new Exception(String.Format("subnode '{0}' not found in node '{1}'", name, node.Name));
			}
			/// <summary>
			/// returns first element from subnodes with a specified name. returns null if element was not found
			/// </summary>
			public static XmlElement GetSubElementNullable(XmlNode DocumentElement, string subElementName)
			{
				foreach (XmlNode subNode in DocumentElement.ChildNodes)
					if (subNode is XmlElement && String.Compare(subNode.Name, subElementName, true) == 0)
						return (XmlElement)subNode;
				return null;
			}
			public static XmlAttribute GetAttribute(XmlElement element, string attributeName)
			{
				var a = element.GetAttributeNode(attributeName);
				if (a != null) return a;
				else throw new Exception(String.Format("attribute '{0}' not found in XML element '{1}'", attributeName, element.Name));
			}
			public static XmlAttribute GetAttributeNullable(XmlElement element, string attributeName)
			{
				return element.GetAttributeNode(attributeName);
			}
			public static string GetAttributeValue(XmlElement element, string attributeName)
			{
				var attr = GetAttribute(element, attributeName);
				return attr.Value;
			}
			public static string GetAttributeValueNullable(XmlElement element, string attributeName)
			{
				var attr = GetAttributeNullable(element, attributeName);
				if (attr == null) return null;
				else return attr.Value;
			}
			public static IEnumerable<XmlElement> GetSubElements(XmlNode DocumentElement)
			{
				var r = new List<XmlElement>();
				foreach (var e in DocumentElement.ChildNodes)
					if (e is XmlElement)
						r.Add((XmlElement)e);
				return r;
			}
			public static IEnumerable<XmlElement> GetSubElements(XmlNode DocumentElement, string name)
			{
				var r = new List<XmlElement>();
				foreach (XmlNode e in DocumentElement.ChildNodes)
					if (e is XmlElement && String.Compare(e.Name, name, true) == 0)
						r.Add((XmlElement)e);
				return r;
			}

		}
        
		class WriteMessageTask
		{
			public readonly string module, objectName, text;
			public readonly LogLevel level;
			public readonly int threadId;
			public readonly DateTime time;
			public WriteMessageTask(string module, string objectName, string text, LogLevel level)
			{
				this.module = module;
				this.objectName = objectName;
				this.text = text;
				this.level = level;
				this.threadId = GetCurrentThreadId();
				this.time = DateTime.Now;
			}
			[DllImport("Kernel32.dll", EntryPoint = "GetCurrentThreadId", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall, PreserveSig = true)]
			private static extern int GetCurrentThreadId();
		}

		Thread thread;
		/// <summary>
		/// is accessed by both writing thread and calling threads
		/// </summary>
		readonly Queue<WriteMessageTask> tasks = new Queue<WriteMessageTask>();
		readonly string fileName, path;
		/// <summary>
		/// used for messages with no module specified
		/// </summary>
		public LogLevel DefaultLevel
		{
			get;
			set;
		}
		Dictionary<string, LogLevel> levelsForSpecificModules = new Dictionary<string, LogLevel>();
		Dictionary<string, LogLevel> levelsForGeneralModules = new Dictionary<string, LogLevel>();
		bool stopingThread = false;
		/// <summary>
		/// file stream is used by log thread only
		/// </summary>
		FileStream fileStream = null;
		/// <summary>
		/// time of last executed task
		/// </summary>
		DateTime lastTime = DateTime.MinValue;
		BinaryWriter binaryWriter = null;
		Log additionalErrorLog = null, additionalWarningLog = null;
		public int MaxQueueCount
		{
			get;
			set;
		}
		int numberOfMessagesToSkipOnOverload = 0;

		public delegate void EventHandler(string message);
		public event EventHandler LineWritten = null;
        public event EventHandler ErrorLineWritten = null;
        public string CurrentFileName { get; private set; }
        public List<string> CurrentFileNames
        {
            get            
            {
                var r = new List<string>();
                if (CurrentFileName != null) r.Add(CurrentFileName);
                if (additionalErrorLog != null && additionalErrorLog.CurrentFileName != null) r.Add(additionalErrorLog.CurrentFileName);
                if (additionalWarningLog != null && additionalWarningLog.CurrentFileName != null) r.Add(additionalWarningLog.CurrentFileName);
                return r;
            } 
        }
        public Log(string path, string name, LogLevel defaultLevel, bool createAdditionalErrorLog, bool createAdditionalWarningLog, Action<string> errorLogFileFound, Action<string> logFileFound)
        {
            this.logFileFound = logFileFound;
            this.errorLogFileFound = errorLogFileFound;
			if (path == null) throw new ArgumentNullException("path");
			if (name == null) throw new ArgumentNullException("name");
			this.fileName = name + ".log";
			this.path = path;
			this.DefaultLevel = defaultLevel;
			this.MaxQueueCount = 10000;

			this.thread = new Thread(new ThreadStart(ThreadEntry));
			thread.Start();
			if (createAdditionalErrorLog) additionalErrorLog = new Log(path, name + "_err", LogLevel.Error, false, false, null, null);
			if (createAdditionalWarningLog) additionalWarningLog = new Log(path, name + "_wrn", LogLevel.Warning, false, false, null, null);

            if (additionalWarningLog == null && additionalErrorLog == null)
            {
                deleteOldLogs = true;
            }
		}
		/// <summary>
		/// reconfigures logging levels for modules
		/// </summary>
		/// <param name="loggingXElement">
		///		example:
		///		<logging>
		///			<module name="unm_iface" level="Debug" />
		///			<module name="unm" level="Info" general="true" />   <!-- all unm_XXXXX modules will have level="Info" -->
		///		</logging>
		/// </param>
		public void SetLogLevels(XmlElement loggingXElement)
		{
			if (loggingXElement == null) throw new ArgumentNullException("loggingXElement");
			var levelsForGeneralModules = new Dictionary<string, LogLevel>();
			var levelsForSpecificModules = new Dictionary<string, LogLevel>();

			foreach (var moduleElement in XmlHelperInternal.GetSubElements(loggingXElement, "module"))
			{
				var nameAttr = XmlHelperInternal.GetAttributeValue(moduleElement, "name");				
				var levelAttr = XmlHelperInternal.GetAttributeValue(moduleElement, "level");				

				if (levelsForGeneralModules.ContainsKey(nameAttr) ||
					levelsForSpecificModules.ContainsKey(nameAttr)
					) throw new ArgumentException(String.Format("ambiguous logging configuration for module '{0}'", nameAttr));
				
				LogLevel level;
				try
				{
					level = (LogLevel)Enum.Parse(typeof(LogLevel), levelAttr, true);
				}
				catch
				{
					throw new ArgumentException(String.Format("invalid log level for module '{0}'. possible values: {1}",
						nameAttr, String.Join(", ", Enum.GetNames(typeof(LogLevel)))));
				}

				if ((string)moduleElement.GetAttribute("general") == "true")
					levelsForGeneralModules.Add(nameAttr.ToLower(), level);
				else
					levelsForSpecificModules.Add(nameAttr.ToLower(), level);
			}

			this.levelsForSpecificModules = levelsForSpecificModules;
			this.levelsForGeneralModules = levelsForGeneralModules;
		}
		public void Dispose()
		{
			stopingThread = true;
			thread.Join();
			if (additionalErrorLog != null)
			{
				additionalErrorLog.Dispose();
				additionalErrorLog = null;
			}
			if (additionalWarningLog != null)
			{
				additionalWarningLog.Dispose();
				additionalWarningLog = null;
			}
		}
		public LogLevel GetLogLevel(string module)
		{
			var r = DefaultLevel;
			if (!String.IsNullOrEmpty(module))
			{
				module = module.ToLower();
				if (!levelsForSpecificModules.TryGetValue(module, out r))
				{
					int index = module.IndexOf('_');
					if (index != -1)
					{
						string generalModule = module.Substring(0, index);
						if (!levelsForGeneralModules.TryGetValue(generalModule, out r))
							r = DefaultLevel;
					}
					else r = DefaultLevel;
				}
			}
			return r;
		}
		public void Write(string module, string objectName, string text, LogLevel level)
		{
			if (numberOfMessagesToSkipOnOverload > 0)
			{
				Interlocked.Decrement(ref numberOfMessagesToSkipOnOverload);
				return;
			}

			LogLevel levelToCompare = GetLogLevel(module);
			if (level > levelToCompare)
				return; // ignore this message			
						
			if (thread == null) throw new ObjectDisposedException("log " + fileName);

			if (tasks.Count > MaxQueueCount)
			{
				numberOfMessagesToSkipOnOverload = 100;
				lock (tasks) tasks.Enqueue(new WriteMessageTask("Log", "Log", "!!!!!!!!!!!! too many tasks in log queue: " + tasks.Count.ToString(), LogLevel.Error));
				return;
			}

			WriteMessageTask task = new WriteMessageTask(module, objectName, text, level);

			lock (tasks) tasks.Enqueue(task);
			if (level == LogLevel.Error && additionalErrorLog != null) additionalErrorLog.Write(module, objectName, text, level);
			if (level == LogLevel.Warning && additionalWarningLog != null) additionalWarningLog.Write(module, objectName, text, level);


			if (LineWritten != null) LineWritten(text);
			if (level == LogLevel.Error && ErrorLineWritten != null) ErrorLineWritten(text);
		}
        public void Write(string module, string objectName, string format, params object[] args)
        {
            Write(module, objectName, String.Format(format, args), LogLevel.Debug);
        }
        public void WriteInfo(string module, string objectName, string format, params object[] args)
        {
            Write(module, objectName, String.Format(format, args), LogLevel.Info);
        }
		public void Write(string module, string objectName, string text)
		{
			Write(module, objectName, text, LogLevel.Debug);
		}
		public void WriteError(string module, string objectName, string format, params object[] args)
		{
			Write(module, objectName, String.Format(format, args), LogLevel.Error);
		}
		public void WriteError(string module, string objectName, string text)
		{
			Write(module, objectName, text, LogLevel.Error);
		}
		public void WriteWarning(string module, string objectName, string format, params object[] args)
		{
			Write(module, objectName, String.Format(format, args), LogLevel.Warning);
		}
		public void WriteWarning(string module, string objectName, string text)
		{
			Write(module, objectName, text, LogLevel.Warning);
		}
		public void WriteException(string module, Exception exc, string operationName)
		{
			WriteException(module, exc, null, operationName, true);
		}
		public void WriteException(string module, Exception exc, string relatedObject, string relatedActionName, bool assertionExceptionsAreCritical)
		{
			bool errorOrWarning = true;
			if (!assertionExceptionsAreCritical)
			{
				if (exc is ObjectDisposedException || exc is InvalidOperationException || exc is ApplicationException || exc is ObjectDisposedException)
					errorOrWarning = false;
			}

			Exception innerException = exc;
			while (innerException.InnerException != null) innerException = innerException.InnerException;
			if (!assertionExceptionsAreCritical &&
				(innerException is ApplicationException || innerException is ObjectDisposedException || innerException is InvalidOperationException))
			{
				errorOrWarning = false;
				exc = innerException;
			}

			string prefix;
			if (relatedActionName != null)
				prefix = String.Format("'{0}' failed: ", relatedActionName);
			else if (relatedActionName != null) prefix = String.Format("'{0}' failed: ", relatedActionName);
			else if (relatedObject != null) prefix = String.Format("exception in '{0}': ", relatedObject);
			else prefix = "exception: ";

			if (errorOrWarning)
			{ // if it is critical error, then we always write stack trace
				WriteError(module, relatedObject, "{0}{1}", (object)prefix, (object)exc);
			}
			else
			{
				bool needStackTrace = (relatedActionName == null);
				WriteWarning(module, relatedObject, "{0}{1}", (object)prefix, needStackTrace ? (object)exc : (object)exc.Message);
			}
		}

		#region writing thread
		void ThreadEntry()
		{
			while (!stopingThread)
			{
				WriteMessageTask task = null;
				lock (tasks)
				{
					if (tasks.Count != 0) task = tasks.Dequeue();
				}

				if (task != null)
				{
					try
					{
						WriteMessage(task);
					}
					catch (Exception exc)
					{
						System.Diagnostics.Debug.WriteLine(String.Format("cannot execute log task: {0}", exc));
					}
				}
				else
					Thread.Sleep(10);

                CheckLogsInDirectoryWhenNeeded();
			}
			CloseFileIfOpened();
		}
		void WriteMessage(WriteMessageTask task)
		{
			ChangeFileIfNewHour(task.time);

			StringBuilder line = new StringBuilder();
			line.AppendFormat("{0:d2}:{1:d2}.{2:d3}[{3:d5}]", task.time.Minute, task.time.Second, task.time.Millisecond, task.threadId);
			switch (task.level)
			{
				case LogLevel.Error: line.Append("~err~ "); break;
				case LogLevel.Warning: line.Append("~wrn~ "); break;
				case LogLevel.Info: line.Append("~inf~ "); break;
				case LogLevel.Trace: line.Append("~trc~ "); break;
				default: line.Append("~dbg~ "); break;
			}
			if (!String.IsNullOrEmpty(task.module)) line.AppendFormat("[{0}]\t", task.module);
			if (!String.IsNullOrEmpty(task.objectName)) line.AppendFormat("[{0}]\t", task.objectName);
			line.Append(task.text);
			line.Append("\r\n");

			/*			
			const int sequenceFixedLength = 15;		
			if (sequenceName.Length > sequenceFixedLength) sequenceName = sequenceName.Substring(0, sequenceFixedLength - 1) + "~";
			else if (sequenceName.Length < sequenceFixedLength) sequenceName += StringCommon.Repeat(" ", sequenceFixedLength - sequenceName.Length);
			*/


			binaryWriter.Write(line.ToString().ToCharArray());
			fileStream.Flush();

			lastTime = task.time;
		}
		static void CreateDirectory(string path)
		{
			int lastIndex = 0;
		_loop:
			int index = path.IndexOf('\\', lastIndex);
			if (index != -1)
			{
				string s = path.Substring(0, index);
				if (Directory.Exists(s))
					Directory.CreateDirectory(s);
				lastIndex = index + 1;
				goto _loop;
			}

			if (!Directory.Exists(path))
				Directory.CreateDirectory(path);
		}
		void OpenFileAndCreateDirectory(DateTime time)
		{
			CreateDirectory(path);
			string directoryPath = Path.Combine(path, String.Format("{0}_{1:d2}_{2:d2}", time.Year, time.Month, time.Day));
			CreateDirectory(directoryPath);
			string fileName = Path.Combine(directoryPath, String.Format("{0:d2}_{1}", time.Hour, this.fileName));
			fileStream = new FileStream(fileName, FileMode.OpenOrCreate, FileAccess.Write, FileShare.Read);
			fileStream.Seek(0, SeekOrigin.End); // go to end of file
			binaryWriter = new BinaryWriter(fileStream, Encoding.GetEncoding(1251));
            CurrentFileName = fileName;
		}
		void ChangeFileIfNewHour(DateTime time)
		{
			if (time.Hour != lastTime.Hour ||
				time.Day != lastTime.Day ||
				time.Month != lastTime.Month)
			{
				CloseFileIfOpened();
				OpenFileAndCreateDirectory(time);
			}
		}
		void CloseFileIfOpened()
		{
			if (fileStream != null)
			{
				fileStream.Close();
				fileStream.Dispose();
				fileStream = null;
			}
		}
		#endregion

        readonly bool deleteOldLogs;
        DateTime lastTimeCheckedLogsInDirectory = DateTime.Now;
        Action<string> errorLogFileFound; // handler which sends it to CP
        Action<string> logFileFound; // handler which sends it to CP
        const int deleteIntervalInDays = 7;
        const int checkLogsIntervalInMinutes = 3;
        void CheckLogsInDirectoryWhenNeeded()
        {
            // enumerate all directories
            // skip current log file names
            // delete old logs
            // callbacks for error logs

            if (DateTime.Now > lastTimeCheckedLogsInDirectory.AddMinutes(1))
            {
                lastTimeCheckedLogsInDirectory = DateTime.Now;

                new Action(() =>
                    {
                        try
                        {
                            // check directories in logs root directory
                            foreach (string directory in System.IO.Directory.GetDirectories(path))
                            {
                                // for each directory: remove it if its date is obsolete

                                string dateStr = System.IO.Path.GetFileName(directory);
                                string[] ymd = dateStr.Split('_');
                                if (ymd.Length == 3)
                                {
                                    int y, m, d;
                                    if (Int32.TryParse(ymd[0], out y) && Int32.TryParse(ymd[1], out m) && Int32.TryParse(ymd[2], out d))
                                    {
                                        DateTime dirDate = new DateTime(y, m, d).AddDays(1);
                                        if (dirDate.AddDays(deleteIntervalInDays) < DateTime.Now)
                                        {
                                            try
                                            {
                                                foreach (string file in System.IO.Directory.GetFiles(directory))
                                                    System.IO.File.Delete(file);
                                                System.IO.Directory.Delete(directory);
                                            }
                                            catch (Exception)
                                            {
                                            }
                                        }
                                    }
                                }


                                if (errorLogFileFound != null)
                                    foreach (string errorLogFile in System.IO.Directory.GetFiles(directory, "*_err.log"))
                                        if (!this.CurrentFileNames.Contains(errorLogFile))
                                        {
                                            errorLogFileFound(errorLogFile);
                                        }

                                if (logFileFound != null)
                                    foreach (string logFile in System.IO.Directory.GetFiles(directory, "*.log"))
                                        if (!this.CurrentFileNames.Contains(logFile))
                                        {
                                            logFileFound(logFile);
                                        }
                            }
                        }
                        catch (Exception)
                        {
                        }
                    }).BeginInvoke((ar) => { }, null);


            }
        }
	}
}
