package cron

import 

// SpecSchedule specifies a duty cycle (to the second granularity), based on a
// traditional crontab specification. It is computed initially and stored as bit sets.
type SpecSchedule struct {
	Second, Minute, Hour, Dom, Month, Dow uint64

	// Override location for this schedule.
	Location *time.Location
}

// bounds provides a range of acceptable values (plus a map of name to value).
type bounds struct {
	min, max uint
	names    map[string]uint
}

// The bounds for each field.
var (
	seconds = bounds{0, 59, nil}
	minutes = bounds{0, 59, nil}
	hours   = bounds{0, 23, nil}
	dom     = bounds{1, 31, nil}
	months  = bounds{1, 12, map[string]uint{
		"jan": 1,
		"feb": 2,
		"mar": 3,
		"apr": 4,
		"may": 5,
		"jun": 6,
		"jul": 7,
		"aug": 8,
		"sep": 9,
		"oct": 10,
		"nov": 11,
		"dec": 12,
	}}
	dow = bounds{0, 6, map[string]uint{
		"sun": 0,
		"mon": 1,
		"tue": 2,
		"wed": 3,
		"thu": 4,
		"fri": 5,
		"sat": 6,
	}}
)

const (
	// Set the top bit if a star was included in the expression.
	starBit = 1 << 63
)

// Next returns the next time this schedule is activated, greater than the given
// time.  If no time can be found to satisfy the schedule, return the zero time.
func ( *SpecSchedule) ( time.Time) time.Time {
	// General approach
	//
	// For Month, Day, Hour, Minute, Second:
	// Check if the time value matches.  If yes, continue to the next field.
	// If the field doesn't match the schedule, then increment the field until it matches.
	// While incrementing the field, a wrap-around brings it back to the beginning
	// of the field list (since it is necessary to re-verify previous field
	// values)

	// Convert the given time into the schedule's timezone, if one is specified.
	// Save the original timezone so we can convert back after we find a time.
	// Note that schedules without a time zone specified (time.Local) are treated
	// as local to the time provided.
	 := .Location()
	 := .Location
	if  == time.Local {
		 = .Location()
	}
	if .Location != time.Local {
		 = .In(.Location)
	}

	// Start at the earliest possible time (the upcoming second).
	 = .Add(1*time.Second - time.Duration(.Nanosecond())*time.Nanosecond)

	// This flag indicates whether a field has been incremented.
	 := false

	// If no time is found within five years, return zero.
	 := .Year() + 5

:
	if .Year() >  {
		return time.Time{}
	}

	// Find the first applicable month.
	// If it's this month, then do nothing.
	for 1<<uint(.Month())&.Month == 0 {
		// If we have to add a month, reset the other parts to 0.
		if ! {
			 = true
			// Otherwise, set the date at the beginning (since the current time is irrelevant).
			 = time.Date(.Year(), .Month(), 1, 0, 0, 0, 0, )
		}
		 = .AddDate(0, 1, 0)

		// Wrapped around.
		if .Month() == time.January {
			goto 
		}
	}

	// Now get a day in that month.
	//
	// NOTE: This causes issues for daylight savings regimes where midnight does
	// not exist.  For example: Sao Paulo has DST that transforms midnight on
	// 11/3 into 1am. Handle that by noticing when the Hour ends up != 0.
	for !dayMatches(, ) {
		if ! {
			 = true
			 = time.Date(.Year(), .Month(), .Day(), 0, 0, 0, 0, )
		}
		 = .AddDate(0, 0, 1)
		// Notice if the hour is no longer midnight due to DST.
		// Add an hour if it's 23, subtract an hour if it's 1.
		if .Hour() != 0 {
			if .Hour() > 12 {
				 = .Add(time.Duration(24-.Hour()) * time.Hour)
			} else {
				 = .Add(time.Duration(-.Hour()) * time.Hour)
			}
		}

		if .Day() == 1 {
			goto 
		}
	}

	for 1<<uint(.Hour())&.Hour == 0 {
		if ! {
			 = true
			 = time.Date(.Year(), .Month(), .Day(), .Hour(), 0, 0, 0, )
		}
		 = .Add(1 * time.Hour)

		if .Hour() == 0 {
			goto 
		}
	}

	for 1<<uint(.Minute())&.Minute == 0 {
		if ! {
			 = true
			 = .Truncate(time.Minute)
		}
		 = .Add(1 * time.Minute)

		if .Minute() == 0 {
			goto 
		}
	}

	for 1<<uint(.Second())&.Second == 0 {
		if ! {
			 = true
			 = .Truncate(time.Second)
		}
		 = .Add(1 * time.Second)

		if .Second() == 0 {
			goto 
		}
	}

	return .In()
}

// dayMatches returns true if the schedule's day-of-week and day-of-month
// restrictions are satisfied by the given time.
func dayMatches( *SpecSchedule,  time.Time) bool {
	var (
		 bool = 1<<uint(.Day())&.Dom > 0
		 bool = 1<<uint(.Weekday())&.Dow > 0
	)
	if .Dom&starBit > 0 || .Dow&starBit > 0 {
		return  && 
	}
	return  || 
}