using EonaCat.LogStack.Core;
using EonaCat.LogStack.EonaCatLogStackCore;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace EonaCat.LogStack.Flows
{
    // 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.

    /// <summary>
    /// An in-process circular buffer that retains the last N log events in memory.
    /// </summary>
    public sealed class RollingBufferFlow : FlowBase
    {
        private readonly LogEvent[] _ring;
        private readonly int _capacity;
        private long _head;   // next write position (mod capacity)
        private long _count;  // total ever written (not capped)
        private readonly object _ringLock = new object();

        private readonly LogLevel _triggerLevel;
        private readonly IFlow _triggerTarget;
        private readonly int _preContextLines;

        /// <param name="capacity">Maximum number of events to retain.</param>
        /// <param name="minimumLevel">Minimum level to store in the buffer.</param>
        /// <param name="triggerLevel">
        ///   When a log event reaches this level or above, the current buffer
        ///   contents are immediately forwarded to <paramref name="triggerTarget"/>.
        ///   Set to <c>LogLevel.None</c> (or omit) to disable.
        /// </param>
        /// <param name="triggerTarget">
        ///   Flow to forward the buffered context to when the trigger fires.
        ///   Can be null even when <paramref name="triggerLevel"/> is set.
        /// </param>
        /// <param name="preContextLines">
        ///   How many buffered lines to forward before the triggering event.
        ///   Defaults to entire buffer (int.MaxValue).
        /// </param>
        public RollingBufferFlow(
            int capacity = 500,
            LogLevel minimumLevel = LogLevel.Trace,
            LogLevel triggerLevel = LogLevel.Error,
            IFlow triggerTarget = null,
            int preContextLines = int.MaxValue)
            : base("RollingBuffer", minimumLevel)
        {
            if (capacity < 1)
            {
                throw new ArgumentOutOfRangeException("capacity", "Must be >= 1.");
            }

            _capacity = capacity;
            _ring = new LogEvent[capacity];
            _triggerLevel = triggerLevel;
            _triggerTarget = triggerTarget;
            _preContextLines = preContextLines < 0 ? 0 : preContextLines;
        }

        /// <summary>Returns a snapshot of the buffer from oldest to newest.</summary>
        public LogEvent[] GetAll()
        {
            lock (_ringLock)
            {
                long total = Math.Min(_count, _capacity);
                if (total == 0)
                {
                    return new LogEvent[0];
                }

                LogEvent[] result = new LogEvent[total];
                long start = (_count > _capacity) ? _head : 0;

                for (long i = 0; i < total; i++)
                {
                    result[i] = _ring[(start + i) % _capacity];
                }

                return result;
            }
        }

        /// <summary>Returns the N most recent events.</summary>
        public LogEvent[] GetRecent(int n)
        {
            lock (_ringLock)
            {
                long total = Math.Min(_count, _capacity);
                long take = Math.Min(n, total);
                if (take <= 0)
                {
                    return new LogEvent[0];
                }

                LogEvent[] result = new LogEvent[take];
                long start = (_head - take + _capacity * 2) % _capacity;

                for (long i = 0; i < take; i++)
                {
                    result[i] = _ring[(start + i) % _capacity];
                }

                return result;
            }
        }

        /// <summary>Returns all events at or above <paramref name="level"/>.</summary>
        public LogEvent[] GetByLevel(LogLevel level)
        {
            LogEvent[] all = GetAll();
            List<LogEvent> filtered = new List<LogEvent>(all.Length);
            foreach (LogEvent e in all)
            {
                if (e.Level >= level)
                {
                    filtered.Add(e);
                }
            }

            return filtered.ToArray();
        }

        /// <summary>Number of events currently held in the buffer.</summary>
        public int Count
        {
            get { lock (_ringLock) { return (int)Math.Min(_count, _capacity); } }
        }

        /// <summary>Total events ever written (may exceed capacity).</summary>
        public long TotalWritten { get { return Interlocked.Read(ref _count); } }

        /// <summary>Clears all stored events.</summary>
        public void Clear()
        {
            lock (_ringLock)
            {
                Array.Clear(_ring, 0, _capacity);
                _head = 0;
                _count = 0;
            }
        }

        public override Task<WriteResult> BlastAsync(
            LogEvent logEvent,
            CancellationToken cancellationToken = default(CancellationToken))
        {
            if (!IsEnabled || !IsLogLevelEnabled(logEvent))
            {
                return Task.FromResult(WriteResult.LevelFiltered);
            }

            bool triggered = false;
            lock (_ringLock)
            {
                _ring[_head % _capacity] = logEvent;
                _head = (_head + 1) % _capacity;
                _count++;

                if (_triggerTarget != null
                    && logEvent.Level >= _triggerLevel
                    && _triggerLevel != LogLevel.None)
                {
                    triggered = true;
                }
            }

            Interlocked.Increment(ref BlastedCount);

            if (triggered)
            {
                ForwardToTarget(logEvent);
            }

            return Task.FromResult(WriteResult.Success);
        }

        public override Task<WriteResult> BlastBatchAsync(
            ReadOnlyMemory<LogEvent> logEvents,
            CancellationToken cancellationToken = default(CancellationToken))
        {
            if (!IsEnabled)
            {
                return Task.FromResult(WriteResult.FlowDisabled);
            }

            foreach (LogEvent e in logEvents.ToArray())
            {
                BlastAsync(e, cancellationToken);
            }

            return Task.FromResult(WriteResult.Success);
        }

        public override Task FlushAsync(CancellationToken cancellationToken = default(CancellationToken))
            => Task.FromResult(0);

        public override ValueTask DisposeAsync()
        {
            IsEnabled = false;
            Clear();
            return base.DisposeAsync();
        }

        private void ForwardToTarget(LogEvent triggeringEvent)
        {
            if (_triggerTarget == null)
            {
                return;
            }

            try
            {
                // Grab context window
                int take = _preContextLines == int.MaxValue ? _capacity : _preContextLines;
                LogEvent[] context = GetRecent(take);

                foreach (LogEvent ev in context)
                {
                    if (ev.Equals(triggeringEvent))
                    {
                        continue; // avoid duplicate
                    }

                    _triggerTarget.BlastAsync(ev).GetAwaiter().GetResult();
                }

                // Always forward the triggering event last
                _triggerTarget.BlastAsync(triggeringEvent).GetAwaiter().GetResult();
            }
            catch (Exception ex)
            {
                Console.Error.WriteLine("[RollingBufferFlow] Trigger forward error: " + ex.Message);
            }
        }
    }
}