dofile(AppPath.."Data/Lua/RSFunctions.lua")
dofile(AppPath.."Data/Lua/events.lua")
PrintToFile(AppPath.."LuaLog.txt")

local _G = _G

local function event(name, ...)
	events.global[name](...)
	events[name](...)
end

ScreenScrollDetectionDelta = 1

Version = 1.11

-----------------------------------------------
-- Objects
-----------------------------------------------

local mmfObj = mmf.Object
local ObjIndex = {}

for k, v in pairs(mmfObj) do
	ObjIndex[k] = function(t, ...)
		return v(t.ID, ...)
	end
end

function ObjIndex:Move(dx, dy)
	local x, y = self:GetPosition()
	self:SetPosition(x + (dx or 0), y + (dy or 0))
	self:Update()
end

function ObjIndex:ReplaceColor(r1, g1, b1, r2, g2, b2)
	if b1 then
		return DoCall("Recolor Object", self.ID, r1 + (g1 + b1*256)*256, r2 + (g2 + b2*256)*256)
	else
		return DoCall("Recolor Object", self.ID, r1, g1)
	end
end

function ObjIndex:GetAnimation()
	local frame, anim, dir = DoCall("Get Object Frame, Animation, Direction", self.ID)
	return anim
end

function ObjIndex:GetAnimationFrame()
	local frame, anim, dir = DoCall("Get Object Frame, Animation, Direction", self.ID)
	return frame
end

function ObjIndex:GetAnimationDirection()
	local frame, anim, dir = DoCall("Get Object Frame, Animation, Direction", self.ID)
	return dir
end

function ObjIndex:SetAnimation(v)
	DoCall("Set Object Animation", self.ID, v)
end

function ObjIndex:SetAnimationFrame(v, animate)
	DoCall("Set Object Frame", self.ID, v)
	if animate then
		DoCall("Restore Object Frame", self.ID, v)
	end
end

function ObjIndex:SetAnimationDirection(v)
	DoCall("Set Object Animation Direction", self.ID, v)
end

function ObjIndex:RestoreAnimationFrame()
	DoCall("Restore Object Frame", self.ID)
end

function ObjIndex:RestoreAnimation()
	DoCall("Restore Object Animation", self.ID)
end

function ObjIndex:RestoreAnimationDirection()
	DoCall("Restore Object Animation Direction", self.ID)
end


-- function ObjIndex:GetSpeed()
	-- return DoCall("GetSpeed", self.ID)
-- end
-- function ObjIndex:SetSpeed(v)
	-- DoCall("SetSpeed", self.ID, v)
-- end

function ObjIndex:GetTransparency()
	return DoCall("GetTransparency", self.ID)
end
function ObjIndex:SetTransparency(v)
	DoCall("SetTransparency", self.ID, v)
end

function ObjIndex:OverlapsObstacle(dx, dy)
	dx = dx or 0
	dy = dy or 0
	if dx ~= 0 or dy ~= 0 then
		local x, y = self:GetPosition()
		self:SetPosition(x + dx, y + dy)
		ObstacleOverlap = (DoCall("CheckObstacleOverlap", self.ID) == true)
		event("CheckObstacleOverlap", self)
		self:SetPosition(x, y)
	else
		ObstacleOverlap = (DoCall("CheckObstacleOverlap", self.ID) == true)
		event("CheckObstacleOverlap", self)
	end
	return ObstacleOverlap
end

function ObjIndex:LoadFrame(file, anim, frame, direction, hotX, hotY, actionX, actionY, transp)
	anim = anim or 0
	direction = direction or 0
	frame = fame or 0
	hotX = hotX or 100000
	hotY = hotY or 100000
	actionX = actionX or hotX
	actionY = actionY or hotY
	transp = transp or 0x1000000
	return DoCall("Load Frame", self.ID, file, anim, direction, frame, hotX, hotY, actionX, actionY, transp)
end

function ObjIndex:GotoPathNode(node)
	DoCall("Goto Path Node", self.ID, tostring(node))
end

function ObjIndex:Stop()
	DoCall("Stop Movement", self.ID)
end

----------- Text Blitter

local TextIndex = setmetatable({}, {__index = ObjIndex})

function TextIndex:GetText()
	return DoCall("T Get Text", self.ID)
end

function TextIndex:SetText(text)
	return DoCall("T Set Text", self.ID, text)
end

----------- MakeObject

local function MakeObject(id, t, meta)
	meta = meta or ObjIndex
	t = t or {}
	t.ID = id
	return setmetatable(t, {__index = meta})
end

local function MakeObjectEx(id, vals, t, meta)
	meta = meta or ObjIndex
	t = t or {}
	t.ID = id

	local function index(t, name)
		local k = vals[name]
		return k and mmfObj.GetValue(id, k - 1) or meta[name]
	end
	
	local function newindex(t, name, v)
		local k = vals[name]
		if k then
			mmfObj.SetValue(id, k - 1, v)
		end
	end

	return setmetatable(t, {__index = index, __newindex = newindex})
end

Objects = {}

Objects.Player = MakeObjectEx(1, {
	Run = 1,
	Climb = 2,
	DoubleJump = 3,
	HighJump = 4,
	Eye = 5,
	Detector = 6,
	Umbrella = 7,
	Hologram = 8,
	Key1 = 9,
	Key2 = 10,
	Key3 = 11,
	Key4 = 12,
	AfterTeleportCountdown = 13,
	TeleportLightX = 14,
	TeleportLightY = 15,
	Cheating = 16,
	CanTeleport	= 17,
})

Objects.PlayerPos = MakeObjectEx(2, {
	Direction = 1,
	CanDoubleJump = 2,
	JustDoubleJumped = 3,
	JustJumped = 4,
	HologramActivationTimer = 5,
	JumpBlock = 6,
	PlayLandSoundOnGroundTouch = 7,
	LoopAction = 8,
	PrevAction = 9,
	DelayX = 10,
	DelayY = 11,
	NormalJumpCountdown = 12,
})

Objects.Umbrella = MakeObjectEx(3, {
	--UnusedA = 1,
	InUse = 2,
})
Objects.Flags = MakeObjectEx(4, {
	Flag0 = 1,
	Flag1 = 2,
	Flag2 = 3,
	Flag3 = 4,
	Flag4 = 5,
	Flag5 = 6,
	Flag6 = 7,
	Flag7 = 8,
	Flag8 = 9,
	Flag9 = 10,
	FlagWarpAX = 11,
	FlagWarpAY = 12,
	FlagWarpBX = 13,
	FlagWarpBY = 14,
	FlagWarpCX = 15,
	FlagWarpCY = 16,
})

Objects.Physics = MakeObjectEx(5, {
	MaxXWalk = 1,
	MaxXRun = 2,
	Jump = 3,
	HiJump = 4,
})

-- Objects.PlayerHologram = MakeObjectEx(6, {
	-- Run = 1,
	-- Climb = 2,
	-- DoubleJump = 3,
	-- HighJump = 4,
	-- Eye = 5,
	-- Detector = 6,
	-- Umbrella = 7,
	-- UndefinedPowerH = 8,
-- })

local PlayerHologramProps = {
	Run = 1,
	Climb = 2,
	DoubleJump = 3,
	HighJump = 4,
	Eye = 5,
	Detector = 6,
	Umbrella = 7,
	UndefinedPowerH = 8,
}

Objects.Sound = MakeObjectEx(7, {
	CurrentAmbianceA = 1,
	CurrentChannelA = 2,
	CurrentAmbianceB = 3,
	CurrentChannelB = 4,
	FaderAA = 5,
	FaderAB = 6,
	FaderBA = 7,
	FaderBB = 8,
	InitFadeupVolumeA = 9,
	NextAllocatedChannel = 10,
	InitFadeupVolumeB = 11,
})

Objects.PlayerVisiblePos = MakeObject(8)
Objects.SignText = MakeObject(9, {}, TextIndex)
Objects.SignFrame = MakeObject(10)
Objects.PickUpLight = MakeObject(11)
Objects.SecretsMarker = MakeObject(12)
Objects.DangerMarker = MakeObject(13)

Objects.KBD = MakeObjectEx(14, {
	LeftRight = 1,
	Jump = 2,
	JumpOneshot = 3,
	UpDown = 4,
	Walk = 5,
	ItemList = 6,
	UmbrellaOneshot = 7,
	DownOneshot = 8,
	Hologram = 9,
})

Objects.BlackScreen = MakeObject(15)
Objects.WhiteScreen = MakeObject(16)



local ObjList, PickObjects

local function obj_enum(t, o)
	o = next(ObjList, o)
	while o do
		local ok = true
		if t then
			for k, v in pairs(t) do
				if o[k] ~= v then
					ok = false
					break
				end
			end
		end
		if ok then
			return o
		end
		o = next(ObjList, o)
	end
end

Objects.Find = obj_enum

function Objects.Enum(t)
	return obj_enum, t, nil
end

function Objects.new(t, st, x, y, z)
	z = z or 7
	DoCall("Create Object", t, st, x, y, z)
	local pick = PickObjects
	-- move to proper Z order position
	for k, obj in ipairs(pick) do
		local oid = obj.ID
		if z < 6 then
			mmf.Object.MoveBelow(oid, Objects.Umbrella.ID)
		end
		for v in Objects.Enum() do
			if z < v.Layer and mmf.Object.IsAbove(oid, v.ID) then
				mmf.Object.MoveBelow(oid, v.ID)
			end
		end
	end
	return unpack(pick)
end

Objects.New = Objects.new

local TakenTemplates

function Objects.NewTemplate(x, y, z)
	for i = 1, 99 do
		if not TakenTemplates[i] then
			return Objects.new(-1, i, x, y, z)
		end
	end
end

-----------------------------------------------
-- XLua Functions
-----------------------------------------------

local OldKeyDown = mmf.Keyboard.KeyDown
local OldKeyUp = mmf.Keyboard.KeyUp

function mmf.Keyboard.KeyDown(...)
	return (DoCall("IsWindowActive") == true) and OldKeyDown(...)
end

function mmf.Keyboard.KeyUp(...)
	return (DoCall("IsWindowActive") ~= true) or OldKeyUp(...)
end

-----------------------------------------------
-- Game events and functions
-----------------------------------------------

local timeIntervals, timeFunctions, tick
local globalTick = 0
local movementTempDeact
local ghosts

function _internal_BeforeLoadScreen(x, y)
	ObjList = {}
	timeIntervals = {}
	timeFunctions = {}
	ghosts = {}
	TakenTemplates = {}
	tick = 0
	event("LeftScreen")
	events.RemoveAll()
	event("BeforeLoadScreen", x, y)
	if ScreenTransition == "white" then
		Objects.WhiteScreen:SetVisibility(true)
	elseif ScreenTransition == "black" then
		Objects.BlackScreen:SetVisibility(true)
	end
end

function _internal_AfterLoadScreen(x, y)
	if movementTempDeact then
		DoCall("Set Standard Movement Engine Active", true)
		movementTempDeact = false
	end
	if ScreenTransition == "white" then
		Objects.WhiteScreen:SetVisibility(false)
	elseif ScreenTransition == "black" then
		Objects.BlackScreen:SetVisibility(false)
	end
	ScreenTransition = nil
	event("LoadScreen", x, y)
	local f = _G['x'..x..'y'..y]
	if f then
		f(x, y)
	end
end

function _internal_AddObject(t, st, x, y, z, ...)
	PickObjects = {...}
	for _, id in ipairs(PickObjects) do
		local obj = MakeObject(id, {Bank = t, Obj = st, BaseX = x, BaseY = y, Layer = z})
		PickObjects[_] = obj
		ObjList[obj] = true
		if t == -1 and TakenTemplates then
			TakenTemplates[st] = true
		end
	end
end

function _internal_CanCreateObject(t, ...)
	CanCreateObject = true
	event("CanCreateObject", t, ...)
	if t == 12 and LoadGhostsWhenEyeIsTaken and CanCreateObject and ghosts and Objects.Player.Eye == 0 then
		ghosts[#ghosts + 1] = {t, ...}
	end
end

function mmf.DestroyCallback(id)
	local o = (id == 6 and Objects.PlayerHologram or Objects.Find{ID = id})
	if o then
		event("DestroyObject", o)
		if o.OnDestroy then
			o:OnDestroy()
		end
		o.ID = nil
		ObjList[o] = nil
		if id == 6 then
			Objects.PlayerHologram = nil
		end
		setmetatable(o, nil)
	end
end

function _internal_HolorgamCreated()
	Objects.PlayerHologram = MakeObjectEx(6, PlayerHologramProps)
	event("ShowHologram")
end

function _internal_OnShift(id, quantize)
	event("Shift", id, quantize)
	event("Shift"..id, id, quantize)
end

local KeyMDown, CurrentTimer

function _internal_OnTimer()
	event("Timer")
	tick = tick + 1
	globalTick = globalTick + 1
	for i = 1, #timeIntervals do
		if tick % timeIntervals[i] == 0 then
			CurrentTimer = i
			timeFunctions[i](tick)
		end
	end
	CurrentTimer = nil

	-- Ctrl+M: music on/off
	local old = KeyMDown
	KeyMDown = mmf.Keyboard.KeyDown(mmf.VK_M)
	if KeyMDown and not old and mmf.Keyboard.KeyDown(mmf.VK_CONTROL) then
		if Game.MusicVolume == 0 then
			Game.MusicVolume = 100
			Game.AmbianceVolume = 100
		else
			Game.MusicVolume = 0
			Game.AmbianceVolume = 0
		end
		DoCall("Stop Music And Ambience")
	end
	
	-- load ghosts when eye is taken
	if ghosts and LoadGhostsWhenEyeIsTaken and Objects.Player.Eye ~= 0 then
		for i = 1, #ghosts do
			Objects.new(unpack(ghosts[i]))
		end
		ghosts = nil
	end	
end

function Timer(int, f)
	local i = #timeIntervals + 1
	timeIntervals[i] = int
	timeFunctions[i] = f
end

local function nullsub()
end

function RemoveTimer(f)
	if f then
		for i = 1, #timeFunctions do
			if timeFunctions[i] == f then
				timeFunctions[i] = nullsub
			end
		end
	elseif CurrentTimer then
		timeFunctions[CurrentTimer] = nullsub
	end
end

function GetTick()
	return tick
end

function GetGlobalTick()
	return globalTick
end

vars = {}  -- variables for use of level scripts

local function serialize(o)
	local tp = type(o)
	if tp == "number" or tp == "boolean" or tp == "nil" then
		return tostring(o)
	elseif tp == "string" then
		return string.format("%q", o)
	elseif tp == "table" then
		local a = {"{"}
		for k, v in pairs(o) do
			a[#a+1] = "["
			a[#a+1] = serialize(k)
			a[#a+1] = "] = "
			a[#a+1] = serialize(v)
			a[#a+1] = ","
		end
		a[#a+1] = "}"
		return table.concat(a)
	else
		error("cannot serialize a " .. tp)
	end
end

function _internal_SaveGame()
	event("SaveGame")
	DoCall("Write To Save Game", "Lua", "vars", serialize(vars))
end

function _internal_LoadGame()
	vars = {}
	local s = DoCall("Read From Save Game", "Lua", "vars")
	if s ~= "" then
		local f = loadstring("return "..s)
		if f then
			setfenv(f, {})
			local ok, v = pcall(f)
			if ok then
				vars = v
			end
		end
	end
	event("LoadGame")
end

function SaveGame(x, y)
	local x1, y1 = Objects.PlayerPos:GetPosition()
	if not x then
		x = math.floor(x1/24)
		if x < 0 then  x = 0  end
		if x > 24 then  x = 24 end
	end
	if not y then
		y = y or math.floor(y1/24)
		if y < 0 then  y = 0  end
		if y > 9 then  y = 9  end
	end
	DoCall("Save Game", x, y)
end

function _internal_OnCheckObstacleOverlap(over)
	ObstacleOverlap = (over == true)
	event("CheckObstacleOverlap", Objects.PlayerPos)
end

function _internal_OnCanClimb(can)
	CanClimb = can
	event("CanClimb")
end

function CheckNoClimb()
	return (DoCall("CheckNoClimb") == true)
end

function CheckNoJump()
	return (DoCall("CheckNoJump") == true)
end

function CheckSticky()
	return (DoCall("CheckSticky") == true)
end

function CheckUpWind()
	return (DoCall("CheckUpWind") == true)
end

function _internal_FilterKBD()
	event("KeysFilter")
end

function _internal_CheckHologramActivation(allow)
	AllowHologram = not not allow
	event("HologramActivateCheck")
end

function _internal_CheckDeath()
	AllowDeath = true
	event("Death")
end

function DeactivateMovement(perm)
	if not perm and not movementTempDeact then
		movementTempDeact = true
	end
	DoCall("Set Standard Movement Engine Active", false)
end

function PlaySound(name, chan)
	if chan then
		chan, Objects.Sound.NextAllocatedChannel = Objects.Sound.NextAllocatedChannel, chan
	end
	DoCall(type(name) == "number" and "Play Default Sound" or "PlaySound", name)
	if chan then
		Objects.Sound.NextAllocatedChannel = chan + 1
	end
end

function MuteSounds()
	local old = Game.MuteSound
	Game.MuteSound = 1
	UpdSoundVolume()
	Game.MuteSound = old
end

function _internal_MayUseSpring(ok)
	MayUseSpring = (ok == true)
	event("MayUseSpring")
end

function path.isfile(s)
	return not not DoCall("path.isfile", s)
end
path.IsFile = path.isfile

-----------------------------------------------
-- Win events
-----------------------------------------------

local function ParseMCoords(a)
	local x = a % 0x10000
	local y = (a - x) / 0x10000
	return (x >= 0x8000 and x - 0x10000 or x), (y >= 0x8000 and y - 0x10000 or y)
end

win.ExportMessage(0x0200, function(wp, lp)
	event("MouseMove", ParseMCoords(lp))
end)

-- left

win.ExportMessage(0x0201, function(wp, lp)
	local x, y = ParseMCoords(lp)
	event("MouseDown", x, y)
	event("MouseClick", x, y, false)
end)

win.ExportMessage(0x0202, function(wp, lp)
	event("MouseUp", ParseMCoords(lp))
end)

win.ExportMessage(0x0203, function(wp, lp)
	local x, y = ParseMCoords(lp)
	event("MouseClick", x, y, true)
end)

-- right

win.ExportMessage(0x0204, function(wp, lp)
	local x, y = ParseMCoords(lp)
	event("MouseRightDown", x, y)
	event("MouseRightClick", x, y, false)
end)

win.ExportMessage(0x0205, function(wp, lp)
	event("MouseRightUp", ParseMCoords(lp))
end)

win.ExportMessage(0x0206, function(wp, lp)
	local x, y = ParseMCoords(lp)
	event("MouseRightClick", x, y, true)
end)

-- right

win.ExportMessage(0x0207, function(wp, lp)
	local x, y = ParseMCoords(lp)
	event("MouseMiddleDown", x, y)
	event("MouseMiddleClick", x, y, false)
end)

win.ExportMessage(0x0208, function(wp, lp)
	event("MouseMiddleUp", ParseMCoords(lp))
end)

win.ExportMessage(0x0209, function(wp, lp)
	local x, y = ParseMCoords(lp)
	event("MouseMiddleClick", x, y, true)
end)

-- wheel

win.ExportMessage(0x020A, function(wp, lp)
	local delta = math.floor(wp / 0x10000)
	event("MouseWheel", (delta >= 0x8000 and delta - 0x10000 or delta), ParseMCoords(lp))
end)

-----------------------------------------------
-- Game variables interface
-----------------------------------------------
Game = {}

do
	local vals = {
		MapX = 1,
		MapY = 2,
		Mode = 3,
		CurrentlyPlayingSong = 4,
		SongFader = 5,
		MusicVolume = 6,
		AmbianceVolume = 7,
		SoundVolume = 8,
		MenuMusicHasStarted = 9,
		MuteSound = 10,
		--UnusedB = 11,
		SettingsLoaded = 12,
		CheatEnabled = 13,
		LevelFormatVersion = 14,
		LastMapX = 15,
		LastMapY = 16,
		DeathDelay = 18,
		WhiteDeathDelay = 19,
	}

	local strs = {
		World = 1,
		FullSavegamePath = 2,
		Cutscene = 3,
		ScreenMode = 4,
		CriticalError = 5,
		CutsceneDestination = 6,
	}
	
	for k, v in pairs(strs) do
		vals[k] = -v
	end
	
	local function index(_, name)
		local id = vals[name]
		if id > 0 then
			return mmf.Global.GetValue(id - 1)
		else
			return mmf.Global.GetString(-id - 1)
		end
	end

	local function newindex(_, name, v)
		local id = vals[name]
		if id > 0 then
			mmf.Global.SetValue(id - 1, v)
		else
			mmf.Global.SetString(-id - 1, v)
		end
	end

	setmetatable(Game, {__index = index, __newindex = newindex})
end


-- make sure Music is on when level starts
Game.MusicVolume = 100
Game.AmbianceVolume = 100

-----------------------------------------------
-- Platform object
-----------------------------------------------

do
	local props = {
		XVelocity = true,
		YVelocity = true,
		MaxXVelocity = true,
		MaxYVelocity = true,
		XAcceleration = true,
		XDeceleration = true,
		Gravity = true,
		JumpStrength = true,
		JumpHoldHeight = true,
		MaxStepUp = true,
		SlopeCorrection = true,
		AdditionalXVelocity = true,
		AdditionalYVelocity = true,
	}

	local function index(_, name)
		if props[name] then
			return DoCall("Platform.Get"..name)
		end
	end

	local function newindex(t, name, v)
		if props[name] then
			DoCall("Platform.Set"..name, v)
		else
			rawset(t, name, v)
		end
	end
	
	Objects.Platform = setmetatable({}, {__index = index, __newindex = newindex})
end

local function PlatformReg(name)
	Objects.Platform[name] = function(_, ...)
		return DoCall("Platform."..name, ...)
	end
end

PlatformReg "Pause"
PlatformReg "Unpause"
PlatformReg "Destroy"
PlatformReg "ObstacleOverlap"

-- PlatformReg "SetVelocity"
-- PlatformReg "SetMaxVelocity"
-- PlatformReg "SetXAcceleration"
-- PlatformReg "SetXDeceleration"
-- PlatformReg "SetGravity"
-- PlatformReg "SetJumpStrength"
-- PlatformReg "SetJumpHoldHeight"
-- PlatformReg "SetMaxStepUp"
-- PlatformReg "SetSlopeCorrection"
-- PlatformReg "SetAdditionalVelocity"
-- PlatformReg "GetVelocity"
-- PlatformReg "GetMaxVelocity"
-- PlatformReg "GetXAcceleration"
-- PlatformReg "GetXDeceleration"
-- PlatformReg "GetGravity"
-- PlatformReg "GetJumpStrength"
-- PlatformReg "GetJumpHoldHeight"
-- PlatformReg "GetMaxStepUp"
-- PlatformReg "GetSlopeCorrection"
-- PlatformReg "GetAdditionalVelocity"

-----------------------------------------------
-- Remove Dangerous Functions
-----------------------------------------------

io = nil
os.execute = nil
os.remove = nil
os.rename = nil

package.loadlib = nil
package.loaders[3] = nil
package.loaders[4] = nil

debug.getupvalue = nil
debug.getlocal = nil

xlua = nil
win = nil

-----------------------------------------------
-- Other Files
-----------------------------------------------

dofile(AppPath.."Data/Lua/cutscene.lua")

function BouncingBallMode()
	dofile(AppPath.."Data/Lua/BouncingBall.lua")
end

function VVVVVVMode()
	dofile(AppPath.."Data/Lua/VVVVVV.lua")
end

function NoWalkOnAir()
	return dofile(AppPath.."Data/Lua/NoWalkOnAir.lua")
end

function NoFishInWalls()
	return dofile(AppPath.."Data/Lua/NoFishInWalls.lua")
end

