#region  --------------- Copyright Dresser Wayne Pignone -------------
/*
 * $Log: /Wrk/WayneLibraries/Wrk/AsyncManager/AsyncManager.cs $
 * 
 * 12    08-03-19 12:09 Mattias.larsson
 * Added GetOperationsByData() and Clear().
 * 
 * 11    08-01-31 12:12 Mattias.larsson
 * 
 * 10    07-05-28 9:40 roger.månsson
 * Be more tolerant to null as user token.
 * 
 * 9     07-04-16 12:37 roger.månsson
 * Added Data, as an application-defined extra value that can be stored
 * with the async operation.
 * 
 * 8     07-03-22 9:53 Mattias.larsson
 * 
 * 7     07-03-13 12:53 roger.månsson
 * Fixed the TryGetOperation return value when getting an operation but
 * with wrong type.
 * 
 * 6     07-02-15 17:48 roger.månsson
 * FxCop updates
 * 
 * 5     07-01-12 16:36 roger.månsson
 * Only log when actually cleaning operations from the internal list
 */
#endregion
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Wayne.Lib.Log;

namespace Wayne.Lib.AsyncManager
{
    /// <summary>
    /// Async manager is used to handle asynchronous call tracking. It saves data about the 
    /// asynchronous call so it can be signaled later on. This implementation is based on a operation id type that
    /// is defined by the type parameter TOperationId. For example if we are communicating with some equipment and
    /// there is a sequence number in the communication, this can be the operation id in the async manager. On the 
    /// </summary>
    public class AsyncManager<TOperationId> : IIdentifiableEntity, IDisposable
    {

        #region Fields
        
        private int id;
        private IIdentifiableEntity parentEntity;
        private readonly IServiceLocator serviceLocator;

        private List<AsyncOperation<TOperationId>> outstandingOperationList = new List<AsyncOperation<TOperationId>>();
        private object outstandingOperationListLock = new object();

        private ITimer cleaningTimer;
        private TimeSpan cleanOutstandingOperationsOlderThan;

        private readonly Func<TOperationId> nextOperationId = null;

        #endregion

        #region Construction
        /// <summary>
        /// Initializes a new instance of the AsyncManager.
        /// Uses a default timeout of 1 hour.
        /// </summary>
        /// <param name="id">The Id.</param>
        /// <param name="parentEntity">The parent entity.</param>
        protected AsyncManager(int id, IIdentifiableEntity parentEntity)
            : this(id, parentEntity, ServiceContainerFactory.Create())
        {
        }

        /// <summary>
        /// Initializes a new instance of the AsyncManager.
        /// Uses a default timeout of 1 hour.
        /// </summary>
        /// <param name="id">The Id.</param>
        /// <param name="parentEntity">The parent entity.</param>
        /// <param name="serviceLocator"></param>
        protected AsyncManager(int id, IIdentifiableEntity parentEntity, IServiceLocator serviceLocator)
            : this(id, parentEntity, serviceLocator, TimeSpan.FromHours(1))
        {
        }

        /// <summary>
        /// Initializes a new instance of the AsyncManager with a specific cleanOutstandingOperations max age.
        /// The timer is triggered after a calculated period between 30 seconds and 10 minutes, 
        /// if the TimeSpan is not null.
        /// </summary>
        /// <param name="id">The Id.</param>
        /// <param name="parentEntity">The parent entity.</param>
        /// <param name="serviceLocator"></param>
        /// <param name="cleanOutstandingOperationsOlderThan">Sets the maximum age an operation can achieve. Minimum age is 1 minute. 
        /// If null no timer is created.</param>
        protected AsyncManager(int id, IIdentifiableEntity parentEntity, IServiceLocator serviceLocator, TimeSpan? cleanOutstandingOperationsOlderThan)
        {
            this.id = id;
            this.parentEntity = parentEntity;
            this.serviceLocator = serviceLocator;

            ITimerFactory timerFactory = serviceLocator.GetServiceOrDefault<ITimerFactory>(() => new WayneTimerFactory());

            if (cleanOutstandingOperationsOlderThan.HasValue)
            {
                this.cleanOutstandingOperationsOlderThan = cleanOutstandingOperationsOlderThan.Value;

                TimeSpan timerDurationAndPeriod =
                    CalculatecleaningTimerTriggerLengthFromcleanOutstandingOperationsOlderThan();

                cleaningTimer = timerFactory.Create(IdentifiableEntity.NoId, this, "CleaningTimer");
                cleaningTimer.OnTimeout += CleanOutstandingOperations;
                cleaningTimer.Change(timerDurationAndPeriod, timerDurationAndPeriod);
            }
        }

        /// <summary>
        /// Initializes a new instance of the AsyncManager with a specific cleanOutstandingOperations max age.
        /// The timer is triggered after a calculated period between 30 seconds and 10 minutes, 
        /// if the TimeSpan is not null.
        /// </summary>
        /// <param name="id">The Id.</param>
        /// <param name="parentEntity">The parent entity.</param>
        /// <param name="serviceLocator"></param>
        /// <param name="cleanOutstandingOperationsOlderThan">Sets the maximum age an operation can achieve. Minimum age is 1 minute. 
        /// If null no timer is created.</param>
        /// <param name="nextOperationId"></param>
        public AsyncManager(int id, IIdentifiableEntity parentEntity, IServiceLocator serviceLocator, TimeSpan? cleanOutstandingOperationsOlderThan, Func<TOperationId> nextOperationId)
        {
            this.id = id;
            this.parentEntity = parentEntity;
            this.serviceLocator = serviceLocator;
            this.nextOperationId = nextOperationId;

            ITimerFactory timerFactory = serviceLocator.GetServiceOrDefault<ITimerFactory>(() => new WayneTimerFactory());

            if (cleanOutstandingOperationsOlderThan.HasValue)
            {
                this.cleanOutstandingOperationsOlderThan = cleanOutstandingOperationsOlderThan.Value;

                TimeSpan timerDurationAndPeriod =
                    CalculatecleaningTimerTriggerLengthFromcleanOutstandingOperationsOlderThan();

                cleaningTimer = timerFactory.Create(IdentifiableEntity.NoId, this, "CleaningTimer");
                cleaningTimer.OnTimeout += CleanOutstandingOperations;
                cleaningTimer.Change(timerDurationAndPeriod, timerDurationAndPeriod);
            }
        }

        /// <summary>
        /// Finalizer
        /// </summary>
        ~AsyncManager()
        {
            Dispose(false);
        }
        #endregion

        #region Protected virtual methods


        /// <summary>
        /// Method that should be overridden by descendant classes in order to create 
        /// a new operation id to be used by the next operation.
        /// </summary>
        /// <returns></returns>
        protected virtual TOperationId CreateNextOperationId()
        {
            return nextOperationId();
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Registers an operation with the owner, result delegate and a user token, so it can be found later on. It creates an operation id that can be read from the AsyncOperation that 
        /// is returned from the function.
        /// </summary>
        /// <typeparam name="TResultEventArgs">Type of the EventArgs to the result delegate.</typeparam>
        /// <param name="owner">The object that is going to be set as sender in the result delegate invocation.</param>
        /// <param name="resultDelegate">Delegate to be</param>
        /// <param name="userToken">User token that is returned in the calback invokation.</param>
        public AsyncOperation<TOperationId, TResultEventArgs> RegisterOperation<TResultEventArgs>(object owner, EventHandler<TResultEventArgs> resultDelegate, object userToken) where TResultEventArgs : EventArgs
        {
            return RegisterOperation(owner, resultDelegate, userToken, null);
        }

        /// <summary>
        /// Registers an operation with the owner, result delegate and a user token, so it can be found later on. It creates an operation id that can be read from the AsyncOperation that 
        /// is returned from the function. The application can an application-defined object that is to be stored with the operation.
        /// </summary>
        /// <typeparam name="TResultEventArgs">Type of the EventArgs to the result delegate.</typeparam>
        /// <param name="owner">The object that is going to be set as sender in the result delegate invocation.</param>
        /// <param name="resultDelegate">Delegate to be</param>
        /// <param name="userToken">User token that is returned in the calback invokation.</param>
        /// <param name="data">Application defined data.</param>
        public AsyncOperation<TOperationId, TResultEventArgs> RegisterOperation<TResultEventArgs>(object owner, EventHandler<TResultEventArgs> resultDelegate, object userToken, object data) where TResultEventArgs : EventArgs
        {
            lock (outstandingOperationListLock)
            {
                TOperationId newOperationId = CreateNextOperationId();

                AsyncOperation<TOperationId, TResultEventArgs> asyncOperation = new AsyncOperation<TOperationId, TResultEventArgs>(owner, newOperationId, userToken, data, resultDelegate, cleanOutstandingOperationsOlderThan);

                outstandingOperationList.Add(asyncOperation);

                asyncOperation.OnOperationCompleted += asyncOperation_OnOperationCompleted;

                return asyncOperation;
            }
        }

        /// <summary>
        /// Retrieves an outstanding operation by the operation id
        /// </summary>
        /// <typeparam name="TResultEventArgs">The type that the operation is expected to be containing, so we know what we expect to get as a result.</typeparam>
        /// <param name="operationId">The operation Id to search for.</param>
        /// <param name="asyncOperation">Out parameter that returns the matching operation. Null if it is not found.</param>        
        /// <returns>True if a matching operation is found, otherwise false.</returns>
        public bool TryGetOperation<TResultEventArgs>(TOperationId operationId, out AsyncOperation<TOperationId, TResultEventArgs> asyncOperation) where TResultEventArgs : EventArgs
        {
            asyncOperation = null;
            lock (outstandingOperationListLock)
            {
                foreach (AsyncOperation<TOperationId> op in outstandingOperationList)
                {
                    if ((op.Id.Equals(operationId)))
                    {
                        asyncOperation = op as AsyncOperation<TOperationId, TResultEventArgs>;
                        if (asyncOperation != null)
                            return true;
                        else
                            return false;
                    }
                }
            }
            return false;
        }

        /// <summary>
        /// Checks whether the requested Operation exists in the list.
        /// </summary>
        /// <param name="operationId">The operation Id to search for.</param>
        /// <returns>True if a matching operation is found, otherwise false.</returns>
        public bool OperationExists(TOperationId operationId)
        {
            lock (outstandingOperationListLock)
            {
                foreach (AsyncOperation<TOperationId> op in outstandingOperationList)
                    if ((op.Id.Equals(operationId)))
                        return true;
            }
            return false;
        }

        /// <summary>
        /// Returns the EventArgs-type if a matching operation is found, otherwise null.
        /// </summary>
        /// <param name="operationId">The operation Id to search for.</param>
        /// <returns>The EventArgs-type if a matching operation is found, otherwise null.</returns>
        public Type GetOperationType(TOperationId operationId)
        {
            lock (outstandingOperationListLock)
            {
                foreach (AsyncOperation<TOperationId> op in outstandingOperationList)
                    if ((op.Id.Equals(operationId)))
                        return op.ResultEventArgsType;
            }
            return null;
        }

        /// <summary>
        /// Gets a list of outstanding operations that matches the the specified userToken. Checks and converts the operation to the specified type. 
        /// </summary>
        /// <typeparam name="TResultEventArgs">Type of the result event args.</typeparam>
        /// <param name="userToken">User token to match the operation against.</param>
        /// <param name="asyncOperationList">List of matching operations.</param>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#")]
        public void GetOperationsByUserToken<TResultEventArgs>(object userToken, out AsyncOperation<TOperationId, TResultEventArgs>[] asyncOperationList) where TResultEventArgs : EventArgs
        {
            List<AsyncOperation<TOperationId, TResultEventArgs>> list = new List<AsyncOperation<TOperationId, TResultEventArgs>>();
            lock (outstandingOperationListLock)
            {
                foreach (AsyncOperation<TOperationId> op in outstandingOperationList)
                {
                    if (op.UserToken != null)
                    {
                        if ((op.UserToken.Equals(userToken)))
                        {
                            AsyncOperation<TOperationId, TResultEventArgs> asyncOperation = op as AsyncOperation<TOperationId, TResultEventArgs>;
                            if (asyncOperation != null)
                                list.Add(asyncOperation);
                        }
                    }
                    else
                    {
                        if (userToken == null)
                        {
                            AsyncOperation<TOperationId, TResultEventArgs> asyncOperation = op as AsyncOperation<TOperationId, TResultEventArgs>;
                            if (asyncOperation != null)
                                list.Add(asyncOperation);
                        }
                    }
                }
            }
            asyncOperationList = list.ToArray();
        }

        /// <summary>
        /// Gets an outstanding operation list using the userToken as argument. Does not check the type of the operation.
        /// </summary>
        /// <param name="userToken">User token that is specified for the outstanding operation.</param>
        /// <returns>A list of Async operations.</returns>
        public AsyncOperation<TOperationId>[] GetOperationsByUserToken(object userToken)
        {
            List<AsyncOperation<TOperationId>> list = new List<AsyncOperation<TOperationId>>();

            lock (outstandingOperationListLock)
            {
                foreach (AsyncOperation<TOperationId> op in outstandingOperationList)
                {
                    if (op.UserToken != null)
                    {
                        if ((op.UserToken.Equals(userToken)))
                        {
                            list.Add(op);
                        }
                    }
                    else
                    {
                        if (userToken == null) //If the requesting user token also is null, it is a valid match!
                            list.Add(op);
                    }
                }
            }

            return list.ToArray();
        }

        /// <summary>
        /// Gets a list of outstanding operations that matches the the specified data object. Checks and converts the operation to the specified type. 
        /// </summary>
        /// <typeparam name="TResultEventArgs">Type of the result event args.</typeparam>
        /// <param name="data">Data object to match the operation against.</param>
        /// <param name="asyncOperationList">List of matching operations.</param>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#")]
        public void GetOperationsByData<TResultEventArgs>(object data, out AsyncOperation<TOperationId, TResultEventArgs>[] asyncOperationList) where TResultEventArgs : EventArgs
        {
            List<AsyncOperation<TOperationId, TResultEventArgs>> list = new List<AsyncOperation<TOperationId, TResultEventArgs>>();
            lock (outstandingOperationListLock)
            {
                foreach (AsyncOperation<TOperationId> op in outstandingOperationList)
                {
                    if (op.Data != null)
                    {
                        if ((op.Data.Equals(data)))
                        {
                            AsyncOperation<TOperationId, TResultEventArgs> asyncOperation = op as AsyncOperation<TOperationId, TResultEventArgs>;
                            if (asyncOperation != null)
                                list.Add(asyncOperation);
                        }
                    }
                    else
                    {
                        if (data == null)
                        {
                            AsyncOperation<TOperationId, TResultEventArgs> asyncOperation = op as AsyncOperation<TOperationId, TResultEventArgs>;
                            if (asyncOperation != null)
                                list.Add(asyncOperation);
                        }
                    }
                }
            }
            asyncOperationList = list.ToArray();
        }

        /// <summary>
        /// Gets an outstanding operation list using the data object as argument. Does not check the type of the operation.
        /// </summary>
        /// <param name="data">User token that is specified for the outstanding operation.</param>
        /// <returns>A list of Async operations.</returns>
        public AsyncOperation<TOperationId>[] GetOperationsByData(object data)
        {
            List<AsyncOperation<TOperationId>> list = new List<AsyncOperation<TOperationId>>();

            lock (outstandingOperationListLock)
            {
                foreach (AsyncOperation<TOperationId> op in outstandingOperationList)
                {
                    if (op.Data != null)
                    {
                        if ((op.Data.Equals(data)))
                        {
                            list.Add(op);
                        }
                    }
                    else
                    {
                        if (data == null) //If the requesting user token also is null, it is a valid match!
                            list.Add(op);
                    }
                }
            }

            return list.ToArray();
        }

        /// <summary>
        /// Clear all outstanding operations.
        /// </summary>
        public void Clear()
        {
            lock (outstandingOperationListLock)
            {
                outstandingOperationList.Clear();
            }
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Calculates the timespan based on simple rules.
        /// </summary>
        /// <returns>The timespan object to be used for the cleaningTimers period and duration</returns>
        private TimeSpan CalculatecleaningTimerTriggerLengthFromcleanOutstandingOperationsOlderThan()
        {
            TimeSpan result;

            if (cleanOutstandingOperationsOlderThan >= TimeSpan.FromHours(1))
           {
               result = TimeSpan.FromMinutes(10);
           }
           else if (cleanOutstandingOperationsOlderThan <= TimeSpan.FromMinutes(1))
           {
               result = TimeSpan.FromSeconds(30);
               //Will never accept less values than 1 minute as oldest time an operation might be.
               cleanOutstandingOperationsOlderThan = TimeSpan.FromMinutes(1);
            }
            else
           {
               //The formula is:
               // x = y - b / m
               // m = (y2 - y1) / (x2 - x1)  => (3600 * 10^3 - 60 * 10^3) / (600 * 10 ^3 - 30 * 10^3) => (354/57) ~= 6,2105...
               // b = y1 - m * x1 => 60 * 10^3 - (354/57) * 30 * 10^3 ~= -126315,789476...
                result = TimeSpan.FromMilliseconds((cleanOutstandingOperationsOlderThan.TotalMilliseconds + 126315.78947368421052631578947368) / (354.0 / 57.0));
           }
            return result;
        }

        /// <summary>
        /// Event handler that is called when an asynchronous operation has completed. It will remove the 
        /// operation from the outstanding operations list.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void asyncOperation_OnOperationCompleted(object sender, EventArgs e)
        {
            AsyncOperation<TOperationId> asyncOperation = sender as AsyncOperation<TOperationId>;
            if (asyncOperation != null)
            {
                lock (outstandingOperationListLock)
                {
                    if (outstandingOperationList.Contains(asyncOperation))
                    {
                        outstandingOperationList.Remove(asyncOperation);
                    }
                }
            }
        }

        /// <summary>
        /// Timer method that is called to clean the outstanding queue. For now, the requests must not stay
        /// more than 1h in the outstanding list.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void CleanOutstandingOperations(object sender, EventArgs e)
        {
            AsyncOperation<TOperationId>[] abandonedOperations;

            lock (outstandingOperationListLock)
            {
                abandonedOperations = outstandingOperationList.Where(x => x.Timeout.IsTimedOut).ToArray();
            }

            using (DebugLogger debugLogger = new DebugLogger(this))
            {
                if (abandonedOperations.Length > 0)
                {
                    //Remove the items that should be removed.
                    if (debugLogger.IsActive())
                    {
                        debugLogger.Add("OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO");
                        debugLogger.Add("Cleaning the outstanding operations.");
                        debugLogger.Add("---------------------------------------------------");
                    }

                    foreach (AsyncOperation<TOperationId> abandonedOperation in abandonedOperations)
                    {
                        if (debugLogger.IsActive())
                        {
                            debugLogger.Add("Removing operation: ");
                            debugLogger.Add(abandonedOperation.ToString());
                        }

                        try
                        {
                            abandonedOperation.Abandoned();
                        }
                        catch (Exception exception)
                        {
                            //Logger.AddExceptionLogEntry(new ExceptionLogEntry(this, ErrorLogSeverity.Recoverable, "Exception when handling abandoned async operation " + abandonedOperation, exception));
                        }
                    }

                    if (debugLogger.IsActive())
                        debugLogger.Add("OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO");
                }
            }

            //Remove the found operations, if not removed by abandoned- logic.
            lock (outstandingOperationListLock)
            {
                abandonedOperations.ForEach(x => outstandingOperationList.Remove(x));
            }
        }

        #endregion

        #region IIdentifiableEntity Members

        /// <summary>
        /// Async manager Id (for logging)
        /// </summary>
        public int Id
        {
            get { return id; }
        }

        /// <summary>
        /// Entity type (for logging)
        /// </summary>
        public string EntityType
        {
            get { return "AsyncManager"; }
        }

        /// <summary>
        /// This is used by the logger and should never be set by inheriting classes
        /// </summary>
        public string FullEntityName { get; set; }

        /// <summary>
        /// Entity sub type (for logging)
        /// </summary>
        public virtual string EntitySubType
        {
            get { return ""; }
        }

        /// <summary>
        /// Parent entity (for logging)
        /// </summary>
        public IIdentifiableEntity ParentEntity
        {
            get { return parentEntity; }
        }

        #endregion

        #region IDisposable Members

        /// <summary>
        /// Dispsose.
        /// </summary>
        /// <param name="disposing"></param>
        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (cleaningTimer != null)
                    cleaningTimer.Dispose();
            }
        }

        /// <summary>
        /// Dispsose.
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        #endregion
    }
}