using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using EonaCat.Logger.Managers;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace EonaCat.Logger.EonaCatCoreLogger.Internal
{
    // This file is part of the EonaCat project(s) which is released under the Apache License.
    // See the LICENSE file or go to https://EonaCat.com/License for full license details.

    public abstract class BatchingLoggerProvider : ILoggerProvider
    {
        protected DateTimeOffset CurrentDateTimeOffset => UseLocalTime ? DateTimeOffset.Now : DateTimeOffset.UtcNow;
        protected DateTime CurrentDateTme => UseLocalTime ? DateTime.Now : DateTime.UtcNow;

        private readonly List<LogMessage> _currentBatch = new List<LogMessage>();
        private readonly TimeSpan _interval;
        private readonly int _batchSize;

        private ConcurrentQueue<LogMessage> _messageQueue;
        private Task _outputTask;
        private CancellationTokenSource _cancellationTokenSource;
        private LoggerSettings _loggerSettings;

        protected bool UseLocalTime { get; set; }

        protected BatchingLoggerProvider(IOptions<BatchingLoggerOptions> options)
        {
            BatchingLoggerOptions loggerOptions = options.Value;

            if (loggerOptions.FlushPeriod <= TimeSpan.Zero)
            {
                throw new ArgumentOutOfRangeException(nameof(loggerOptions.FlushPeriod), $"{nameof(loggerOptions.FlushPeriod)} must be longer than zero.");
            }

            if (options.Value is FileLoggerOptions fileLoggerOptions)
            {
                UseLocalTime = fileLoggerOptions.UseLocalTime;
            }

            _interval = loggerOptions.FlushPeriod;
            _batchSize = loggerOptions.BatchSize;

            Start();
        }

        protected LoggerSettings LoggerSettings
        {
            get
            {
                if (_loggerSettings != null) return _loggerSettings;

                _loggerSettings = new LoggerSettings();
                _loggerSettings.UseLocalTime = UseLocalTime;
                return _loggerSettings;
            }

            set => _loggerSettings = value;
        }

        protected abstract Task WriteMessagesAsync(IEnumerable<LogMessage> messages, CancellationToken token);

        private async Task ProcessLogQueueAsync(object state)
        {
            var startupMessage = $"{DllInfo.ApplicationName} started.{Environment.NewLine}";
            startupMessage = LogHelper.FormatMessageWithHeader(LoggerSettings, ELogType.INFO, startupMessage, CurrentDateTme);

            AddMessage(DateTimeOffset.Now, startupMessage);

            while (!_cancellationTokenSource.IsCancellationRequested)
            {
                int limit = _batchSize <= 0 ? int.MaxValue : _batchSize;

                while (limit > 0 && _messageQueue.TryDequeue(out LogMessage message))
                {
                    _currentBatch.Add(message);
                    limit--;
                }

                if (_currentBatch.Count > 0)
                {
                    bool isBatchWritten = false;
                    try
                    {
                        await WriteMessagesAsync(_currentBatch, _cancellationTokenSource.Token).ConfigureAwait(false);
                        isBatchWritten = true;
                    }
                    catch
                    {
                        // ignored
                    }

                    if (isBatchWritten)
                    {
                        _currentBatch.Clear();
                    }
                }

                await IntervalAsync(_interval, _cancellationTokenSource.Token).ConfigureAwait(false);
            }
            await WriteMessagesAsync(new List<LogMessage> { new LogMessage { Message = $"[{DllInfo.ApplicationName}] {DllInfo.ApplicationName} stopped.{Environment.NewLine}", Timestamp = CurrentDateTimeOffset } }, _cancellationTokenSource.Token).ConfigureAwait(false);
        }

        protected virtual Task IntervalAsync(TimeSpan interval, CancellationToken cancellationToken)
        {
            return Task.Delay(interval, cancellationToken);
        }

        internal void AddMessage(DateTimeOffset timestamp, string message)
        {
            _messageQueue.Enqueue(new LogMessage { Message = message, Timestamp = timestamp });
        }

        private void Start()
        {
            _messageQueue = new ConcurrentQueue<LogMessage>();

            _cancellationTokenSource = new CancellationTokenSource();
            _outputTask = Task.Factory.StartNew(
                ProcessLogQueueAsync,
                null,
                TaskCreationOptions.LongRunning);
        }

        private async Task StopAsync()
        {
            _cancellationTokenSource.Cancel();

            try
            {
                await _outputTask.ConfigureAwait(false);
            }
            catch (TaskCanceledException)
            {
            }
            catch (AggregateException exception) when (exception.InnerExceptions.Count == 1 && exception.InnerExceptions[0] is TaskCanceledException)
            {
            }
        }

        public void Dispose()
        {
#pragma warning disable CS4014
            StopAsync();
#pragma warning restore CS4014
            GC.SuppressFinalize(this);
        }

        ~BatchingLoggerProvider()
        {
            Dispose();
        }

        public ILogger CreateLogger(string categoryName)
        {
            return new BatchingLogger(this, categoryName, LoggerSettings);
        }
    }
}