// Copyright (c) 2009, Yves Goergen, http://unclassified.de
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without modification, are permitted
// provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright notice, this list of conditions
//   and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright notice, this list of
//   conditions and the following disclaimer in the documentation and/or other materials provided
//   with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
// IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
// FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.

using System;
using System.Collections.Generic;
using System.Threading;

namespace MetroFramework.Animation
{
    internal class DelayedCall : IDisposable
    {
        public delegate void Callback();

        protected static List<DelayedCall> dcList;

        protected System.Timers.Timer timer;
        protected object timerLock;
        private Callback callback;
        protected bool cancelled = false;

        protected SynchronizationContext context;

        static DelayedCall()
        {
            dcList = new List<DelayedCall>();
        }

        protected DelayedCall()
        {
            timerLock = new object();
        }

        #region Compatibility code
        private DelayedCall<object>.Callback oldCallback = null;
        private object oldData = null;

        [Obsolete("Use the static method DelayedCall.Create instead.")]
        public DelayedCall(Callback cb)
            : this()
        {
            PrepareDCObject(this, 0, false);
            callback = cb;
        }

        [Obsolete("Use the static method DelayedCall.Create instead.")]
        public DelayedCall(DelayedCall<object>.Callback cb, object data)
            : this()
        {
            PrepareDCObject(this, 0, false);
            oldCallback = cb;
            oldData = data;
        }

        [Obsolete("Use the static method DelayedCall.Start instead.")]
        public DelayedCall(Callback cb, int milliseconds)
            : this()
        {
            PrepareDCObject(this, milliseconds, false);
            callback = cb;
            if (milliseconds > 0) Start();
        }

        [Obsolete("Use the static method DelayedCall.Start instead.")]
        public DelayedCall(DelayedCall<object>.Callback cb, int milliseconds, object data)
            : this()
        {
            PrepareDCObject(this, milliseconds, false);
            oldCallback = cb;
            oldData = data;
            if (milliseconds > 0) Start();
        }

        [Obsolete("Use the method Restart of the generic class instead.")]
        public void Reset(object data)
        {
            Cancel();
            oldData = data;
            Start();
        }

        [Obsolete("Use the method Restart of the generic class instead.")]
        public void Reset(int milliseconds, object data)
        {
            Cancel();
            oldData = data;
            Reset(milliseconds);
        }

        [Obsolete("Use the method Restart instead.")]
        public void SetTimeout(int milliseconds)
        {
            Reset(milliseconds);
        }
        #endregion

        public static DelayedCall Create(Callback cb, int milliseconds)
        {
            DelayedCall dc = new DelayedCall();
            PrepareDCObject(dc, milliseconds, false);
            dc.callback = cb;
            return dc;
        }

        public static DelayedCall CreateAsync(Callback cb, int milliseconds)
        {
            DelayedCall dc = new DelayedCall();
            PrepareDCObject(dc, milliseconds, true);
            dc.callback = cb;
            return dc;
        }

        public static DelayedCall Start(Callback cb, int milliseconds)
        {
            DelayedCall dc = Create(cb, milliseconds);
            if (milliseconds > 0) dc.Start();
            else if (milliseconds == 0) dc.FireNow();
            return dc;
        }

        public static DelayedCall StartAsync(Callback cb, int milliseconds)
        {
            DelayedCall dc = CreateAsync(cb, milliseconds);
            if (milliseconds > 0) dc.Start();
            else if (milliseconds == 0) dc.FireNow();
            return dc;
        }

        protected static void PrepareDCObject(DelayedCall dc, int milliseconds, bool async)
        {
            if (milliseconds < 0)
            {
                throw new ArgumentOutOfRangeException("milliseconds", "The new timeout must be 0 or greater.");
            }

            dc.context = null;
            if (!async)
            {
                dc.context = SynchronizationContext.Current;
                if (dc.context == null)
                    throw new InvalidOperationException("Cannot delay calls synchronously on a non-UI thread. Use the *Async methods instead.");
            }
            if (dc.context == null) dc.context = new SynchronizationContext();   // Run asynchronously silently

            dc.timer = new System.Timers.Timer();
            if (milliseconds > 0) dc.timer.Interval = milliseconds;
            dc.timer.AutoReset = false;
            dc.timer.Elapsed += dc.Timer_Elapsed;

            Register(dc);
        }

        protected static void Register(DelayedCall dc)
        {
            lock (dcList)
            {
                if (!dcList.Contains(dc))
                    dcList.Add(dc);
            }
        }

        protected static void Unregister(DelayedCall dc)
        {
            lock (dcList)
            {
                dcList.Remove(dc);
            }
        }

        public static int RegisteredCount
        {
            get
            {
                lock (dcList)
                {
                    return dcList.Count;
                }
            }
        }

        public static bool IsAnyWaiting
        {
            get
            {
                lock (dcList)
                {
                    foreach (DelayedCall dc in dcList)
                    {
                        if (dc.IsWaiting) return true;
                    }
                }
                return false;
            }
        }

        public static void CancelAll()
        {
            lock (dcList)
            {
                foreach (DelayedCall dc in dcList)
                {
                    dc.Cancel();
                }
            }
        }

        public static void FireAll()
        {
            lock (dcList)
            {
                foreach (DelayedCall dc in dcList)
                {
                    dc.Fire();
                }
            }
        }

        public static void DisposeAll()
        {
            lock (dcList)
            {
                while (dcList.Count > 0)
                {
                    dcList[0].Dispose();
                }
            }
        }

        protected virtual void Timer_Elapsed(object o, System.Timers.ElapsedEventArgs e)
        {
            FireNow();
            Unregister(this);
        }

        public void Dispose()
        {
            Unregister(this);
            timer.Dispose();
        }

        public void Start()
        {
            lock (timerLock)
            {
                cancelled = false;
                timer.Start();
                Register(this);
            }
        }

        public void Cancel()
        {
            lock (timerLock)
            {
                cancelled = true;
                Unregister(this);
                timer.Stop();
            }
        }

        public bool IsWaiting
        {
            get
            {
                lock (timerLock)
                {
                    return timer.Enabled && !cancelled;
                }
            }
        }

        public void Fire()
        {
            lock (timerLock)
            {
                if (!IsWaiting) return;
                timer.Stop();
            }
            FireNow();
        }

        public void FireNow()
        {
            OnFire();
            Unregister(this);
        }

        protected virtual void OnFire()
        {
            context.Post(delegate
            {
                lock (timerLock)
                {
                    if (cancelled) return;
                }

                if (callback != null) callback();
                #region Compatibility code
                if (oldCallback != null) oldCallback(oldData);
                #endregion
            }, null);
        }

        public void Reset()
        {
            lock (timerLock)
            {
                Cancel();
                Start();
            }
        }

        public void Reset(int milliseconds)
        {
            lock (timerLock)
            {
                Cancel();
                Milliseconds = milliseconds;
                Start();
            }
        }

        public int Milliseconds
        {
            get
            {
                lock (timerLock)
                {
                    return (int)timer.Interval;
                }
            }
            set
            {
                lock (timerLock)
                {
                    if (value < 0)
                    {
                        throw new ArgumentOutOfRangeException("Milliseconds", "The new timeout must be 0 or greater.");
                    }
                    else if (value == 0)
                    {
                        Cancel();
                        FireNow();
                        Unregister(this);
                    }
                    else
                    {
                        timer.Interval = value;
                    }
                }
            }
        }
    }

    internal class DelayedCall<T> : DelayedCall
    {
        public new delegate void Callback(T data);

        private Callback callback;
        private T data;

        public static DelayedCall<T> Create(Callback cb, T data, int milliseconds)
        {
            DelayedCall<T> dc = new DelayedCall<T>();
            PrepareDCObject(dc, milliseconds, false);
            dc.callback = cb;
            dc.data = data;
            return dc;
        }

        public static DelayedCall<T> CreateAsync(Callback cb, T data, int milliseconds)
        {
            DelayedCall<T> dc = new DelayedCall<T>();
            PrepareDCObject(dc, milliseconds, true);
            dc.callback = cb;
            dc.data = data;
            return dc;
        }

        public static DelayedCall<T> Start(Callback cb, T data, int milliseconds)
        {
            DelayedCall<T> dc = Create(cb, data, milliseconds);
            dc.Start();
            return dc;
        }

        public static DelayedCall<T> StartAsync(Callback cb, T data, int milliseconds)
        {
            DelayedCall<T> dc = CreateAsync(cb, data, milliseconds);
            dc.Start();
            return dc;
        }

        protected override void OnFire()
        {
            context.Post(delegate
            {
                lock (timerLock)
                {
                    if (cancelled) return;
                }

                if (callback != null) callback(data);
            }, null);
        }

        public void Reset(T data, int milliseconds)
        {
            lock (timerLock)
            {
                Cancel();
                this.data = data;
                Milliseconds = milliseconds;
                Start();
            }
        }
    }

    internal class DelayedCall<T1, T2> : DelayedCall
    {
        public new delegate void Callback(T1 data1, T2 data2);

        private Callback callback;
        private T1 data1;
        private T2 data2;

        public static DelayedCall<T1, T2> Create(Callback cb, T1 data1, T2 data2, int milliseconds)
        {
            DelayedCall<T1, T2> dc = new DelayedCall<T1, T2>();
            PrepareDCObject(dc, milliseconds, false);
            dc.callback = cb;
            dc.data1 = data1;
            dc.data2 = data2;
            return dc;
        }

        public static DelayedCall<T1, T2> CreateAsync(Callback cb, T1 data1, T2 data2, int milliseconds)
        {
            DelayedCall<T1, T2> dc = new DelayedCall<T1, T2>();
            PrepareDCObject(dc, milliseconds, true);
            dc.callback = cb;
            dc.data1 = data1;
            dc.data2 = data2;
            return dc;
        }

        public static DelayedCall<T1, T2> Start(Callback cb, T1 data1, T2 data2, int milliseconds)
        {
            DelayedCall<T1, T2> dc = Create(cb, data1, data2, milliseconds);
            dc.Start();
            return dc;
        }

        public static DelayedCall<T1, T2> StartAsync(Callback cb, T1 data1, T2 data2, int milliseconds)
        {
            DelayedCall<T1, T2> dc = CreateAsync(cb, data1, data2, milliseconds);
            dc.Start();
            return dc;
        }

        protected override void OnFire()
        {
            context.Post(delegate
            {
                lock (timerLock)
                {
                    if (cancelled) return;
                }

                if (callback != null) callback(data1, data2);
            }, null);
        }

        public void Reset(T1 data1, T2 data2, int milliseconds)
        {
            lock (timerLock)
            {
                Cancel();
                this.data1 = data1;
                this.data2 = data2;
                Milliseconds = milliseconds;
                Start();
            }
        }
    }

    internal class DelayedCall<T1, T2, T3> : DelayedCall
    {
        public new delegate void Callback(T1 data1, T2 data2, T3 data3);

        private Callback callback;
        private T1 data1;
        private T2 data2;
        private T3 data3;

        public static DelayedCall<T1, T2, T3> Create(Callback cb, T1 data1, T2 data2, T3 data3, int milliseconds)
        {
            DelayedCall<T1, T2, T3> dc = new DelayedCall<T1, T2, T3>();
            PrepareDCObject(dc, milliseconds, false);
            dc.callback = cb;
            dc.data1 = data1;
            dc.data2 = data2;
            dc.data3 = data3;
            return dc;
        }

        public static DelayedCall<T1, T2, T3> CreateAsync(Callback cb, T1 data1, T2 data2, T3 data3, int milliseconds)
        {
            DelayedCall<T1, T2, T3> dc = new DelayedCall<T1, T2, T3>();
            PrepareDCObject(dc, milliseconds, true);
            dc.callback = cb;
            dc.data1 = data1;
            dc.data2 = data2;
            dc.data3 = data3;
            return dc;
        }

        public static DelayedCall<T1, T2, T3> Start(Callback cb, T1 data1, T2 data2, T3 data3, int milliseconds)
        {
            DelayedCall<T1, T2, T3> dc = Create(cb, data1, data2, data3, milliseconds);
            dc.Start();
            return dc;
        }

        public static DelayedCall<T1, T2, T3> StartAsync(Callback cb, T1 data1, T2 data2, T3 data3, int milliseconds)
        {
            DelayedCall<T1, T2, T3> dc = CreateAsync(cb, data1, data2, data3, milliseconds);
            dc.Start();
            return dc;
        }

        protected override void OnFire()
        {
            context.Post(delegate
            {
                lock (timerLock)
                {
                    if (cancelled) return;
                }

                if (callback != null) callback(data1, data2, data3);
            }, null);
        }

        public void Reset(T1 data1, T2 data2, T3 data3, int milliseconds)
        {
            lock (timerLock)
            {
                Cancel();
                this.data1 = data1;
                this.data2 = data2;
                this.data3 = data3;
                Milliseconds = milliseconds;
                Start();
            }
        }
    }
}