
local unicode = require("unicode")
local fs = require("filesystem")
local compressor = {}

------------------------------------------------------------------------------------------------------------------

local function numberToByteArray(number)
	local byteArray = {}
	while number > 0 do
		table.insert(byteArray, 1, bit32.band(number, 0xFF))
		number = bit32.rshift(number, 8)
	end
	return byteArray
end

local function byteArrayToNumber(byteArray)
	local number = byteArray[1]
	for i = 2, #byteArray do
		number = bit32.bor(byteArray[i], bit32.lshift(number, 8))
	end
	return number
end

local function info(showInfo, text)
	if showInfo then
		print(text)
	end
end

------------------------------------------------------------------------------------------------------------------

local function writePath(compressedFile, path)
	-- Получаем юникод-байтики названия файла или папки
	local pathBytes = {}
	for i = 1, unicode.len(path) do
		local charBytes = { string.byte(unicode.sub(path, i, i), 1, 6) }
		for j = 1, #charBytes do
			table.insert(pathBytes, charBytes[j])
		end
	end
	-- Записываем количество байт, необходимое для записи РАЗМЕРА байт пути
	local bytesForCountOfBytesForPath = numberToByteArray(#pathBytes)
	compressedFile:write(string.char(#bytesForCountOfBytesForPath))
	-- Записываем количество байт, необходимое для записи самого пути
	for i = 1, #bytesForCountOfBytesForPath do
		compressedFile:write(string.char(bytesForCountOfBytesForPath[i]))
	end
	-- Записываем байтики пути
	for i = 1, #pathBytes do
		compressedFile:write(string.char(pathBytes[i]))
	end
end

local function writeFileSize(compressedFile, path)
	local size = fs.size(path)
	local bytesForSize = numberToByteArray(size)
	-- Записываем количество байт, необходимое для записи РАЗМЕРА байт размера файла
	compressedFile:write(string.char(#bytesForSize))
	-- Записываем сами байты размера файла
	for i = 1, #bytesForSize do
		compressedFile:write(string.char(bytesForSize[i]))
	end
end

local function getFileList(path)
	local fileList = {}
	for file in fs.list(path) do
		table.insert(fileList, path .. file)
	end
	return fileList
end

local function doCompressionRecursively(compressedFile, fileList, currentPackPath, pathToCompressedFile, showInfo)
	for file = 1, #fileList do
		local filename = (fs.name(fileList[file]) or "")
		local filePackPath = currentPackPath .. filename

		-- info(showInfo, "Local path: " .. filePackPath)
		if fileList[file] == pathToCompressedFile or filename == "mnt" or filename == "dev" or filename == ".DS_Store" then
			info(showInfo, "Skipping restricted path \"" .. fileList[file] .. "\"")
		else
			if fs.isDirectory(fileList[file]) then
				info(showInfo, "Packing directory " .. fileList[file])

				compressedFile:write("D")
				writePath(compressedFile, filePackPath .. "/")
				
				doCompressionRecursively(compressedFile, getFileList(fileList[file]), filePackPath .. "/", pathToCompressedFile, showInfo)
			else
				info(showInfo, "Packing file " .. fileList[file])

				compressedFile:write("F")
				writePath(compressedFile, filePackPath)
				writeFileSize(compressedFile, fileList[file])
				
				local fileToCompress = io.open(fileList[file], "rb")
				compressedFile:write(fileToCompress:read("*a"))
				fileToCompress:close()
			end
		end
		-- require("ECSAPI").wait()
	end
end

function compressor.pack(pathToCompressedFile, ...)
	local data, showInfo = {...}, false
	if type(data[#data]) == "boolean" then showInfo = data[#data]; data[#data] = nil end

	info(showInfo, "Packing data to file \"" .. pathToCompressedFile .. "\"...")
	info(showInfo, " ")
	fs.makeDirectory(fs.path(pathToCompressedFile))
	
	-- Открываем файл со сжатым контентом
	local compressedFile, reason = io.open(pathToCompressedFile, "wb")
	if not compressedFile then
		error("Failed to open package file for writing: " .. tostring(reason))
	end
	-- Записываем сигнатурку
	compressedFile:write("ARCH")
	-- Пакуем данные
	doCompressionRecursively(compressedFile, data, "", pathToCompressedFile, showInfo)
	-- Закрываем файл со сжатым контентом
	compressedFile:close()
	info(showInfo, " ")
	info(showInfo, "Data packing finished")
end

------------------------------------------------------------------------------------------------------------------

local function readPath(compressedFile)
	local countOfBytesForPathBytes = string.byte(compressedFile:read(1))
	local pathBytes = {}
	for i = 1, countOfBytesForPathBytes do
		table.insert(pathBytes, string.byte(compressedFile:read(1)))
	end
	local pathSize = byteArrayToNumber(pathBytes)
	local path = compressedFile:read(pathSize)
	-- info(showInfo, "Колво байт под байты пути: ", countOfBytesForPathBytes)
	-- info(showInfo, "Колво байт под путь: ", pathSize)
	-- info(showInfo, "Путь: ", path)
	return path
end

local function readFileSize(compressedFile)
	local countOfBytesForFileSize = string.byte(compressedFile:read(1))
	local fileSizeBytes = {}
	for i = 1, countOfBytesForFileSize do
		table.insert(fileSizeBytes, string.byte(compressedFile:read(1)))
	end
	local fileSize = byteArrayToNumber(fileSizeBytes)
	-- info(showInfo, "Размер файла: ", fileSize)
	return fileSize
end

function compressor.unpack(pathToCompressedFile, pathWhereToUnpack, showInfo)
	info(showInfo, "Unpacking data from file \"" .. pathToCompressedFile .. "\"...")
	info(showInfo, " ")
	fs.makeDirectory(pathWhereToUnpack)
	
	local compressedFile, reason = io.open(pathToCompressedFile, "rb")
	if not compressedFile then
		error("Failed to open package file for reading: " .. tostring(reason))
	end

	local signature = compressedFile:read(4)
	if signature == "ARCH" then
		while true do
			local type = compressedFile:read(1)
			if type == "D" then
				local path = readPath(compressedFile)
				fs.makeDirectory(pathWhereToUnpack .. path)
				info(showInfo, "Unpacking directory \"" .. path .. "\"")
			elseif type == "F" then
				local path = readPath(compressedFile)
				local size = readFileSize(compressedFile)
				
				info(showInfo, "Unpacking file \"" .. path .. "\"")
				local file, reason = io.open(pathWhereToUnpack .. path, "wb")
				if file then
					file:write(compressedFile:read(size))
					file:close()
				else
					compressedFile:read(size)
					info(showInfo, "Failed to open file for writing while unpacking: " .. tostring(reason))
				end				
			elseif not type then
				break
			else
				compressedFile:close()
				error("Packed file is corrupted, unknown path type: " .. tostring(type))
			end
		end
	else
		compressedFile:close()
		error("Packed file is corrupted, wrong signature: " .. tostring(signature))
	end

	compressedFile:close()
	info(showInfo, " ")
	info(showInfo, "Unpacking data finished")
end

------------------------------------------------------------------------------------------------------------------

-- compressor.pack("/test1.pkg", "/MineOS/System/OS/", "/etc/", true)
-- info(showInfo, " ")
-- compressor.unpack("/test1.pkg", "/papkaUnpacked/", true)

------------------------------------------------------------------------------------------------------------------

return compressor



