//
// Scheduler.cs
//
// Author:
//       Carsten Sonne Larsen <cs@innolan.dk>
//
// Copyright (c) 2013-2016 Carsten Sonne Larsen
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
using Ntp.Analyzer.Log;
using Ntp.Config;

namespace Ntp.Process
{
    /// <summary>
    /// A scheduler performs scheduling og jobs according to job schedule descriptions.
    /// </summary>
    public sealed class Scheduler : IScheduler
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="Ntp.Process.Scheduler"/> class.
        /// </summary>
        /// <param name="log">Log.</param>
        public Scheduler (LogBase log)
        {
            start = DateTime.Now;
            firstRun = true;
            active = false;

            waitHandle = new EventWaitHandle(false, EventResetMode.AutoReset);

            schedule = new List<ScheduledJob> ();
            jobs = new List<Job> ();
            activityLog = new ActivityLog ();

            LogGroup logGroup = new LogGroup ();
            logGroup.Add (log);
            logGroup.Add (activityLog);

            this.log = logGroup;
        }

        private readonly EventWaitHandle waitHandle;
        private readonly ActivityLog activityLog;
        private readonly List<Job> jobs;
        private readonly List<ScheduledJob> schedule;
        private readonly LogBase log;
        private readonly DateTime start;
        private ScheduledJob nextJob;
        private bool firstRun;
        private bool active;

        /// <summary>
        /// Gets the enumerator.
        /// </summary>
        /// <returns>The enumerator.</returns>
        public IEnumerator<Job> GetEnumerator ()
        {
            return jobs.GetEnumerator ();
        }

        /// <summary>
        /// Gets the enumerator.
        /// </summary>
        /// <returns>The enumerator.</returns>
        IEnumerator IEnumerable.GetEnumerator ()
        {
            return jobs.GetEnumerator ();
        }

        /// <summary>
        /// Gets the wait handle of this <see cref="Scheduler" />.
        /// </summary>
        /// <value>The wait handle.</value>
        public EventWaitHandle WaitHandle {
            get { return waitHandle; }
        }

        /// <summary>
        /// Gets the startup time of this <see cref="Scheduler" />.
        /// </summary>
        /// <value>The startup time.</value>
        public DateTime StartTime {
            get { return start; }
        }

        /// <summary>
        /// Gets a value indicating whether this <see cref="Ntp.Process.Scheduler"/> is active.
        /// </summary>
        /// <value><c>true</c> if active; otherwise, <c>false</c>.</value>
        public bool Active {
            get { return active; }
        }

        /// <summary>
        /// Gets the log used by this <see cref="Scheduler" />.
        /// </summary>
        /// <value>The log.</value>
        public LogBase Log {
            get { return log; }
        }

        /// <summary>
        /// Gets the activity log.
        /// </summary>
        /// <value>The activity log.</value>
        public ActivityLog ActivityLog {
            get { return activityLog; }
        }

        /// <summary>
        /// Gets the schedule.
        /// </summary>
        /// <value>The schedule.</value>
        public IEnumerable<ScheduledJob> Schedule {
            get { return schedule; }
        }

        /// <summary>
        /// Gets the next job to be executed.
        /// </summary>
        /// <value>The next job.</value>
        public ScheduledJob NextJob {
            get { return nextJob; }
        }

        /// <summary>
        /// Add the specified job to the scheduler queue.
        /// </summary>
        /// <param name="description">Description.</param>
        public void Add (JobDescription description)
        {
            if (description.Configuration == null || description.Configuration.Frequency == -1)
                return;

            JobScheduleDescription schedule = new JobScheduleDescription (
                                                  description.Configuration.InitialRun,
                                                  description.Configuration.FixedRun,
                                                  description.Configuration.Frequency);

            Job job = new Job (description, schedule, log);

            jobs.Add (job);
            QueueJob (job, start);

            log.WriteLine ("Job added to scheduler: " + job.ConfigString, Severity.Debug);
        }

        /// <summary>
        /// Run the scheduler queue pump method.
        /// </summary>
        public void RunOneCycle ()
        {
            Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;

            if (firstRun) {
                firstRun = false;
                active = true;

                log.WriteLine (String.Format ("Starting scheduler with {0} jobs.", schedule.Count ()), Severity.Notice);
            }

            ScheduledJob next = schedule.OrderBy (j => j.Run).ThenBy (j => j.Job.Description.Priority).First ();
            schedule.Remove (next);
            next.Job.Queued = false;
            nextJob = next;

            Thread thread = new Thread (new ThreadStart (next.Job.Execute));
            thread.CurrentCulture = CultureInfo.InvariantCulture;
            thread.Name = next.Job.Description.Name;

            int wait = Convert.ToInt32 (next.Run.Subtract (DateTime.Now).TotalMilliseconds);

            if (wait < 0) {
                if (wait < -5000)
                    log.WriteLine ("Behind schedule. Trying to catch up.", Severity.Info);

                wait = 0;
            }

            bool signal = waitHandle.WaitOne (wait);
            if (signal)
                return;

            if (next.Job.Description.SingleThread && schedule.Count (j => j.Job.Description.SingleThread && j.Job.Running) != 0) {
                PostponeJob (next.Job);
                return;
            }

            thread.Start ();

            // Dont re-schedule "run-only-once" jobs.
            if (next.Job.Schedule.Frequency == 0)
                return;

            QueueJob (next.Job, DateTime.Now);
        }

        /// <summary>
        /// Queue job for scheduled run.
        /// </summary>
        /// <param name="job">Job.</param>
        /// <param name="run">Run.</param>
        private void QueueJob (Job job, DateTime run)
        {
            DateTime next = job.Schedule.CalculateNextRun (run);
            double offset = 0;

            // Adjust offset to avoid simultaneously job execution.
            if (job.Schedule.CanMove) {
                while (schedule.Where (j => Math.Abs (next.Subtract (j.Run).TotalMilliseconds) < 5000).Count () != 0) {
                    double move = job.Schedule.Frequency / 40.0;
                    next = next.AddMinutes (move);
                    offset += move;
                }
            }

            lock (schedule) {
                ScheduledJob scheduledJob = job.Schedule.CreateNew (job, run, offset);
                schedule.Add (scheduledJob);
                job.Queued = true;
                job.Postponed = false;
                log.WriteLine (scheduledJob.ToString (), Severity.Debug);
            }
        }

        /// <summary>
        /// Postpones the job when putting it to the queue.
        /// </summary>
        /// <param name="job">Job.</param>
        private void PostponeJob (Job job)
        {
            lock (schedule) {
                ScheduledJob scheduledJob = job.Schedule.CreatePostponed (job);
                schedule.Add (scheduledJob);
                job.Queued = true;
                job.Postponed = true;
                log.WriteLine (scheduledJob.ToString (), Severity.Debug);
            } 
        }
    }
}
