local npcManager = require("npcManager")
local npcutils = require("npcs/npcutils")

local battlePlayer = require("scripts/battlePlayer")

local onlinePlay = require("scripts/onlinePlay")
local onlinePlayNPC = require("scripts/onlinePlay_npc")
local onlinePlayPlayers = require("scripts/onlinePlay_players")


local iceBlock = {}
local npcID = NPC_ID


local shakeCommand = onlinePlayNPC.createNPCCommand("iceBlock_shake",onlinePlay.IMPORTANCE_MAJOR)

local freezeSound = Misc.resolveSoundFile("resources/freeze")


local function releasePlayer(v)
	local data = v.data

	local p = Player(data.frozenPlayerIdx)
	local playerData = battlePlayer.getPlayerData(p)

	p:mem(0x140,FIELD_WORD,100)
	p:mem(0x11C,FIELD_WORD,Defines.jumpheight)
	p.speedX = 0
	p.speedY = -5.7

	playerData.iceBlockNPC = nil
	onlinePlayNPC.forceKillNPC(v,HARM_TYPE_NPC)
end


local function isSafeSpot(blocks,v,x,y)
	local collider = Colliders.Box(x,y,v.width,v.height)

	for _,block in ipairs(blocks) do
		if collider:collide(block) then
			return false
		end
	end

	return true
end

local checkDirections = {vector(0,-1),vector(0,1),vector(-1,0),vector(1,0),vector(-1,-1),vector(1,-1),vector(-1,1),vector(1,1)}
local checkDistances = {4,8,12,16}

-- If the ice block is inside a solid block, this tries to move it to get it out.
-- Returns whether or not it succeeded.
local function unstuckNPC(v)
	-- Check for solid blocks in the general area
	local blocks = Colliders.getColliding{
		a = Colliders.Box(v.x - 24,v.y - 24,v.width + 48,v.height + 48),
		btype = Colliders.BLOCK,
		filter = function(block)
			if block.isHidden or block:mem(0x5A,FIELD_BOOL) then
				return false
			end
			
			local blockConfig = Block.config[block.id]

			if blockConfig.passthrough or blockConfig.semisolid or blockConfig.sizeable then
				return false
			end

			if blockConfig.npcfilter < 0 or blockConfig.npcfilter == v.id then
				return false
			end

			return true
		end,
	}

	if #blocks == 0 or isSafeSpot(blocks,v,v.x,v.y) then
		return true
	end

	-- The NPC is inside of a block, so try to get it out
	for _,distance in ipairs(checkDistances) do
		for _,direction in ipairs(checkDirections) do
			local checkX = v.x + direction.x*distance
			local checkY = v.y + direction.y*distance

			if isSafeSpot(blocks,v,checkX,checkY) then
				v.x = checkX
				v.y = checkY
				return true
			end
		end
	end

	-- Could not get it out
	return false
end


function shakeCommand.onReceive(v,sourcePlayerIdx, shakeCount)
	local data = v.data

	data.shakeCount = shakeCount
	data.shakeTimer = 12

	if shakeCount >= 8 then
		releasePlayer(v)
	end
end


function iceBlock.onInitAPI()
	npcManager.registerEvent(npcID, iceBlock, "onTickEndNPC")
	npcManager.registerEvent(npcID, iceBlock, "onDrawNPC")
	npcManager.registerEvent(npcID, iceBlock, "onDrawEndNPC")
	registerEvent(iceBlock, "onPostNPCKill")
end

function iceBlock.onTickEndNPC(v)
	local data = v.data
	
	if data.frozenPlayerIdx == nil then
		return
	end

	if not data.didInitialStuff then
		if battleCamera.isOnScreen(v.x,v.y,v.width,v.height) then
			SFX.play(freezeSound,0.75)
		end

		for i = 1,20 do
			local e = Effect.spawn(80,0,0)

			e.x = v.x + RNG.random(0,v.width)
			e.y = v.y + RNG.random(0,v.height)
			e.speedX = RNG.random(-1,1)
			e.speedY = RNG.random(-1,1)
			e.animationFrame = RNG.randomInt(0,3)
		end

		unstuckNPC(v)

		data.didInitialStuff = true
	end

	local p = Player(data.frozenPlayerIdx)
	local playerData = battlePlayer.getPlayerData(p)

	p.x = v.x + (v.width - p.width)*0.5
	p.y = v.y + v.height - p.height
	p.section = v.section

	data.lifetime = math.max(0,data.lifetime - 1)

	if onlinePlayPlayers.ownsPlayer(p) then
		-- Breaking free from the ice block
		if p.keys.jump == KEYS_PRESSED then
			data.shakeQueued = true
		end

		if data.shakeQueued and data.shakeTimer == 0 then
			data.shakeTimer = 12
			data.shakeQueued = false
			data.shakeCount = data.shakeCount + 1

			if onlinePlay.currentMode ~= onlinePlay.MODE_OFFLINE then
				shakeCommand:send(v,0, data.shakeCount)
			end

			if data.shakeCount >= 8 then
				releasePlayer(v)
				return
			end
		end

		if data.lifetime <= 0 and onlinePlayPlayers.ownsPlayer(p) then
			releasePlayer(v)
			return
		end
	end

	if onlinePlayNPC.ownsNPC(v) or onlinePlayPlayers.ownsPlayer(p) then
		local sectionObj = v.sectionObj
		local boundary = sectionObj.boundary

		if not sectionObj.wrapH and (v.x <= boundary.left or (v.x + v.width) >= boundary.right) then
			onlinePlayNPC.forceKillNPC(v,HARM_TYPE_NPC)
		end
	end


	if data.shakeTimer > 0 then
		data.shakeTimer = math.max(0,data.shakeTimer - 1)
	end
end


local playerBuffer = Graphics.CaptureBuffer(200,200)

function iceBlock.onDrawNPC(v)
	if v.despawnTimer <= 0 then
		return
	end

	local data = v.data

	if data.frozenPlayerIdx == nil then
		return
	end

	if data.lifetime < 48 then
		data.shakeOffset = math.sin(data.lifetime*math.pi/1.5)*2
	elseif data.shakeTimer > 0 then
		data.shakeOffset = math.sin(data.shakeTimer*math.pi/1.5)*math.min(1,data.shakeTimer*0.25)*2
	else
		data.shakeOffset = 0
	end

	v.x = v.x + data.shakeOffset


	local p = Player(data.frozenPlayerIdx)

	local insideWidth = v.width - 4
	local insideHeight = v.height - 4
	local priority = -50.1

	-- Draw player
	playerBuffer:clear(priority)
	p:render{
		priority = priority,target = playerBuffer,ignorestate = true,sceneCoords = false,
		x = (insideWidth - p.width)*0.5,
		y = (insideHeight - p.height)*0.5,
	}

	-- Draw the buffer to the screen
	Graphics.drawBox{
		texture = playerBuffer,priority = priority,sceneCoords = true,centred = true,
		sourceWidth = insideWidth,sourceHeight = insideHeight,
		x = v.x + v.width*0.5,
		y = v.y + v.height*0.5,
	}
end

function iceBlock.onDrawEndNPC(v)
	if v.despawnTimer <= 0 then
		return
	end

	local data = v.data

	if data.frozenPlayerIdx == nil then
		return
	end

	v.x = v.x - data.shakeOffset
end


function iceBlock.onPostNPCKill(v,reason)
	if v.id ~= npcID then
		return
	end

	local data = v.data

	if data.frozenPlayerIdx == nil then
		return
	end

	local p = Player(data.frozenPlayerIdx)
	local playerData = battlePlayer.getPlayerData(p)

	local notStuck = unstuckNPC(v)

	if notStuck then
		p.x = v.x + (v.width - p.width)*0.5
		p.y = v.y + v.height - p.height
	end

	p.forcedState = FORCEDSTATE_NONE
	p.forcedTimer = 0
	p.speedX = 0
	p.speedY = 0
	p:mem(0xBA,FIELD_WORD,0)

	if notStuck then
		if playerData.iceBlockNPC == v and reason ~= HARM_TYPE_VANISH and onlinePlayPlayers.ownsPlayer(p) then
			battlePlayer.harmPlayer(p,battlePlayer.HARM_TYPE.NORMAL)
		end
	else
		if onlinePlayPlayers.ownsPlayer(p) then
			p:kill()
		end
	end
end


onlinePlayNPC.onlineHandlingConfig[npcID] = {
	getExtraData = function(v)
		local data = v.data
		if data.frozenPlayerIdx == nil then
			return nil
		end

		return {
			frozenPlayerIdx = data.frozenPlayerIdx,
			shakeTimer = data.shakeTimer,
			shakeQueued = data.shakeQueued,
			shakeOffset = data.shakeOffset,
			shakeCount = data.shakeCount,
			lifetime = data.lifetime,
		}
	end,
	setExtraData = function(v, receivedData)
		local data = v.data

		if receivedData == nil then
			data.frozenPlayerIdx = nil
			return
		end

		local p = Player(receivedData.frozenPlayerIdx)
		local playerData = battlePlayer.getPlayerData(p)

		if playerData.iceBlockNPC ~= v then
			p.forcedState = FORCEDSTATE_SWALLOWED
            p.forcedTimer = p.idx
            p:mem(0xBA,FIELD_WORD,p.idx)

			playerData.iceBlockNPC = v
		end

		data.frozenPlayerIdx = receivedData.frozenPlayerIdx
		data.lifetime = receivedData.lifetime

		data.shakeQueued = data.shakeQueued or receivedData.shakeQueued
		data.shakeOffset = data.shakeOffset or receivedData.shakeOffset
		data.shakeTimer = data.shakeTimer or receivedData.shakeTimer
		data.shakeCount = data.shakeCount or receivedData.shakeCount
	end,
	canClaimHarmFunc = function(v)
		return (v.data.frozenPlayerIdx == onlinePlay.playerIdx)
	end,
	canClaimKillFunc = function(v)
		return (v.data.frozenPlayerIdx == onlinePlay.playerIdx)
	end,
}


return iceBlock