#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

namespace Volpe.Cafe.Logging
{
    /// <summary>
    /// Provides output file streams for logging errors, warnings, summary, and compliance findings of the compliance
    /// models.
    /// </summary>
    [Serializable]
    public sealed class Logger : ILogger, IDisposable
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="Logger"/> class using the specified paths for standard and
        /// debugging log files.
        /// </summary>
        /// <param name="logLevel">The level of logging to use.</param>
        /// <param name="logPath">The path where all standard log files will be stored.</param>
        /// <param name="debugLogPath">The path where all debugging log files will be stored.</param>
        /// <remarks>
        /// If the directories at <paramref name="logPath"/> and <paramref name="debugLogPath"/> do not exist, the
        /// constructor will attempt to create them. Afterwards, the constructor will attempt to open a "Summary.txt",
        /// "Warnings.txt", and "Errors.txt" log files. If any of these operations fail, the exceptions thrown by
        /// <see cref="Directory.CreateDirectory(string)"/> and <see cref="StreamWriter(string)"/> will be propagated
        /// to the caller.
        /// </remarks>
        /// <exception cref="ArgumentOutOfRangeException">log level is outside of allowed range.</exception>
        public Logger(LogLevel logLevel, string logPath, string debugLogPath)
        {
            if (logLevel < LogLevel.L1 || logLevel > LogLevel.L3) {
                ThrowHelper.InvalidLogLevel(nameof(logLevel));
            }

            this.LogLevel = logLevel;
            this.LogPath = logPath;
            this.DebugLogPath = debugLogPath;

            Directory.CreateDirectory(logPath);
            Directory.CreateDirectory(debugLogPath);

            this.Summary = new StreamWriter($"{logPath}\\Summary.txt");
            this.Warnings = new StreamWriter($"{logPath}\\Warnings.txt");
            this.Errors = new StreamWriter($"{logPath}\\Errors.txt");

            this.CfLogs = null;
            this.CtLogs = null;
            this.CustomLogs = new Dictionary<string, LogFileWriter>();
        }

        public object SyncRoot { get; } = new object();

        public bool IsDisposed { get; private set; }

        public LogLevel LogLevel { get; }

        public string LogPath { get; private set; }
        public string DebugLogPath { get; private set; }

        public TextWriter Summary { get; private set; }
        public TextWriter Warnings { get; private set; }
        public TextWriter Errors { get; private set; }

        /// <summary>
        /// Gets a list of file streams to use for logging of compliance findings. This value may be null.
        /// </summary>
        private List<BufferedComplianceWriter> CfLogs { get; set; }

        /// <summary>
        /// Gets a list of file streams to use for logging of credit trading transactions. This value may be null.
        /// </summary>
        private List<LogFileWriter> CtLogs { get; set; }

        /// <summary>
        /// Gets a dictionary of additional custom logs managed by the logger.
        /// </summary>
        private Dictionary<string, LogFileWriter> CustomLogs { get; set; }

        public void Close()
        {
            this.Dispose();
        }

        public void Dispose()
        {
            lock (this.SyncRoot) {
                if (this.IsDisposed) {
                    return;
                }

                this.Summary?.Close();
                this.Warnings?.Close();
                this.Errors?.Close();
                this.CfLogs?.ForEach(log => log?.Close());
                this.CtLogs?.ForEach(log => log?.Close());

                foreach (var log in this.CustomLogs.Values) {
                    log?.Close();
                }

                this.Summary = null;
                this.Warnings = null;
                this.Errors = null;
                this.CfLogs = null;
                this.CtLogs = null;
                this.CustomLogs = null;

                this.LogPath = null;
                this.DebugLogPath = null;

                this.IsDisposed = true;
            }

            GC.SuppressFinalize(this);
        }

        //----- compliance logs -----
        public void InitCfLogs(int logCount, int bufferCount)
        {
            if (this.IsDisposed) {
                ThrowHelper.LoggerDisposed();
            }
            if (this.LogLevel < LogLevel.L2 || this.CfLogs != null) {
                return;
            }
            if (logCount < 1) {
                ThrowHelper.LogCountTooSmall(nameof(logCount));
            }

            this.CfLogs = Enumerable.Range(0, logCount)
                .Select(i => new BufferedComplianceWriter($"{this.LogPath}\\cf_trace_sn{i}.txt", bufferCount, false))
                .ToList();
        }

        public BufferedComplianceWriter GetCfLog(int index)
        {
            if (this.IsDisposed) {
                ThrowHelper.LoggerDisposed();
            }

            return this.CfLogs?[index];
        }

        public void FlushCfLogs()
        {
            if (this.IsDisposed) {
                ThrowHelper.LoggerDisposed();
            }

            this.CfLogs?.ForEach(log => log?.Flush());
        }

        //----- credit trading logs -----
        public void InitCreditTradingLogs(int logCount)
        {
            if (this.IsDisposed) {
                ThrowHelper.LoggerDisposed();
            }
            if (this.LogLevel < LogLevel.L2 || this.CtLogs != null) {
                return;
            }
            if (logCount < 1) {
                ThrowHelper.LogCountTooSmall(nameof(logCount));
            }

            string[] headers = {
                "Scenario", "Iteration", "Mfr", "Program",
                "eCredits", "rCredits", "eRC", "eYear", "outECredits", "uRC", "uYear", "outRCredits"
            };
            this.CtLogs = Enumerable.Range(0, logCount)
                .Select(i => this.GetCustomLog($"credit_trades_sn{i}.txt", headers, persistent: false))
                .ToList();
        }

        public LogFileWriter GetCreditTradingLog(int index)
        {
            if (this.IsDisposed) {
                ThrowHelper.LoggerDisposed();
            }

            return this.CtLogs?[index];
        }

        //----- custom & debug logs -----
        public LogFileWriter GetCustomLog(string fileName, string[] headers, bool persistent = true)
        {
            return this.GetCustomLogHelper(this.LogPath, fileName, headers, persistent, LogLevel.L2);
        }

        public LogFileWriter GetDebugLog(string fileName, string[] headers, bool persistent = true)
        {
            return this.GetCustomLogHelper(this.DebugLogPath, fileName, headers, persistent, LogLevel.L3);
        }

        private LogFileWriter GetCustomLogHelper(
            string basePath, string fileName, string[] headers, bool persistent, LogLevel minLogLevel)
        {
            if (this.IsDisposed) {
                ThrowHelper.LoggerDisposed();
            }
            if (this.LogLevel < minLogLevel) {
                return null;
            }

            string path = Path.GetFullPath(Path.Combine(basePath, fileName));
            bool logExists = this.CustomLogs.TryGetValue(path, out var log);

            if (!logExists || log.IsDisposed) {
                log = LogFile.OpenWrite(path, headers);
                if (persistent) {
                    this.CustomLogs.Add(path, log);
                }
            }

            return log;
        }

        //----- helpers for logging Summary/Warning/Error -----
        public void LogSummary(string value)
        {
            this.WriteLog(this.Summary, value);
        }
        public void LogWarning(string value)
        {
            this.WriteLog(this.Warnings, value);
        }
        public void LogError(string value)
        {
            this.WriteLog(this.Errors, value);
        }

        public void SafeLogSummary(string value)
        {
            this.SafeWriteLog(this.Summary, value);
        }
        public void SafeLogWarning(string value)
        {
            this.SafeWriteLog(this.Warnings, value);
        }
        public void SafeLogError(string value)
        {
            this.SafeWriteLog(this.Errors, value);
        }

        private void WriteLog(TextWriter log, string value)
        {
            if (this.IsDisposed) {
                ThrowHelper.LoggerDisposed();
            }

            if (log != null) {
                log.Write(value);
                log.Write(LogFile.NewLine);
            }
        }

        private void SafeWriteLog(TextWriter log, string value)
        {
            if (this.IsDisposed) {
                ThrowHelper.LoggerDisposed();
            }

            lock (this.SyncRoot) {
                this.WriteLog(log, value);
            }
        }

    }
}
