using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text;
using ZeroLevel.Services.FileSystem;

namespace ZeroLevel.Logging
{
    public sealed class TextFileLoggerOptions
    {
        public TextFileLoggerOptions()
        {
            Folder = null!;
            LimitFileSize = 0;
            TextEncoding = DEFAULT_ENCODING;
            RemoveOlderThen = TimeSpan.MinValue;
            RemoveOldFiles = false;
            ZipOldFiles = false;
        }

        public static readonly Encoding DEFAULT_ENCODING = Encoding.UTF8;
        internal string Folder { get; private set; }
        internal long LimitFileSize { get; private set; }
        internal Encoding TextEncoding { get; private set; }

        /// <summary>
        /// Delete files older than (aging period)
        /// </summary>
        internal TimeSpan RemoveOlderThen { get; private set; }

        /// <summary>
        /// Delete outdate files
        /// </summary>
        internal bool RemoveOldFiles { get; private set; }

        /// <summary>
        /// Archive files
        /// </summary>
        internal bool ZipOldFiles { get; private set; }

        internal TextFileLoggerOptions Commit()
        {
            if (string.IsNullOrWhiteSpace(Folder))
            {
                throw new ArgumentException("Not set log folder path");
            }
            return this;
        }

        /// <summary>
        /// Enable automatic archiving
        /// </summary>
        public TextFileLoggerOptions EnableAutoArchiving()
        {
            this.ZipOldFiles = true;
            return this;
        }

        /// <summary>
        ///Enable automatic deletion of outdate files.
        /// </summary>
        /// <param name="age">The age of the log file at which removal is required</param>
        public TextFileLoggerOptions EnableAutoCleaning(TimeSpan age)
        {
            this.RemoveOldFiles = true;
            this.RemoveOlderThen = age;
            return this;
        }

        public TextFileLoggerOptions SetFolderPath(string folder)
        {
            if (folder.IndexOf(':') < 0)
            {
                this.Folder = Path.Combine(Configuration.BaseDirectory, folder);
            }
            else
            {
                this.Folder = folder;
            }
            return this;
        }

        public TextFileLoggerOptions SetMaximumFileSizeInKb(long size)
        {
            this.LimitFileSize = size;
            return this;
        }

        public TextFileLoggerOptions SetEncoding(Encoding encoding)
        {
            this.TextEncoding = encoding;
            return this;
        }

        internal static TextFileLoggerOptions CreateOptionsBy(IConfiguration config, string path, string logPrefix)
        {
            var options = new TextFileLoggerOptions().
                SetFolderPath(path);

            config.DoWithFirst<long>($"{logPrefix}backlog", backlog =>
            {
                if (backlog > 0)
                {
                    Log.Backlog(backlog);
                }
            });
            config.DoWithFirst<bool>($"{logPrefix}archive", enable =>
            {
                if (enable)
                {
                    options.EnableAutoArchiving();
                }
            });
            config.DoWithFirst<int>($"{logPrefix}sizeinkb", size =>
            {
                if (size >= 1)
                {
                    options.SetMaximumFileSizeInKb(size);
                }
            });

            config.DoWithFirst<int>($"{logPrefix}cleanolderdays", days =>
            {
                if (days > 0)
                {
                    options.EnableAutoCleaning(TimeSpan.FromDays(days));
                }
            });
            return options;
        }
    }

    public sealed class TextFileLogger : ILogger
    {
        #region Fields

        private readonly TextFileLoggerOptions _options;

        private int _todayCountLogFiles = 0;

        /// <summary>
        /// Current log file
        /// </summary>
        private string _currentLogFile;

        /// <summary>
        /// Stream to output to file
        /// </summary>
        private TextWriter _writer;

        /// <summary>
        /// Lock on re-create file
        /// </summary>
        private readonly object _fileRecreating = new object();

        private readonly HashSet<long> _taskKeys = new HashSet<long>();

        public static readonly Encoding DEFAULT_ENCODING = Encoding.UTF8;

        #endregion Fields

        #region Ctors

        /// <summary>
        /// Constructor indicating the directory for recording log files, the encoding is set to Unicode by default.
        /// </summary>
        public TextFileLogger(TextFileLoggerOptions options)
        {
            _options = options.Commit();
            if (!Directory.Exists(_options.Folder))
            {
                var dir = Directory.CreateDirectory(_options.Folder);
                if (dir.Exists == false)
                {
                    throw new ArgumentException($"Can't create or found directory '{_options.Folder}'");
                }
            }
            RecreateLogFile();
            // Maintenance tasks
            if (_options.LimitFileSize > 0)
            {
                _taskKeys.Add(Sheduller.RemindEvery(TimeSpan.FromSeconds(20), CheckRecreateFileLogByOversize));
            }
            if (_options.RemoveOldFiles)
            {
                _taskKeys.Add(Sheduller.RemindEvery(TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(60), RemoveOldFiles));
            }
            _taskKeys.Add(Sheduller.RemindEveryNonlinearPeriod(() =>
                (DateTime.Now.AddDays(1).Date - DateTime.Now).Add(TimeSpan.FromMilliseconds(100)),
                RecreateLogFile));
        }

        #endregion Ctors

        #region Private member

        private void RemoveOldFiles()
        {
            try
            {
                var dir = new DirectoryInfo(_options.Folder);
                dir.
                    GetFiles().
                    Do(files =>
                    {
                        foreach (var file in files.Where(f => (DateTime.Now - f.CreationTime) > _options.RemoveOlderThen).ToArray())
                        {
                            file.Delete();
                        }
                    });
                dir = null!;
            }
            catch { }
        }

        /// <summary>
        /// Closing the current log
        /// </summary>
        private void CloseCurrentWriter()
        {
            if (_writer != null!)
            {
                _writer.Flush();
                _writer.Close();
                _writer.Dispose();
                _writer = null!;
            }
        }

        private string GetNextFileName()
        {
            string fileName = Path.Combine(_options.Folder, string.Format(CultureInfo.CurrentCulture, "{0:yyyyMMdd}_{1:D4}.txt", DateTime.Now, _todayCountLogFiles));
            if (_options.LimitFileSize > 0)
            {
                if (!File.Exists(fileName))
                {
                    _todayCountLogFiles = 0;
                }
                else
                {
                    while (File.Exists(fileName))
                    {
                        var length = (new FileInfo(fileName).Length >> 10);
                        if (length >= _options.LimitFileSize)
                        {
                            _todayCountLogFiles++;
                            fileName = Path.Combine(_options.Folder, string.Format(CultureInfo.CurrentCulture, "{0:yyyyMMdd}_{1:D4}.txt", DateTime.Now, _todayCountLogFiles));
                        }
                        else
                        {
                            break;
                        }
                    }
                }
            }
            return fileName;
        }

        private void CheckRecreateFileLogByOversize()
        {
            var fi = new FileInfo(_currentLogFile);
            if (fi.Exists && (fi.Length >> 10) >= _options.LimitFileSize)
            {
                RecreateLogFile();
            }
            fi = null!;
        }

        /// <summary>
        /// Checking the name of the log file (changes when the date changes to the next day)
        /// </summary>
        private void RecreateLogFile()
        {
            lock (_fileRecreating)
            {
                var nextFileName = GetNextFileName();
                CloseCurrentWriter();
                Stream stream = null!;
                PackOldLogFile(_currentLogFile);
                try
                {
                    _currentLogFile = nextFileName;
                    stream = new FileStream(_currentLogFile, FileMode.Append, FileAccess.Write, FileShare.Read);
                    _writer = new StreamWriter(stream, _options.TextEncoding);
                    stream = null!;
                }
                catch
                {
                    if (stream != null!)
                    {
                        stream.Dispose();
                    }
                    throw;
                }
            }
        }

        private void PackOldLogFile(string filePath)
        {
            if (null != filePath && File.Exists(filePath) && _options.ZipOldFiles)
            {
                using (var stream = new FileStream($"{filePath}.zip", FileMode.Create))
                {
                    using (var zipStream = new GZipStream(stream, CompressionLevel.Optimal, false))
                    {
                        var buffer = new byte[1024 * 1024];
                        int count = 0;
                        using (BinaryReader reader = new BinaryReader(File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.None)))
                        {
                            while ((count = reader.Read(buffer, 0, buffer.Length)) > 0)
                            {
                                zipStream.Write(buffer, 0, count);
                            }
                            reader.Close();
                        }
                        buffer = null!;
                    }
                    stream.Close();
                }
                File.Delete(filePath);
            }
        }

        #endregion Private member

        #region ILog

        public void Write(LogLevel level, string message)
        {
            if (false == _disposed)
            {
                lock (_fileRecreating)
                {
                    if (level == LogLevel.Raw)
                    {
                        _writer.WriteLine(message);
                    }
                    else
                    {
                        var meta = string.Format("[{0:dd'.'MM'.'yyyy HH':'mm':'ss} {1}]\t", DateTime.Now, LogLevelNameMapping.CompactName(level));
                        _writer.Write(meta);
                        _writer.WriteLine(message);
                    }
                    _writer.Flush();
                }
            }
        }

        #endregion ILog

        #region IDisposable

        private bool _disposed = false;

        public void Dispose()
        {
            foreach (var tk in _taskKeys)
            {
                Sheduller.Remove(tk);
            }
            if (false == _disposed)
            {
                _disposed = true;
                CloseCurrentWriter();
            }
        }

        #endregion IDisposable
    }

    public sealed class FileLogger : ILogger
    {
        #region Fields

        /// <summary>
        /// Stream to output to file
        /// </summary>
        private TextWriter _writer;

        public static readonly Encoding DEFAULT_ENCODING = Encoding.UTF8;

        #endregion Fields

        #region Ctors

        public FileLogger(string path)
        {
            CreateLogFile(PreparePath(path));
        }

        #endregion Ctors

        #region Private member

        private static void PrepareFolder(string path)
        {
            if (Directory.Exists(path) == false)
            {
                Directory.CreateDirectory(path);
            }
        }

        private static string PreparePath(string path)
        {
            if (path.IndexOf(':') == -1)
            {
                path = Path.Combine(Configuration.BaseDirectory, path);
            }
            path = FSUtils.PathCorrection(path);
            PrepareFolder(Path.GetDirectoryName(path));
            return path;
        }

        /// <summary>
        /// Closing the current log
        /// </summary>
        private void CloseCurrentWriter()
        {
            if (_writer != null!)
            {
                _writer.Flush();
                _writer.Close();
                _writer.Dispose();
                _writer = null!;
            }
        }

        private void CreateLogFile(string path)
        {
            Stream stream = null!;
            try
            {
                stream = new FileStream(path, FileMode.Append, FileAccess.Write, FileShare.Read);
                _writer = new StreamWriter(stream, DEFAULT_ENCODING);
                stream = null!;
            }
            catch
            {
                if (stream != null!)
                {
                    stream.Dispose();
                }
                throw;
            }
        }

        #endregion Private member

        #region ILog

        public void Write(LogLevel level, string message)
        {
            if (false == _disposed)
            {
                if (level == LogLevel.Raw)
                {
                    _writer.WriteLine(message);
                }
                else
                {
                    var meta = string.Format("[{0:dd'.'MM'.'yyyy HH':'mm':'ss} {1}]\t", DateTime.Now, LogLevelNameMapping.CompactName(level));
                    _writer.Write(meta);
                    _writer.WriteLine(message);
                }
                _writer.Flush();
            }
        }

        #endregion ILog

        #region IDisposable

        private bool _disposed = false;

        public void Dispose()
        {
            if (false == _disposed)
            {
                _disposed = true;
                CloseCurrentWriter();
            }
        }

        #endregion IDisposable
    }
}