local max = math.max
local min = math.min
local abs = math.abs
local floor = math.floor

BouncingBall = {
	-- force
	GravityX = 0,
	GravityY = 45,
	GravityMul = 1,
	ForceX = 0,
	ForceY = 0,
	ForceMul = 1,

	-- friction
	Friction = 0.4,
	
	-- keys
	KeysAccel = 50,
	MaxKeysSpeed = 300,

	-- speed limits
	MaxSpeed = 800,
	MaxBounceSpeed = 700,
	MinBounceSpeed = 200,
	
	LiteBounceSoundSpeed = 380,
	
	-- climbing
	CanClimb = true,
	ClimbUpSpeed = 500,
	ClimbStaySpeed = 220,
	ClimbDownSpeed = -250,
	
	-- bounce speed multipliers
	BounceJump = 1.5,
	BounceNorm = 0.85,
	BounceStop = 0.3,
	
	-- collision speed which leads to ball death
	--BreakSpeed = 650,
	
	-- flying
	FlyDelay = 10,
	FlyCount = 0,
	FreeFly = false,
	FlySpeed = 220,
	FlyMul = 0.5,
	
	-- current speed
	SpeedX = 0,
	SpeedY = 0,
	
	-- max number of pixels the ball may move before a check for collision occurs
	-- set this to a value less than the radius of the ball
	MaxStep = 2,
	
	-- current coordinates
	--X
	--Y

	-- called when a ball bounces
	--OnCollision
}

local ball = BouncingBall
local wasUmbrC

local AccelCoef = 1/2;
local SpeedCoef = 1/100;


local LastIntX, LastIntY
local BounceMul, WallClimbSpeed
local FlyCount = 0
local LastFloorBounceX, LastFloorBounceY
local CheckCollisions

DeactivateMovement(true)
Objects.Platform:Destroy()
EnableKeysInput(false)
ScreenScrollDetectionDelta = 0



local function sgn(a)  -- not quite 'sgn'
	if a >= 0 then
		return 1
	else
		return -1
	end
end

local KeysEnabled = true

function EnableKeysInput(on)
	KeysEnabled = on
end

local function CheckKey(key)
	return KeysEnabled and mmf.Keyboard.KeyDown(key)
end

local CheckOverlap

do
	local function DoCheckOverlap()
		ObstacleOverlap = (DoCall("CheckObstacleOverlap", Objects.PlayerPos.ID) == true)
		CanClimb = ObstacleOverlap and not CheckNoClimb()
		events.global.CheckObstacleOverlap(Objects.PlayerPos)
		events.CheckObstacleOverlap(Objects.PlayerPos)
	end
	
	function CheckOverlap(dx, dy)
		dx = dx or 0
		dy = dy or 0
		if dx ~= 0 or dy ~= 0 then
			local x, y = Objects.PlayerPos:GetPosition()
			Objects.PlayerPos:SetPosition(x + dx, y + dy)
			DoCheckOverlap()
			Objects.PlayerPos:SetPosition(x, y)
		else
			DoCheckOverlap()
		end
		return ObstacleOverlap
	end
end


local function ConfigFromPowers()
	ball.MaxKeysSpeed = (Objects.Player.Run ~= 0) and 10000 or 250
	ball.CanClimb = Objects.Player.Climb ~= 0
	ball.MaxBounceSpeed = (Objects.Player.HighJump ~= 0) and 530 or 400
	ball.FlyCount = (Objects.Player.DoubleJump ~= 0) and 3 or 0
	ball.FreeFly = Objects.Player.Umbrella and CheckUpWind()
	ball.MaxSpeed = 800
	ball.GravityMul = 1
	if Objects.Player.Umbrella and CheckKey(mmf.VK_D) then
		ball.MaxSpeed = 300
		ball.GravityMul = 0.5
		if not wasUmbrC then
			wasUmbrC = true
			Objects.Player:ReplaceColor(230, 185, 157, 50, 8, 8)
			Objects.Player:ReplaceColor(240, 215, 199, 60, 9, 9)
		end
	elseif wasUmbrC then
		wasUmbrC = false
		Objects.Player:ReplaceColor(50, 8, 8, 230, 185, 157)
		Objects.Player:ReplaceColor(60, 9, 9, 240, 215, 199)
	end
end


local function ApplyPos()
	LastIntX, LastIntY = floor(ball.X + 0.5), floor(ball.Y + 0.5)
	Objects.PlayerPos:SetPosition(LastIntX, LastIntY)
end

local function ApplySpeed(spdDone)
	spdDone = spdDone or 0
	local sx, sy = ball.SpeedX, ball.SpeedY
	local spd = math.sqrt(sx*sx + sy*sy)
	local sx, sy = sx/spd, sy/spd
	if spd > ball.MaxSpeed then
		ball.SpeedX, ball.SpeedY = sx*ball.MaxSpeed, sy*ball.MaxSpeed
		spd = ball.MaxSpeed
	end
	if not nomove and spd > spdDone then
		spd = spd - spdDone
		if spd*SpeedCoef > ball.MaxStep then
			ball.X = ball.X + sx*ball.MaxStep
			ball.Y = ball.Y + sy*ball.MaxStep
			ApplyPos()
			CheckCollisions()
			return ApplySpeed(spdDone + ball.MaxStep/SpeedCoef)
		else
			ball.X = ball.X + sx*spd*SpeedCoef
			ball.Y = ball.Y + sy*spd*SpeedCoef
		end
	end
	if ball.SpeedX ~= 0 then
		Objects.PlayerPos:SetDirection(ball.SpeedX < 0 and 16 or 0)
		Objects.PlayerPos:SetAnimationDirection(0)
	end
	return ApplyPos()
end

local function DoBounce(s)
	return -sgn(s) * min(ball.MaxBounceSpeed, max(ball.MinBounceSpeed, abs(s) * BounceMul))
end

local function DoFriction(s, impact)
	return sgn(s)*max(0, abs(s) - ball.Friction*abs(impact))
end

function CheckCollisions()
	local ix, iy = Objects.PlayerPos:GetPosition()
	if CheckOverlap() then
		events.global.CanClimb()
		events.CanClimb()
		local canClimb = CanClimb
		local impact = -1
		local sx, sy = ball.SpeedX, ball.SpeedY
		local bounceX, bounceY

		local function CheckInScreenX(x, xo)
			return not (x < xo and x < 0 and x >= -24 or x > xo and x >= 600 and x < 624)
		end

		local function CheckInScreenY(y, yo)
			return not (y < yo and y < 0 and y >= -24 or y > yo and y >= 240 and y < 264)
		end
		
		local function test(dx, dy)
			if (dx ~= 0 or dy ~= 0) and (dx == 0 or dx*sx < 0) and (dy == 0 or dy*sy < 0) and
					CheckInScreenX(ix + dx, ix) and CheckInScreenY(iy + dy, iy) and not CheckOverlap(dx, dy) then
				if dx ~= 0 then
					ball.X = ix + dx
					bounceX = true
				end
				if dy ~= 0 then
					ball.Y = iy + dy
					bounceY = true
				end
				return true
			end
		end

		if sx ~= 0 or sy ~= 0 then
			for i = 1, 50 do
				local j = math.floor(i*0.7)
				if test(0, -i) or test(0, i) or test(-i, 0) or test(i, 0) or
					 test(-j, -j) or test(j, -j) or test(-j, j) or test(j, j) then
					break
				end
			end
		end
		if bounceX then
			ball.SpeedX = DoBounce(sx)
			impact = abs(ball.SpeedX)
			if ball.CanClimb and canClimb then
				FlyCount = 0
			end
		end
		if bounceY then
			ball.SpeedY = DoBounce(sy)
			impact = max(impact, abs(ball.SpeedY))
			if sy > 0 then
				FlyCount = 0
				if abs(ball.SpeedY) > ball.MinBounceSpeed then
					Objects.KBD.JumpOneshot = 1
					Objects.PlayerPos.JustJumped = 1
				end
				LastFloorBounceX, LastFloorBounceY = ball.X, ball.Y
			end
		end
		if not bounceX then
			ball.SpeedX = DoFriction(sx, sy)--ball.SpeedY - sy)
		end
		if not bounceY then
			ball.SpeedY = DoFriction(sy, sx) --ball.SpeedX - sx)
			if WallClimbSpeed ~= 0 and canClimb then
				local spd = math.sqrt(ball.SpeedX*ball.SpeedX + ball.SpeedY*ball.SpeedY)
				ball.SpeedY = ball.SpeedY - WallClimbSpeed
				spd = spd/math.sqrt(ball.SpeedX*ball.SpeedX + ball.SpeedY*ball.SpeedY)
				ball.SpeedX = ball.SpeedX*spd
				ball.SpeedY = ball.SpeedY*spd
			end
		end
		ApplySpeed(math.huge)
		if ball.BreakSpeed and impact > ball.BreakSpeed then
			return Die()
		end
		if math.sqrt((sx - ball.SpeedX)^2 + (sy - ball.SpeedY)^2) <= ball.LiteBounceSoundSpeed*2 then
			PlaySound(3)
		else
			PlaySound(2)
		end
		if ball.OnCollision then
			ball.OnCollision()
		end
	end
end

local BounceNames = {"BounceStop", "BounceNorm", "BounceJump"}
local BounceColors = {0x000066, 0x000008, 0x663308}
local ballColor = 0x000008
local wasHolo
local nextFly = 0

local function CheckKeys()
	ConfigFromPowers()
	
	Objects.KBD.JumpOneshot = 0
	Objects.KBD.DownOneshot = 0
	Objects.KBD.ItemList = CheckKey(mmf.VK_Q) and 1 or 0
	Objects.KBD.Hologram = not wasHolo and CheckKey(mmf.VK_W) and 1 or 0
	wasHolo = CheckKey(mmf.VK_W)
	Objects.PlayerPos.JustJumped = 0
	
	local v = 0
	if not CheckSticky() then
		if CheckKey(mmf.VK_LEFT) then
			v = v - 1
		end
		if CheckKey(mmf.VK_RIGHT) then
			v = v + 1
		end
	end
	Objects.KBD.LeftRight = v
	
	v = 0
	if CheckKey(mmf.VK_UP) then
		v = v - 1
	end
	if CheckKey(mmf.VK_DOWN) then
		v = v + 1
		if Objects.KBD.UpDown < v then
			Objects.KBD.DownOneshot = 1
		end
	end
	Objects.KBD.UpDown = v
	if not ball.CanClimb then
		WallClimbSpeed = 0
	elseif v < 0 then
		WallClimbSpeed = ball.ClimbUpSpeed
	elseif v > 0 then
		WallClimbSpeed = ball.ClimbDownSpeed
	else
		WallClimbSpeed = ball.ClimbStaySpeed
	end
	
	local BounceKind = 2
	if CheckKey(mmf.VK_S) and not CheckNoJump() then
		BounceKind = BounceKind + 1
	end
	if CheckKey(mmf.VK_A) then
		BounceKind = BounceKind - 1
	end
	BounceMul = ball[BounceNames[BounceKind]]
	v = BounceColors[BounceKind]
	-- if BounceKind == 3 and GetTick() % 10 >= 5 then
		-- v = BounceColors[2]*
	-- end
	if v ~= ballColor then
		Objects.Player:ReplaceColor(ballColor, v)
		ballColor = v
	end
	--WallClimbSpeed = (BounceKind == 1 and 0 or ball.WallClimbSpeed)
end

local NeedCollisionsCheck

local function MoveBall(tick)
	local ix, iy = Objects.PlayerPos:GetPosition()
	if IsMovementPaused() or Objects.Player.Cheating ~= 0 then
		ball.X, ball.Y = ix, iy
		if Objects.Player.Cheating ~= 0 then
			NeedCollisionsCheck = true
		end
		return
	end
	if not ball.X or ix ~= LastIntX or iy ~= LastIntY then
		ball.X, ball.Y = ix, iy
	end
	CheckKeys()
	if NeedCollisionsCheck then
		NeedCollisionsCheck = false
		CheckCollisions()
	end
	ball.SpeedX = ball.SpeedX + ball.GravityX*ball.GravityMul*AccelCoef + ball.ForceX*ball.ForceMul*AccelCoef
	ball.SpeedY = ball.SpeedY + ball.GravityY*ball.GravityMul*AccelCoef + ball.ForceY*ball.ForceMul*AccelCoef
	local v = ball.SpeedX + Objects.KBD.LeftRight*ball.KeysAccel*AccelCoef
	if abs(v) < abs(ball.SpeedX) then
		ball.SpeedX = v
	elseif abs(ball.SpeedX) < ball.MaxKeysSpeed then
		ball.SpeedX = max(-ball.MaxKeysSpeed, min(ball.MaxKeysSpeed, v))
	end
	if (ball.FreeFly or ball.FlyCount > FlyCount) and GetGlobalTick() >= nextFly and Objects.KBD.UpDown < 0 then
		if not ball.FreeFly then
			FlyCount = FlyCount + 1
		end
		nextFly = GetGlobalTick() + ball.FlyDelay
		ball.SpeedY = ball.SpeedY*ball.FlyMul - ball.FlySpeed
		Objects.KBD.JumpOneshot = 1
		Objects.PlayerPos.JustJumped = 1
		Objects.PlayerPos.JustDoubleJumped = 1
		MakeDoubleJumpCloud()
	else
		Objects.PlayerPos.JustDoubleJumped = 0
	end
	ApplySpeed()
	CheckCollisions()
	Objects.PlayerPos:Update()
end






local OldLoadScreen = _internal_AfterLoadScreen

function _internal_AfterLoadScreen(...)
	ball.X, ball.Y = nil, nil
	LastFloorBounceX, LastFloorBounceY = nil, nil
	NeedCollisionsCheck = true
	Timer(1, MoveBall)
	return OldLoadScreen(...)
end


local OldOnShift = _internal_OnShift

function _internal_OnShift(id, quant, ...)
	ball.X, ball.Y = nil, nil
	if quant then
		ball.SpeedX, ball.SpeedY = 0, 0
	end
	NeedCollisionsCheck = true
	return OldOnShift(id, quant, ...)
end


local OldLoadGame = _internal_LoadGame

function _internal_LoadGame(...)
	ball.X, ball.Y = nil, nil
	ball.SpeedX, ball.SpeedY = 0, 0
	nextFly = 0
	FlyCount = 0
	return OldLoadGame(...)
end


local OldCheckHolo = _internal_CheckHologramActivation
function _internal_CheckHologramActivation(allow)
	allow = LastFloorBounceX and ((Objects.KBD.Hologram ~= 0 or Objects.PlayerPos.HologramActivationTimer >= 30))
	return OldCheckHolo(allow)
end


local OldShowHolo = _internal_HolorgamCreated
function _internal_HolorgamCreated(...)
	if LastFloorBounceX then
		mmf.Object.SetPosition(6, LastFloorBounceX, LastFloorBounceY - 5)
	end
	return OldShowHolo(...)
end


local OldMayUseSpring = _internal_MayUseSpring
function _internal_MayUseSpring()
	OldMayUseSpring(ball.SpeedY > 10)
	if MayUseSpring then
		local sy = ball.SpeedY
		ball.SpeedY = CheckKey(mmf.VK_S) and -700 or -500
		ball.SpeedX = DoFriction(ball.SpeedX, (abs(ball.SpeedY) + abs(sy))/2)
		FlyCount = 0
	end
end

-- Temp: Load Climb powerup as character

DoCall("Load Frame", Objects.Player.ID, AppPath.."Data/Objects/Bank0/Object4.png", 0, 0, 0, 12, 17-5, 12, 17-5, 0xFF00FF)
DoCall("Load Frame", Objects.PlayerPos.ID, AppPath.."Data/Objects/Bank0/Object4.png", 0, 0, 0, 12, 17, 12, 17, 0xFF00FF)
--DoCall("Load Frame", Objects.PlayerHologram.ID, AppPath.."Data/Objects/Bank0/Object4.png", 0, 0, 0, 12, 17, 12, 17, 0xFF00FF)
DoCall("Load Frame", Objects.PlayerVisiblePos.ID, AppPath.."Data/Objects/Bank0/Object4.png", 0, 0, 0, 12, 17, 12, 17, 0xFF00FF)
