/*
 * 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/.
 */

#include "ArchiveReader.hpp"
#include "ArchiveConfiguration.hpp"
#include "LibArchiveHelper.hpp"
#include "Base/Utils.hpp"
#include <archive.h>
#include <archive_entry.h>
#include <fstream>
#include <regex>

namespace OpenVulkano
{
	namespace
	{
		constexpr int BUFFER_SIZE = 16384;
	}

	ArchiveReader::ArchiveReader(const std::shared_ptr<spdlog::logger>& logger)
			: ArchiveBase(archive_read_new(), nullptr, logger)
	{}

	ArchiveReader::ArchiveReader(const std::string& archiveFile, const std::shared_ptr<spdlog::logger>& logger)
			: ArchiveReader(archiveFile.c_str(), logger)
	{}

	ArchiveReader::ArchiveReader(const char* archiveFile, const std::shared_ptr<spdlog::logger>& logger)
			: ArchiveReader(logger)
	{
		Open(archiveFile);
	}

	ArchiveReader::ArchiveReader(const void* archiveBuffer, const size_t size, const std::shared_ptr<spdlog::logger>& logger)
			: ArchiveReader(logger)
	{
		OpenMemory(archiveBuffer, size);
	}

	ArchiveReader::~ArchiveReader() = default;

	void ArchiveReader::PrepOpen()
	{
		if (m_open)
		{
			ChkErr(archive_read_close(m_archive));
			ChkErr(archive_free(m_archive));
			m_archive = archive_read_new();
		}
		ChkErr(archive_read_support_filter_all(m_archive));
		ChkErr(archive_read_support_format_all(m_archive));
		m_open = true;
	}

	bool ArchiveReader::Open(const char* archiveFile)
	{
		if (archiveFile[0] == '\0')
		{
			if (m_logger)
			{
				m_logger->error("Unable to open archive file with an empty name!");
			}
			return false;
		}
		PrepOpen();
		ChkErr(archive_read_open_filename(m_archive, archiveFile, BUFFER_SIZE));
		ReadNextHeader();
		return HasNext();
	}

	bool ArchiveReader::Open(const std::string& archiveFile)
	{
		return Open(archiveFile.c_str());
	}

	bool ArchiveReader::OpenMemory(const void* archiveBuffer, size_t size)
	{
		PrepOpen();
		ChkErr(archive_read_open_memory(m_archive, archiveBuffer, size));
		ReadNextHeader();
		return HasNext();
	}

	bool ArchiveReader::Open(const std::vector<std::string>& archiveFiles)
	{
		if (archiveFiles.empty()) return false;
		ArchiveConfiguration ac = ArchiveConfiguration::FromFileName(archiveFiles.front().c_str());
		if (ac.type == ArchiveType::TAR) // TODO handle all archive types correctly
		{ // Queue based approach for all archive types that do not natively support split archives
			for(const std::string& file : archiveFiles)
			{
				m_archivesToRead.push(file);
			}
			bool state = Open(m_archivesToRead.front());
			m_archivesToRead.pop();
			return state;
		}
		else
		{ // This might only be used for archive types that support split archives (like zip, rar, 7zip, ...)
			auto files = Utils::toCString(archiveFiles);
			files.push_back(nullptr);
			PrepOpen();
			ChkErr(archive_read_open_filenames(m_archive, files.data(), BUFFER_SIZE));
			ReadNextHeader();
			return HasNext();
		}
	}

	bool ArchiveReader::Open(const std::filesystem::path& dir, const std::string& fileNamePattern, size_t* outSize)
	{
		std::vector<std::string> files;

		std::regex fileRegex(fileNamePattern);
		for(const std::filesystem::directory_entry& dirEntry : std::filesystem::directory_iterator(dir))
		{
			std::string entryPath = dirEntry.path().string();
			if (!dirEntry.is_directory() && std::regex_match(entryPath, fileRegex))
			{
				files.push_back(std::move(entryPath));
				if (outSize)
				{
					*outSize += dirEntry.file_size();
				}
			}
		}

		std::sort(files.begin(), files.end());
		return Open(files);
	}

	void ArchiveReader::ReadNextHeader()
	{
		if (m_eof || !m_open)
		{
			m_archiveEntry = nullptr;
			return;
		}
		int result = archive_read_next_header(m_archive, &m_archiveEntry);
		if (!ChkErr(result))
		{
			m_archiveEntry = nullptr;
		}
		if (result == ARCHIVE_EOF)
		{
			m_archiveEntry = nullptr;
			if (!m_archivesToRead.empty())
			{
				std::string next = std::move(m_archivesToRead.front());
				m_archivesToRead.pop();
				Open(next);
			}
			else m_eof = true;
		}
	}

	size_t ArchiveReader::ExtractRemaining(std::string_view targetDir)
	{
		size_t count = 0;
		while (ExtractNext(targetDir))
		{
			count++;
		}
		return count;
	}

	bool ArchiveReader::HasNext() const
	{
		return m_archiveEntry != nullptr;
	}

	void ArchiveReader::SkipNext()
	{
		ReadNextHeader();
	}

	bool ArchiveReader::SkipTill(std::filesystem::file_type type)
	{
		mode_t typeId = LibArchiveHelper::GetFileType(type);
		if (typeId == AE_IFMT) throw std::invalid_argument("Invalid file type");
		while(HasNext() && archive_entry_filetype(m_archiveEntry) != typeId)
		{
			SkipNext();
		}
		return HasNext();
	}

	FileDescription ArchiveReader::GetNextDescription() const
	{
		return {
				LibArchiveHelper::GetFileType(archive_entry_filetype(m_archiveEntry)),
				std::string(archive_entry_pathname_utf8(m_archiveEntry)),
				archive_entry_size_is_set(m_archiveEntry) ? static_cast<size_t>(archive_entry_size(m_archiveEntry)) : SIZE_MAX,
				static_cast<std::filesystem::perms>(archive_entry_perm(m_archiveEntry)) & std::filesystem::perms::mask,
				archive_entry_ctime(m_archiveEntry),
				archive_entry_mtime(m_archiveEntry)
		};
	}

	std::optional<FileDescription> ArchiveReader::GetNextDirectory()
	{
		if (SkipTill(std::filesystem::file_type::directory))
		{
			return GetNextDescription();
		}
		return std::nullopt;
	}

	std::optional<FileDescription> ArchiveReader::ExtractNext(std::string_view targetDir)
	{
		if (SkipTill(std::filesystem::file_type::regular))
		{
			FileDescription fileDescription = GetNextDescription();
			std::string outputFileName;
			outputFileName.reserve(targetDir.size() + 1 + fileDescription.path.size());
			outputFileName.append(targetDir).append("/").append(fileDescription.path);

			std::unique_ptr<archive, decltype(archive_write_free)*> a(archive_write_disk_new(), archive_write_free);
			std::unique_ptr<archive_entry, decltype(archive_entry_free)*> entry(archive_entry_clone(m_archiveEntry), archive_entry_free);

			archive_entry_set_pathname(entry.get(), outputFileName.c_str());

			ChkErr(archive_write_disk_set_options(a.get(), ARCHIVE_EXTRACT_TIME | ARCHIVE_EXTRACT_PERM | ARCHIVE_EXTRACT_ACL | ARCHIVE_EXTRACT_FFLAGS));

			ChkErr(archive_write_header(a.get(), entry.get()));
			long result = LibArchiveHelper::CopyArchiveData<true>(m_archive, a.get());
			ChkErr(result);
			LibArchiveHelper::CheckError(result, a.get(), m_logger);

			ChkErr(archive_write_finish_entry(a.get()));

			ReadNextHeader();
			return fileDescription;
		}
		return std::nullopt;
	}

	std::optional<std::pair<FileDescription, Array<char>>> ArchiveReader::GetNextFile()
	{
		if (SkipTill(std::filesystem::file_type::regular))
		{
			std::pair<FileDescription, Array<char>> file = { GetNextDescription(), Array<char>() };
			file.second = Array<char>(file.first.size);
			ChkErr(archive_read_data(m_archive, file.second.Data(), file.second.Size()));
			ReadNextHeader();
			return file;
		}
		return std::nullopt;
	}

	std::optional<std::pair<FileDescription, std::vector<char>>> ArchiveReader::GetNextFileAsVector()
	{
		if (SkipTill(std::filesystem::file_type::regular))
		{
			std::pair<FileDescription, std::vector<char>> file = { GetNextDescription(), std::vector<char>() };
			file.second.resize(file.first.size);
			ChkErr(archive_read_data(m_archive, file.second.data(), file.second.size()));
			ReadNextHeader();
			return file;
		}
		return std::nullopt;
	}

	bool ArchiveReader::GetNextFileAsStream(const std::function<void(const FileDescription&, std::istream&)>& streamReader)
	{
		if (SkipTill(std::filesystem::file_type::regular))
		{
			FileDescription fileDescription = GetNextDescription();
			LibArchiveEntryStreamBuffer streamBuffer(m_archive);
			std::istream stream(&streamBuffer);
			streamReader(fileDescription, stream);
			ReadNextHeader();
			return true;
		}
		return false;
	}

	std::optional<FileDescription> ArchiveReader::StreamNextFile(std::ostream& stream)
	{
		if (SkipTill(std::filesystem::file_type::regular))
		{
			const void *buffer;
			size_t size;
			int64_t offset, r;
			while ((r = archive_read_data_block(m_archive, &buffer, &size, &offset)) != ARCHIVE_EOF)
			{
				ChkErr(r);
				stream.write(static_cast<const char*>(buffer), size);
			}
			FileDescription fileDescription = GetNextDescription();
			ReadNextHeader();
			return fileDescription;
		}
		return std::nullopt;
	}
}
