// HARFANG(R) Copyright (C) 2021 Emmanuel Julien, NWNC HARFANG. Released under GPL/LGPL/Commercial Licence, see licence.txt for details.

#include "engine/assets.h"
#include "foundation/dir.h"
#include "foundation/file.h"
#include "foundation/format.h"
#include "foundation/log.h"
#include "foundation/path_tools.h"

#include <algorithm>
#include <cstdio>
#include <cstring>
#include <deque>
#include <functional>
#include <memory>
#include <mutex>
#include <set>
#include <string>

#include <miniz/miniz.h>

namespace hg {

enum AssetsSourceType { AssetsFolder, AssetsPackage };

struct AssetsSource {
	AssetsSourceType type;
	std::string name;
};

//
static std::mutex assets_mutex;

static std::deque<std::string> assets_folders;

bool AddAssetsFolder(const char *path) {
	std::lock_guard<std::mutex> lock(assets_mutex);
	if (std::find(std::begin(assets_folders), std::end(assets_folders), path) == std::end(assets_folders)) {
		assets_folders.push_front(path);
		return true;
	}
	return false;
}

void RemoveAssetsFolder(const char *path) {
	std::lock_guard<std::mutex> lock(assets_mutex);
	assets_folders.erase(
		std::remove_if(std::begin(assets_folders), std::end(assets_folders), [&](const std::string &i) { return i == path; }), std::end(assets_folders));
}

//
struct ZipPackage {
	mz_zip_archive archive;
	std::string filename;

	ZipPackage() { mz_zip_zero_struct(&archive); }
	~ZipPackage() { mz_zip_reader_end(&archive); }
};

static std::deque<ZipPackage> assets_packages;

bool AddAssetsPackage(const char *path) {
	std::lock_guard<std::mutex> lock(assets_mutex);
	auto it = std::find_if(std::begin(assets_packages), std::end(assets_packages), [path](const ZipPackage &h) { return h.filename == path; });
	if (it != std::end(assets_packages)) {
		return false;
	}

	assets_packages.emplace_front();
	ZipPackage &pkg = assets_packages.front();

	pkg.filename = path;
	if (mz_zip_reader_init_file(&pkg.archive, path, MZ_ZIP_FLAG_VALIDATE_HEADERS_ONLY) == MZ_FALSE) {
		return false;
	}

	return true;
}

void RemoveAssetsPackage(const char *path) {
	std::lock_guard<std::mutex> lock(assets_mutex);
	assets_packages.erase(std::begin(assets_packages),
		std::remove_if(std::begin(assets_packages), std::end(assets_packages), [path](const ZipPackage &h) { return h.filename == path; }));
}

struct PackageFile {
	std::string data;
	ptrdiff_t cursor;
};

//
struct Asset_ {
	File file;
	PackageFile pkg_file;

	size_t (*get_size)(Asset_ &asset);
	size_t (*read)(Asset_ &asset, void *data, size_t size);
	bool (*seek)(Asset_ &asset, ptrdiff_t offset, SeekMode mode);
	size_t (*tell)(Asset_ &asset);
	void (*close)(Asset_ &asset);
	bool (*is_eof)(Asset_ &asset);
};

//
static size_t Asset_file_GetSize(Asset_ &asset) { return GetSize(asset.file); }
static size_t Asset_file_Read(Asset_ &asset, void *data, size_t size) { return Read(asset.file, data, size); }
static bool Asset_file_Seek(Asset_ &asset, ptrdiff_t offset, SeekMode mode) { return Seek(asset.file, offset, mode); }
static size_t Asset_file_Tell(Asset_ &asset) { return Tell(asset.file); }
static void Asset_file_Close(Asset_ &asset) { Close(asset.file); }
static bool Asset_file_is_EOF(Asset_ &asset) { return IsEOF(asset.file); }

//
static size_t Package_file_GetSize(Asset_ &asset) { return asset.pkg_file.data.size(); }
static size_t Package_file_Read(Asset_ &asset, void *data, size_t size) {
	if (asset.pkg_file.cursor + size <= asset.pkg_file.data.size()) {
		memcpy(data, asset.pkg_file.data.data() + sizeof(char) * asset.pkg_file.cursor, size);
		asset.pkg_file.cursor += size;
		return size;
	}
	return 0;
}
static bool Package_file_Seek(Asset_ &asset, ptrdiff_t offset, SeekMode mode) {
	if (offset <= asset.pkg_file.data.size()) {
		asset.pkg_file.cursor = offset;
		return true;
	}
	return false;
}
static size_t Package_file_Tell(Asset_ &asset) { return asset.pkg_file.cursor; }
static void Package_file_Close(Asset_ &asset) {}
static bool Package_file_is_EOF(Asset_ &asset) { return asset.pkg_file.cursor >= asset.pkg_file.data.size(); }

//
static generational_vector_list<Asset_> assets;

std::string FindAssetPath(const char *name) {
	std::lock_guard<std::mutex> lock(assets_mutex);

	for (auto &p : assets_folders) {
		const auto asset_path = hg::PathJoin({p, name});
		if (IsFile(asset_path.c_str()))
			return asset_path;
	}
	return "";
}

Asset OpenAsset(const char *name, bool silent) {
	std::lock_guard<std::mutex> lock(assets_mutex);

	for (auto &p : assets_folders) {
		const auto asset_path = PathJoin({p, name});

		const auto file = Open(asset_path.c_str(), true);
		if (IsValid(file))
			return {assets.add_ref({file, {}, Asset_file_GetSize, Asset_file_Read, Asset_file_Seek, Asset_file_Tell, Asset_file_Close, Asset_file_is_EOF})};
	}

	// look in archive
	for (auto &p : assets_packages) {
		const int index = mz_zip_reader_locate_file(&p.archive, name, NULL, MZ_ZIP_FLAG_CASE_SENSITIVE);
		if (index == -1)
			continue; // missing file

		size_t size;
		const char *buffer = (char *)mz_zip_reader_extract_to_heap(&p.archive, index, &size, 0);
		if (buffer) {
			std::string data(buffer, size);
			return {assets.add_ref({{}, {std::move(data), 0}, Package_file_GetSize, Package_file_Read, Package_file_Seek, Package_file_Tell, Package_file_Close,
				Package_file_is_EOF})};
		} else {
			const mz_zip_error err = mz_zip_get_last_error(&p.archive);
			if (!silent)
				warn(format("Failed to open asset '%1' from file '%2' (asset was found but failed to open) : %3")
						  .arg(name)
						  .arg(p.filename)
						  .arg(mz_zip_get_error_string(err)));
			break;
		}
	}

	if (!silent)
		warn(format("Failed to open asset '%1' (file not found)").arg(name));

	return {};
}

void Close(Asset asset) {
	std::lock_guard<std::mutex> lock(assets_mutex);

	if (assets.is_valid(asset.ref)) {
		auto &asset_ = assets[asset.ref.idx];
		asset_.close(asset_);
		assets.remove_ref(asset.ref);
	}
}

bool IsAssetFile(const char *name) {
	std::lock_guard<std::mutex> lock(assets_mutex);

	for (auto &p : assets_folders)
		if (IsFile(PathJoin({p, name}).c_str()))
			return true;

	// look in archive
	for (auto &p : assets_packages) {
		if (mz_zip_reader_locate_file(&p.archive, name, NULL, MZ_ZIP_FLAG_CASE_SENSITIVE) != -1)
			return true;
	}

	return false;
}

size_t GetSize(Asset asset) {
	std::lock_guard<std::mutex> lock(assets_mutex);
	if (assets.is_valid(asset.ref)) {
		auto &asset_ = assets[asset.ref.idx];
		return asset_.get_size(asset_);
	}
	return 0;
}

size_t Read(Asset asset, void *data, size_t size) {
	std::lock_guard<std::mutex> lock(assets_mutex);
	if (assets.is_valid(asset.ref)) {
		auto &asset_ = assets[asset.ref.idx];
		return asset_.read(asset_, data, size);
	}
	return 0;
}

bool Seek(Asset asset, ptrdiff_t offset, SeekMode mode) {
	std::lock_guard<std::mutex> lock(assets_mutex);
	if (assets.is_valid(asset.ref)) {
		auto &asset_ = assets[asset.ref.idx];
		return asset_.seek(asset_, offset, mode);
	}
	return false;
}

size_t Tell(Asset asset) {
	std::lock_guard<std::mutex> lock(assets_mutex);
	if (assets.is_valid(asset.ref)) {
		auto &asset_ = assets[asset.ref.idx];
		return asset_.tell(asset_);
	}
	return 0;
}

bool IsEOF(Asset asset) {
	std::lock_guard<std::mutex> lock(assets_mutex);
	if (assets.is_valid(asset.ref)) {
		auto &asset_ = assets[asset.ref.idx];
		return asset_.is_eof(asset_);
	}
	return false;
}

//
std::string AssetToString(const char *name) {
	const auto h = OpenAsset(name);
	if (h.ref == invalid_gen_ref)
		return {};
	const auto size = GetSize(h);
	std::string str;
	str.resize(size + 1);
	Read(h, &str[0], size);
	Close(h);
	return str;
}

Data AssetToData(const char *name) {
	Data data;
	Asset asset = OpenAsset(name);

	if (IsValid(asset)) {
		data.Resize(GetSize(asset));
		Read(asset, data.GetData(), data.GetSize());
		Close(asset);
	}

	return data;
}

} // namespace hg
