using System;
using System.Collections;

namespace Orciid.Core.Util
{
    /// <summary>
    /// Scheduler
    /// </summary>
    /// <remarks>
    /// This class can determine at what time in the future an event happens, based
    /// on one or more rules that define recurring event times.
    /// </remarks>
	public class Scheduler
	{
        /// <summary>
        /// Weekdays
        /// </summary>
        /// <remarks>
        /// Used for the <see cref="Scheduler"/> class
        /// </remarks>
		[Flags]
		public enum Weekdays: int
		{
            /// <summary>
            /// No days
            /// </summary>
            /// <remarks>
            /// <c>0</c>
            /// </remarks>
			None		= 0,
            /// <summary>
            /// Sunday
            /// </summary>
            /// <remarks>
            /// <c>1</c>
            /// </remarks>
            Sunday = 1 << 0,
            /// <summary>
            /// Monday
            /// </summary>
            /// <remarks>
            /// <c>2</c>
            /// </remarks>
            Monday = 1 << 1,
            /// <summary>
            /// Tuesday
            /// </summary>
            /// <remarks>
            /// <c>4</c>
            /// </remarks>
            Tuesday = 1 << 2,
            /// <summary>
            /// Wednesday
            /// </summary>
            /// <remarks>
            /// <c>8</c>
            /// </remarks>
            Wednesday = 1 << 3,
            /// <summary>
            /// Thursday
            /// </summary>
            /// <remarks>
            /// <c>16</c>
            /// </remarks>
            Thursday = 1 << 4,
            /// <summary>
            /// Friday
            /// </summary>
            /// <remarks>
            /// <c>32</c>
            /// </remarks>
            Friday = 1 << 5,
            /// <summary>
            /// Saturday
            /// </summary>
            /// <remarks>
            /// <c>64</c>
            /// </remarks>
            Saturday = 1 << 6,
            /// <summary>
            /// All days
            /// </summary>
            /// <remarks>
            /// <c>127</c>
            /// </remarks>
            All = Sunday | Monday | Tuesday | Wednesday | 
						  Thursday | Friday | Saturday
		}

        /// <summary>
        /// Scheduler rule
        /// </summary>
        /// <remarks>
        /// Any scheduled event can have multiple scheduling rules.
        /// </remarks>
		public class Rule
		{
			private Weekdays days;
			private DateTime starttime;
			private TimeSpan duration;
			private TimeSpan interval;

            /// <summary>
            /// Create new scheduling rule
            /// </summary>
            /// <remarks>
            /// Once created, the parameters for a scheduling rule cannot be changed, a new
            /// rule must be created instead.
            /// </remarks>
            /// <param name="days">The days the schedule should match</param>
            /// <param name="starttime">The earliest time the schedule should match</param>
            /// <param name="duration">The duration of the schedule, starting with 
            /// <see cref="starttime"/></param>
            /// <param name="interval">The interval between scheduled events</param>
			public Rule(Weekdays days, DateTime starttime, TimeSpan duration, 
				TimeSpan interval)
			{
				this.days = days;
				this.starttime = starttime;
				this.duration = duration;
				this.interval = interval;
			}

            /// <summary>
            /// Get next scheduled event time
            /// </summary>
            /// <remarks>
            /// The next event after <see cref="DateTime.Now"/>.
            /// </remarks>
            /// <returns>The next scheduled event time</returns>
			public DateTime GetNextTime()
			{
				return GetNextTime(DateTime.Now);
			}

            /// <summary>
            /// Get next scheduled event time
            /// </summary>
            /// <remarks>
            /// The next event after the given time.
            /// </remarks>
            /// <param name="after">The earliest time for the event</param>
            /// <returns>The next scheduled event time after the given time</returns>
			public DateTime GetNextTime(DateTime after)
			{
				// if no days are allowed, or interval is zero or negative,
				// return farthest future date
				if (days == Weekdays.None || 
					(interval < TimeSpan.FromSeconds(1) && duration > TimeSpan.Zero))
					return DateTime.MaxValue;
				
				// start with earliest possible time on the specified day
				DateTime next = new DateTime(after.Year, after.Month, after.Day,
					starttime.Hour, starttime.Minute, starttime.Second);

				// check if day of week is in list of allowed days
				while (((int)days & (1 << (int)next.DayOfWeek)) == 0)
					next += TimeSpan.FromDays(1);

				// find earliest possible date and time
				while (next <= after)
				{
					// if duration is greater than zero, check for later time on same day
					if (duration > TimeSpan.Zero)
					{
						// increase time until we are after the specified time
						TimeSpan span = TimeSpan.Zero;
						while (next + span <= after && span <= duration)
						{
							span += interval;
						}

						if (span <= duration)
						{
							// we found a time within the duration window
							return next + span;
						}
					}

					// did not find a time within the duration window
					// so try the following day
					next += TimeSpan.FromDays(1);

					// check if day of week is in list of allowed days
					while (((int)days & (1 << (int)next.DayOfWeek)) == 0)
						next += TimeSpan.FromDays(1);
				}

				return next;
			}

            /// <summary>
            /// Time until next event
            /// </summary>
            /// <remarks>
            /// The time until the next event after <see cref="DateTime.Now"/>
            /// </remarks>
            /// <returns>The time until the next event</returns>
			public TimeSpan GetTimeUntilNextTime()
			{
				return GetTimeUntilNextTime(DateTime.Now);
			}

            /// <summary>
            /// Time until next event
            /// </summary>
            /// <remarks>
            /// The time until the next event after the given time
            /// </remarks>
            /// <param name="after">The earliest time for the event</param>
            /// <returns>The time until the next event after the given time</returns>
			public TimeSpan GetTimeUntilNextTime(DateTime after)
			{
				return GetNextTime(after) - DateTime.Now;
			}
		}
		
		private ArrayList rules = new ArrayList();

        /// <summary>
        /// Create a new scheduler
        /// </summary>
        /// <remarks>
        /// Create a new scheduler without any rules
        /// </remarks>
		public Scheduler()
		{			
		}

        /// <summary>
        /// Create a new scheduler
        /// </summary>
        /// <remarks>
        /// Create a new scheduler with a given set of rules
        /// </remarks>
        /// <param name="rules">The rules to add to the new scheduler</param>
        public Scheduler(params Rule[] rules)
		{
			this.rules.AddRange(rules);
		}

        /// <summary>
        /// Add rules to scheduler
        /// </summary>
        /// <remarks>
        /// Adds additional rules to the scheduler.  A scheduler can have any number
        /// of rules.
        /// </remarks>
        /// <param name="rules">The rules to add to the scheduler</param>
		public void AddRules(params Rule[] rules)
		{
			this.rules.AddRange(rules);
		}

        /// <summary>
        /// Get next scheduled event time
        /// </summary>
        /// <remarks>
        /// The next event after <see cref="DateTime.Now"/>. If the scheduler has no rules,
        /// <see cref="DateTime.MaxValue"/> is returned.  If the scheduler has multiple
        /// rules, the soonest event from any of the rules is returned.
        /// </remarks>
        /// <returns>The next scheduled event time</returns>
        public DateTime GetNextTime()
		{
			return GetNextTime(DateTime.Now);
		}

        /// <summary>
        /// Get next scheduled event time
        /// </summary>
        /// <remarks>
        /// The next event after the given time.  If the scheduler has no rules,
        /// <see cref="DateTime.MaxValue"/> is returned.  If the scheduler has multiple
        /// rules, the soonest event from any of the rules is returned.
        /// </remarks>
        /// <param name="after">The earliest time for the event</param>
        /// <returns>The next scheduled event time after the given time</returns>
        public DateTime GetNextTime(DateTime after)
		{
			DateTime min = DateTime.MaxValue;
			foreach (Rule rule in rules)
			{
				DateTime next = rule.GetNextTime(after);
				if (next < min)
					min = next;
			}
			return min;
		}

        /// <summary>
        /// Time until next event
        /// </summary>
        /// <remarks>
        /// The time until the next event after <see cref="DateTime.Now"/>
        /// </remarks>
        /// <returns>The time until the next event</returns>
        public TimeSpan GetTimeUntilNextTime()
		{
			return GetTimeUntilNextTime(DateTime.Now);
		}

        /// <summary>
        /// Time until next event
        /// </summary>
        /// <remarks>
        /// The time until the next event after the given time
        /// </remarks>
        /// <param name="after">The earliest time for the event</param>
        /// <returns>The time until the next event after the given time</returns>
        public TimeSpan GetTimeUntilNextTime(DateTime after)
		{
			return GetNextTime(after) - DateTime.Now;
		}
	}
}
