package gocron

import (
	
	
	
	
	
	
	

	
	
	
)

type limitMode int8

// Scheduler struct stores a list of Jobs and the location of time used by the Scheduler
type Scheduler struct {
	jobsMutex sync.RWMutex
	jobs      map[uuid.UUID]*Job

	locationMutex sync.RWMutex
	location      *time.Location
	running       *atomic.Bool // represents if the scheduler is running at the moment or not

	time     TimeWrapper // wrapper around time.Time
	timer    func(d time.Duration, f func()) *time.Timer
	executor *executor // executes jobs passed via chan

	tags sync.Map // for storing tags when unique tags is set

	tagsUnique      bool // defines whether tags should be unique
	updateJob       bool // so the scheduler knows to create a new job or update the current
	waitForInterval bool // defaults jobs to waiting for first interval to start
	singletonMode   bool // defaults all jobs to use SingletonMode()

	startBlockingStopChanMutex sync.Mutex
	startBlockingStopChan      chan struct{} // stops the scheduler

	// tracks whether we're in a chain of scheduling methods for a job
	// a chain is started with any of the scheduler methods that operate
	// upon a job and are ended with one of [ Do(), Update() ] - note that
	// Update() calls Do(), so really they all end with Do().
	// This allows the caller to begin with any job related scheduler method
	// and only with one of [ Every(), EveryRandom(), Cron(), CronWithSeconds(), MonthFirstWeekday() ]
	inScheduleChain *uuid.UUID
}

// days in a week
const allWeekDays = 7

// NewScheduler creates a new Scheduler
func ( *time.Location) *Scheduler {
	 := newExecutor()

	 := &Scheduler{
		location:   ,
		running:    atomic.NewBool(false),
		time:       &trueTime{},
		executor:   &,
		tagsUnique: false,
		timer:      afterFunc,
	}
	.jobsMutex.Lock()
	.jobs = map[uuid.UUID]*Job{}
	.jobsMutex.Unlock()
	return 
}

// SetMaxConcurrentJobs limits how many jobs can be running at the same time.
// This is useful when running resource intensive jobs and a precise start time is not critical.
//
// Note: WaitMode and RescheduleMode provide details on usage and potential risks.
func ( *Scheduler) ( int,  limitMode) {
	.executor.limitModeMaxRunningJobs = 
	.executor.limitMode = 
}

// StartBlocking starts all jobs and blocks the current thread.
// This blocking method can be stopped with Stop() from a separate goroutine.
func ( *Scheduler) () {
	.StartAsync()
	.startBlockingStopChanMutex.Lock()
	.startBlockingStopChan = make(chan struct{}, 1)
	.startBlockingStopChanMutex.Unlock()

	<-.startBlockingStopChan

	.startBlockingStopChanMutex.Lock()
	.startBlockingStopChan = nil
	.startBlockingStopChanMutex.Unlock()
}

// StartAsync starts all jobs without blocking the current thread
func ( *Scheduler) () {
	if !.IsRunning() {
		.start()
	}
}

// start starts the scheduler, scheduling and running jobs
func ( *Scheduler) () {
	.executor.start()
	.setRunning(true)
	.runJobs()
}

func ( *Scheduler) () {
	.jobsMutex.RLock()
	defer .jobsMutex.RUnlock()
	for ,  := range .jobs {
		,  := context.WithCancel(context.Background())
		.mu.Lock()
		.ctx = 
		.cancel = 
		.mu.Unlock()
		.runContinuous()
	}
}

func ( *Scheduler) ( bool) {
	.running.Store()
}

// IsRunning returns true if the scheduler is running
func ( *Scheduler) () bool {
	return .running.Load()
}

// Jobs returns the list of Jobs from the scheduler
func ( *Scheduler) () []*Job {
	.jobsMutex.RLock()
	defer .jobsMutex.RUnlock()
	 := make([]*Job, len(.jobs))
	var  int
	for ,  := range .jobs {
		[] = 
		++
	}
	return 
}

// JobsMap returns a map of job uuid to job
func ( *Scheduler) () map[uuid.UUID]*Job {
	.jobsMutex.RLock()
	defer .jobsMutex.RUnlock()
	 := make(map[uuid.UUID]*Job, len(.jobs))
	for ,  := range .jobs {
		[] = 
	}
	return 
}

// Name sets the name of the current job.
//
// If the scheduler is running using WithDistributedLocker(), the job name is used
// as the distributed lock key. If the job name is not set, the function name is used as the distributed lock key.
func ( *Scheduler) ( string) *Scheduler {
	 := .getCurrentJob()
	.jobName = 
	return 
}

// Len returns the number of Jobs in the Scheduler
func ( *Scheduler) () int {
	.jobsMutex.RLock()
	defer .jobsMutex.RUnlock()
	return len(.jobs)
}

// ChangeLocation changes the default time location
func ( *Scheduler) ( *time.Location) {
	.locationMutex.Lock()
	defer .locationMutex.Unlock()
	.location = 
}

// Location provides the current location set on the scheduler
func ( *Scheduler) () *time.Location {
	.locationMutex.RLock()
	defer .locationMutex.RUnlock()
	return .location
}

type nextRun struct {
	duration time.Duration
	dateTime time.Time
}

// scheduleNextRun Compute the instant when this Job should run next
func ( *Scheduler) ( *Job) (bool, nextRun) {
	 := .now()
	if !.jobPresent() {
		return false, nextRun{}
	}

	 := 

	if .neverRan() {
		// Increment startAtTime to the future
		if !.startAtTime.IsZero() && .startAtTime.Before() {
			 := .durationToNextRun(.startAtTime, ).duration
			.setStartAtTime(.startAtTime.Add())
			if .startAtTime.Before() {
				 := .Sub(.startAtTime)
				 := .durationToNextRun(.startAtTime, ).duration
				var  time.Duration
				if  != 0 {
					 =  / 
					if % != 0 {
						++
					}
				}
				.setStartAtTime(.startAtTime.Add( * ))
			}
		}
	} else {
		 = .NextRun()
	}

	if !.shouldRun() {
		_ = .RemoveByID()
		return false, nextRun{}
	}

	 := .durationToNextRun(, )

	 := .NextRun()
	if .After() {
		.setLastRun()
	} else {
		.setLastRun()
	}

	if .dateTime.IsZero() {
		.dateTime = .Add(.duration)
		.setNextRun(.dateTime)
	} else {
		.setNextRun(.dateTime)
	}
	return true, 
}

// durationToNextRun calculate how much time to the next run, depending on unit
func ( *Scheduler) ( time.Time,  *Job) nextRun {
	// job can be scheduled with .StartAt()
	if .getFirstAtTime() == 0 && .getStartAtTime().After() {
		 := .getStartAtTime()
		if .unit == days || .unit == weeks || .unit == months {
			.addAtTime(
				time.Duration(.Hour())*time.Hour +
					time.Duration(.Minute())*time.Minute +
					time.Duration(.Second())*time.Second,
			)
		}
		return nextRun{duration: .Sub(.now()), dateTime: }
	}

	var  nextRun
	switch .getUnit() {
	case milliseconds, seconds, minutes, hours:
		.duration = .calculateDuration()
	case days:
		 = .calculateDays(, )
	case weeks:
		if len(.scheduledWeekdays) != 0 { // weekday selected, Every().Monday(), for example
			 = .calculateWeekday(, )
		} else {
			 = .calculateWeeks(, )
		}
		if .dateTime.Before(.getStartAtTime()) {
			return .(.getStartAtTime(), )
		}
	case months:
		 = .calculateMonths(, )
	case duration:
		.duration = .getDuration()
	case crontab:
		.dateTime = .cronSchedule.Next()
		.duration = .dateTime.Sub()
	}
	return 
}

func ( *Scheduler) ( *Job,  time.Time) nextRun {
	// Special case: negative days from the end of the month
	if len(.daysOfTheMonth) == 1 && .daysOfTheMonth[0] < 0 {
		return calculateNextRunForLastDayOfMonth(, , , .daysOfTheMonth[0])
	}

	if len(.daysOfTheMonth) != 0 { // calculate days to job.daysOfTheMonth
		 := make(map[int]nextRun)
		for ,  := range .daysOfTheMonth {
			[] = calculateNextRunForMonth(, , , )
		}

		 := nextRun{}
		for ,  := range  {
			if .dateTime.IsZero() {
				 = 
			} else if .dateTime.Sub(.dateTime).Milliseconds() > 0 {
				 = 
			}
		}

		return 
	}
	 := .roundToMidnightAndAddDSTAware(, .getFirstAtTime()).AddDate(0, .getInterval(), 0)
	return nextRun{duration: until(, ), dateTime: }
}

func calculateNextRunForLastDayOfMonth( *Scheduler,  *Job,  time.Time,  int) nextRun {
	// Calculate the last day of the next month, by adding job.interval+1 months (i.e. the
	// first day of the month after the next month), and subtracting one day, unless the
	// last run occurred before the end of the month.
	 := .getInterval()
	 := .getAtTime()
	if  := .AddDate(0, 0, -); .Month() != .Month() &&
		!.roundToMidnightAndAddDSTAware(, ).After() {
		// Our last run was on the last day of this month.
		++
		 = .getFirstAtTime()
	}

	 := time.Date(.Year(), .Month(), 1, 0, 0, 0, 0, .Location()).
		Add().
		AddDate(0, , 0).
		AddDate(0, 0, )
	return nextRun{duration: until(, ), dateTime: }
}

func calculateNextRunForMonth( *Scheduler,  *Job,  time.Time,  int) nextRun {
	 := .getAtTime()
	 := 

	, ,  := .deconstructDuration()
	 := time.Date(.Year(), .Month(), , , , , 0, .Location())

	 := absDuration(.Sub())
	 := 
	if .Before() { // shouldn't run this month; schedule for next interval minus day difference
		 = .AddDate(0, .getInterval(), -0)
		 = .Add(-)
		 = .getFirstAtTime()
	} else {
		if .getInterval() == 1 && !.Equal() { // every month counts current month
			 = .AddDate(0, .getInterval()-1, 0)
		} else { // should run next month interval
			 = .AddDate(0, .getInterval(), 0)
			 = .getFirstAtTime()
		}
		 = .Add()
	}
	if  !=  {
		 = .Add(-).Add()
	}
	return nextRun{duration: until(, ), dateTime: }
}

func ( *Scheduler) ( *Job,  time.Time) nextRun {
	 := .remainingDaysToWeekday(, )
	 := .calculateTotalDaysDifference(, , )
	 := .getAtTime()
	if  > 0 {
		 = .getFirstAtTime()
	}
	 := .roundToMidnightAndAddDSTAware(, ).AddDate(0, 0, )
	return nextRun{duration: until(, ), dateTime: }
}

func ( *Scheduler) ( *Job,  time.Time) nextRun {
	 := int(.getInterval()) * 7

	var  time.Time

	 := .atTimes
	for ,  := range  {
		 := .roundToMidnightAndAddDSTAware(, )
		if .After(.now()) {
			 = 
			break
		}
	}

	if .IsZero() {
		 = .roundToMidnightAndAddDSTAware(, .getFirstAtTime()).AddDate(0, 0, )
	}

	return nextRun{duration: until(, ), dateTime: }
}

func ( *Scheduler) ( time.Time,  int,  *Job) int {
	if .getInterval() > 1 {
		 := .Weekdays()
		if .lastRun.Weekday() != [len()-1] {
			return 
		}
		if  > 0 {
			return int(.getInterval())*7 - (allWeekDays - )
		}
		return int(.getInterval()) * 7
	}

	if  == 0 { // today, at future time or already passed
		 := time.Date(.Year(), .Month(), .Day(), 0, 0, 0, 0, .Location()).Add(.getAtTime())
		if .Before() {
			return 0
		}
		return 7
	}
	return 
}

func ( *Scheduler) ( *Job,  time.Time) nextRun {
	 := .roundToMidnightAndAddDSTAware(, .getAtTime()).In(.Location())
	if .now().After() || .now() ==  {
		 = .AddDate(0, 0, .getInterval())
	}
	return nextRun{duration: until(, ), dateTime: }
}

func until( time.Time,  time.Time) time.Duration {
	return .Sub()
}

func in( []time.Weekday,  time.Weekday) bool {
	 := false

	for ,  := range  {
		if int() == int() {
			 = true
			break
		}
	}
	return 
}

func ( *Scheduler) ( *Job) time.Duration {
	 := .getInterval()
	switch .getUnit() {
	case milliseconds:
		return time.Duration() * time.Millisecond
	case seconds:
		return time.Duration() * time.Second
	case minutes:
		return time.Duration() * time.Minute
	default:
		return time.Duration() * time.Hour
	}
}

func ( *Scheduler) ( time.Time,  *Job) int {
	 := .Weekdays()
	sort.Slice(, func(,  int) bool {
		return [] < []
	})

	 := false
	 := .Weekday()
	 := sort.Search(len(), func( int) bool {
		 := [] >= 
		if  {
			 = [] == 
		}
		return 
	})
	// check atTime
	if  {
		if .roundToMidnightAndAddDSTAware(, .getAtTime()).After() {
			return 0
		}
		++
	}

	if  < len() {
		return int([] - )
	}

	return int([0]) + allWeekDays - int()
}

// absDuration returns the abs time difference
func absDuration( time.Duration) time.Duration {
	if  >= 0 {
		return 
	}
	return -
}

func ( *Scheduler) ( time.Duration) ( int,  int,  int) {
	 = int(.Seconds()) / int(time.Hour/time.Second)
	 = (int(.Seconds()) % int(time.Hour/time.Second)) / int(time.Minute/time.Second)
	 = int(.Seconds()) % int(time.Minute/time.Second)
	return
}

// roundToMidnightAndAddDSTAware truncates time to midnight and "adds" duration in a DST aware manner
func ( *Scheduler) ( time.Time,  time.Duration) time.Time {
	, ,  := .deconstructDuration()
	return time.Date(.Year(), .Month(), .Day(), , , , 0, .Location())
}

// NextRun datetime when the next Job should run.
func ( *Scheduler) () (*Job, time.Time) {
	.jobsMutex.RLock()
	defer .jobsMutex.RUnlock()
	if len(.jobs) <= 0 {
		return nil, time.Time{}
	}

	var  uuid.UUID
	var  time.Time
	for ,  := range .jobs {
		 := .NextRun()
		if (.Before() || .IsZero()) && .now().Before() {
			 = 
			 = .id
		}
	}

	return .jobs[], 
}

// EveryRandom schedules a new period Job that runs at random intervals
// between the provided lower (inclusive) and upper (inclusive) bounds.
// The default unit is Seconds(). Call a different unit in the chain
// if you would like to change that. For example, Minutes(), Hours(), etc.
func ( *Scheduler) (,  int) *Scheduler {
	 := .getCurrentJob()

	.setRandomInterval(, )
	return 
}

// Every schedules a new periodic Job with an interval.
// Interval can be an int, time.Duration or a string that
// parses with time.ParseDuration().
// Negative intervals will return an error.
// Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
//
// The job is run immediately, unless:
// * StartAt or At is set on the job,
// * WaitForSchedule is set on the job,
// * or WaitForScheduleAll is set on the scheduler.
func ( *Scheduler) ( interface{}) *Scheduler {
	 := .getCurrentJob()

	switch interval := .(type) {
	case int:
		.interval = 
		if  <= 0 {
			.error = wrapOrError(.error, ErrInvalidInterval)
		}
	case time.Duration:
		if  <= 0 {
			.error = wrapOrError(.error, ErrInvalidInterval)
		}
		.setInterval(0)
		.setDuration()
		.setUnit(duration)
	case string:
		,  := time.ParseDuration()
		if  != nil {
			.error = wrapOrError(.error, )
		}
		if  <= 0 {
			.error = wrapOrError(.error, ErrInvalidInterval)
		}
		.setDuration()
		.setUnit(duration)
	default:
		.error = wrapOrError(.error, ErrInvalidIntervalType)
	}

	return 
}

func ( *Scheduler) ( *Job) {
	if !.IsRunning() {
		return
	}

	.mu.Lock()

	if .function == nil {
		.mu.Unlock()
		.Remove()
		return
	}

	defer .mu.Unlock()

	if .runWithDetails {
		switch len(.parameters) {
		case .parametersLen:
			.parameters = append(.parameters, .copy())
		case .parametersLen + 1:
			.parameters[.parametersLen] = .copy()
		default:
			// something is really wrong and we should never get here
			.error = wrapOrError(.error, ErrInvalidFunctionParameters)
			return
		}
	}

	.executor.jobFunctions <- .jobFunction.copy()
}

func ( *Scheduler) ( *Job) {
	,  := .scheduleNextRun()
	if ! {
		return
	}

	if !.getStartsImmediately() {
		.setStartsImmediately(true)
	} else {
		.run()
	}
	 := .dateTime.Sub(.now())
	if  < 0 {
		.setLastRun(.now())
		,  := .scheduleNextRun()
		if ! {
			return
		}
		 = .dateTime.Sub(.now())
	}

	.setTimer(.timer(, func() {
		if !.dateTime.IsZero() {
			for {
				 := .now().UnixNano() - .dateTime.UnixNano()
				if  >= 0 {
					break
				}
				select {
				case <-.executor.ctx.Done():
				case <-time.After(time.Duration()):
				}
			}
		}
		.()
	}))
}

// RunAll run all Jobs regardless if they are scheduled to run or not
func ( *Scheduler) () {
	.RunAllWithDelay(0)
}

// RunAllWithDelay runs all Jobs with the provided delay in between each Job
func ( *Scheduler) ( time.Duration) {
	.jobsMutex.RLock()
	defer .jobsMutex.RUnlock()
	for ,  := range .jobs {
		.run()
		.time.Sleep()
	}
}

// RunByTag runs all the Jobs containing a specific tag
// regardless of whether they are scheduled to run or not
func ( *Scheduler) ( string) error {
	return .RunByTagWithDelay(, 0)
}

// RunByTagWithDelay is same as RunByTag but introduces a delay between
// each Job execution
func ( *Scheduler) ( string,  time.Duration) error {
	,  := .FindJobsByTag()
	if  != nil {
		return 
	}
	for ,  := range  {
		.run()
		.time.Sleep()
	}
	return nil
}

// Remove specific Job by function
//
// Removing a job stops that job's timer. However, if a job has already
// been started by the job's timer before being removed, the only way to stop
// it through gocron is to use DoWithJobDetails and access the job's Context which
// informs you when the job has been canceled.
//
// Alternatively, the job function would need to have implemented a means of
// stopping, e.g. using a context.WithCancel() passed as params to Do method.
//
// The above are based on what the underlying library suggests https://pkg.go.dev/time#Timer.Stop.
func ( *Scheduler) ( interface{}) {
	 := getFunctionName()
	 := .findJobByTaskName()
	.removeJobsUniqueTags()
	.removeByCondition(func( *Job) bool {
		return .funcName == 
	})
}

// RemoveByReference removes specific Job by reference
func ( *Scheduler) ( *Job) {
	_ = .RemoveByID()
}

func ( *Scheduler) ( string) *Job {
	.jobsMutex.RLock()
	defer .jobsMutex.RUnlock()
	for ,  := range .jobs {
		if .funcName ==  {
			return 
		}
	}
	return nil
}

func ( *Scheduler) ( *Job) {
	if  == nil {
		return
	}
	if .tagsUnique && len(.tags) > 0 {
		for ,  := range .tags {
			.tags.Delete()
		}
	}
}

func ( *Scheduler) ( func(*Job) bool) {
	.jobsMutex.Lock()
	defer .jobsMutex.Unlock()
	for ,  := range .jobs {
		if () {
			.stopJob()
			delete(.jobs, .id)
		}
	}
}

func ( *Scheduler) ( *Job) {
	.mu.Lock()
	if .runConfig.mode == singletonMode {
		.executor.singletonWgs.Delete(.singletonWg)
	}
	.mu.Unlock()
	.stop()
}

// RemoveByTag will remove jobs that match the given tag.
func ( *Scheduler) ( string) error {
	return .RemoveByTags()
}

// RemoveByTags will remove jobs that match all given tags.
func ( *Scheduler) ( ...string) error {
	,  := .FindJobsByTag(...)
	if  != nil {
		return 
	}

	for ,  := range  {
		_ = .RemoveByID()
	}
	return nil
}

// RemoveByTagsAny will remove jobs that match any one of the given tags.
func ( *Scheduler) ( ...string) error {
	var  error
	 := make(map[*Job]struct{})
	for ,  := range  {
		,  := .FindJobsByTag()
		if  != nil {
			 = wrapOrError(, fmt.Errorf("%s: %s", .Error(), ))
		}
		for ,  := range  {
			[] = struct{}{}
		}
	}

	for  := range  {
		_ = .RemoveByID()
	}

	return 
}

// RemoveByID removes the job from the scheduler looking up by id
func ( *Scheduler) ( *Job) error {
	.jobsMutex.Lock()
	defer .jobsMutex.Unlock()
	if ,  := .jobs[.id];  {
		.removeJobsUniqueTags()
		.stopJob()
		delete(.jobs, .id)
		return nil
	}
	return ErrJobNotFound
}

// FindJobsByTag will return a slice of jobs that match all given tags
func ( *Scheduler) ( ...string) ([]*Job, error) {
	var  []*Job

	.jobsMutex.RLock()
	defer .jobsMutex.RUnlock()
:
	for ,  := range .jobs {
		if .hasTags(...) {
			 = append(, )
			continue 
		}
	}

	if len() > 0 {
		return , nil
	}
	return nil, ErrJobNotFoundWithTag
}

// MonthFirstWeekday sets the job to run the first specified weekday of the month
func ( *Scheduler) ( time.Weekday) *Scheduler {
	, ,  := .time.Now(time.UTC).Date()

	if  < 7 {
		return .Cron(fmt.Sprintf("0 0 %d %d %d", , , ))
	}

	return .Cron(fmt.Sprintf("0 0 %d %d %d", , +1, ))
}

// LimitRunsTo limits the number of executions of this job to n.
// Upon reaching the limit, the job is removed from the scheduler.
func ( *Scheduler) ( int) *Scheduler {
	 := .getCurrentJob()
	.LimitRunsTo()
	return 
}

// SingletonMode prevents a new job from starting if the prior job has not yet
// completed its run
//
// Warning: do not use this mode if your jobs will continue to stack
// up beyond the ability of the limit workers to keep up. An example of
// what NOT to do:
//
//	 s.Every("1s").SingletonMode().Do(func() {
//	     // this will result in an ever-growing number of goroutines
//		   // blocked trying to send to the buffered channel
//	     time.Sleep(10 * time.Minute)
//	 })
func ( *Scheduler) () *Scheduler {
	 := .getCurrentJob()
	.SingletonMode()
	return 
}

// SingletonModeAll prevents new jobs from starting if the prior instance of the
// particular job has not yet completed its run
//
// Warning: do not use this mode if your jobs will continue to stack
// up beyond the ability of the limit workers to keep up. An example of
// what NOT to do:
//
//	 s := gocron.NewScheduler(time.UTC)
//	 s.SingletonModeAll()
//
//	 s.Every("1s").Do(func() {
//	     // this will result in an ever-growing number of goroutines
//		   // blocked trying to send to the buffered channel
//	     time.Sleep(10 * time.Minute)
//	 })
func ( *Scheduler) () {
	.singletonMode = true
}

// TaskPresent checks if specific job's function was added to the scheduler.
func ( *Scheduler) ( interface{}) bool {
	.jobsMutex.RLock()
	defer .jobsMutex.RUnlock()
	for ,  := range .jobs {
		if .funcName == getFunctionName() {
			return true
		}
	}
	return false
}

func ( *Scheduler) ( *Job) bool {
	.jobsMutex.RLock()
	defer .jobsMutex.RUnlock()
	if ,  := .jobs[.id];  {
		return true
	}
	return false
}

// Clear clears all Jobs from this scheduler
func ( *Scheduler) () {
	.stopJobs()
	.jobsMutex.Lock()
	defer .jobsMutex.Unlock()
	.jobs = make(map[uuid.UUID]*Job)
	// If unique tags was enabled, delete all the tags loaded in the tags sync.Map
	if .tagsUnique {
		.tags.Range(func( interface{},  interface{}) bool {
			.tags.Delete()
			return true
		})
	}
}

// Stop stops the scheduler. This is a no-op if the scheduler is already stopped.
// It waits for all running jobs to finish before returning, so it is safe to assume that running jobs will finish when calling this.
func ( *Scheduler) () {
	if .IsRunning() {
		.stop()
	}
}

func ( *Scheduler) () {
	.stopJobs()
	.executor.stop()
	.StopBlockingChan()
	.setRunning(false)
}

func ( *Scheduler) () {
	.jobsMutex.RLock()
	defer .jobsMutex.RUnlock()
	for ,  := range .jobs {
		.stop()
	}
}

func ( *Scheduler) ( interface{},  ...interface{}) (*Job, error) {
	 := .getCurrentJob()
	.inScheduleChain = nil

	 := .getUnit()
	 := .LastRun()
	if .getAtTime() != 0 && ( <= hours ||  >= duration) {
		.error = wrapOrError(.error, ErrAtTimeNotSupported)
	}

	if len(.scheduledWeekdays) != 0 &&  != weeks {
		.error = wrapOrError(.error, ErrWeekdayNotSupported)
	}

	if .unit != crontab && .getInterval() == 0 {
		if .unit != duration {
			.error = wrapOrError(.error, ErrInvalidInterval)
		}
	}

	if .error != nil {
		// delete the job from the scheduler as this job
		// cannot be executed
		_ = .RemoveByID()
		return nil, .error
	}

	 := reflect.ValueOf()
	for .Kind() == reflect.Ptr {
		 = .Elem()
	}

	if .Kind() != reflect.Func {
		// delete the job for the same reason as above
		_ = .RemoveByID()
		return nil, ErrNotAFunction
	}

	var  string
	if  == reflect.ValueOf() {
		 = getFunctionName()
	} else {
		 = getFunctionNameOfPointer()
	}

	if .funcName !=  {
		.function = 
		if  != reflect.ValueOf() {
			.function = .Interface()
		}

		.parameters = 
		.funcName = 
	}

	 := .Type().NumIn()
	if .runWithDetails {
		--
	}

	if len() !=  {
		_ = .RemoveByID()
		.error = wrapOrError(.error, ErrWrongParams)
		return nil, .error
	}

	if .runWithDetails && .Type().In(len()).Kind() != reflect.ValueOf(*).Kind() {
		_ = .RemoveByID()
		.error = wrapOrError(.error, ErrDoWithJobDetails)
		return nil, .error
	}

	// we should not schedule if not running since we can't foresee how long it will take for the scheduler to start
	if .IsRunning() {
		.runContinuous()
	}

	return , nil
}

// Do specifies the jobFunc that should be called every time the Job runs
func ( *Scheduler) ( interface{},  ...interface{}) (*Job, error) {
	return .doCommon(, ...)
}

// DoWithJobDetails specifies the jobFunc that should be called every time the Job runs
// and additionally passes the details of the current job to the jobFunc.
// The last argument of the function must be a gocron.Job that will be passed by
// the scheduler when the function is called.
func ( *Scheduler) ( interface{},  ...interface{}) (*Job, error) {
	 := .getCurrentJob()
	.runWithDetails = true
	.parametersLen = len()
	return .doCommon(, ...)
}

// At schedules the Job at a specific time of day in the form "HH:MM:SS" or "HH:MM"
// or time.Time (note that only the hours, minutes, seconds and nanos are used).
// When the At time(s) occur on the same day on which the scheduler is started
// the Job will be run at the first available At time.
// For example: a schedule for every 2 days at 9am and 11am
// - currently 7am -> Job runs at 9am and 11am on the day the scheduler was started
// - currently 12 noon -> Job runs at 9am and 11am two days after the scheduler started
func ( *Scheduler) ( interface{}) *Scheduler {
	 := .getCurrentJob()

	switch t := .(type) {
	case string:
		for ,  := range strings.Split(, ";") {
			, , ,  := parseTime()
			if  != nil {
				.error = wrapOrError(.error, )
				return 
			}
			// save atTime start as duration from midnight
			.addAtTime(time.Duration()*time.Hour + time.Duration()*time.Minute + time.Duration()*time.Second)
		}
	case time.Time:
		.addAtTime(time.Duration(.Hour())*time.Hour + time.Duration(.Minute())*time.Minute + time.Duration(.Second())*time.Second + time.Duration(.Nanosecond())*time.Nanosecond)
	default:
		.error = wrapOrError(.error, ErrUnsupportedTimeFormat)
	}
	.startsImmediately = false
	return 
}

// Tag will add a tag when creating a job.
func ( *Scheduler) ( ...string) *Scheduler {
	 := .getCurrentJob()

	if .tagsUnique {
		for ,  := range  {
			if ,  := .tags.Load();  {
				.error = wrapOrError(.error, ErrTagsUnique())
				return 
			}
			.tags.Store(, struct{}{})
		}
	}

	.tags = append(.tags, ...)
	return 
}

// GetAllTags returns all tags.
func ( *Scheduler) () []string {
	var  []string
	.jobsMutex.RLock()
	defer .jobsMutex.RUnlock()
	for ,  := range .jobs {
		 = append(, .Tags()...)
	}
	return 
}

// StartAt schedules the next run of the Job. If this time is in the past, the configured interval will be used
// to calculate the next future time
func ( *Scheduler) ( time.Time) *Scheduler {
	 := .getCurrentJob()
	.setStartAtTime()
	.startsImmediately = false
	return 
}

// setUnit sets the unit type
func ( *Scheduler) ( schedulingUnit) {
	 := .getCurrentJob()
	 := .getUnit()
	if  == duration ||  == crontab {
		.error = wrapOrError(.error, ErrInvalidIntervalUnitsSelection)
		return
	}
	.setUnit()
}

// Millisecond sets the unit with milliseconds
func ( *Scheduler) () *Scheduler {
	return .Milliseconds()
}

// Milliseconds sets the unit with milliseconds
func ( *Scheduler) () *Scheduler {
	.setUnit(milliseconds)
	return 
}

// Second sets the unit with seconds
func ( *Scheduler) () *Scheduler {
	return .Seconds()
}

// Seconds sets the unit with seconds
func ( *Scheduler) () *Scheduler {
	.setUnit(seconds)
	return 
}

// Minute sets the unit with minutes
func ( *Scheduler) () *Scheduler {
	return .Minutes()
}

// Minutes sets the unit with minutes
func ( *Scheduler) () *Scheduler {
	.setUnit(minutes)
	return 
}

// Hour sets the unit with hours
func ( *Scheduler) () *Scheduler {
	return .Hours()
}

// Hours sets the unit with hours
func ( *Scheduler) () *Scheduler {
	.setUnit(hours)
	return 
}

// Day sets the unit with days
func ( *Scheduler) () *Scheduler {
	return .Days()
}

// Days set the unit with days
func ( *Scheduler) () *Scheduler {
	.setUnit(days)
	return 
}

// Week sets the unit with weeks
func ( *Scheduler) () *Scheduler {
	.setUnit(weeks)
	return 
}

// Weeks sets the unit with weeks
func ( *Scheduler) () *Scheduler {
	.setUnit(weeks)
	return 
}

// Month sets the unit with months
// Note: Only days 1 through 28 are allowed for monthly schedules
// Note: Multiple of the same day of month is not allowed
// Note: Negative numbers are special values and can only occur as single argument
// and count backwards from the end of the month -1 == last day of the month, -2 == penultimate day of the month
func ( *Scheduler) ( ...int) *Scheduler {
	return .Months(...)
}

// MonthLastDay sets the unit with months at every last day of the month
// The optional parameter is a negative integer denoting days previous to the
// last day of the month. E.g. -1 == the penultimate day of the month,
// -2 == two days for the last day of the month
func ( *Scheduler) ( ...int) *Scheduler {
	 := .getCurrentJob()

	switch  := len();  {
	case 0:
		return .Months(-1)
	case 1:
		 := [0]
		if  >= 0 {
			.error = wrapOrError(.error, ErrInvalidMonthLastDayEntry)
			return 
		}
		return .Months( - 1)
	default:
		.error = wrapOrError(.error, ErrInvalidMonthLastDayEntry)
		return 
	}
}

// Months sets the unit with months
// Note: Only days 1 through 28 are allowed for monthly schedules
// Note: Multiple of the same day of month is not allowed
// Note: Negative numbers are special values and can only occur as single argument
// and count backwards from the end of the month -1 == last day of the month, -2 == penultimate day of the month
func ( *Scheduler) ( ...int) *Scheduler {
	 := .getCurrentJob()

	if len() == 0 {
		.error = wrapOrError(.error, ErrInvalidDayOfMonthEntry)
	} else if len() == 1 {
		 := [0]
		if  < -28 ||  == 0 ||  > 28 {
			.error = wrapOrError(.error, ErrInvalidDayOfMonthEntry)
		}
	} else {
		 := make(map[int]int)
		for ,  := range  {
			if  < 1 ||  > 28 {
				.error = wrapOrError(.error, ErrInvalidDayOfMonthEntry)
				break
			}

			for ,  := range .daysOfTheMonth {
				if  ==  {
					.error = wrapOrError(.error, ErrInvalidDaysOfMonthDuplicateValue)
					break
				}
			}

			if ,  := [];  {
				.error = wrapOrError(.error, ErrInvalidDaysOfMonthDuplicateValue)
				break
			}
			[]++
		}
	}
	if .daysOfTheMonth == nil {
		.daysOfTheMonth = make([]int, 0)
	}
	.daysOfTheMonth = append(.daysOfTheMonth, ...)
	.startsImmediately = false
	.setUnit(months)
	return 
}

// NOTE: If the dayOfTheMonth for the above two functions is
// more than the number of days in that month, the extra day(s)
// spill over to the next month. Similarly, if it's less than 0,
// it will go back to the month before

// Weekday sets the scheduledWeekdays with a specifics weekdays
func ( *Scheduler) ( time.Weekday) *Scheduler {
	 := .getCurrentJob()

	if  := in(.scheduledWeekdays, ); ! {
		.scheduledWeekdays = append(.scheduledWeekdays, )
	}

	.startsImmediately = false
	.setUnit(weeks)
	return 
}

func ( *Scheduler) () *Scheduler {
	return .At("12:00")
}

// Monday sets the start day as Monday
func ( *Scheduler) () *Scheduler {
	return .Weekday(time.Monday)
}

// Tuesday sets the start day as Tuesday
func ( *Scheduler) () *Scheduler {
	return .Weekday(time.Tuesday)
}

// Wednesday sets the start day as Wednesday
func ( *Scheduler) () *Scheduler {
	return .Weekday(time.Wednesday)
}

// Thursday sets the start day as Thursday
func ( *Scheduler) () *Scheduler {
	return .Weekday(time.Thursday)
}

// Friday sets the start day as Friday
func ( *Scheduler) () *Scheduler {
	return .Weekday(time.Friday)
}

// Saturday sets the start day as Saturday
func ( *Scheduler) () *Scheduler {
	return .Weekday(time.Saturday)
}

// Sunday sets the start day as Sunday
func ( *Scheduler) () *Scheduler {
	return .Weekday(time.Sunday)
}

func ( *Scheduler) () *Job {
	if .inScheduleChain == nil {
		.jobsMutex.Lock()
		 := .newJob(0)
		.jobs[.id] = 
		.jobsMutex.Unlock()
		.inScheduleChain = &.id
		return 
	}

	.jobsMutex.RLock()
	defer .jobsMutex.RUnlock()

	return .jobs[*.inScheduleChain]
}

func ( *Scheduler) () time.Time {
	return .time.Now(.Location())
}

// TagsUnique forces job tags to be unique across the scheduler
// when adding tags with (s *Scheduler) Tag().
// This does not enforce uniqueness on tags added via
// (j *Job) Tag()
func ( *Scheduler) () {
	.tagsUnique = true
}

// Job puts the provided job in focus for the purpose
// of making changes to the job with the scheduler chain
// and finalized by calling Update()
func ( *Scheduler) ( *Job) *Scheduler {
	if ,  := .JobsMap()[.id]; ! {
		return 
	} else if  !=  {
		return 
	}
	.inScheduleChain = &.id
	.updateJob = true
	return 
}

// Update stops the job (if running) and starts it with any updates
// that were made to the job in the scheduler chain. Job() must be
// called first to put the given job in focus.
func ( *Scheduler) () (*Job, error) {
	 := .getCurrentJob()

	if !.updateJob {
		return , wrapOrError(.error, ErrUpdateCalledWithoutJob)
	}
	.updateJob = false
	.stop()
	.setStartsImmediately(false)

	if .runWithDetails {
		 := .parameters
		if len() > 0 {
			 = .parameters[:len(.parameters)-1]
		}
		return .DoWithJobDetails(.function, ...)
	}

	if .runConfig.mode == singletonMode {
		.SingletonMode()
	}

	return .Do(.function, .parameters...)
}

func ( *Scheduler) ( string) *Scheduler {
	return .cron(, false)
}

func ( *Scheduler) ( string) *Scheduler {
	return .cron(, true)
}

func ( *Scheduler) ( string,  bool) *Scheduler {
	 := .getCurrentJob()

	var  string
	if strings.HasPrefix(, "TZ=") || strings.HasPrefix(, "CRON_TZ=") {
		 = 
	} else {
		 = fmt.Sprintf("CRON_TZ=%s %s", .location.String(), )
	}

	var (
		 cron.Schedule
		          error
	)

	if  {
		 := cron.NewParser(cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor)
		,  = .Parse()
	} else {
		,  = cron.ParseStandard()
	}

	if  != nil {
		.error = wrapOrError(, ErrCronParseFailure)
	}

	.cronSchedule = 
	.setUnit(crontab)
	.startsImmediately = false

	return 
}

func ( *Scheduler) ( int) *Job {
	return newJob(, !.waitForInterval, .singletonMode)
}

// WaitForScheduleAll defaults the scheduler to create all
// new jobs with the WaitForSchedule option as true.
// The jobs will not start immediately but rather will
// wait until their first scheduled interval.
func ( *Scheduler) () {
	.waitForInterval = true
}

// WaitForSchedule sets the job to not start immediately
// but rather wait until the first scheduled interval.
func ( *Scheduler) () *Scheduler {
	 := .getCurrentJob()
	.startsImmediately = false
	return 
}

// StartImmediately sets the job to run immediately upon
// starting the scheduler or adding the job to a running
// scheduler. This overrides the jobs start status of any
// previously called methods in the chain.
//
// Note: This is the default behavior of the scheduler
// for most jobs, but is useful for overriding the default
// behavior of Cron scheduled jobs which default to
// WaitForSchedule.
func ( *Scheduler) () *Scheduler {
	 := .getCurrentJob()
	.startsImmediately = true
	return 
}

// CustomTime takes an in a struct that implements the TimeWrapper interface
// allowing the caller to mock the time used by the scheduler. This is useful
// for tests relying on gocron.
func ( *Scheduler) ( TimeWrapper) {
	.time = 
}

// CustomTimer takes in a function that mirrors the time.AfterFunc
// This is used to mock the time.AfterFunc function used by the scheduler
// for testing long intervals in a short amount of time.
func ( *Scheduler) ( func( time.Duration,  func()) *time.Timer) {
	.timer = 
}

func ( *Scheduler) () {
	.startBlockingStopChanMutex.Lock()
	if .IsRunning() && .startBlockingStopChan != nil {
		close(.startBlockingStopChan)
	}
	.startBlockingStopChanMutex.Unlock()
}

// WithDistributedLocker prevents the same job from being run more than once
// when multiple schedulers are trying to schedule the same job.
//
// One strategy to reduce splay in the job execution times when using
// intervals (e.g. 1s, 1m, 1h), on each scheduler instance, is to use
// StartAt with time.Now().Round(interval) to start the job at the
// next interval boundary.
//
// Another strategy is to use the Cron or CronWithSeconds methods as they
// use the same behavior described above using StartAt.
//
// NOTE - the Locker will NOT lock jobs using the singleton options:
// SingletonMode, or SingletonModeAll
//
// NOTE - beware of potential race conditions when running the Locker
// with SetMaxConcurrentJobs and WaitMode as jobs are not guaranteed
// to be locked when each scheduler's is below its limit and able
// to run the job.
func ( *Scheduler) ( Locker) {
	.executor.distributedLocker = 
}

// WithDistributedElector prevents the same job from being run more than once
// when multiple schedulers are trying to schedule the same job, by allowing only
// the leader to run jobs. Non-leaders wait until the leader instance goes down
// and then a new leader is elected.
//
// Compared with the distributed lock, the election is the same as leader/follower framework.
// All jobs are only scheduled and execute on the leader scheduler instance. Only when the leader scheduler goes down
// and one of the scheduler instances is successfully elected, then the new leader scheduler instance can schedule jobs.
func ( *Scheduler) ( Elector) {
	.executor.distributedElector = 
}

// RegisterEventListeners accepts EventListeners and registers them for all jobs
// in the scheduler at the time this function is called.
// The event listeners are then called at the times described by each listener.
// If a new job is added, an additional call to this method, or the job specific
// version must be executed in order for the new job to trigger event listeners.
func ( *Scheduler) ( ...EventListener) {
	.jobsMutex.RLock()
	defer .jobsMutex.RUnlock()
	for ,  := range .jobs {
		.RegisterEventListeners(...)
	}
}

func ( *Scheduler) ( bool) {
	.executor.skipExecution.Store()
}