using System;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace IPA.Utilities.Async
{
    /// <summary>
    /// A single-threaded task scheduler that runs all of its tasks on the same thread.
    /// </summary>
    public class SingleThreadTaskScheduler : TaskScheduler, IDisposable
    {
        private readonly Thread runThread = new Thread(ExecuteTasksS);
        private readonly BlockingCollection<Task> tasks = new BlockingCollection<Task>();
        private readonly CancellationTokenSource exitTokenSource = new CancellationTokenSource();

        /// <summary>
        /// Gets whether or not the underlying thread has been started.
        /// </summary>
        /// <exception cref="InvalidOperationException">Thrown if this object has already been disposed.</exception>
        public bool IsRunning
        {
            get
            {
                ThrowIfDisposed();
                return runThread.IsAlive;
            }
        }

        /// <summary>
        /// Starts the thread that executes tasks scheduled with this <see cref="TaskScheduler"/>
        /// </summary>
        /// <exception cref="InvalidOperationException">Thrown if this object has already been disposed.</exception>
        public void Start()
        {
            ThrowIfDisposed();

            runThread.Start(this);
        }

        /// <summary>
        /// Terminates the runner thread, and waits for the currently running task to complete.
        /// </summary>
        /// <remarks>
        /// After this method returns, this object has been disposed and is no longer in a valid state.
        /// </remarks>
        /// <returns>an <see cref="IEnumerable{T}"/> of <see cref="Task"/>s that did not execute</returns>
        /// <exception cref="InvalidOperationException">Thrown if this object has already been disposed.</exception>
        public IEnumerable<Task> Exit()
        {
            ThrowIfDisposed();

            tasks.CompleteAdding();
            exitTokenSource.Cancel();
            runThread.Join();

            var retTasks = new List<Task>();
            retTasks.AddRange(tasks);

            return retTasks;
        }

        /// <summary>
        /// Waits for the runner thread to complete all tasks in the queue, then exits.
        /// </summary>
        /// <remarks>
        /// After this method returns, this object has been disposed and is no longer in a valid state.
        /// </remarks>
        /// <exception cref="InvalidOperationException">Thrown if this object has already been disposed.</exception>
        public void Join()
        {
            ThrowIfDisposed();

            tasks.CompleteAdding();
            runThread.Join();
        }

        /// <summary>
        /// Throws a <see cref="NotSupportedException"/>.
        /// </summary>
        /// <returns>nothing</returns>
        /// <exception cref="NotSupportedException">Always.</exception>
        protected override IEnumerable<Task> GetScheduledTasks()
        {
            // this is only for debuggers which we can't use sooooo
            throw new NotSupportedException();
        }

        /// <summary>
        /// Queues a given <see cref="Task"/> to this scheduler. The <see cref="Task"/> <i>must></i> be
        /// scheduled for this <see cref="TaskScheduler"/> by the runtime.
        /// </summary>
        /// <param name="task">the <see cref="Task"/> to queue</param>
        /// <exception cref="InvalidOperationException">Thrown if this object has already been disposed.</exception>
        protected override void QueueTask(Task task)
        {
            ThrowIfDisposed();

            tasks.Add(task);
        }

        /// <summary>
        /// Rejects any attempts to execute a task inline.
        /// </summary>
        /// <remarks>
        /// This task scheduler <i>always</i> runs its tasks on the thread that it manages, therefore it doesn't
        /// make sense to run it inline.
        /// </remarks>
        /// <param name="task">the task to attempt to execute</param>
        /// <param name="taskWasPreviouslyQueued">whether the task was previously queued to this scheduler</param>
        /// <returns><see langword="false"/></returns>
        /// <exception cref="InvalidOperationException">Thrown if this object has already been disposed.</exception>
        protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
        {
            ThrowIfDisposed();

            return false;
        }

        private void ThrowIfDisposed()
        {
            if (disposedValue)
                throw new InvalidOperationException("Object already disposed");
        }

        private void ExecuteTasks()
        {
            ThrowIfDisposed();

            var token = exitTokenSource.Token;

            try
            {
                // while we are still accepting tasks, and we can pull out a task with an infinite wait duration
                while (!tasks.IsCompleted && tasks.TryTake(out var task, -1, token))
                {
                    TryExecuteTask(task);
                }
            }
            catch (OperationCanceledException)
            {
                // TryTake was cancelled, we'll just leave
            }
        }

        private static void ExecuteTasksS(object param)
        {
            var self = param as SingleThreadTaskScheduler;
            self.ExecuteTasks();
        }

        #region IDisposable Support
        private bool disposedValue = false; // To detect redundant calls

        /// <summary>
        /// Disposes this object.
        /// </summary>
        /// <param name="disposing">whether or not to dispose managed objects</param>
        protected virtual void Dispose(bool disposing)
        {
            if (!disposedValue)
            {
                if (disposing) 
                {
                    exitTokenSource.Dispose();
                    tasks.Dispose();
                }

                disposedValue = true;
            }
        }

        /// <summary>
        /// Disposes this object. This puts the object into an unusable state.
        /// </summary>
        public void Dispose()
        {
            // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
            Dispose(true);
        }
        #endregion
    }
}
