/*
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
 */

#pragma once

#include <archive.h>
#include <archive_entry.h>
#include <streambuf>
#include "Base/Logger.hpp"

namespace OpenVulkano
{
#ifdef _MSC_VER
	using mode_t = unsigned short;
	using ssize_t = int64_t;
#endif

	namespace LibArchiveHelper
	{
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wswitch"
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wswitch"
		constexpr mode_t GetFileType(std::filesystem::file_type type)
		{
			switch (type)
			{
				case std::filesystem::file_type::regular: return AE_IFREG;
				case std::filesystem::file_type::directory: return AE_IFDIR;
				case std::filesystem::file_type::symlink: return AE_IFLNK;
				case std::filesystem::file_type::block: return AE_IFBLK;
				case std::filesystem::file_type::character: return AE_IFCHR;
				case std::filesystem::file_type::fifo: return AE_IFIFO;
				case std::filesystem::file_type::socket: return AE_IFSOCK;
			}
			return AE_IFMT;
		}

		constexpr std::filesystem::file_type GetFileType(mode_t type)
		{
			switch(type & AE_IFMT)
			{
				case AE_IFREG: return std::filesystem::file_type::regular;
				case AE_IFDIR: return std::filesystem::file_type::directory;
				case AE_IFLNK: return std::filesystem::file_type::symlink;
				case AE_IFBLK: return std::filesystem::file_type::block;
				case AE_IFCHR: return std::filesystem::file_type::character;
				case AE_IFIFO: return std::filesystem::file_type::fifo;
				case AE_IFSOCK: return std::filesystem::file_type::socket;
			}
			return std::filesystem::file_type::unknown;
		}
#pragma GCC diagnostic pop
#pragma clang diagnostic pop

		template<bool BLOCK_WRITE = false>
		inline ssize_t CopyArchiveData(struct archive* archiveReader, struct archive* archiveWriter)
		{
			const void* buffer;
			size_t size;
			int64_t offset, r;

			while (true)
			{
				r = archive_read_data_block(archiveReader, &buffer, &size, &offset);
				if (r == ARCHIVE_EOF) return ARCHIVE_OK;
				if (r < ARCHIVE_OK) return r;
				if constexpr (BLOCK_WRITE)
				{
					r = archive_write_data_block(archiveWriter, buffer, size, offset);
				}
				else
				{
					r = archive_write_data(archiveWriter, buffer, size);
				}
				if (r < ARCHIVE_OK) return r;
			}
		}

		inline bool CheckError(long archiveResult, struct archive* arch, const Logger::Ptr& logger)
		{
			if (archiveResult >= ARCHIVE_OK) return true;
			spdlog::level::level_enum lvl = spdlog::level::level_enum::warn;
			if (archiveResult <= ARCHIVE_FATAL) lvl = spdlog::level::level_enum::critical;
			else if (archiveResult <= ARCHIVE_FAILED) lvl = spdlog::level::level_enum::err;
			const char* errorString = archive_error_string(arch);
			if (logger) logger->log(lvl, errorString ? errorString : "Unknown error while handling archive");
			if (archiveResult == ARCHIVE_FAILED || archiveResult == ARCHIVE_FATAL) throw std::runtime_error(errorString);
			return false;
		}
	}

	class LibArchiveEntryStreamBuffer final : public std::streambuf
	{
	private:
		struct archive* m_archive;
		char* m_buffer;
		size_t m_currentBlockSize;
		int64_t offset;

		bool ReadNextBlock()
		{
			const void* buffer;
			bool ok = archive_read_data_block(m_archive, &buffer, &m_currentBlockSize, &offset) == ARCHIVE_OK;
			m_buffer = static_cast<char*>(const_cast<void*>(buffer));
			return ok;
		}

	public:
		LibArchiveEntryStreamBuffer(struct archive* arch) : m_archive(arch), m_currentBlockSize(0)
		{}

		int underflow() override
		{
			if (gptr() == egptr())
			{
				if (ReadNextBlock())
				{
					setg(m_buffer, m_buffer, m_buffer + m_currentBlockSize);
				}
				else
				{
					return std::char_traits<char>::eof();
				}
			}
			return std::char_traits<char>::to_int_type(*this->gptr());
		}
	};
}
