Room = class("Room")

require "libs/TSerial"
require "player"
require "box"
require "wall"
require "artifact"
require "npc"
require "art"
require "assorted"
require "switch"
require "door"
require "dialogue_manager"
require "generic_moveable"
require "generic_unmoveable"

-- cache loaded images so repeated placing/loading doesn't re-decode PNGs
local placeImageCache = {}
local function loadPlaceImage(path)
	if not placeImageCache[path] then
		placeImageCache[path] = love.graphics.newImage(path)
	end
	return placeImageCache[path]
end

function Room:initialize(t)
	self.player = {}
	self.switches = { red = {}, green = {}, blue = {} }
	self.doors = {}
	self.collideables = { red = {}, green = {}, blue = {} }
	self.npcs = {}
	self.activeColors = {red = false, green = false, blue = false}
	self.colorsPlayerHas = {}

	if t.name then
		print("loaded!!")
		self:load(t.name) -- registers objects (and any saved players) from the .sav
		print(t.name)

		if t.player then
			-- entering from another room: place the carried colors at the entry point
			for _, color in ipairs(t.player.activeColors) do
				if self.player[color] then
					self.player[color].x, self.player[color].y = t.player.x, t.player.y
				else
					self:registerPlayer(Player:new(t.player.x, t.player.y, color), color)
				end
				self:setActiveColor(color, true)
			end
		elseif not next(self.player) then
			-- fresh load of a room with no saved player: default red spawn so it's
			-- playable (the player starts with only red)
			self:registerPlayer(Player:new(1, 1, "red"), "red")
			self:setActiveColor("red", true)
		end

		self.name = t.name
	else
		self.colorsPlayerHas = {"red", "green", "blue"}
		

		for _, color in ipairs(t.player.activeColors) do
			self.player[color] = Player:new(t.player.x, t.player.y, color)
		end

		self.activeColors = {}
		for _, color in ipairs(t.player.activeColors) do self.activeColors[color] = true end

		-- for color, coords in pairs(t.player) do
		-- 	self.player[color] = Player:new(coords.x, coords.y, color)
		-- 	self.activeColors[color] = true
		-- end

		-- for i=1, 1 do
		-- 	local x, y = randomPosition()
		-- 	-- local door = Door:new(x, y, self.colorsPlayerHas)
		-- 	-- table.insert(self.doors, door)

		-- 	for color, active in pairs(self.activeColors) do
		-- 		table.insert(self.collideables[color], Wall:new(x, y, color))
		-- 	end
		-- end

		-- for i=0, 5 do
		-- 	local x, y = randomPosition()
		-- 	for color, active in pairs(self.activeColors) do
		-- 		table.insert(self.collideables[color], Box:new(x, y, color))
		-- 	end
		-- end

		for i = 0, 5 do
			local x, y = randomPosition()
			print(x, y)
			local c = randomColor()
			table.insert(self.switches[c], Switch:new(x, y, c))
		end

		for color, active in pairs(self.activeColors) do
			local x, y = 6, 3
			-- if active then self.collideables[color]["n: " .. x .. ", " .. y] = Ass:new(x, y, color, "animals/processed/", "me_rach") end

			local x, y = 1, 1
			-- if active then table.insert(self.collideables[color], Ass:new(x, y, color, "nature/", "palm_scaled_reduced")) end

			-- local x, y = 7, 7
			-- if active then self.collideables[color]["n: " .. x .. ", " .. y] = Ass:new(x, y, color, "food/processed/", "beer_mug") end

			-- self.collideables[color]["fpp"] = Artifact:new(6, 6, color)
		end
	end

	self.switchMatrices = {}
	self.collidableMatrices = {} --{red = {{...}{...}{...}}, ...
	self:createSwitchMatrix()
	self:createCollideablesMatrix()
end


function Room:update(dt)
	for color, player in pairs(self.player) do
		player:update(dt)
	end
end

function Room:draw()

	-- you only perceive the color channels the player actually has: with only a
	-- red copy, only the red layer is drawn (a stacked r+g+b wall shows as red,
	-- not white). Doors span all channels, so dedupe and draw them once.
	local visible = {}
	for _, c in ipairs(self.colorsPlayerHas) do visible[c] = true end

	for color, player in pairs(self.player) do
		if visible[color] then player:draw() end
	end
	local drawnDoors = {}
	for color, entityArray in pairs(self.collideables) do
		if visible[color] then
			for _, entity in pairs(entityArray) do
				if entity:isInstanceOf(Door) then
					if not drawnDoors[entity] then
						drawnDoors[entity] = true
						entity:draw()
					end
				else
					entity:draw()
				end
			end
		end
	end
	for color, switchArray in pairs(self.switches) do
		if visible[color] then
			for _, switch in pairs(switchArray) do
				switch:draw()
			end
		end
	end
	-- npcs (old "sages") are just markers for now: draw a "?" in their cell,
	-- tinted to only the channels the player has (never a white pixel)
	local cellW, cellH = width / gridWidth, height / gridHeight
	love.graphics.setColor(visible.red and 255 or 0, visible.green and 255 or 0, visible.blue and 255 or 0)
	for _, npc in ipairs(self.npcs) do
		love.graphics.print("?", (npc.x - 1) * cellW + cellW * 0.3, (npc.y - 1) * cellH + cellH * 0.1, 0, 2, 2)
	end
end

function Room:createCollideablesMatrix()
	for color, entities in pairs(self.collideables) do
		self.collidableMatrices[color] = self:createEntityMatrix(entities)
	end
end

function Room:createSwitchMatrix()
	for color, switches in pairs(self.switches) do
		self.switchMatrices[color] = self:createEntityMatrix(switches)
	end
end

function Room:createEntityMatrix(entities)
	local matrix = {}

	--lets populate it w/ empty stuff first
	for i = 1, gridWidth do
		local row = {}
		for i = 1, gridHeight do
			table.insert(row, false)
		end
		table.insert(matrix, row)
	end


	--assumes that entities table is set up li_e {index = ent1, anotherIndex = ent2, ...}
	for _, entity in pairs(entities) do
		for _, occupiedCells in pairs(entity:getOccupiedCells()) do
			--we store a table reference so we can call a method on it during a chec_
			matrix[occupiedCells.y][occupiedCells.x] = entity
		end	
	end
	-- printMatrix(matrix)

	return matrix
end

function Room:getCellInMatrix(matrix, cell)
	return matrix[cell.y][cell.x]
end

function Room:movePlayer(key)
	
	local moved = false
	local previousPositions = {} --these are so we can perform the overlap chec_
	local newPositions = {} 

	for _, color in pairs(self:getActiveColors()) do
		local entitiesToPush, entitiesToPull = {}, {}
		local pos = self.player[color]:getGridPos()
		previousPositions[color] = pos
		local x, y = pos.x, pos.y
		local dir = {x = 0, y = 0}
		local axis

		if key == "up" then
			dir.y, axis = -1, "y"
		elseif key == "down" then
			dir.y, axis = 1, "y"
		elseif key == "left" then
			dir.x, axis = -1, "x"
		elseif key == "right" then
			dir.x, axis = 1, "x"
		end

		local move = true
		-- PUSHHHHHHHHHHHHHHHHHHHHHH---------------------

		local initialTargetCell = {
			x = pos.x + dir.x, y = pos.y + dir.y
		} -- this is for reference for each color as we change the targetCells table

		local initialTargetEntity

		if self:isInBounds(initialTargetCell) then
			initialTargetEntity = self:getCellInMatrix(self.collidableMatrices[color], initialTargetCell)
		end

		if initialTargetEntity then initialTargetEntity:bump() end

		if not self:isInBounds(initialTargetCell) then
			-- let's us move out of bounds to Change Rooms without pulling a bloc_
			move = true
		elseif initialTargetEntity and initialTargetEntity:canPassOver() then
			--we should put this in the else statement actually
			move = true
		else
			local targetCells

			targetCells = {initialTargetCell} -- ...we start with our attempted movement...
			local doneSearching
			while not doneSearching do
				nextTargetCells = {} -- ...then we create this array to save our next group of cells
				local emptyCells = {}
				for _, cell in pairs(targetCells) do
					if self:isInBounds(cell) then
						local entity = self:getCellInMatrix(self.collidableMatrices[color], cell)
						if entity then
							--something is here!!
							if not entitiesToPush[entity] then --if the entity hasnt been found already
								if entity:canMove() then
									local projectedCells = entity:getOccupiedCells(dir) -- we project the movement here
									entitiesToPush[entity] = entity
									for _, projectedCell in ipairs(projectedCells) do
										table.insert(nextTargetCells, projectedCell)
									end
								else
									move = false
									doneSearching = true
								end
							end
						else 
							--there's nothin there!!
							table.insert(emptyCells, cell)
						end
					else
						--we are out of bounds now
						move = false
						doneSearching = true
					end
				end

				--if every projected cell is empty then we can do the move
				if #emptyCells == #targetCells then
					move = true
					doneSearching = true
				end

				targetCells = nextTargetCells
			end
			--let's get out of this loop b/c if we can't move one color we can't move at all
		end


		---PULL CHEC

		local initialTargetCell = {x = pos.x - dir.x, y = pos.y - dir.y}

		local targetCells 

		local pull
		--for each color...
		targetCells = {initialTargetCell} -- ...we start with our attempted movement...
		local doneSearching
		while not doneSearching do
			nextTargetCells = {} -- ...then we create this array to save our next group of cells
			local emptyCells = {}
			for _, cell in pairs(targetCells) do
				if self:isInBounds(cell) then
					local entity = self:getCellInMatrix(self.collidableMatrices[color], cell)
					if entity then
						--something is here!!
						if not entitiesToPull[entity] then --if the entity hasnt been found already
							if entity:canMove() then
								local projectedCells = entity:getOccupiedCells(dir) -- we project the movement here
								entitiesToPull[entity] = entity
								for _, projectedCell in ipairs(projectedCells) do
									table.insert(nextTargetCells, projectedCell)
								end
							else
								pull = false
								doneSearching = true
							end
						end
					else 
						--there's nothin there!!
						table.insert(emptyCells, cell)
					end
				else
					--we are out of bounds now
					doneSearching = true
				end
			end

			--if every projected cell is empty then we can do the move
			if #emptyCells == #targetCells then
				pull = true
				doneSearching = true
			end

			targetCells = nextTargetCells
		end

		if move then
			for _, entity in pairs(entitiesToPush) do
				entity:move(axis, dir[axis])
			end
			if pull == true then
				for _, entity in pairs(entitiesToPull) do
					entity:move(axis, dir[axis])
				end
			end
			self.player[color]:move(axis, dir[axis])
			moved = true
		end
		newPositions[color] = self.player[color]:getGridPos()
	end

	if moved then
		self:playerJoinCheck(previousPositions, newPositions)
		self:tick()
	end
end

function Room:switchPlayerColor(color)
	local canSwitch

	if self:onlyColorIsActive(color) then
		canSwitch = false
	else
		for _, c in pairs(self:getPossibleColors()) do
			if c == color then
				self:setActiveColor(c, true)
			else
				self:setActiveColor(c, false)
			end
		end
	end

	self.player[color]:playSwitchSound(canSwitch)
end

function Room:setActiveColor(color, bool)
	self.activeColors[color] = bool
end

function Room:getActiveColors()
	local colors = {}

	for color, active in pairs(self.activeColors) do
		if active then
			table.insert(colors, color)
		end
	end

	return colors
end

function Room:colorIsActive(color)
	if self.activeColors[color] then
		return true
	end
	return false
end

function Room:getInactiveColors()
	-- only colors the player actually has a copy of but that aren't active right
	-- now (a left-behind copy). Colors the player hasn't acquired have no copy to
	-- merge with, so they must not appear here.
	local colors = {}

	for _, color in ipairs(self.colorsPlayerHas) do
		if not self.activeColors[color] then
			table.insert(colors, color)
		end
	end

	return colors
end

function Room:getPossibleColors()
	local colors = {}

	for _, color in ipairs(self.colorsPlayerHas) do
		table.insert(colors, color)
	end

	return colors
end

function Room:onlyColorIsActive(color)
	local activeColors = self:getActiveColors()
	if #activeColors == 1 and activeColors[1] == color then
		return true
	else
		return false
	end
end

function Room:tick()
	for color, entityArray in pairs(self.collideables) do
		for _, entity in pairs(entityArray) do
			entity:tick()
		end
	end
	self:createCollideablesMatrix()
	self:createSwitchMatrix()
	local setDoorColors = self:switchCheck()
	for _, door in pairs(self.doors) do
		door:setColors(setDoorColors)
	end
	self:nextRoomCheck()
end

function Room:playerJoinCheck(previousPositions, newPositions)
	local activate = {}
	local deactivate = {}
	local activeColors = self:getActiveColors()

	for _, color in ipairs(activeColors) do
		if #activeColors > 1 then
			if Entity.positionsAreEqual(previousPositions[color], newPositions[color]) then
				table.insert(deactivate, color)
			end
		end
	end

	for _, inactiveColor in ipairs(self:getInactiveColors()) do
		local overlapping = 0
		local activeColors = self:getActiveColors()
		local inactivePos = self:getPlayerPos(inactiveColor)
		for _, activeColor in ipairs(activeColors) do
			local activePos = self:getPlayerPos(activeColor)
			if activePos.x == inactivePos.x and activePos.y == inactivePos.y then
				overlapping = overlapping + 1
			end
		end
		if overlapping == #activeColors then
			table.insert(activate, inactiveColor)
		end
	end

	for _, color in ipairs(activate) do
		self:setActiveColor(color, true)
	end

	for _, color in ipairs(deactivate) do
		self:setActiveColor(color, false)
	end
end

function Room:getPlayerPos(color)
	return self.player[color]:getGridPos()
end

function Room:getPlayerObject(color)
	return self.player[color]
end

function Room:getDoors()
	return self.doors
end

function Room:getCollideables()
	return self.collideables
end

function Room:getSwitches()
	return self.switches
end

function Room:switchCheck()
	local doorsToUnlock = {}
	for color, switchArray in pairs(self.switches) do
		local onSwitches = {}
		for _, switch in pairs(switchArray) do
			local targetCell = self:getCellInMatrix(self.collidableMatrices[color], switch:getGridPos())
			if targetCell then
				--switch is on
				switch:activate()
				table.insert(onSwitches, switch) --doesn't rly matter what we put here as long as it's true-y
			else
				switch:deactivate()
			end
		end
		if #onSwitches == #switchArray then doorsToUnlock[color] = true else doorsToUnlock[color] = false end
	end

	return doorsToUnlock
end

function Room:isInBounds(cell)
	if cell.x > 0 and cell.x <= gridWidth and
	  cell.y > 0 and cell.y <= gridHeight then
		return true
	end
	return false
end

function Room:nextRoomCheck()
	for _, color in ipairs(self:getActiveColors()) do
		local player = self.player[color]
		if player and not self:isInBounds(player:getGridPos()) then
			nextRoom(player:getGridPos())
			return
		end
	end
end


-- Instantiate one object from the flat {class,color,x,y} schema. `class` is a
-- key: the built-in behaviour classes (door/switch/player) plus the two generic
-- grid pieces (wall/static_wall), and finally any art asset resolved by name via
-- the globalAssetProperties registry (this is what the level editor places).
function Room:createEntity(class, color, x, y)
	local entity
	if class == "door" then
		entity = Door:new(x, y)
		self:registerDoor(entity)
	elseif class == "switch" then
		entity = Switch:new(x, y, color)
		self:registerSwitch(entity, color)
	elseif class == "player" then
		entity = Player:new(x, y, color)
		self:registerPlayer(entity, color)
		self:setActiveColor(color, true)
	elseif class == "wall" then
		entity = GenericUnmoveable:new(x, y, color, sprites.box[color])
		self:registerCollidable(entity, {color})
	elseif class == "static_wall" then
		entity = GenericUnmoveable:new(x, y, color, sprites.wall)
		self:registerCollidable(entity, {color})
	elseif class == "npc" then
		self:registerNpc(x, y)
	else
		-- generic art asset placed via the editor, resolved by name
		local props = globalAssetProperties and globalAssetProperties[class]
		if props and props.sprites and props.sprites[color] then
			local sprite = loadPlaceImage(props.sprites[color])
			if props.class == "moveable" then
				entity = GenericMoveable:new(x, y, color, sprite, class)
			else
				entity = GenericUnmoveable:new(x, y, color, sprite)
			end
			self:registerCollidable(entity, {color})
		end
	end
	if entity then entity.saveClass = class end
	return entity
end

function Room:registerCollidable(entityPointer, colorArray) 
	if type(colorArray) ~= "table" then colorArray = {colorArray} end
	for _, color in ipairs(colorArray) do
		table.insert(self.collideables[color], entityPointer)
	end
end

function Room:registerDoor(entityPointer)
	table.insert(self.doors, entityPointer)
	self:registerCollidable(entityPointer, {"red", "green", "blue"})
end

function Room:registerSwitch(entityPointer, color)
	table.insert(self.switches[color], entityPointer)
end

function Room:registerPlayer(entityPointer, color)
	table.insert(self.colorsPlayerHas, color)
	self.player[color] = entityPointer
end

function Room:registerNpc(x, y)
	table.insert(self.npcs, { x = x, y = y })
end

function Room:getCellFromMousePos(mouseX, mouseY)
	local cell = {}
	cell.x = math.ceil(mouseX * gridWidth / width)
	cell.y = math.ceil(mouseY * gridHeight / height)
	return cell
end

function Room:attemptDelete(mouseX, mouseY, color)
	local cell = self:getCellFromMousePos(mouseX, mouseY)

	-- "white" (or nil) deletes every channel; otherwise just the picked one
	local colors
	if not color or color == "white" then
		colors = {"red", "green", "blue"}
	else
		colors = {color}
	end

	for _, c in ipairs(colors) do
		-- player
		if self.player[c] and self.player[c]:occupiesCell(cell) then
			self.player[c] = nil
			self:setActiveColor(c, false)
			for i = #self.colorsPlayerHas, 1, -1 do
				if self.colorsPlayerHas[i] == c then table.remove(self.colorsPlayerHas, i) end
			end
		end
		-- collidables (iterate backwards so table.remove is safe)
		local entityArray = self.collideables[c]
		for index = #entityArray, 1, -1 do
			if entityArray[index]:occupiesCell(cell) then
				table.remove(entityArray, index)
			end
		end
		-- switches
		local switchArray = self.switches[c]
		for index = #switchArray, 1, -1 do
			if switchArray[index]:occupiesCell(cell) then
				table.remove(switchArray, index)
			end
		end
	end
	self:tick()
end

function Room:attemptPlace(name, props, mouseX, mouseY, color)
	if not props or not props.sprites then return end
	local cell = self:getCellFromMousePos(mouseX, mouseY)

	-- "white" (or nil) means every channel; otherwise just the picked channel
	local colors
	if not color or color == "white" then
		colors = {"red", "green", "blue"}
	else
		colors = {color}
	end

	-- one entity per placed channel, via the same path level-loading uses; the
	-- additive blend combines the channels into full color
	for _, c in ipairs(colors) do
		if props.sprites[c] then
			self:createEntity(name, c, cell.x, cell.y)
		end
	end
	self:tick()
end

function Room:load(name)
	local room = love.filesystem.read("rooms/" .. name .. ".sav")
	if not room then
		-- no save yet: start with a blank room to build in the editor
		print("no save for '" .. name .. "', starting blank")
		return
	end
	self:deserialize(TSerial.unpack(room))
end

function Room:deserialize(saveTable)
	for _, obj in ipairs(saveTable.objects or {}) do
		self:createEntity(obj.class, obj.color, obj.x, obj.y)
	end
end

function Room:save()
	local name = self.name or "test"
	local room = TSerial.pack(self:serialize(), nil, true)

	local success = love.filesystem.write("rooms/" .. name .. ".sav", room)
	if not success then error "great job u fool" end
end

function Room:serialize()
	local objects = {}

	local function emit(class, color, entity)
		local pos = entity:getGridPos()
		table.insert(objects, { class = class, color = color, x = pos.x, y = pos.y })
	end

	for color, player in pairs(self.player) do
		emit("player", color, player)
	end

	-- collideables hold walls/static_walls/generics on one color each, plus
	-- doors registered across all three colors -> dedupe doors, emit once
	local seenDoor = {}
	for color, collideableArray in pairs(self.collideables) do
		for _, collideable in pairs(collideableArray) do
			if collideable:isInstanceOf(Door) then
				if not seenDoor[collideable] then
					seenDoor[collideable] = true
					emit("door", nil, collideable)
				end
			else
				emit(collideable.saveClass or "unmoveable", color, collideable)
			end
		end
	end

	for color, switchArray in pairs(self.switches) do
		for _, switch in ipairs(switchArray) do
			emit("switch", color, switch)
		end
	end

	for _, npc in ipairs(self.npcs) do
		table.insert(objects, { class = "npc", x = npc.x, y = npc.y })
	end

	return { objects = objects }
end

function nextRoom(pos)
	local exit
	local newPos = {x = pos.x, y = pos.y}
	if pos.x > gridWidth then
		exit = "right"
		newPos.x = 1
	elseif pos.x < 1 then
		exit = "left"
		newPos.x = gridWidth
	elseif pos.y > gridHeight then
		exit = "bottom"
		newPos.y = 1
	elseif pos.y < 1 then
		exit = "top"
		newPos.y = gridHeight
	end

	currentRoom:save()

	currentRoom = Room:new{ 
		-- name = "test",
		player = {
			x = newPos.x, y = newPos.y, activeColors = currentRoom:getActiveColors()
		}
	}

end

