-- LOVE Asset Async Loader (Thread Part)
-- Copyright (c) 2021 Miku AuahDark
--
-- This software is provided 'as-is', without any express or implied
-- warranty. In no event will the authors be held liable for any damages
-- arising from the use of this software.
--
-- Permission is granted to anyone to use this software for any purpose,
-- including commercial applications, and to alter it and redistribute it
-- freely, subject to the following restrictions:
--
-- 1. The origin of this software must not be misrepresented; you must not
--    claim that you wrote the original software. If you use this software
--    in a product, an acknowledgment in the product documentation would be
--    appreciated but is not required.
-- 2. Altered source versions must be plainly marked as such, and must not be
--    misrepresented as being the original software.
-- 3. This notice may not be removed or altered from any source distribution.

-- Task channel structure (inside table)
-- request ID (string) or error channel to signal quit
-- Task name (string)
-- Amount of arguments (number)
-- n-amount of arguments (Variant)

-- Load LOVE module
local love = require("love")
require("love.event")
require("love.data")
-- Non-thread-safe modules
local ntsModules = {"graphics", "window"}
-- But love.graphics must be treated specially
local hasGraphics = false

-- See lily.lua initThreads() function for more information about
-- arguments passed
local modules, errorChannel, taskChannel, dataPullChannel, updateModeChannel = ...

-- Load modules
for _, v in pairs(modules) do
	local f = false
	if v == "graphics" then hasGraphics = true end

	for i = 1, #ntsModules do
		if ntsModules[i] == v then
			f = true
			break
		end
	end

	if not(f) then
		require("love."..v)
	end
end

-- Handlers
local lilyProcessor = {}
local getUpdateMode
do
	local function atomicFuncGetUpdateMode()
		return updateModeChannel:peek()
	end
	function getUpdateMode()
		return updateModeChannel:performAtomic(atomicFuncGetUpdateMode)
	end
end

-- Function to push data
local function pushData(reqID, v1, v2)
	local updateMode = getUpdateMode()
	if updateMode == "automatic" then
		-- Event push
		love.event.push("lily_resp", reqID, v1, v2)
	elseif updateMode == "manual" then
		-- Channel push
		dataPullChannel:push({reqID, v1, v2})
	end
end

-- Macro function to create handler
local function lilyHandlerFunc(reqtype, minarg, handler)
	lilyProcessor[reqtype] = {minarg = minarg, handler = handler}
end


if love.audio then
	lilyHandlerFunc("newSource", 2, function(t)
		return love.audio.newSource(t[1], t[2])
	end)
end

-- Always exist
if love.data then
	local function isCompressedData(t)
		return type(t) == "userdata" and t:typeOf("CompressedData")
	end

	lilyHandlerFunc("compress", 1, function(t)
		return love.data.compress("data", t[1] or "lz4", t[2], t[3])
	end)
	lilyHandlerFunc("decompress", 1, function(t)
		if type(t[2]) == "userdata" and t[2]:typeOf("Data") and love._version < "11.2" then
			-- Prior to LOVE 11.2, love.data.decompress can't decompress
			-- Data object (not CompressedData) due to bug in the code
			-- when handling this variant. So, convert it to string before
			-- passing it.
			t[2] = t[2]:getString()
		end
		return love.data.decompress("data", t[1], t[2])
	end)
end

-- Always exist
if love.filesystem then
	lilyHandlerFunc("append", 2, function(t)
		return assert(love.filesystem.append(t[1], t[2], t[3]))
	end)
	lilyHandlerFunc("newFileData", 1, function(t)
		return assert(love.filesystem.newFileData(t[1], t[2], t[3]))
	end)
	lilyHandlerFunc("read", 1, function(t)
		return assert(love.filesystem.read(t[1], t[2]))
	end)
	lilyHandlerFunc("readFile", 1, function(t)
		return t[1].read(t[1], t[2])
	end)
	lilyHandlerFunc("write", 2, function(t)
		return assert(love.filesystem.write(t[1], t[2], t[3]))
	end)
	lilyHandlerFunc("writeFile", 2, function(t)
		return t[1].write(t[1], t[2], t[3])
	end)
end

if hasGraphics then
	lilyHandlerFunc("newFont", 1, function(t)
		return love.font.newRasterizer(t[1], t[2], t[3], t[4])
	end)
	lilyHandlerFunc("newImage", 1, function(t)
		local s, x = pcall(love.image.newImageData, t[1])
		return (s and x or love.image.newCompressedData(t[1])), select(2, unpack(t))
	end)
	lilyHandlerFunc("newVideo", 1, function(t)
		return love.video.newVideoStream(t[1]), t[2]
	end)
	lilyHandlerFunc("newCubeImage", 1, function(t)
		-- If it's not table, then it should be processed with
		-- love.image.newCubeFaces (undocumented function)
		if type(t[1]) ~= "table" then
			local id = t[1]
			if type(id) ~= "userdata" or id:type() ~= "ImageData" then
				id = love.image.newImageData(id)
			end
			t[1] = {love.image.newCubeFaces(id)}
		end
		for i = 1, 6 do
			local v = t[1][i]
			local t = v:type()
			if t ~= "userdata" or (t ~= "ImageData" and t ~= "CompressedImageData") then
				t[1][i] = love.image.newImageData(v)
			end
		end
		return t[1], t[2]
	end)
end

if love.image then
	lilyHandlerFunc("encodeImageData", 1, function(t)
		return t[1].encode(t[1], t[2])
	end)
	lilyHandlerFunc("newImageData", 1, function(t)
		return love.image.newImageData(t[1])
	end)
	lilyHandlerFunc("newCompressedData", 1, function(t)
		return love.image.newCompressedData(t[1])
	end)
	lilyHandlerFunc("pasteImageData", 7, function(t)
		return t[1].paste(t[1], t[2], t[3], t[4], t[5], t[6], t[7])
	end)
end

if love.sound then
	lilyHandlerFunc("newSoundData", 1, function(t)
		return love.sound.newSoundData(t[1], t[2], t[3], t[4])
	end)
end

if love.video then
	lilyHandlerFunc("newVideoStream", 1, function(t)
		return love.video.newVideoStream(t[1])
	end)
end

local handlerFunc
local handlerArg
local function callHandler()
	return handlerFunc(handlerArg)
end

-- Main loop
while true do
	collectgarbage()
	collectgarbage()
	-- Get request (see the beginning of file for table format)
	local request = taskChannel:demand()
	-- If it's not table then quit signaled
	if type(request) ~= "table" then return end
	local tasktype = request[2]

	-- If it's exist in lilyProcessor table that means it's valid event
	if lilyProcessor[tasktype] then
		local task = lilyProcessor[tasktype]
		local argc = request[3]
		-- Check minarg count
		if argc < task.minarg then
			-- Too few arguments
			pushData(request[1], errorChannel, string.format(
				"'%s': too few arguments (at least %d is required)",
				tasktype,
				task.minarg
			))
		else
			local argv = {}
			-- Get arguments
			for i = 1, argc do
				argv[i] = request[3 + i] -- 4th and later are arguments
			end

			-- Call
			handlerFunc = task.handler
			handlerArg = argv
			local result = {xpcall(callHandler, debug.traceback)}
			if result[1] == false then
				-- Error
				pushData(request[1], errorChannel, string.format("'%s': %s", tasktype, result[2]))
			end
			-- Push data (v1 is true, v2 is return values)
			table.remove(result, 1)
			pushData(request[1], true, result)
		end
	end
end
