-- TIMECYCLE EDITOR - derpy54320
require("utility/timecycle")
require("utility/menu")

-- generated file header
gHeader = "-- file generated by derpy's timecycle editor --\r\nRequireLoaderVersion(10)\r\n\r\n"

-- extra names
gWeathers = {"Cloudy","Rainy","Foggy","ExtraSunny","Hurricane"}
gWeathers[0] = "Sunny"
gExtras = {
	-- all names from timecycE.dat in order
	"School Hallways",
	"Cafeteria",
	"Chemistry",
	"Principals Office",
	"Biology",
	"Infirmary",
	"Janitors Room",
	"Library",
	"Boys Bathroom",
	"Girls Bathroom",
	"Wrestling Gym",
	"Pool and Gym",
	"Boys Dorm",
	"Classroom",
	"Trailer",
	"Art Room",
	"Auto Shop",
	"Auditorium",
	"(???)Chem_Plant",
	"(???)Nerd_Fortress",
	"(???)Island_3",
	"Staff Room",
	"Attic",
	"(???)Funhouse",
	"General Store",
	"Boxing Club",
	"Fire Works",
	"Bike Shop",
	"Comic Shop",
	"Test Area(Bike, Peds, Pickups)",
	"Prep House",
	"Clothing (Rich)",
	"Clothing (Poor)",
	"Girls Dorm",
	"Tenements",
	"Funhouse",
	"Asylum",
	"Barber",
	"Observatory",
	"IGOCart",
	"TGOCart",
	"Junkyard",
	"iLPStore",
	"Midway",
	"Hair_Salon",
	"Underwater",
	"GoKart2",
	"Gift_Shop",
	"Souvenir",
	"iMGRaceA",
	"iMGRaceB",
	"iMGRaceC",
	"WareHouse",
	"Freak_Show",
	"Poor_Hair",
	"iDropS",
	"iNerdS",
	"iJockS",
	"iPrepS",
	"iGrsrS",
	"BMXTrack",
	"Library2",
}

-- mods
gMods = {}

-- main menu
function main()
	local menu = CreateMenu("Timecycle Editor")
	menu.can_exit = false
	while menu:active() do
		if menu:option("Seasonal Timecycles",nil,"See main (outdoor) timecycles.") then
			O_Seasons(menu)
		elseif menu:option("Extra Timecycles",nil,"See extra (interior) timecycles.") then
			M_Extras()
		elseif menu:option("Restore Timecycles",nil,"Restore all timecycles to default settings.") then
			RestoreTimecycles()
			gMods = {}
		elseif menu:option("Save Timecycles(s)",nil,"Save timecycle data.") then
			M_Save()
		elseif menu:option("Load Timecycle(s)",nil,"Load timecycle data.") then
			M_Load()
		end
		menu:draw()
		Wait(0)
	end
	StopCurrentScriptCollection() -- marks the script collection (mod) as inactive in /list
end

-- seasonal menu
function O_Seasons(menu)
	local i = 1
	local seasons = {"fall","winter","summer"}
	while menu:active() do
		menu:draw("> "..(seasons[i] or "unused").." <")
		Wait(0)
		if menu:up() then
			i = math.mod(i,4) + 1
		elseif menu:down() then
			i = i - 1
			if i < 0 then
				i = 4
			end
		elseif menu:left() then
			return
		elseif menu:right() then
			return M_Season(i)
		end
	end
end
function M_Season(season)
	local menu = CreateMenu((({"Fall","Winter","Summer"})[season] or "Unused").." Timecycles")
	local current = WeatherGet() + 1
	if gWeathers[current] then
		menu.i = current
	end
	while menu:active() do
		current = WeatherGet()
		for weather = 0,5 do
			if menu:option(gWeathers[weather],weather == current and "[current]" or nil) then
				O_Weather(menu,season,weather)
				break
			end
		end
		menu:draw()
		Wait(0)
	end
end
function O_Weather(menu,season,weather)
	local h,m = ClockGet()
	if m >= 30 then
		h = h + 1
	end
	while menu:active() do
		menu:draw("> "..F_Time(h).." <")
		Wait(0)
		if menu:up() then
			h = math.mod(h+1,24)
		elseif menu:down() then
			h = h - 1
			if h < 0 then
				h = 23
			end
		elseif menu:left() then
			return
		elseif menu:right() then
			return M_Timecycle("tc = GetTimecycle("..h..", "..season..", "..weather..")",GetTimecycle(h,season,weather),h,season,weather)
		end
	end
end
function F_Time(h)
	if h < 12 then
		if h == 0 then
			h = 12
		end
		return h..":00 AM"
	elseif h > 12 then
		h = h - 12
	end
	return h..":00 PM"
end

-- extras menu
function M_Extras()
	local menu = CreateMenu("Extra Timecycles","Timecycles for areas that don't use the main outdoor timecycles (like interiors).")
	while menu:active() do
		for i = 0,71 do
			local index = math.floor(i/24)
			local area = i - index * 24
			local name = gExtras[i+1]
			if menu:option((name or "Unused").." ("..area..", "..index..")") then
				M_Timecycle("tc = GetExtraTimecycle("..area..", "..index..")",GetExtraTimecycle(area,index))
				break
			end
		end
		menu:draw()
		Wait(0)
	end
end

-- edit menu
function M_Timecycle(code,tc,h,season,weather)
	local locked
	local menu = CreateMenu("Edit Timecycle",code)
	while menu:active() do
		if menu:option("< apply globally >",nil,code.."\nApply this timecycle onto all timecycles.") and O_Sure(menu) then
			F_SetAll(code,tc)
		elseif h and menu:option("< make active >",locked and "[locked]",code.."\nSet the chapter, weather, and hour needed to see this timecycle.") then
			locked = F_Activate(locked,season,weather)
		end
		for _,f in ipairs(gFields) do
			if menu:option(f[1],f[2](tc[f[1]])) then
				local str = O_Type(menu)
				local value = str and f[3](str)
				if value then
					F_Set(code,tc,f,value)
					SoundPlay2D("RightBtn")
				else
					SoundPlay2D("WrongBtn")
				end
				break
			end
		end
		if locked then
			if not AreaIsLoading() and (AreaGetVisible() ~= 0 or SeasonGet() ~= season --[[or WeatherGet() ~= weather]]) then
				locked()
				locked = nil
			else
				ClockSet(h,0)
			end
		end
		menu:draw()
		Wait(0)
	end
	if locked then
		while AreaIsLoading() do
			menu:draw()
			Wait(0)
		end
		locked()
	end
end
function F_Activate(locked,season,weather)
	if AreaIsLoading() then
		menu:alert("Cannot activate while an area is loading.",3)
		SoundPlay2D("WrongBtn")
	elseif locked then
		locked()
		locked = nil
	elseif AreaGetVisible() == 0 then
		local c,w,h,m = ChapterGet(),WeatherGet(),ClockGet()
		if SeasonGet() ~= season then
			ChapterSet(({[0]=6,[1]=0,[2]=2,[3]=3})[season])
		end
		WeatherSet(weather)
		locked = function()
			if ChapterGet() ~= c then
				ChapterSet(c)
			end
			WeatherSet(w)
			WeatherRelease()
			ClockSet(h,m)
		end
	else
		menu:alert("Cannot activate indoors.",3)
		SoundPlay2D("WrongBtn")
	end
	return locked
end
function F_SetAll(code,tc)
	local state = {}
	for k,mods in pairs(gMods) do
		if mods.all or k == tc then
			for k,v in pairs(mods.state) do
				state[k] = v -- merge all changes to this timecycle into one
			end
		end
	end
	gMods = {{all = true,code = code,state = state}} -- has no associated tc (and instead uses key 1)
	SetAllTimecycles(tc)
end
function F_Set(code,tc,f,v)
	local mods = gMods[tc]
	if not mods then
		mods = {code = code,state = {}}
		gMods[tc] = mods
	end
	tc[f[1]] = v
	mods.state[f[1]] = f[2](tc[f[1]])
end

-- save menu
function M_Save()
	local x,y,w,h = 0,0,0,0
	local draw = F_GetDraw()
	local menu = CreateMenu("Save Timecycle(s)")
	while menu:active() do
		if menu:option("Save All (as script)",nil,"Save *all* timecycle data as a script.") then
			O_Save(menu,false,F_SaveAll)
		elseif menu:option("Save All (as DATS)",nil,"Save *all* timecycle data as DAT files.") then
			O_Save(menu,true,F_SaveDATS)
		elseif next(gMods) then
			if menu:option("Save Modifications",nil,"Only save changes made using this menu.") then
				O_Save(menu,false,F_Save)
			elseif w ~= 0 and menu:hover() then
				local ph = 0.005
				local pw = ph / GetDisplayAspectRatio()
				SetTextFont("Consolas")
				SetTextColor(255,255,255,255)
				SetTextScale(0.5)
				local tw,th = MeasureText(draw)
				DrawRectangle(x+w,y,math.max(tw,0.25/GetDisplayAspectRatio())+pw*2,th+ph*2,0,0,0,255)
				SetTextPosition(x+w+pw,y+ph)
				SetTextAlign("L","T")
				DrawText(draw)
			end
			if menu:option("Clear Modifications",nil,"Clear all changes from the save record. Does not actually reset timecycle data.") and O_Sure(menu) then
				gMods = {}
			end
		end
		x,y,w,h = menu:draw()
		Wait(0)
	end
end
function O_Save(menu,folder,cb)
	local name = O_Type(menu)
	if name and not string.find(name,"[^%w-_. ]") then
		if not folder and string.lower(string.sub(name,-4)) ~= ".lua" then
			name = name..".lua"
		end
		if xpcall(function()
			if folder then
				CreateFolder("saves/"..name)
			else
				CreateFolder("saves")
			end
			cb("saves/"..name)
			menu:alert("Saved \""..name.."\"!",3)
			SoundPlay2D("RightBtn")
		end,function(error)
			menu:alert(tostring(error),3)
		end) then
			return
		end
	end
	SoundPlay2D("WrongBtn")
end
function F_Save(name)
	local sorted = F_GetMods()
	local f = OpenFile(name,"wb")
	WriteFile(f,gHeader)
	for i,mods in ipairs(sorted) do
		WriteFile(f,mods.code.."\r\n")
		for k,v in pairs(mods.state) do
			WriteFile(f,"tc."..k.." = "..v.."\r\n")
		end
		if mods.all then
			WriteFile(f,"SetAllTimecycles(tc)\r\n")
		end
		if sorted[i+1] then
			WriteFile(f,"\r\n")
		end
	end
	CloseFile(f)
end
function F_SaveAll(name)
	local f = OpenFile(name,"wb")
	WriteFile(f,gHeader)
	for h = 0,23 do
		for s = 0,2 do
			for w = 0,5 do
				WriteFile(f,"tc = GetTimecycle("..h..", "..s..", "..w..")\r\n")
				F_Dump(f,GetTimecycle(h,s,w))
				WriteFile(f,"\r\n")
			end
		end
		for i = 0,2 do
			WriteFile(f,"tc = GetExtraTimecycle("..h..", "..i..")\r\n")
			F_Dump(f,GetExtraTimecycle(h,i))
			if h ~= 23 or i ~= 2 then
				WriteFile(f,"\r\n")
			end
		end
	end
	CloseFile(f)
end
function F_SaveDATS(name)
	SaveTimecycleDATS(name.."/timecyc?.dat")
end
function F_Dump(file,tc)
	for _,f in ipairs(gFields) do
		WriteFile(file,"tc."..f[1].." = "..f[2](tc[f[1]]).."\r\n")
	end
end
function F_GetDraw()
	local limit = 1000
	local str = "-- output script preview\n"
	local sorted = F_GetMods()
	for i,mods in ipairs(sorted) do
		str = str..mods.code.."\n"
		for k,v in pairs(mods.state) do
			if string.len(str) > limit then
				return str.."..."
			end
			str = str.."tc."..k.." = "..v
			if sorted[i+1] or next(mods.state,k) then
				str = str.."\n"
			end
		end
		if mods.all then
			str = str.."SetAllTimecycles(tc)"
			if sorted[i+1] then
				str = str.."\n"
			end
		end
	end
	return str
end
function F_GetMods()
	local sorted = {n = 0}
	for _,mods in pairs(gMods) do
		table.insert(sorted,mods)
	end
	table.sort(sorted,function(a,b)
		if a.all ~= b.all then
			return a.all
		end
		return a.code < b.code
	end)
	return sorted
end

-- load menu
function M_Load()
	local menu = CreateMenu("Load Timecycle(s)")
	while menu:active() do
		if menu:option("Load Defaults",nil,"Same as restoring from the main menu.") then
			RestoreTimecycles()
			gMods = {}
		elseif menu:option("Load Script",nil,"Load a script from the \"saves\" folder.") then
			M_LoadScript()
		elseif menu:option("Load DATS",nil,"Load DAT files from the \"saves\" folder.") then
			M_LoadDATS()
		end
		menu:draw()
		Wait(0)
	end
end
function M_LoadScript()
	local menu = CreateMenu("Load Script","Load timecycle data from a script.")
	local scripts = {n = 0}
	for file in FindFiles("saves/*.lua") do
		if not file.directory then
			table.insert(scripts,file.name)
		end
	end
	table.sort(scripts)
	while menu:active() do
		for _,name in ipairs(scripts) do
			if menu:option(name) then
				F_Load(menu,name,F_LoadScript)
				break
			end
		end
		menu:draw()
		Wait(0)
	end
end
function F_LoadScript(name)
end
function M_LoadDATS()
	local menu = CreateMenu("Load DATS","Load timecycle data from a folder.")
	local scripts = {n = 0}
	for file in FindFiles("saves/*") do
		if file.directory and file.name ~= "." and file.name ~= ".." then
			table.insert(scripts,file.name)
		end
	end
	table.sort(scripts)
	while menu:active() do
		for _,name in ipairs(scripts) do
			if menu:option(name) then
				F_Load(menu,name,F_LoadDATS)
				break
			end
		end
		menu:draw()
		Wait(0)
	end
end
function F_Load(menu,name,cb)
	local tracker = F_BeginTracking()
	xpcall(function()
		cb("saves/"..name)
		F_FinishTracking(tracker)
		menu:alert("Loaded \""..name.."\"!",3)
		SoundPlay2D("RightBtn")
	end,function(error)
		menu:alert(tostring(error),3)
		SoundPlay2D("WrongBtn")
	end)
end
function F_LoadScript(name)
	local f = loadfile(GetScriptFilePath(name))
	setfenv(f,setmetatable({},{__index = _G}))
	return f()
end
function F_LoadDATS(name)
	RestoreTimecycles()
	ApplyTimecycleDATS(LoadTimecycleDATS(name.."/timecyc?.dat"))
end
function F_BeginTracking()
	local tcs = {n = 0}
	for h = 0,23 do
		for s = 0,2 do
			for w = 0,5 do
				table.insert(tcs,CopyTimecycle(GetTimecycle(h,s,w)))
			end
		end
		for i = 0,2 do
			table.insert(tcs,CopyTimecycle(GetExtraTimecycle(h,i)))
		end
	end
	return tcs
end
function F_FinishTracking(tcs)
	local tci = 0
	for h = 0,23 do
		for s = 0,2 do
			for w = 0,5 do
				tci = tci + 1
				F_AddChanges(tcs[tci],GetTimecycle(h,s,w),"tc = GetTimecycle("..h..", "..s..", "..w..")")
			end
		end
		for i = 0,2 do
			tci = tci + 1
			F_AddChanges(tcs[tci],GetExtraTimecycle(h,i),"tc = GetExtraTimecycle("..h..", "..i..")")
		end
	end
end
function F_AddChanges(a,b,code)
	for _,f in ipairs(gFields) do
		local k = f[1]
		if a[k] ~= b[k] then
			local mods = gMods[b]
			if not mods then
				mods = {code = code,state = {}}
				gMods[b] = mods
			end
			mods.state[k] = f[2](b[k])
		end
	end
end

-- common options
function O_Sure(menu)
	while menu:active() do
		menu:draw("are you sure?")
		Wait(0)
		if menu:right() then
			SoundPlay2D("RightBtn")
			return true
		elseif menu:left() then
			SoundPlay2D("WrongBtn")
			return
		end
	end
end
function O_Type(menu,filter)
	local started = GetTimer()
	local typing = StartTyping()
	local enable
	if not typing then
		return
	end
	enable = F_DisableControls()
	while menu:active() do
		if not IsTypingActive(typing) then
			if not WasTypingAborted(typing) then
				enable()
				return GetTypingString(typing)
			end
			break
		end
		if math.mod(math.floor((GetTimer()-started)/500),2) == 0 then
			menu:draw("> "..GetTypingString(typing).."| <")
		else
			menu:draw("> "..GetTypingString(typing).." <")
		end
		Wait(0)
	end
	while IsKeyPressed("ESCAPE") do
		menu:draw("> "..GetTypingString(typing).." <")
		Wait(0)
	end
	enable()
end

-- utility
function F_DisableControls()
	local events = {RegisterLocalEventHandler("ControllerUpdating",function(c)
		ZeroController(c)
	end),RegisterLocalEventHandler("ControllersUpdated",function()
		for c = 0,3 do
			ZeroController(c)
		end
	end)}
	return function()
		RemoveEventHandler(events[1])
		RemoveEventHandler(events[2])
	end
end

-- types
function F_GetRGB(v)
	return string.format("{%d, %d, %d}",v.r,v.g,v.b)
end
function F_SetRGB(s)
	local start,finish,r,g,b = string.find(s,"(%d+)[,%s]+(%d+)[,%s]+(%d+)")
	r,g,b = tonumber(r),tonumber(g),tonumber(b)
	if r and g and b then
		return {r,g,b}
	end
end
function F_GetRGBA(v)
	return string.format("{%d, %d, %d, %d}",v.r,v.g,v.b,v.a)
end
function F_SetRGBA(s)
	local start,finish,r,g,b,a = string.find(s,"(%d+)[,%s]+(%d+)[,%s]+(%d+)[,%s]+(%d+)")
	r,g,b,a = tonumber(r),tonumber(g),tonumber(b),tonumber(a)
	if r and g and b and a then
		return {r,g,b,a}
	end
end
function F_GetRGBF(v)
	return string.format("{%.2f, %.2f, %.2f}",v.r,v.g,v.b)
end
function F_SetRGBF(s)
	local start,finish,r,g,b = string.find(s,"([%d.]+)[,%s]+([%d.]+)[,%s]+([%d.]+)")
	r,g,b = tonumber(r),tonumber(g),tonumber(b)
	if r and g and b then
		return {r,g,b}
	end
end
function F_GetInt(v)
	return v
end
function F_SetInt(v)
	return tonumber(v)
end
function F_GetFloat(v)
	return string.format("%.2f",v)
end
function F_SetFloat(v)
	return tonumber(v)
end

-- fields
gFields = {
	-- {name,			get,		set}
	{"Amb_Props",		F_GetRGB,	F_SetRGB},
	{"Amb_World",		F_GetRGB,	F_SetRGB},
	{"Amb_Peds",		F_GetRGB,	F_SetRGB},
	{"SunRGB",			F_GetRGB,	F_SetRGB},
	{"Back",			F_GetRGB,	F_SetRGB},
	{"BackInt",			F_GetFloat,	F_SetFloat},
	{"NightFactor",		F_GetFloat,	F_SetFloat},
	{"lowcloudsRGB",	F_GetRGBF,	F_SetRGBF},
	{"TopCloudRGB",		F_GetRGB,	F_SetRGB},
	{"BottomCloudRGB",	F_GetRGB,	F_SetRGB},
	{"GlowThresh",		F_GetInt,	F_SetInt},
	{"GlowStrength",	F_GetInt,	F_SetInt},
	{"SKYTOP",			F_GetRGB,	F_SetRGB},
	{"Skybot",			F_GetRGB,	F_SetRGB},
	{"SunRGBcore",		F_GetRGB,	F_SetRGB},
	{"SunRGBcorona",	F_GetRGB,	F_SetRGB},
	{"SunRGBsz",		F_GetFloat,	F_SetFloat},
	{"Sprsz",			F_GetFloat,	F_SetFloat},
	{"Sprbrght",		F_GetFloat,	F_SetFloat},
	{"Shdw",			F_GetInt,	F_SetInt},
	{"Lightshd",		F_GetFloat,	F_SetFloat},
	{"Poleshd",			F_GetFloat,	F_SetFloat},
	{"Farclip",			F_GetInt,	F_SetInt},
	{"Fogst",			F_GetInt,	F_SetInt},
	{"Unknown1",		F_GetInt,	F_SetInt},
	{"Unknown2",		F_GetInt,	F_SetInt},
	{"CloudSpeed",		F_GetFloat,	F_SetFloat},
	{"CloudTopRGB",		F_GetRGB,	F_SetRGB},
	{"CloudBotRGB",		F_GetRGB,	F_SetRGB},
	{"ColFilterMode",	F_GetInt,	F_SetInt},
	{"ColFilterRGBA",	F_GetRGBA,	F_SetRGBA},
	{"AmbLRange",		F_GetFloat,	F_SetFloat},
	{"AmbHRange",		F_GetFloat,	F_SetFloat},
	{"SunLRange",		F_GetFloat,	F_SetFloat},
	{"SunHRange",		F_GetFloat,	F_SetFloat},
	{"BackLRange",		F_GetFloat,	F_SetFloat},
	{"BackHRange",		F_GetFloat,	F_SetFloat},
	{"NearFarRatio",	F_GetInt,	F_SetInt},
}
