local sw, sh = 1280, 720
local scx, scy = sw/2, sh/2
local songdir = GAMESTATE:GetCurrentSong():GetSongDir()
local base_BPM = 135*GAMESTATE:GetSongOptionsObject('ModsLevel_Song'):MusicRate()
local beat, last_beat = 0, -16
local TS
local bt = 60/base_BPM
local Cameras, Actors
local FG, elements

-- Reflesh rate.
local rr = 1/120

-- Why original eases? Just my (bad) habit.
local ease = loadfile(songdir..'fg/eases.lua')()

-- Unified tables for 'classical' strings and easing tables.
-- for string, {abs_pos, mods}
-- for easing, {abs_pos, {last_pos, 'ease_type', 'mod_type', initial strength, target strength}}
-- We can see the absolute position using iBMSC.
-- https://github.com/zardoru/iBMSC
local mods = {
	{0, 'clearall'},
	{0, '*-1 1x, *-1 100% dark, *-1 50% stealth'},
	{3384, {3456, 'x', 'dizzy', 0, 100}},
	{3576, {3648, 'x', 'dizzy', 100, 0}},
	{3648, '*-1 0% dizzy'},
	{3768, {3840, 'x', 'beat', 0, 200}},
	{3960, {4032, 'x', 'beat', 200, 0}},
	{4032, '*-1 0% beat'},
	{4152, {4224, 'x', 'tipsy', 0, 100}},
	{4344, {4416, 'x', 'tipsy', 100, 0}},
	{4416, '*-1 0% tipsy'},
	{4536, {4608, 'x', 'reverse', 0, 100}},
	{4716, '*-1 87.5% reverse'},
	{4728, '*-1 75.0% reverse'},
	{4740, '*-1 62.5% reverse'},
	{4752, '*-1 50.0% reverse'},
	{4764, '*-1 37.5% reverse'},
	{4776, '*-1 25.0% reverse'},
	{4788, '*-1 12.5% reverse'},
	{4800, '*-1 0% reverse'},
	{4800, {4896, 'rootx', 'reverse', 0, 100}},
	{4992, {5088, 'rootx', 'reverse', 100, 0}},
	{5184, {5280, 'rootx', 'cross', 0, 100}},
	{5376, {5472, 'rootx', 'cross', 100, 0}},
	{5568, {5664, 'rootx', 'reverse', 0, 100}},
	{5568, {5664, 'rootx', 'alternate', 0, 100}},
	{5760, {5856, 'rootx', 'reverse', 100, 0}},
	{5760, {5856, 'rootx', 'alternate', 100, 0}},
	{5856, '*-1 0% alternate, *-1 0% reverse, *-1 0% cross'},
	{10128, '*-1 1x'},
	{12096, '*-1 0.75x, *-1 0% dark, *-1 0% stealth'},
	{13632, {13680, 'x', 'x', 0.75, 1}},
	{13632, {13680, 'rootx', 'dark', 0, 100}},
	{13632, {13680, 'rootx', 'stealth', 0, 50}},
}

local sections = {
	{0, 'underlay', 'Start'},
	{0, 'overlay', 'Start'},
	{0, 'section1', 'Start'},
	{3216, 'section2', 'Start'},
	{3264, 'section1', 'End'},
	{6240, 'section3', 'Start'},
	{6240, 'section2', 'End'},
	{10128, 'section3', 'End'},
	{10176, 'section4', 'Start'},
	{12096, 'section5', 'Start'},
	{13632, 'section6', 'Start'},
	{13776, 'section4', 'End'},
	{13776, 'section5', 'End'}
}

-- Screen height ratio. This is applied only for the 'Player' manipulating table.
local hr = SCREEN_HEIGHT/720

-- Grab Player options.
local pe, po, hs = {false,false}, {nil,nil}, {3,3}
for i = 1, 2 do
	if GAMESTATE:IsPlayerEnabled(i-1) then
		pe[i] = true
		po[i] = GAMESTATE:GetPlayerState(i-1):GetPlayerOptions('ModsLevel_Song')
		if po[i]:XMod() then hs[i] = po[i]:XMod()
		elseif po[i]:MMod() then hs[i] = po[i]:MMod()/base_BPM
		elseif po[i]:CMod() then hs[i] = po[i]:CMod()/base_BPM
		elseif PlayerOptions.AMod and po[i]:AMod() then hs[i] = po[i]:AMod()/base_BPM end
		po[i]:XMod(hs[i], 10000)
		if hs[i] < 2 then hs[i] = 2 end
	end
end

-- Convert XMod to multiply of set speed mod cuz BMS players hate forced hispeed.
local function x2scroll(mod, pn)
	local mod = string.gsub(mod, '%*%-1', '*10000')
	if string.find(mod, '%d+%% x') then
		local tmp_hs = string.match(mod, '(%d+)%% x')
		mod = string.gsub(mod, '%d+%% x', tmp_hs..'x')
	end
	if not string.find(mod, '%d+x') then return mod
	else
		local a,b,xm = string.find(mod, '(%d+)x')
		if string.find(mod, '%d+%.%d+x') then
			a,b,xm = string.find(mod, '(%d+%.%d+)x')
		end
		local s1, s2, s3 = string.sub(mod, 1,a-1), 'x', ''
		if b < string.len(mod)-1 then
			s2 = 'x,'
			s3 = string.sub(mod, b+1, -1)
		end
		return s1..tostring(tonumber(xm)*hs[pn])..s2..s3
	end
end

-- NVIDIA check.
local nvidia = false
local am = 1
if string.find(string.lower(PREFSMAN:GetPreference('LastSeenVideoDriver')), 'nvidia') then
	Trace('NVidia graphics driver detected.')
	Trace('AFT multiplier set to 0.9')
	nvidia = true
	am = 0.9
end

-- Grab ratio for AFTs.
local wratio, hratio = 1,1
local tab_sp = function(str, ts)
	if ts == nil then return {} end
	local t = {}
	local itr = 1
	for s in string.gmatch(str, '([^'..ts..']+)') do
		t[itr] = s;
		itr = itr+1;
	end
	return t
end
local RF = RageFileUtil.CreateRageFile()
if RF:Open(songdir..'fg/ratio.lua', 1) then
	local ratio = tab_sp(RF:Read(), ',')
	wratio = ratio[1]
	hratio = ratio[2]
	RF:Close()
	RF:destroy()
end

-- Preparation of base_parameter table to be passed.
local base_param = {sw=sw, sh=sh, scx=scx, scy=scy, songdir=songdir, rr=rr, am=am, bt=bt, hs=hs, pe=pe, hr=hr, ease=ease, wratio=wratio, hratio=hratio}

-- Load paraph-made functions.
local paraph = loadfile(songdir..'fg/paraph_functions.lua')(base_param)
base_param.paraph = paraph

------------------------------------------------------------------------------------------------------------------------------------------------------
--															ActorFrame things start
------------------------------------------------------------------------------------------------------------------------------------------------------


local songpos = GAMESTATE:GetPlayerState(GAMESTATE:GetMasterPlayerNumber()):GetSongPosition()
return Def.ActorFrame{
-- Name 'FG' is required to refer this actor from BG.
Name='FG',
InitCommand=function(self)
	-- Centers the base AF. Basezoom to this quite well in terms of resolution compatibility.
	self:Center():basezoom(SCREEN_HEIGHT/sh):queuecommand('On')
	-- Shortcuts.
	TS = SCREENMAN:GetTopScreen()
	FG = self
	elements = self:GetChild('elements')
	Cameras = self:GetChild('Cameras')
	Actors = self:GetChild('Actors')
end,

-- Augumented player which includes column divider (and more in future?).
loadfile(songdir..'fg/player_aug.lua')({pe = pe, color = {1,1,1,0.75}, mode = 'alpha'}),
loadfile(songdir..'fg/player_aug_orig.lua')({pe = pe, color = {1,1,1,0.75}, mode = 'alpha'}),

-- Preparation of player AFT.
paraph.AFT{Command='InitCommand'}..{
	Name='PlayerAFT',
	Def.ActorProxy{
		Name='Player',
		OnCommand=function(self)
			self:SetTarget(FG:GetChild('Player'))
		end
	}
},
paraph.AFT{Command='InitCommand'}..{
	Name='PlayerAFTorig',
	Def.ActorProxy{
		Name='Player',
		OnCommand=function(self)
			self:SetTarget(FG:GetChild('PlayerOrig'))
		end,
	},
},

-- Control actor, originally taken from Rosemary's Nocturne (KUSOFUMASU 2020).
-- Slightly modified to avoid global varibales on 6,Feb,2021.
-- Further modified to use ease functions to control cameras and actors, on 23,Feb,2021.
Def.Actor{OnCommand=function(self) self:sleep(9999) end},
Def.Actor{OnCommand=function(self) self:playcommand('Update') end,
	UpdateCommand=function(self)
		beat = songpos:GetSongBeat()*48

		for m, mod in ipairs(mods) do
			if beat >= mod[1] then
				for pn = 1, 2 do
					if pe[pn] then
						if type(mod[2]) == 'string' and last_beat < mod[1] then
							po[pn]:FromString(x2scroll(mod[2],pn))
						elseif type(mod[2]) == 'table' and beat < mod[2][1] then
							po[pn]:FromString(x2scroll('*-1 '..ease[mod[2][2]](beat-mod[1], mod[2][1]-mod[1], mod[2][4], mod[2][5])..'% '..mod[2][3],pn))
						elseif type(mod[2]) == 'table' and last_beat < mod[1] then
							po[pn]:FromString(x2scroll('*-1 '..mod[2][5]..'% '..mod[2][3],pn))
						end
					end
				end
			elseif beat < mod[1] then
				break
			end
		end

		for i, section in ipairs(sections) do
			if beat >= section[1] then
				if not elements:GetChild(section[2]) then table.remove(sections, i) break end
				local af = elements:GetChild(section[2])
				if section[3] == 'Start' then
					af:visible(true):playcommand('Initialize')
				elseif section[3] == 'End' then
					elements:RemoveChild(section[2])
				end
				table.remove(sections, i)
			elseif beat < section[1] then
				break
			end
		end

		for i, camera in ipairs(Cameras) do
			if camera.Name and elements:GetChild(camera.Name) then
				for j, tween in ipairs(camera.Tween) do
					if beat >= tween[1] then
						local a = elements:GetChild(camera.Name)
						if camera.Target then
							local target = paraph.split(camera.Target, '/')
							for k = 1, #target do
								if a:GetChild(target[k]) then
									a = a:GetChild(target[k])
								end
							end
						end
						if not a then table.remove(camera.Tween,j) break end
						if type(tween[2]) == 'string' and tween[3] then
							if tween[4] then
								paraph[tween[2]](a,tween[3],tween[4])
							else
								paraph[tween[2]](a,tween[3])
							end
							table.remove(camera.Tween,j)
						elseif type(tween[2]) == 'table' then
							if beat < tween[2][1] then
								if type(tween[2][4]) == 'number' then
									paraph[tween[2][3]](a, ease[tween[2][2]](beat-tween[1], tween[2][1]-tween[1], tween[2][4], tween[2][5]))
								elseif type(tween[2][4]) == 'table' and type(tween[2][5]) == 'table' and #tween[2][4] == #tween[2][5] then
									local temp = {}
									for k = 1, #tween[2][4] do
										temp[k] = ease[tween[2][2]](beat-tween[1],tween[2][1]-tween[1], tween[2][4][k], tween[2][5][k])
									end
									paraph[tween[2][3]](a, temp)
								else SCREENMAN:SystemMessage('Dimension inconsistency in camera manipulation table: '..camera.Name)
								end
							elseif beat >= tween[2][1] then
								paraph[tween[2][3]](a, tween[2][5])
								table.remove(camera.Tween,j)
							end
						end
					elseif beat < tween[1] then
						break
					end
				end
			end
		end

		for i, actor in ipairs(Actors) do
			if actor.Name and elements:GetChild(actor.Name) then
				for j, tween in ipairs(actor.Tween) do
					if beat >= tween[2] then
						local a = elements:GetChild(actor.Name)
						local target = paraph.split(tween[1], '/')
						for k = 1, #target do
							if a:GetChild(target[k]) then
								a = a:GetChild(target[k])
							end
						end
						if not a then table.remove(actor.Tween,j) break end
						if type(tween[3]) == 'string' and tween[4] then
							if tween[5] then
								paraph[tween[3]](a,tween[4],tween[5])
							else
								paraph[tween[3]](a,tween[4])
							end
							table.remove(actor.Tween,j)
						elseif type(tween[3]) == 'table' then
							if beat < tween[3][1] then
								if type(tween[3][4]) == 'number' then
									paraph[tween[3][3]](a, ease[tween[3][2]](beat-tween[2], tween[3][1]-tween[2], tween[3][4], tween[3][5]))
								elseif type(tween[3][4]) == 'table' and type(tween[3][5]) == 'table' and #tween[3][4] == #tween[3][5] then
									local temp = {}
									for k = 1, #tween[3][4] do
										temp[k] = ease[tween[3][2]](beat-tween[2],tween[3][1]-tween[2], tween[3][4][k], tween[3][5][k])
									end
									paraph[tween[3][3]](a, temp)
								else SCREENMAN:SystemMessage('Dimension inconsistency in actor manipulation table: '..actor.Name)
								end
							elseif beat >= tween[3][1] then
								paraph[tween[3][3]](a, tween[3][5])
								table.remove(actor.Tween,j)
							end
						end
					elseif beat < tween[2] then
						break
					end
				end
			-- This is too redundant but the lightest way to distinguish the display and the element things I think?
			elseif actor.Name == 'display' and FG:GetChild('display') then
				for j, tween in ipairs(actor.Tween) do
					if beat >= tween[2] then
						local a = FG:GetChild('display')
						local target = paraph.split(tween[1], '/')
						for k = 1, #target do
							if a:GetChild(target[k]) then
								a = a:GetChild(target[k])
							end
						end
						if not a then table.remove(actor.Tween,j) break end
						if type(tween[3]) == 'string' and tween[4] then
							if tween[5] then
								paraph[tween[3]](a,tween[4],tween[5])
							else
								paraph[tween[3]](a,tween[4])
							end
							table.remove(actor.Tween,j)
						elseif type(tween[3]) == 'table' then
							if beat < tween[3][1] then
								if type(tween[3][4]) == 'number' then
									paraph[tween[3][3]](a, ease[tween[3][2]](beat-tween[2], tween[3][1]-tween[2], tween[3][4], tween[3][5]))
								elseif type(tween[3][4]) == 'table' and type(tween[3][5]) == 'table' and #tween[3][4] == #tween[3][5] then
									local temp = {}
									for k = 1, #tween[3][4] do
										temp[k] = ease[tween[3][2]](beat-tween[2],tween[3][1]-tween[2], tween[3][4][k], tween[3][5][k])
									end
									paraph[tween[3][3]](a, temp)
								else SCREENMAN:SystemMessage('Dimension inconsistency in actor manipulation table: '..actor.Name)
								end
							elseif beat >= tween[3][1] then
								paraph[tween[3][3]](a, tween[3][5])
								table.remove(actor.Tween,j)
							end
						end
					elseif beat < tween[2] then
						break
					end
				end
			end
		end

		last_beat = beat
		self:sleep(rr):queuecommand('Update')
	end,
},
Def.Actor{Name='Cameras'},
Def.Actor{Name='Actors'},

-- Display elements.
paraph.AFT{Command='OnCommand'}..{
	Name='elements',
	loadfile(songdir..'fg/underlay/underlay.lua')(base_param),
	loadfile(songdir..'fg/section1/section1.lua')(base_param),
	loadfile(songdir..'fg/section2/section2.lua')(base_param),
	loadfile(songdir..'fg/section3/section3.lua')(base_param),
	loadfile(songdir..'fg/section6/section6.lua')(base_param),
	loadfile(songdir..'fg/section5/section5.lua')(base_param),
	loadfile(songdir..'fg/section4/section4.lua')(base_param),
	loadfile(songdir..'fg/overlay/overlay.lua')(base_param),
},

-- Display itself.
loadfile(songdir..'fg/display.lua')(base_param),
}