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

namespace Mercury.Logging.Loggers
{
    /// <summary>
    /// Provides durable log entries by writing to an alternate logger that writes to the disk.
    /// </summary>
    public class DurableLogger
        : Logger,IDisposable
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="Mercury.Logging.Loggers.DurableLogger"/> 
        /// class with the specified primary logger and file path to use for the durable logger.
        /// </summary>
        /// <param name="primary">The primary logger.</param>
        /// <param name="durableLogPath">The full path to the file to use for the durable log.</param>
        public DurableLogger(Logger primary, string durableLogPath)
            : this(primary, durableLogPath, Encoding.Unicode)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="Mercury.Logging.Loggers.DurableLogger"/> 
        /// class with the specified primary logger and the durable log file path and encoding.
        /// </summary>
        /// <param name="primary">The primary logger.</param>
        /// <param name="durableLogPath">The full path to the file to use for the durable log.</param>
        /// <param name="encoding">The text encoding to use for the durable log file.</param>
        public DurableLogger(Logger primary, string durableLogPath, Encoding encoding)
            : this(primary, durableLogPath, encoding, DurabilityMode.WriteThrough, 0)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="Mercury.Logging.Loggers.DurableLogger"/> class with 
        /// the specified primary logger, alternate logger, durability mode, and batch threshold.
        /// </summary>
        /// <param name="primary">The primary logger.</param>
        /// <param name="durableLogPath">The full path to the file to use for the durable log.</param>
        /// <param name="encoding">The text encoding to use for the durable log file.</param>
        /// <param name="mode">The durability mode for this logger.</param>
        /// <param name="threshold">The minimum number of write operations that can trigger a flush to the primary logger.  
        /// This is only used when the durability mode is set to DurabilityMode.BatchThrough.</param>
        public DurableLogger(Logger primary, string durableLogPath,Encoding encoding, DurabilityMode mode, int threshold)
            : base("")
        {
            if (primary == null)
                throw new ArgumentNullException("primary");
            if (durableLogPath == null)
                throw new ArgumentNullException("durableLogPath");
            this.Primary = primary;
            this.DurableLogPath = durableLogPath;
            this.DurableLogEncoding = encoding;
            this._durable = new FileLogger(durableLogPath, encoding, false);
            this.Mode = mode;
            this.Threshold = threshold;
        }

        private const string DEFAULT_DELIMITER = "__%%";
        private static readonly int NewLineLength = Environment.NewLine.Length;
        private int m_writeCount;
        private object _lock = new object();
        private FileLogger _durable;
        private string _delimiter = DurableLogger.DEFAULT_DELIMITER;
        private bool isDisposed;
        private long _readPosition;

        #region DurableLogger.DurabilityMode
        /// <summary>
        /// Describes the durability setting used by a <see cref="Mercury.Logging.Loggers.DurableLogger"/>.
        /// </summary>
        public enum DurabilityMode
        {
            /// <summary>
            /// Writes to the durable disk log and then to the primary log.
            /// </summary>
            WriteThrough,

            /// <summary>
            /// Writes to durable disk storage.  Data can be flushed manually to the primary log.
            /// </summary>
            WriteDisk,

            /// <summary>
            /// Writes to the durable disk log up to a specified threshold, then flushes to the primary log.
            /// </summary>
            BatchThrough
        }
        #endregion

        /// <summary>
        /// Gets the primary logger.
        /// </summary>
        public Logger Primary { get; private set; }

        /// <summary>
        /// Gets the full path to the file used to provide a durable log.
        /// </summary>
        public string DurableLogPath { get; private set; }

        /// <summary>
        /// Gets the text encoding used by the durable log file.
        /// </summary>
        public Encoding DurableLogEncoding { get; private set; }

        /// <summary>
        /// Gets the durability mode for this logger.
        /// </summary>
        public DurabilityMode Mode { get; private set; }

        /// <summary>
        /// Gets or sets the minimum number of write operations that can trigger a flush to the primary logger.  
        /// This is only used when the durability mode is set to DurabilityMode.BatchThrough.
        /// </summary>
        public int Threshold { get; set; }

        /// <summary>
        /// Gets or sets the delimiter string used to separate log entries in the durable log file.
        /// </summary>
        public string Delimiter
        {
            get { return this._delimiter; }
            set
            {
                if (string.IsNullOrEmpty(value))
                    throw new ArgumentException("A delimiter cannot be null or empty.");
                this._delimiter = value;
            }
        }

        /// <summary>
        /// Flushes any output buffers, forcing data to be written to the log.
        /// </summary>
        public override void Flush()
        {
            lock (this._lock)
            {
                this._UnsafeFlush();
            }
        }

        /// <summary>
        /// Logs the specified message string.
        /// </summary>
        /// <param name="message">The message string to log.</param>
        /// <returns>True if logging was successful; otherwise, false.</returns>
        protected override bool WriteLog(string message)
        {
            switch (this.Mode)
            {
                case DurabilityMode.WriteThrough:
                    this._WriteToDurable(message);
                    this.Primary.Write(message);
                    return true;
                case DurabilityMode.WriteDisk:
                    this._WriteToDurable(message);
                    return true;
                case DurabilityMode.BatchThrough:
                    lock (this._lock)
                    {
                        this._WriteToDurable(message);
                        this.m_writeCount++;
                        if (this.m_writeCount >= this.Threshold)
                            this._UnsafeFlush();
                        return true;
                    }
            }
            return false;
        }

        private void _UnsafeFlush()
        {
            this._durable.Flush();
            if (this.Mode > DurabilityMode.WriteThrough)
            {
                this.m_writeCount = 0;
                this._ConsumeDurableLog();
            }
            this.Primary.Flush();
        }

        private void _ConsumeDurableLog()
        {
            StreamReader reader = this._durable.GetReader();
            long position = reader.BaseStream.Position;
            try
            {
                this._WriteFromDurableLog(reader);
            }
            finally
            {
                if (reader != null && reader.BaseStream != null)
                {
                    this._readPosition = reader.BaseStream.Position;
                    reader.BaseStream.Position = position;
                }
                reader = null;
            }
        }

        private void _WriteToDurable(string message)
        {
            var altMessage = string.Format("{1}{0}{2}{0}", Environment.NewLine, message, DEFAULT_DELIMITER);
            this._durable.Write(altMessage);
        }

        private void _WriteFromDurableLog(StreamReader reader)
        {
            reader.BaseStream.Position = this._readPosition;
            string line;
            StringBuilder sb = new StringBuilder();
            while ((line = reader.ReadLine()) != null)
            {
                if (line == this.Delimiter)
                {
                    this.Primary.Write(sb.ToString());
                    sb.Clear();
                    if (reader.ReadLine() == null)
                        break;
                }
                else
                {
                    sb.Append(line);
                }
            }
        }

        /// <summary>
        /// Dispsoses of the resources associated with this logger.
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// Dispsoses of the resources associated with this logger.
        /// </summary>
        /// <param name="disposing">True if called through the IDisposable.Dispose method; otherwise, false.</param>
        protected virtual void Dispose(bool disposing)
        {
            if (!this.isDisposed)
            {
                if (disposing)
                {
                    if (this._durable != null)
                        this._durable.Dispose();
                    IDisposable primaryDisp = this.Primary as IDisposable;
                    if (primaryDisp != null)
                        primaryDisp.Dispose();
                }
            }
            this.isDisposed = true;
        }
    }
}
