using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
using RoboSharp.Interfaces;
using RoboSharp.EventArgObjects;
using RoboSharp.Results;
using WhereToAdd = RoboSharp.Results.ProgressEstimator.WhereToAdd;
using System.Runtime.CompilerServices;

namespace RoboSharp.Results
{
    /// <summary>
    /// Updates the Statistics every 250ms
    /// </summary>
    /// <remarks>
    /// <see href="https://github.com/tjscience/RoboSharp/wiki/RoboQueueProgressEstimator"/>
    /// </remarks>
    internal class RoboQueueProgressEstimator : IProgressEstimator, IResults, IDisposable
    {
        #region < Constructors >

        internal RoboQueueProgressEstimator()
        {
            tmpDirs = new Statistic(Statistic.StatType.Directories);
            tmpFiles = new Statistic(Statistic.StatType.Files);
            tmpBytes = new Statistic(Statistic.StatType.Bytes);

            tmpDirs.EnablePropertyChangeEvent = false;
            tmpFiles.EnablePropertyChangeEvent = false;
            tmpBytes.EnablePropertyChangeEvent = false;
        }

        #endregion

        #region < Private Members >

        //ThreadSafe Bags/Queues
        private readonly ConcurrentDictionary<IProgressEstimator, object> SubscribedStats = new ConcurrentDictionary<IProgressEstimator, object>();

        //Stats
        private readonly Statistic DirStatField = new Statistic(Statistic.StatType.Directories, "Directory Stats Estimate");
        private readonly Statistic FileStatsField = new Statistic(Statistic.StatType.Files, "File Stats Estimate");
        private readonly Statistic ByteStatsField = new Statistic(Statistic.StatType.Bytes, "Byte Stats Estimate");

        //Add Tasks
        private int UpdatePeriodInMilliSecond = 250;
        private readonly Statistic tmpDirs;
        private readonly Statistic tmpFiles;
        private readonly Statistic tmpBytes;
        private DateTime NextUpdate = DateTime.Now;
        private readonly object StatLock = new object();
        private readonly object UpdateLock = new object();
        private bool disposedValue;

        #endregion

        #region < Public Properties > 

        /// <summary>
        /// Estimate of current number of directories processed while the job is still running. <br/>
        /// Estimate is provided by parsing of the LogLines produces by RoboCopy.
        /// </summary>
        public IStatistic DirectoriesStatistic => DirStatField;

        /// <summary>
        /// Estimate of current number of files processed while the job is still running. <br/>
        /// Estimate is provided by parsing of the LogLines produces by RoboCopy.
        /// </summary>
        public IStatistic FilesStatistic => FileStatsField;

        /// <summary>
        /// Estimate of current number of bytes processed while the job is still running. <br/>
        /// Estimate is provided by parsing of the LogLines produces by RoboCopy.
        /// </summary>
        public IStatistic BytesStatistic => ByteStatsField;

        RoboCopyExitStatus IResults.Status => new RoboCopyExitStatus((int)GetExitCode());

        /// <inheritdoc cref="IProgressEstimator.ValuesUpdated"/>
        public event ProgressEstimator.UIUpdateEventHandler ValuesUpdated;

        #endregion

        #region < Public Methods >

        /// <summary>
        /// Parse this object's stats into a <see cref="RoboCopyExitCodes"/> enum.
        /// </summary>
        /// <returns></returns>
        public RoboCopyExitCodes GetExitCode()
        {
            Results.RoboCopyExitCodes code = 0;

            //Files Copied
            if (FilesStatistic.Copied > 0)
                code |= Results.RoboCopyExitCodes.FilesCopiedSuccessful;

            //Extra
            if (DirectoriesStatistic.Extras > 0 || FilesStatistic.Extras > 0)
                code |= Results.RoboCopyExitCodes.ExtraFilesOrDirectoriesDetected;

            //MisMatch
            if (DirectoriesStatistic.Mismatch > 0 || FilesStatistic.Mismatch > 0)
                code |= Results.RoboCopyExitCodes.MismatchedDirectoriesDetected;

            //Failed
            if (DirectoriesStatistic.Failed > 0 || FilesStatistic.Failed > 0)
                code |= Results.RoboCopyExitCodes.SomeFilesOrDirectoriesCouldNotBeCopied;

            return code;

        }

        #endregion

        #region < Counting Methods ( private ) >

        /// <summary>
        /// Subscribe to the update events of a <see cref="ProgressEstimator"/> object
        /// </summary>
        internal void BindToProgressEstimator(IProgressEstimator estimator)
        {
            if (!SubscribedStats.ContainsKey(estimator))
            {
                SubscribedStats.TryAdd(estimator, null);
                estimator.ValuesUpdated += Estimator_ValuesUpdated;
            }
        }

        private void Estimator_ValuesUpdated(IProgressEstimator sender, IProgressEstimatorUpdateEventArgs e)
        {
            Statistic bytes = null;
            Statistic files = null;
            Statistic dirs = null;
            bool updateReqd = false;

            //Wait indefinitely until the tmp stats are released -- Previously -> lock(StatLock) { }
            Monitor.Enter(StatLock);
            
            //Update the Temp Stats
            tmpBytes.AddStatistic(e.ValueChange_Bytes);
            tmpFiles.AddStatistic(e.ValueChange_Files);
            tmpDirs.AddStatistic(e.ValueChange_Directories);

            //Check if an update should be pushed to the public fields
            //If another thread already has this locked, then it is pushing out an update -> This thread exits the method after releasing StatLock
            if (Monitor.TryEnter(UpdateLock))
            {
                updateReqd = DateTime.Now >= NextUpdate && (tmpFiles.NonZeroValue || tmpDirs.NonZeroValue || tmpBytes.NonZeroValue);
                if (updateReqd)
                {
                    if (tmpBytes.NonZeroValue)
                    {
                        bytes = tmpBytes.Clone();
                        tmpBytes.Reset();
                    }

                    if (tmpFiles.NonZeroValue)
                    {
                        files = tmpFiles.Clone();
                        tmpFiles.Reset();
                    }

                    if (tmpDirs.NonZeroValue)
                    {
                        dirs = tmpDirs.Clone();
                        tmpDirs.Reset();
                    }
                }
            }

            Monitor.Exit(StatLock); //Release the tmp Stats lock

            if (updateReqd)
            {
                // Perform the Add Events
                if (bytes != null) ByteStatsField.AddStatistic(bytes);
                if (files != null) FileStatsField.AddStatistic(files);
                if (dirs != null) DirStatField.AddStatistic(dirs);
                //Raise the event, then update the NextUpdate time period
                ValuesUpdated?.Invoke(this, new IProgressEstimatorUpdateEventArgs(this, bytes, files, dirs));
                NextUpdate = DateTime.Now.AddMilliseconds(UpdatePeriodInMilliSecond);
            }

            //Release the UpdateLock 
            if (Monitor.IsEntered(UpdateLock)) 
                Monitor.Exit(UpdateLock); 
        }

        /// <summary>
        /// Unsubscribe from all bound Statistic objects
        /// </summary>
        internal void UnBind()
        {
            if (SubscribedStats != null)
            {
                foreach (var est in SubscribedStats.Keys)
                {
                    est.ValuesUpdated -= Estimator_ValuesUpdated;
                }
            }
        }

        #endregion

        #region < CancelTasks & DisposePattern >

        /// <summary>
        /// Unbind all the ProgressEstimators
        /// </summary>
        internal void CancelTasks() => CancelTasks(true);

        private void CancelTasks(bool RunUpdateTask)
        {
            //Preventn any additional events coming through
            UnBind();
            //Push the last update out after a short delay to allow any pending events through
            if (RunUpdateTask)
            {
                Task.Run( async () => { 
                    lock (UpdateLock)
                        NextUpdate = DateTime.Now.AddMilliseconds(124);
                    await Task.Delay(125);
                    Estimator_ValuesUpdated(null, IProgressEstimatorUpdateEventArgs.DummyArgs);
                });
            }
        }

        protected virtual void Dispose(bool disposing)
        {
            if (!disposedValue)
            {
                if (disposing)
                {
                    // TODO: dispose managed state (managed objects)
                }

                //Cancel the tasks
                CancelTasks(false);
                disposedValue = true;
            }
        }

        // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources
        ~RoboQueueProgressEstimator()
        {
            // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
            Dispose(disposing: false);
        }

        public void Dispose()
        {
            // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
            Dispose(disposing: true);
            GC.SuppressFinalize(this);
        }

        #endregion
    }
}
