package scheduler

import (
	"errors"
	"reflect"
	"time"
)

const MAXJOBNUM = 10000

var _location = time.Local

type Job interface {
	Do(jobFun interface{}, params ...interface{})
	At(t string) Job
	NextScheduledTime() time.Time
	Second() (job Job)
	Seconds() (job Job)
	Minute() (job Job)
	Minutes() (job Job)
	Hour() (job Job)
	Hours() (job Job)
	Day() (job Job)
	Days() (job Job)
	Monday() (job Job)
	Tuesday() (job Job)
	Wednesday() (job Job)
	Thursday() (job Job)
	Friday() (job Job)
	Saturday() (job Job)
	Sunday() (job Job)
	Weeks() (job Job)

	shouldRun() bool
	run() (result []reflect.Value, err error)
	scheduleNextRun()
	nextRun() time.Time
	jobFunc() string
}

func NewJob(intervel uint64) Job {
	return &innerJob{
		intervel,
		"", "", "",
		time.Unix(0, 0),
		time.Unix(0, 0), 0,
		time.Sunday,
		make(map[string]interface{}),
		make(map[string]([]interface{})),
	}
}

type innerJob struct {
	// pause interval * unit bettween runs
	interval uint64

	// the job _jobFunc to run, func[_jobFunc]
	_jobFunc string
	// time units, ,e.g. 'minutes', 'hours'...
	unit string
	// optional time at which this job runs
	atTime string

	// datetime of last run
	lastRun time.Time
	// datetime of next run
	_nextRun time.Time
	// cache the period between last an next run
	period time.Duration

	// Specific day of the week to start on
	startDay time.Weekday

	// Map for the function task store
	funcs map[string]interface{}

	// Map for function and  params of function
	fparams map[string]([]interface{})
}

// True if the job should be run now
func (j *innerJob) shouldRun() bool {
	return time.Now().After(j._nextRun)
}

// True if the job should be run now
func (j *innerJob) nextRun() time.Time {
	return j._nextRun
}

// True if the job should be run now
func (j *innerJob) jobFunc() string {
	return j._jobFunc
}

//Run the job and immediately reschedule it
func (j *innerJob) run() (result []reflect.Value, err error) {
	f := reflect.ValueOf(j.funcs[j._jobFunc])
	params := j.fparams[j._jobFunc]
	if len(params) != f.Type().NumIn() {
		err = errors.New("the number of param is not adapted")
		return
	}
	in := make([]reflect.Value, len(params))
	for k, param := range params {
		in[k] = reflect.ValueOf(param)
	}
	result = f.Call(in)
	j.lastRun = time.Now()
	j.scheduleNextRun()
	return
}

func (j *innerJob) Do(jobFun interface{}, params ...interface{}) {
	typ := reflect.TypeOf(jobFun)
	if typ.Kind() != reflect.Func {
		panic("only function can be schedule into the job queue.")
	}

	fname := getFunctionName(jobFun)
	j.funcs[fname] = jobFun
	j.fparams[fname] = params
	j._jobFunc = fname
	//schedule the next run
	j.scheduleNextRun()
}

//	s.Every(1).Day().At("10:30").Do(task)
//	s.Every(1).Monday().At("10:30").Do(task)
func (j *innerJob) At(t string) Job {
	hour, min, err := formatTime(t)
	if err != nil {
		panic(err)
	}

	// time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)
	mock := time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), int(hour), int(min), 0, 0, _location)

	if j.unit == "days" {
		if time.Now().After(mock) {
			j.lastRun = mock
		} else {
			j.lastRun = time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day()-1, hour, min, 0, 0, _location)
		}
	} else if j.unit == "weeks" {
		if j.startDay != time.Now().Weekday() || (time.Now().After(mock) && j.startDay == time.Now().Weekday()) {
			i := mock.Weekday() - j.startDay
			if i < 0 {
				i = 7 + i
			}
			j.lastRun = time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day()-int(i), hour, min, 0, 0, _location)
		} else {
			j.lastRun = time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day()-7, hour, min, 0, 0, _location)
		}
	}
	return j
}

//Compute the instant when this job should run next
func (j *innerJob) scheduleNextRun() {
	if j.lastRun == time.Unix(0, 0) {
		if j.unit == "weeks" {
			i := time.Now().Weekday() - j.startDay
			if i < 0 {
				i = 7 + i
			}
			j.lastRun = time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day()-int(i), 0, 0, 0, 0, _location)

		} else {
			j.lastRun = time.Now()
		}
	}

	if j.period != 0 {
		// translate all the units to the Seconds
		j._nextRun = j.lastRun.Add(j.period * time.Second)
	} else {
		switch j.unit {
		case "minutes":
			j.period = time.Duration(j.interval * 60)
			break
		case "hours":
			j.period = time.Duration(j.interval * 60 * 60)
			break
		case "days":
			j.period = time.Duration(j.interval * 60 * 60 * 24)
			break
		case "weeks":
			j.period = time.Duration(j.interval * 60 * 60 * 24 * 7)
			break
		case "seconds":
			j.period = time.Duration(j.interval)
		}
		j._nextRun = j.lastRun.Add(j.period * time.Second)
	}
}

// NextScheduledTime returns the time of when this job is to run next
func (j *innerJob) NextScheduledTime() time.Time {
	return j._nextRun
}

// the follow functions set the job's unit with seconds,minutes,hours...

// Set the unit with second
func (j *innerJob) Second() (job Job) {
	if j.interval != 1 {
		panic("")
	}
	job = j.Seconds()
	return
}

// Set the unit with seconds
func (j *innerJob) Seconds() (job Job) {
	j.unit = "seconds"
	return j
}

// Set the unit  with minute, which interval is 1
func (j *innerJob) Minute() (job Job) {
	if j.interval != 1 {
		panic("")
	}
	job = j.Minutes()
	return
}

//set the unit with minute
func (j *innerJob) Minutes() (job Job) {
	j.unit = "minutes"
	return j
}

//set the unit with hour, which interval is 1
func (j *innerJob) Hour() (job Job) {
	if j.interval != 1 {
		panic("")
	}
	job = j.Hours()
	return
}

// Set the unit with hours
func (j *innerJob) Hours() (job Job) {
	j.unit = "hours"
	return j
}

// Set the job's unit with day, which interval is 1
func (j *innerJob) Day() (job Job) {
	if j.interval != 1 {
		panic("")
	}
	job = j.Days()
	return
}

// Set the job's unit with days
func (j *innerJob) Days() Job {
	j.unit = "days"
	return j
}

// s.Every(1).Monday().Do(task)
// Set the start day with Monday
func (j *innerJob) Monday() (job Job) {
	if j.interval != 1 {
		panic("")
	}
	j.startDay = 1
	job = j.Weeks()
	return
}

// Set the start day with Tuesday
func (j *innerJob) Tuesday() (job Job) {
	if j.interval != 1 {
		panic("")
	}
	j.startDay = 2
	job = j.Weeks()
	return
}

// Set the start day woth Wednesday
func (j *innerJob) Wednesday() (job Job) {
	if j.interval != 1 {
		panic("")
	}
	j.startDay = 3
	job = j.Weeks()
	return
}

// Set the start day with thursday
func (j *innerJob) Thursday() (job Job) {
	if j.interval != 1 {
		panic("")
	}
	j.startDay = 4
	job = j.Weeks()
	return
}

// Set the start day with friday
func (j *innerJob) Friday() (job Job) {
	if j.interval != 1 {
		panic("")
	}
	j.startDay = 5
	job = j.Weeks()
	return
}

// Set the start day with saturday
func (j *innerJob) Saturday() (job Job) {
	if j.interval != 1 {
		panic("")
	}
	j.startDay = 6
	job = j.Weeks()
	return
}

// Set the start day with sunday
func (j *innerJob) Sunday() (job Job) {
	if j.interval != 1 {
		panic("")
	}
	j.startDay = 0
	job = j.Weeks()
	return
}

//Set the units as weeks
func (j *innerJob) Weeks() Job {
	j.unit = "weeks"
	return j
}
