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

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


local magicMushroom = {}
local npcID = NPC_ID

local magicMushroomSettings = {
	id = npcID,
	
	gfxwidth = 0,
	gfxheight = 0,

	gfxoffsetx = 0,
	gfxoffsety = 2,
	
	width = 32,
	height = 32,
	
	frames = 1,
	framestyle = 0,
	framespeed = 8,
	
	speed = 1,
	
	npcblock = false,
	npcblocktop = false, --Misnomer, affects whether thrown NPCs bounce off the NPC.
	playerblock = false,
	playerblocktop = false, --Also handles other NPCs walking atop this NPC.

	nohurt = false,
	nogravity = false,
	noblockcollision = false,
	nofireball = false,
	noiceball = false,
	noyoshi = false,
	nowaterphysics = false,
	
	jumphurt = false,
	spinjumpsafe = false,
	harmlessgrab = false,
	harmlessthrown = false,

	isinteractable = true,

	maxSpeed = 1.8,
	deceleration = 0.05,
}

npcManager.setNpcSettings(magicMushroomSettings)
npcManager.registerHarmTypes(npcID,{},{})


local invalidSound = Misc.resolveSoundFile("resources/menu/unselectable")


function magicMushroom.onInitAPI()
	npcManager.registerEvent(npcID, magicMushroom, "onTickNPC")
	registerEvent(magicMushroom, "onPostNPCCollect")

	registerEvent(magicMushroom, "onInputUpdate")
end

function magicMushroom.onTickNPC(v)
	if Defines.levelFreeze or v.despawnTimer <= 0 then
		return
	end

	local config = NPC.config[v.id]

	-- Mushroom movement
	if v.direction == 0 then
		local p = npcutils.getNearestPlayer(v)

		v.direction = -p.direction
	end

	if v.speedX > config.maxSpeed then
		v.speedX = math.max(config.maxSpeed,v.speedX - config.deceleration)
	elseif v.speedX < -config.maxSpeed then
		v.speedX = math.min(-config.maxSpeed,v.speedX + config.deceleration)
	elseif not v:mem(0x136,FIELD_BOOL) then
		v.speedX = config.maxSpeed*v.direction
	end
end


local swapRequestCommand = onlinePlay.createCommand("battle_swap_request",onlinePlay.IMPORTANCE_MAJOR)
local swapResponseCommand = onlinePlay.createCommand("battle_swap_response",onlinePlay.IMPORTANCE_MAJOR)
local swapGoCommand = onlinePlay.createCommand("battle_swap_go",onlinePlay.IMPORTANCE_MAJOR)

local playerPropertiesMap
local waitingResponseList
local pausedTime


local function playerCanSwap(p)
	return (p.forcedState == FORCEDSTATE_NONE and p:mem(0x140,FIELD_WORD) == 0)
end

local function findSwappingPlayersLocal()
	local list = {}
	
	for _,p in ipairs(Player.get()) do
		if playerCanSwap(p) then
			table.insert(list,p.idx)
		end
	end

	return list
end

local function findSwappingPlayersOnline()
	local list = {}
	
	for _,p in ipairs(Player.get()) do
		if playerPropertiesMap[p.idx] ~= nil and playerPropertiesMap[p.idx].canSwap then
			table.insert(list,p.idx)
		end
	end

	return list
end

local function randomRemoveFromList(list,exemptionIndex)
	-- Randomnly pick an index from the list to remove
	-- If an exemption index is provided, it will pick any index BUT that one
	local listCount = #list
	local index

	if exemptionIndex ~= nil then
		if listCount <= 1 then
			return nil
		end

		-- One less valid index to pick
		index = RNG.randomInt(1,listCount - 1)

		if index >= exemptionIndex then
			index = index + 1
		end
	else
		if listCount <= 0 then
			return
		end

		index = RNG.randomInt(1,listCount)
	end

	-- Remove it from the list and return its value
	return table.remove(list,index)
end

local function createSwappedPlayerList(playerIndexList)
	-- Creates a darangement of the player index list.
	-- This means that the list is shuffled, but no entry is left in its original place.
	local isValidDerangement = false
	local swappedList

	while (not isValidDerangement) do
		local unusedIndices = table.iclone(playerIndexList)

		isValidDerangement = true
		swappedList = {}

		for i,indexA in ipairs(playerIndexList) do
			local indexB = randomRemoveFromList(unusedIndices,table.ifind(unusedIndices,indexA))

			if indexB == nil then
				isValidDerangement = false
				break
			end

			swappedList[i] = indexB
		end
	end

	return swappedList
end


local function getPlayerProperties(p)
	if not playerCanSwap(p) then
		return {canSwap = false}
	end

	return {
		canSwap = true,
		x = p.x + p.width*0.5,
		y = p.y + p.height,
		direction = p.direction,
	}
end

local function swapPlayers(baseIndexList,swapIndexList,playerProperties)
	if #baseIndexList ~= #swapIndexList then
		return
	end

	-- Store the original position of each player
	if playerProperties == nil then
		playerProperties = {}

		for _,playerIndex in ipairs(baseIndexList) do
			playerProperties[playerIndex] = getPlayerProperties(Player(playerIndex))
		end
	end
	
	-- Swap the positions of every applicable player
	for i,baseIndex in ipairs(baseIndexList) do
		local swapPlayerIndex = swapIndexList[i]
		local swapProperties = playerProperties[swapPlayerIndex]

		if swapProperties.canSwap then
			local p = Player(baseIndex)

			local holdingNPC = p.holdingNPC

			if holdingNPC ~= nil then
				if holdingNPC:mem(0x130,FIELD_WORD) == p.idx then
					holdingNPC:mem(0x130,FIELD_WORD,swapPlayerIndex)
				end

				if holdingNPC:mem(0x132,FIELD_WORD) == p.idx then
					holdingNPC:mem(0x132,FIELD_WORD,swapPlayerIndex)
				end

				holdingNPC.speedX = 0
				holdingNPC.speedY = 0
				holdingNPC.heldIndex = 0

				p:mem(0x154,FIELD_WORD,0)
			end

			p.x = swapProperties.x - p.width*0.5
			p.y = swapProperties.y - p.height
			p.direction = swapProperties.direction
		end
	end

	-- Spawn an effect for each player
	for _,playerIndex in ipairs(baseIndexList) do
		local p = Player(playerIndex)

		-- Spawn a poof effect
		local e = Effect.spawn(10,p.x + p.width*0.5,p.y + p.height*0.5)

		e.x = e.x - e.width *0.5
		e.y = e.y - e.height*0.5

		-- Give a tiny bit of invincibility
		p:mem(0x140,FIELD_WORD,p:mem(0x140,FIELD_WORD) + 20)
	end
end


function magicMushroom.onPostNPCCollect(v,p)
	if v.id ~= npcID then
		return
	end

	if not onlinePlayPlayers.ownsPlayer(p) then
		-- Ignore if we weren't the one to collect it
		return
	end

	if not playerCanSwap(p) then
		if onlinePlayPlayers.canMakeSound(p) then
			SFX.play(invalidSound)
		end

		Effect.spawn(10,v.x + v.width*0.5 - 16,v.y + v.height*0.5 - 16)
		return
	end

	
	if onlinePlay.currentMode == onlinePlay.MODE_OFFLINE then
		-- Pretty simple...
		-- Make a list of players that should swap
		local baseIndexList = findSwappingPlayersLocal()

		if #baseIndexList < 2 then
			if onlinePlayPlayers.canMakeSound(p) then
				SFX.play(invalidSound)
			end
	
			Effect.spawn(10,v.x + v.width*0.5 - 16,v.y + v.height*0.5 - 16)
			return
		end

		-- Shuffle that list in such a way that no value is left in the same position that it started in
		local swapIndexList = createSwappedPlayerList(baseIndexList)

		-- Swap them around
		swapPlayers(baseIndexList,swapIndexList)
		SFX.play(20)
	else
		-- Not so simple...
		-- Message everybody to say "HEY A SWAP SHOULD HAPPEN!"
		waitingResponseList = {}

		for _,user in ipairs(onlinePlay.getUsers()) do
			if user.playerIdx ~= onlinePlay.playerIdx then
				swapRequestCommand:send(user.playerIdx)
				table.insert(waitingResponseList,user.playerIdx)
			end
		end

		if #waitingResponseList == 0 then
			if onlinePlayPlayers.canMakeSound(p) then
				SFX.play(invalidSound)
			end
	
			Effect.spawn(10,v.x + v.width*0.5 - 16,v.y + v.height*0.5 - 16)
			waitingResponseList = nil
			return
		end

		playerPropertiesMap = {
			[onlinePlay.playerIdx] = getPlayerProperties(Player(onlinePlay.playerIdx)),
		}

		pausedTime = onlinePlay.localTime
		Misc.pause()
	end
end


function swapRequestCommand.onReceive(sourcePlayerIdx)
	swapResponseCommand:send(sourcePlayerIdx, getPlayerProperties(Player(onlinePlay.playerIdx)))

	pausedTime = onlinePlay.localTime
	Misc.pause()
end

function swapResponseCommand.onReceive(sourcePlayerIdx, playerProperties)
	if waitingResponseList == nil then
		return
	end

	local index = table.ifind(waitingResponseList,sourcePlayerIdx)

	if index == nil then
		return
	end

	playerPropertiesMap[sourcePlayerIdx] = playerProperties
	table.remove(waitingResponseList,index)

	if #waitingResponseList == 0 then
		-- Make a list of players that should swap
		local baseIndexList = findSwappingPlayersOnline()

		if #baseIndexList >= 2 then
			-- Shuffle that list in such a way that no value is left in the same position that it started in
			local swapIndexList = createSwappedPlayerList(baseIndexList)

			-- Swap them around
			swapPlayers(baseIndexList,swapIndexList,playerPropertiesMap)

			swapGoCommand:send(0, baseIndexList,swapIndexList,playerPropertiesMap)

			SFX.play(20)
		else
			SFX.play(invalidSound)

			swapGoCommand:send(0)
		end

		waitingResponseList = nil
		playerPropertiesMap = nil
		pausedTime = nil

		Misc.unpause()
	end
end

function swapGoCommand.onReceive(sourcePlayerIdx, baseIndexList,swapIndexList,playerProperties)
	if baseIndexList ~= nil and swapIndexList ~= nil and playerProperties ~= nil then
		swapPlayers(baseIndexList,swapIndexList,playerProperties)
		SFX.play(20)
	else
		SFX.play(invalidSound)
	end

	waitingResponseList = nil
	playerPropertiesMap = nil
	pausedTime = nil
	
	Misc.unpause()
end


function magicMushroom.onInputUpdate()
	-- Timeout
	if pausedTime ~= nil and pausedTime <= (onlinePlay.localTime - 3) then
		waitingResponseList = nil
		playerPropertiesMap = nil
		pausedTime = nil

		Misc.unpause()
	end
end


return magicMushroom