package handlers

import (
	"github.com/yuin/gopher-lua"
	"time"
)

const luaDateTypeName = "date"
const timeFormat = "2006-01-02"

func luaRegisterDates(L *lua.LState) {
	mt := L.NewTypeMetatable(luaDateTypeName)
	L.SetGlobal("date", mt)
	L.SetField(mt, "new", L.NewFunction(luaDateNew))
	L.SetField(mt, "now", L.NewFunction(luaDateNow))
	L.SetField(mt, "__index", L.NewFunction(luaDate__index))
	L.SetField(mt, "__tostring", L.NewFunction(luaDate__tostring))
	L.SetField(mt, "__eq", L.NewFunction(luaDate__eq))
	L.SetField(mt, "__lt", L.NewFunction(luaDate__lt))
	L.SetField(mt, "__le", L.NewFunction(luaDate__le))
	L.SetField(mt, "__add", L.NewFunction(luaDate__add))
	L.SetField(mt, "__sub", L.NewFunction(luaDate__sub))
	L.SetField(mt, "__metatable", lua.LString("protected"))
}

func TimeToLua(L *lua.LState, date *time.Time) *lua.LUserData {
	ud := L.NewUserData()
	ud.Value = date
	L.SetMetatable(ud, L.GetTypeMetatable(luaDateTypeName))
	return ud
}

// Checks whether the first lua argument is a *LUserData with *Time and returns this *Time.
func luaCheckTime(L *lua.LState, n int) *time.Time {
	ud := L.CheckUserData(n)
	if date, ok := ud.Value.(*time.Time); ok {
		return date
	}
	L.ArgError(n, "date expected")
	return nil
}

func luaWeakCheckTime(L *lua.LState, n int) *time.Time {
	v := L.Get(n)
	if ud, ok := v.(*lua.LUserData); ok {
		if date, ok := ud.Value.(*time.Time); ok {
			return date
		}
	}
	return nil
}

func luaWeakCheckTableFieldInt(L *lua.LState, T *lua.LTable, n int, name string, def int) int {
	lv := T.RawGetString(name)
	if lv == lua.LNil {
		return def
	}
	if i, ok := lv.(lua.LNumber); ok {
		return int(i)
	}
	L.ArgError(n, "table field '"+name+"' expected to be int")
	return def
}

func luaDateNew(L *lua.LState) int {
	// TODO make this track the user's timezone
	v := L.Get(1)
	if s, ok := v.(lua.LString); ok {
		date, err := time.ParseInLocation(timeFormat, s.String(), time.Local)
		if err != nil {
			L.ArgError(1, "error parsing date string: "+err.Error())
			return 0
		}
		L.Push(TimeToLua(L, &date))
		return 1
	}
	var year, month, day int
	if t, ok := v.(*lua.LTable); ok {
		year = luaWeakCheckTableFieldInt(L, t, 1, "year", 0)
		month = luaWeakCheckTableFieldInt(L, t, 1, "month", 1)
		day = luaWeakCheckTableFieldInt(L, t, 1, "day", 1)
	} else {
		year = L.CheckInt(1)
		month = L.CheckInt(2)
		day = L.CheckInt(3)
	}
	date := time.Date(year, time.Month(month), day, 0, 0, 0, 0, time.Local)
	L.Push(TimeToLua(L, &date))
	return 1
}

func luaDateNow(L *lua.LState) int {
	now := time.Now()
	date := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local)
	L.Push(TimeToLua(L, &date))
	return 1
}

func luaDate__index(L *lua.LState) int {
	d := luaCheckTime(L, 1)
	field := L.CheckString(2)

	switch field {
	case "Year", "year":
		L.Push(lua.LNumber(d.Year()))
	case "Month", "month":
		L.Push(lua.LNumber(float64(d.Month())))
	case "Day", "day":
		L.Push(lua.LNumber(float64(d.Day())))
	default:
		L.ArgError(2, "unexpected date attribute: "+field)
	}

	return 1
}

func luaDate__tostring(L *lua.LState) int {
	a := luaCheckTime(L, 1)

	L.Push(lua.LString(a.Format(timeFormat)))

	return 1
}

func luaDate__eq(L *lua.LState) int {
	a := luaCheckTime(L, 1)
	b := luaCheckTime(L, 2)

	L.Push(lua.LBool(a.Equal(*b)))

	return 1
}

func luaDate__lt(L *lua.LState) int {
	a := luaCheckTime(L, 1)
	b := luaCheckTime(L, 2)

	L.Push(lua.LBool(a.Before(*b)))

	return 1
}

func luaDate__le(L *lua.LState) int {
	a := luaCheckTime(L, 1)
	b := luaCheckTime(L, 2)

	L.Push(lua.LBool(a.Equal(*b) || a.Before(*b)))

	return 1
}

func luaDate__add(L *lua.LState) int {
	a := luaCheckTime(L, 1)
	b := luaCheckTime(L, 2)

	date := a.AddDate(b.Year(), int(b.Month()), b.Day())
	L.Push(TimeToLua(L, &date))

	return 1
}

func luaDate__sub(L *lua.LState) int {
	a := luaCheckTime(L, 1)
	b := luaCheckTime(L, 2)

	date := a.AddDate(-b.Year(), -int(b.Month()), -b.Day())
	L.Push(TimeToLua(L, &date))

	return 1
}