using log4net;
using log4net.Appender;
using log4net.Config;
using log4net.Repository.Hierarchy;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Windows.Forms;
using static log4net.Appender.RollingFileAppender;

namespace JWH
{

    /// <summary>
    /// log4net을 활용한 로그작성 라이브러리
    /// [assembly: log4net.Config.XmlConfigurator(ConfigFile = "log4net.config", Watch = true)]
    /// </summary>
    public class XLogger
    {

        #region [ Static ] ====================================================

        /// <summary>
        /// Logger root
        /// </summary>
        public static XLogger Instance { get; set; } = new XLogger();

        /// <summary>
        /// RollingFileAppender를 생성한다
        /// </summary>
        /// <param name="name">Appender.Name</param>
        /// <param name="file">Appender.File</param>
        /// <param name="pattern">Appender.ConversionPattern</param>
        /// <returns></returns>
        public static RollingFileAppender CreateRollingFileAppender(string name, string path, string pattern)
        {
            log4net.Appender.RollingFileAppender appender = new log4net.Appender.RollingFileAppender();
            try
            {
                appender.Name = name;
                appender.File = $"{path}{name}_";
                appender.DatePattern = $"yyyyMMddHH'.log'";
                //appender.MaxSizeRollBackups = 100;
                appender.MaximumFileSize = "10MB";
                appender.StaticLogFileName = false;
                appender.AppendToFile = true;
                appender.PreserveLogFileNameExtension = true;
                appender.RollingStyle = log4net.Appender.RollingFileAppender.RollingMode.Date;
                appender.LockingModel = new RollingFileAppender.MinimalLock();
                //appender.CountDirection = XLogger.GetCountDirection(path, $"{name}_", appender.DatePattern);
                log4net.Layout.PatternLayout layout = new log4net.Layout.PatternLayout() { ConversionPattern = pattern };
                layout.ActivateOptions();
                appender.Layout = layout;
                appender.ActivateOptions();
            }
            catch (Exception ex)
            {
                XLogger.Instance.Fatal(ex);
            }

            return appender;
        }

        /// <summary>
        /// log4net.GlobalContext.Properties
        /// <para>You must call ConfigureAndWatch</para>
        /// </summary>
        /// <param name="name"></param>
        /// <param name="value"></param>
        public static void AddProperties(string name, string value)
        {
            log4net.GlobalContext.Properties[name] = value;
        }

        /// <summary>
        /// log4net.GlobalContext.Properties
        /// </summary>
        /// <param name="name"></param>
        /// <returns></returns>
        public static string GetProperties(string name)
        {
            return log4net.GlobalContext.Properties[name] as string;
        }

        /// <summary>
        /// XmlConfigurator.ConfigureAndWatch
        /// </summary>
        /// <param name="file"></param>
        public static void ConfigureAndWatch(FileInfo file)
        {
            if (file?.Exists == false) return;

            XmlConfigurator.ConfigureAndWatch(file);
        }

        #endregion

        #region [ Enum ] ======================================================

        /// <summary>XLogLevel</summary>
        public enum eXLogLevel
        {
            None = 0,
            Debug = 1,
            Info = 2,
            Warn = 3,
            Error = 4,
            Fatal = 5
        }

        #endregion

        #region [ Variable ] ==================================================

        /// <summary>Filter Types</summary>
        private List<Type> m_FilterTypes = new List<Type>();

        #endregion

        #region [ Properties ] ================================================

        /// <summary>Logger Name</summary>
        public string Name { get; set; } = string.Empty;

        /// <summary>log4net.Repository.Hierarchy.Logger</summary>
        public Logger Logger { get { return this.GetLogger(); } }

        /// <summary>LOG Control</summary>
        public TextBoxBase Control { get; set; } = null;

        /// <summary>Control.ScrollToCaret</summary>
        public bool OnScrollToCaret { get; set; } = true;

        /// <summary>LOG Control : 0.DateTime, 1.LogLevel, 2.LogName, 3.ClassFullName, 4.Method, 5.Message, 6.ClassName</summary>
        /// <example></example>
        public string ControlPatternLayout { get; set; } = "[{0}] {1} {2} {3}.{4}() {5}";

        /// <summary>Last Deleted LogFile</summary>
        public DateTime LastDeletedDateTime { get; set; } = DateTime.MinValue;

        /// <summary>Filter Types</summary>
        public List<Type> FilterTypes
        {
            get { return m_FilterTypes; }
        }

        /// <summary>RichTextBox.SelectionColor</summary>
        public Color ForeColor { get; set; } = SystemColors.ControlText;

        #endregion

        #region [ XLogger ] ===================================================

        /// <summary>
        /// XLogger
        /// </summary>
        public XLogger()
        {
        }

        /// <summary>
        /// XLogger
        /// </summary>
        /// <param name="name">Logger Name defined in log4net.config</param>
        public XLogger(string name)
        {
            this.Name = name;
        }

        #endregion

        #region [ Method ] ====================================================

        #region [ Appender ] --------------------------------------------------

        /// <summary>
        /// 동적으로 RollingFileAppender를 추가한다
        /// </summary>
        public IAppender AddRollingFileAppender(string path, string pattern)
        {
            FileAppender appender = null;
            try
            {
                string name = this.Name;
                appender = XLogger.CreateRollingFileAppender(name, path, pattern);
                this.AddAppender(appender);
            }
            catch (Exception ex)
            {
                XLogger.Instance.Fatal(ex);
            }

            return appender;
        }

        /// <summary>
        /// 동적으로 RollingFileAppender를 추가한다
        /// </summary>
        /// <param name="name"></param>
        /// <param name="file"></param>
        /// <param name="pattern"></param>
        public void AddRollingFileAppender(string name, string file, string pattern)
        {
            try
            {
                log4net.Repository.Hierarchy.Logger logger = this.Logger;
                RollingFileAppender appender = XLogger.CreateRollingFileAppender(name, file, pattern);
                this.AddAppender(appender);
            }
            catch (Exception ex)
            {
                XLogger.Instance.Fatal(ex);
            }
        }

        /// <summary>
        /// 다른 XLogger의 Appender를 추가한다
        /// </summary>
        /// <param name="srcXLogger"></param>
        /// <param name="index"></param>
        public void AddRollingFileAppender(XLogger srcXLogger, int index = 0)
        {
            try
            {
                log4net.Repository.Hierarchy.Logger srclogger = srcXLogger.Logger;
                if (srclogger.Appenders.Count <= index) return;

                IAppender srcAppender = srclogger.Appenders[index];
                this.AddAppender(srcAppender);
            }
            catch (Exception ex)
            {
                XLogger.Instance.Fatal(ex);
            }
        }

        /// <summary>
        /// Appender를 추가한다
        /// </summary>
        /// <param name="appender"></param>
        public void AddAppender(IAppender appender)
        {
            try
            {
                log4net.Repository.Hierarchy.Logger logger = this.Logger;
                FileAppender fileAppender = null;
                IAppender removeItem = null;
                foreach (IAppender item in logger.Appenders)
                {
                    if (item.Name == appender.Name)
                    {
                        removeItem = item;
                        break;
                    }
                }
                if (removeItem != null)
                {
                    fileAppender = removeItem as FileAppender;
                    logger.RemoveAppender(removeItem).Close();
                }

                fileAppender = appender as FileAppender;
                logger.AddAppender(appender);
            }
            catch (Exception ex)
            {
                XLogger.Instance.Fatal(ex);
            }
        }

        /// <summary>
        /// 모든 Appender를 제거한다
        /// </summary>
        public void ClearAppender()
        {
            log4net.Repository.Hierarchy.Logger logger = this.Logger;
            List<IAppender> lstAppender = new List<IAppender>();

            foreach (IAppender item in logger.Appenders)
                lstAppender.Add(item);

            foreach (IAppender item in lstAppender)
                logger.RemoveAppender(item).Close();
        }

        #endregion

        #region [ MDC ] -------------------------------------------------------

        /// <summary>
        /// MDC Key
        /// </summary>
        private enum eMDCKey
        {
            Logger,
            Name,
            Class,
            Method
        }

        /// <summary>
        /// Set MDC
        /// </summary>
        /// <param name="mdcKey"></param>
        /// <param name="value"></param>
        private void SetMDC(eMDCKey mdcKey, string value)
        {
            try
            {
                MDC.Set(mdcKey.ToString().ToLower(), value);
            }
            catch
            {
            }
        }

        /// <summary>
        /// Set MDC
        /// </summary>
        /// <param name="mdcKey"></param>
        /// <param name="value"></param>
        private void SetMDC(string mdcKey, string value)
        {
            try
            {
                MDC.Set(mdcKey.ToLower(), value);
            }
            catch
            {
            }
        }

        /// <summary>
        /// Clear MDC
        /// </summary>
        private void ClearMDC()
        {
            foreach (string mdcKey in Enum.GetNames(typeof(eMDCKey)))
                SetMDC(mdcKey, string.Empty);
        }

        #endregion

        #region [ Filter ] ----------------------------------------------------

        /// <summary>
        /// Add Filter(Type)
        /// </summary>
        /// <param name="types"></param>
        public void AddFilterType(params Type[] types)
        {
            m_FilterTypes.AddRange(types);
        }

        /// <summary>
        /// Clear Filter(Type)
        /// </summary>
        public void ClearFilterType()
        {
            m_FilterTypes.Clear();
        }

        #endregion

        #region [ Debug ] -----------------------------------------------------

        /// <summary>
        /// Debug
        /// </summary>
        /// <param name="value"></param>
        /// <param name="color"></param>
        public string Debug(object value = null, Color? color = null)
        {
            return this.WriteLine(eXLogLevel.Debug, value, color);
        }

        /// <summary>
        /// Debug
        /// </summary>
        /// <param name="message"></param>
        /// <param name="e"></param>
        /// <param name="color"></param>
        public string Debug(string message, Exception e, Color? color = null)
        {
            return this.WriteLine(eXLogLevel.Debug, message, e, color);
        }

        #endregion

        #region [ Info ] ------------------------------------------------------

        /// <summary>
        /// Info
        /// </summary>
        /// <param name="value"></param>
        /// <param name="color"></param>
        public string Info(object value = null, Color? color = null)
        {
            return this.WriteLine(eXLogLevel.Info, value, color);
        }

        /// <summary>
        /// Info
        /// </summary>
        /// <param name="message"></param>
        /// <param name="e"></param>
        /// <param name="color"></param>
        public string Info(string message, Exception e, Color? color = null)
        {
            return this.WriteLine(eXLogLevel.Info, message, e, color);
        }

        #endregion

        #region [ Warn ] ------------------------------------------------------

        /// <summary>
        /// Warn
        /// </summary>
        /// <param name="value"></param>
        /// <param name="color"></param>
        public string Warn(object value = null, Color? color = null)
        {
            return this.WriteLine(eXLogLevel.Warn, value, color);
        }

        /// <summary>
        /// Warn
        /// </summary>
        /// <param name="message"></param>
        /// <param name="e"></param>
        /// <param name="color"></param>
        public string Warn(string message, Exception e, Color? color = null)
        {
            return this.WriteLine(eXLogLevel.Warn, message, e, color);
        }

        #endregion

        #region [ Error ] -----------------------------------------------------

        /// <summary>
        /// Error
        /// </summary>
        /// <param name="value"></param>
        /// <param name="color"></param>
        public string Error(object value = null, Color? color = null)
        {
            return this.WriteLine(eXLogLevel.Error, value, color);
        }

        /// <summary>
        /// Error
        /// </summary>
        /// <param name="message"></param>
        /// <param name="e"></param>
        /// <param name="color"></param>
        public string Error(string message, Exception e, Color? color = null)
        {
            return this.WriteLine(eXLogLevel.Error, message, e, color);
        }

        #endregion

        #region [ Fatal ] -----------------------------------------------------

        /// <summary>
        /// Fatal
        /// </summary>
        /// <param name="value"></param>
        public string Fatal(object value, bool showMessageBox)
        {
            string message = this.WriteLine(eXLogLevel.Fatal, value);

            if (showMessageBox)
                MessageBox.Show(message, "Fatal", MessageBoxButtons.OK, MessageBoxIcon.Error);

            return message;
        }

        /// <summary>
        /// Fatal
        /// </summary>
        /// <param name="value"></param>
        /// <param name="color"></param>
        public string Fatal(object value = null, Color? color = null)
        {
            string result = this.WriteLine(eXLogLevel.Fatal, value, color);
            return result;
        }

        /// <summary>
        /// Fatal
        /// </summary>
        /// <param name="message"></param>
        /// <param name="e"></param>
        /// <param name="color"></param>
        public string Fatal(string message, Exception e, Color? color = null)
        {
            string result = this.WriteLine(eXLogLevel.Fatal, message, e, color);
            return result;
        }

        /// <summary>
        /// Fatal
        /// </summary>
        /// <param name="value"></param>
        /// <param name="showMessageBox">Show MessageBox</param>
        /// <param name="color"></param>
        private string xFatal(object value = null, bool showMessageBox = false, Color? color = null)
        {
            string result = this.WriteLine(eXLogLevel.Fatal, value, color);
            if (showMessageBox)
                MessageBox.Show(result, "Fatal", MessageBoxButtons.OK, MessageBoxIcon.Error);

            return result;
        }

        /// <summary>
        /// Fatal
        /// </summary>
        /// <param name="message"></param>
        /// <param name="e"></param>
        /// <param name="showMessageBox">Show MessageBox</param>
        /// <param name="color"></param>
        private string xFatal(string message, Exception e, bool showMessageBox = false, Color? color = null)
        {
            string result = this.WriteLine(eXLogLevel.Fatal, message, e, color);
            if (showMessageBox)
                MessageBox.Show(result, "Fatal", MessageBoxButtons.OK, MessageBoxIcon.Error);

            return result;
        }

        #endregion

        #region [ WriteLine ] -------------------------------------------------

        private StackFrame GetStackFrame(StackTrace trace)
        {
            try
            {
                foreach (StackFrame frame in trace.GetFrames())
                {
                    MethodBase method = frame.GetMethod();
                    if (method.DeclaringType != typeof(XLogger))
                        return frame;
                }

                return trace.GetFrame(2);
            }
            catch (Exception ex)
            {
                Trace.WriteLine(ex.Message);
                return trace.GetFrame(2);
            }
        }

        /// <summary>
        /// 지정된 문자열 값을 log4net 혹은 표준 출력 스트림에 출력합니다
        /// </summary>
        /// <param name="level"></param>
        /// <param name="value"></param>
        public string WriteLine(eXLogLevel level, object value = null, Color? color = null)
        {
            try
            {
                StackTrace trace = new StackTrace();
                //StackFrame frame = trace.GetFrame(2);
                StackFrame frame = this.GetStackFrame(trace);
                MethodBase method = frame.GetMethod();
                Exception ex = null;
                string strMessage = string.Empty;

                if (value != null)
                {
                    if (value is Exception)
                    {
                        ex = value as Exception;
                        //strMessage = ex.Message;
                    }
                    else
                    {
                        strMessage = value.ToString();
                    }
                }

                return WriteLine(level, strMessage, method, ex, color);
            }
            catch (Exception ex)
            {
                Trace.WriteLine(ex.Message);
                return ex.Message;
            }
        }

        /// <summary>
        /// 지정된 문자열 값을 log4net 혹은 표준 출력 스트림에 출력합니다
        /// </summary>
        /// <param name="level"></param>
        /// <param name="message"></param>
        /// <param name="e"></param>
        public string WriteLine(eXLogLevel level, string message, Exception e, Color? color = null)
        {
            try
            {
                StackTrace trace = new StackTrace();
                //StackFrame frame = trace.GetFrame(2);
                StackFrame frame = this.GetStackFrame(trace);
                MethodBase method = frame.GetMethod();
                string strMessage = string.Empty;

                strMessage = message;

                return WriteLine(level, strMessage, method, e);
            }
            catch (Exception ex)
            {
                Trace.WriteLine(ex.Message);
                return ex.Message;
            }
        }

        /// <summary>
        /// 지정된 문자열 값을 log4net 혹은 표준 출력 스트림에 출력합니다
        /// </summary>
        /// <param name="level"></param>
        /// <param name="message"></param>
        /// <param name="method"></param>
        public string WriteLine(eXLogLevel level, string message, MethodBase method, Exception e = null, Color? color = null)
        {
            string result = string.Empty;
            try
            {
                string name = this.Name;
                string strDTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
                string strClassFullName = string.Empty;
                string strClassName = string.Empty;
                string strMethod = string.Empty;
                string strLog = string.Empty;
                string strConsole = string.Empty;

                ILog log = LogManager.GetLogger(name);
                ClearMDC();

                if (method == null)
                {
                    StackTrace trace = new StackTrace();
                    StackFrame frame = trace.GetFrame(1);
                    method = frame.GetMethod();
                }

                if (method != null && method.DeclaringType != null)
                {
                    if (m_FilterTypes.Count > 0 && m_FilterTypes.Contains(method.DeclaringType) == false) return result;

                    strClassFullName = method.DeclaringType.FullName;
                    strClassName = method.DeclaringType.Name;
                    strMethod = method.Name;
                }

                if (message.EndsWith(Environment.NewLine))
                    message = message.Remove(message.Length - Environment.NewLine.Length);
                strLog = message;
                strConsole = string.Format(this.ControlPatternLayout, strDTime, level.ToString().ToUpper(), name, strClassFullName, strMethod, message, strClassName);

                SetMDC(eMDCKey.Name, name);
                SetMDC(eMDCKey.Class, strClassFullName);
                SetMDC(eMDCKey.Method, strMethod);

                result = $"{strConsole}" + (e != null ? string.Format("{0}{2}", Environment.NewLine, e.Message, e.ToString()) : "");
                switch (level)
                {
                    case eXLogLevel.Debug:
                        if (log.IsDebugEnabled) { log.Debug(strLog, e); this.WriteControl(result, color); }
                        break;
                    case eXLogLevel.Info:
                        if (log.IsInfoEnabled) { log.Info(strLog, e); this.WriteControl(result, color); }
                        break;
                    case eXLogLevel.Warn:
                        if (log.IsWarnEnabled) { log.Warn(strLog, e); this.WriteControl(result, color); }
                        break;
                    case eXLogLevel.Error:
                        if (log.IsErrorEnabled) { log.Error(strLog); this.WriteControl(result, color); }
                        break;
                    case eXLogLevel.Fatal:
                        if (log.IsFatalEnabled) { log.Fatal(strLog, e); this.WriteControl(result, color); }
                        break;
                }

                //this.DeleteRollingFiles();

                return result;
            }
            catch (Exception ex)
            {
                Trace.WriteLine(ex.ToString());
                return ex.ToString();
            }
        }

        /// <summary>
        /// Control.Text에 로그를 기록한다
        /// </summary>
        /// <param name="message"></param>
        private void WriteControl(string message, Color? color = null)
        {
            try
            {
                if (this.Control == null) return;
                if (color.Equals(Color.Transparent)) return;
                if (this.Control.InvokeRequired)
                {
                    this.Control.Invoke((MethodInvoker)delegate { this.WriteControl(message, color); });
                    return;
                }

                Application.DoEvents();
                this.Control.SuspendLayout();
                if (this.Control.MaxLength <= this.Control.TextLength + message.Length)
                {
                    int count = 0;
                    while (this.Control.MaxLength * 0.9 <= this.Control.TextLength + message.Length)
                    {
                        this.Control.SelectionStart = 0;
                        this.Control.SelectionLength = this.Control.GetFirstCharIndexFromLine(count + 1);
                        this.Control.SelectedText = string.Empty;
                        count++;
                    }
                }

                int index = this.Control.TextLength;
                this.Control.AppendText(message + Environment.NewLine);
                if (this.Control.GetType() == typeof(RichTextBox))
                {
                    RichTextBox richTextBox = (RichTextBox)this.Control;
                    if (color == null) color = richTextBox.ForeColor;

                    richTextBox.SelectionStart = index;
                    richTextBox.SelectionLength = message.Length;
                    richTextBox.SelectionColor = (Color)color;
                }

                if (this.OnScrollToCaret)
                    this.Control.ScrollToCaret();
            }
            catch (Exception ex)
            {
                Trace.WriteLine(ex.ToString());
            }
            finally
            {
                if (this.Control != null) this.Control.ResumeLayout();
            }
        }

        #endregion

        #region [ ETC ] -------------------------------------------------------

        /// <summary>
        /// Change LogLevel
        /// </summary>
        /// <param name="level"></param>
        public void SetLevel(log4net.Core.Level level)
        {
            Logger logger = this.Logger;
            logger.Repository.Threshold = logger.Repository.LevelMap["DEBUG"];
            logger.Level = level;
        }

        /// <summary>
        /// Logger를 반환한다
        /// </summary>
        /// <returns></returns>
        private log4net.Repository.Hierarchy.Logger GetLogger(string name = "")
        {
            if (string.IsNullOrEmpty(name)) name = this.Name;
            ILog log = LogManager.GetLogger(name);

            log4net.Repository.Hierarchy.Logger logger = log.Logger as log4net.Repository.Hierarchy.Logger;

            return logger;
        }

        /// <summary>
        /// RollingAppender 로그파일을 삭제합니다.
        /// </summary>
        /// <param name="interval">동작주기(분)</param>
        /// <remarks>
        /// RollingMode='Date'일 경우 동작하며,
        /// MaxSizeRollBackups 값을 보관일로 적용하여 기간이 경과된 파일들을 삭제한다
        /// </remarks>
        public void DeleteRollingFiles(int interval = 30)
        {
            try
            {
                if (DateTime.Now.Subtract(this.LastDeletedDateTime).TotalMinutes < interval) return;
                this.LastDeletedDateTime = DateTime.Now;

                var repositories = LogManager.GetAllRepositories().FirstOrDefault();
                if (repositories == null) return;

                foreach (IAppender appender in repositories.GetAppenders())
                {
                    FileAppender fileAppender = appender as FileAppender;
                    RollingFileAppender rollingAppender = appender as RollingFileAppender;
                    if (rollingAppender != null)
                    {
                        int maxSizeRollBackups = rollingAppender.MaxSizeRollBackups;
                        if (maxSizeRollBackups < 1) continue;

                        RollingMode rollingStyle = rollingAppender.RollingStyle;
                        if (rollingStyle != RollingMode.Date) continue;

                        string path = System.IO.Path.GetDirectoryName(rollingAppender.File);
                        DirectoryInfo directory = new DirectoryInfo(path);

                        //string pattern = this.GetDatePatten(rollingAppender.DatePattern);
                        string extension = System.IO.Path.GetExtension(rollingAppender.File);
                        List<FileInfo> files = directory.GetFiles($"*{extension}")
                            .Where(x => DateTime.Now.Subtract(x.CreationTime).TotalDays > maxSizeRollBackups)
                            .OrderBy(x => x.CreationTime).ToList();
                        foreach (FileInfo file in files)
                        {
                            Trace.WriteLine($"Delete {file.FullName}");
                            file.Delete();
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                Trace.WriteLine(ex.ToString());
            }
        }

        private string GetDatePatten(string name)
        {
            StringBuilder sb = new StringBuilder();
            string pattern = @"'([^']*)'";

            MatchCollection matches = Regex.Matches(name, pattern);
            for (int i = 0; i < matches.Count; i++)
            {
                sb.Append(matches[i].Groups[1].Value);
                if (i + 1 < matches.Count) sb.Append("*");
            }

            return sb.ToString();
        }

        public static string GetCallStack()
        {
            StringBuilder sb = new StringBuilder();
            try
            {
                StackTrace trace = new StackTrace();
                for (int i = 0; i < trace.FrameCount; i++)
                {
                    StackFrame frame = trace.GetFrame(i);
                    MethodBase method = frame.GetMethod();
                    sb.AppendLine($"{method?.DeclaringType?.Name}.{method.Name}");
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.ToString());
            }

            return sb.ToString();
        }

        #endregion

        #endregion

    }

}

