#include "StdAfx.h"
#include "fs/CustomFilesystem.h"
#include "fs/WritableFolderPack.h"
#include "tl/assert.h"
//#include "tl/algorithm/find.h"
#include "fs/CopyStream.h"
#include "fs/NativeFilesystem.h"
#include "tl/unordered_set.h"
#include "tl/algorithm/find.h"
#include <tl/atomic.h>

//////////////////////////////////////////////////////////////////////////

template class tl::abs_path<tl::path_systems<fs::WindowsSystem, fs::PosixSystem, fs::UncSystem>>;
template class tl::rel_path<tl::path_systems<fs::WindowsSystem, fs::PosixSystem, fs::UncSystem>>;

namespace fs
{
//////////////////////////////////////////////////////////////////////////

namespace
{
tl::atomic<int> s_lastId(0);
}

//////////////////////////////////////////////////////////////////////////

tl::result<CustomFilesystem::PackId> CustomFilesystem::mountFront(AbsPathView mountPoint, tl::unique_ref<IPack> pack)
{
	return mount(std::move(mountPoint), std::move(pack), MountPolicy::Front);
}

//////////////////////////////////////////////////////////////////////////

tl::result<CustomFilesystem::PackId> CustomFilesystem::mountBack(AbsPathView mountPoint, tl::unique_ref<IPack> pack)
{
	return mount(std::move(mountPoint), std::move(pack), MountPolicy::Back);
}

//////////////////////////////////////////////////////////////////////////

cppcoro::generator<tl::unique_ref<IPack>> CustomFilesystem::unmountAll()
{
	tl::fixed_vector<tl::unique_ref<IPack>, 32> packs;
	packs.reserve(m_packsData.size());
	for (PackData& pd: m_packsData)
		packs.push_back(std::move(pd.pack));

	m_packsData.clear();

	for (auto& pack: packs)
		co_yield std::move(pack);
}

//////////////////////////////////////////////////////////////////////////

tl::unique_ptr<IPack> CustomFilesystem::unmount(PackId packId)
{
	if (const auto it = tl::find_if(m_packsData, [packId](const PackData& p) { return p.id == packId; }); it != m_packsData.end())
	{
		auto p = std::move(it->pack); //move it before erasing
		m_packsData.erase(it);
		return std::move(p);
	}
	return nullptr;
}

//////////////////////////////////////////////////////////////////////////

OpenSourceResult CustomFilesystem::openSource(AbsPathView path, SourceFlags flags) const
{
	AbsPath filePackPath;
	for (const auto& pd : m_packsData)
	{
		if (pd.mountPoint.is_prefix_of(path))
		{
			convertToPackPath(filePackPath, path, pd.mountPoint);

			const ExistsResult existsResult = pd.pack->exists(filePackPath);
			if (existsResult == tl::success() && existsResult.value() == false)
				continue;

			tl::result<tl::unique_ref<ISource>, Error> result = pd.pack->openSource(filePackPath, flags);
			if (result.has_value())
				return std::move(result.value());
			if (result.error().code != ErrorCode::NotFound)
				return result;
		}
	}

	return tl::make_error<Error>(ErrorCode::NotFound, "Path '{}' was not found", path);
}

//////////////////////////////////////////////////////////////////////////

OpenStreamSourceResult CustomFilesystem::openStreamSource(AbsPathView path, SourceFlags flags) const
{
	AbsPath filePackPath;
	for (const auto& pd : m_packsData)
	{
		if (pd.mountPoint.is_prefix_of(path))
		{
			convertToPackPath(filePackPath, path, pd.mountPoint);

			const ExistsResult existsResult = pd.pack->exists(filePackPath);
			if (existsResult == tl::success() && existsResult.value() == false)
				continue;

			tl::result<tl::unique_ref<IStreamSource>, Error> result = pd.pack->openStreamSource(filePackPath, flags);
			if (result.has_value())
				return std::move(result.value());
			if (result.error().code != ErrorCode::NotFound)
				return result;
		}
	}

	return tl::make_error<Error>(ErrorCode::NotFound, "Path '{}' was not found", path);
}

//////////////////////////////////////////////////////////////////////////

OpenMapSourceResult CustomFilesystem::openMapSource(AbsPathView path, MapView mapView, SourceFlags flags) const
{
	AbsPath filePackPath;
	for (const auto& pd : m_packsData)
	{
		if (pd.mountPoint.is_prefix_of(path))
		{
			convertToPackPath(filePackPath, path, pd.mountPoint);

			const ExistsResult existsResult = pd.pack->exists(filePackPath);
			if (existsResult == tl::success() && existsResult.value() == false)
				continue;

			tl::result<tl::unique_ref<IMapSource>, Error> result = pd.pack->openMapSource(filePackPath, mapView, flags);
			if (result.has_value())
				return std::move(result.value());
			if (result.error().code != ErrorCode::NotFound)
				return result.error();
		}
	}

	return tl::make_error<Error>(ErrorCode::NotFound, "Path '{}' was not found", path);
}

//////////////////////////////////////////////////////////////////////////

OpenMapSinkResult CustomFilesystem::openMapSink(AbsPathView path, Mode mode, size_t size, SinkFlags flags)
{
	AbsPath filePackPath;
	for (const auto& pd : m_packsData)
	{
		if (pd.mountPoint.is_prefix_of(path))
		{
			if (const tl::lent_ptr<IWritablePack> pack = tl::dynamic_lent_cast<IWritablePack>(pd.pack))
			{
				convertToPackPath(filePackPath, path, pd.mountPoint);

				OpenMapSinkResult result = pack->openMapSink(filePackPath, mode, size, flags);
				if (result.has_value())
					return std::move(result.value());
				if (result.error().code != ErrorCode::NotFound)
					return result;
			}
			else
				return tl::make_error<Error>(ErrorCode::NotAllowed, "The filesystem is read only");
		}
	}

	return tl::make_error<Error>(ErrorCode::NotFound, "Path '{}' was not found", path);
}

//////////////////////////////////////////////////////////////////////////

OpenSinkResult CustomFilesystem::openSink(AbsPathView path, Mode mode, SinkFlags flags)
{
	AbsPath filePackPath;
	for (const auto& pd : m_packsData)
	{
		if (pd.mountPoint.is_prefix_of(path))
		{
			if (const tl::lent_ptr<IWritablePack> pack = tl::dynamic_lent_cast<IWritablePack>(pd.pack))
			{
				convertToPackPath(filePackPath, path, pd.mountPoint);

				OpenSinkResult result = pack->openSink(filePackPath, mode, flags);
				if (result.has_value())
					return std::move(result.value());
				if (result.error().code != ErrorCode::NotFound)
					return result;
			}
		}
	}

	return tl::make_error<Error>(ErrorCode::NotFound, "Path '{}' was not found or all mounted packs are read only", path);
}

//////////////////////////////////////////////////////////////////////////

OpenStreamSinkResult CustomFilesystem::openStreamSink(AbsPathView path, Mode mode, SinkFlags flags)
{
	AbsPath filePackPath;
	for (const auto& pd : m_packsData)
	{
		if (pd.mountPoint.is_prefix_of(path))
		{
			if (const tl::lent_ptr<IWritablePack> pack = tl::dynamic_lent_cast<IWritablePack>(pd.pack))
			{
				convertToPackPath(filePackPath, path, pd.mountPoint);

				OpenStreamSinkResult result = pack->openStreamSink(filePackPath, mode, flags);
				if (result.has_value())
					return std::move(result.value());
				if (result.error().code != ErrorCode::NotFound)
					return result;
			}
		}
	}

	return tl::make_error<Error>(ErrorCode::NotFound, "Path '{}' was not found or all mounted packs are read only", path);
}

//////////////////////////////////////////////////////////////////////////

ExistsResult CustomFilesystem::exists(AbsPathView path) const
{
	AbsPath filePackPath;
	for (const auto& pd : m_packsData)
	{
		if (pd.mountPoint.is_prefix_of(path))
		{
			convertToPackPath(filePackPath, path, pd.mountPoint);

			if (filePackPath.empty()) //This is the exact mount point of the pack, which means this folder exists
				return true;

			auto result = pd.pack->exists(filePackPath);
			if (result.has_value() && result.value() == true)
				return true;

			if (result.has_error())
			{
				//exists should not return not found
				TL_ASSERT(result.error().code != ErrorCode::NotFound);
				return result;
			}
		}
	}

	return false;
}

//////////////////////////////////////////////////////////////////////////

IsFileResult CustomFilesystem::isFile(AbsPathView path) const
{
	AbsPath filePackPath;
	for (const auto& pd : m_packsData)
	{
		if (pd.mountPoint.is_prefix_of(path))
		{
			convertToPackPath(filePackPath, path, pd.mountPoint);

			if (filePackPath.empty()) //This is the exact mount point of the pack, which means it is not a file
				return false;

			auto result = pd.pack->isFile(filePackPath);
			if (result.has_value())
				return result.value();
			if (result.error().code != Error::code_t::NotFound)
				return result;
		}
	}

	return tl::make_error<Error>(Error::code_t::NotFound, "Path '{}' not found in the filesystem", path);
}

//////////////////////////////////////////////////////////////////////////

RenameResult CustomFilesystem::rename(AbsPathView path, AbsPathView newPath)
{
	AbsPath fromPackPath;
	AbsPath toPackPath;
	for (const auto& pd : m_packsData)
	{
		if (pd.mountPoint.is_prefix_of(path))
		{
			if (const tl::lent_ptr<IWritablePack> pack = tl::dynamic_lent_cast<IWritablePack>(pd.pack))
			{
				convertToPackPath(fromPackPath, path, pd.mountPoint);
				convertToPackPath(toPackPath, newPath, pd.mountPoint);

				auto result = pack->rename(fromPackPath, toPackPath);
				if (result.has_value())
					return tl::success();
				if (result.error().code != Error::code_t::NotFound)
					return result;
			}
		}
	}

	return tl::make_error<Error>(Error::code_t::NotFound, "Path '{}' was not found or all mounted packs are read only", path);
}

//////////////////////////////////////////////////////////////////////////

CopyResult CustomFilesystem::copy(AbsPathView path, AbsPathView newPath)
{
	OUTCOME_TRY(const auto source, openStreamSource(path, SourceFlags(SourceFlag::SequentialHint, SourceFlag::Uncached)));
	OUTCOME_TRY(const auto destination, openStreamSink(newPath, Mode::CreateOrOpenAndClear));

	CopyStreamResult copyResult = copyStream(*destination, *source);
	if (copyResult.has_error())
		return copyResult.error();

	return tl::success();
}

//////////////////////////////////////////////////////////////////////////

MakeHardLinkResult CustomFilesystem::makeHardLink(AbsPathView sourcePath, AbsPathView linkPath)
{
	OUTCOME_TRY(const auto sp, convertToNativePath(sourcePath));
	OUTCOME_TRY(const auto lp, convertToNativePath(linkPath));
	return native.makeHardLink(sp, lp);
}

//////////////////////////////////////////////////////////////////////////

MakeSoftLinkResult CustomFilesystem::makeSymLink(AbsPathView sourcePath, AbsPathView linkPath)
{
	OUTCOME_TRY(const auto sp, convertToNativePath(sourcePath));
	OUTCOME_TRY(const auto lp, convertToNativePath(linkPath));
	return native.makeSymLink(sp, lp);
}

//////////////////////////////////////////////////////////////////////////

IsFolderResult CustomFilesystem::isFolder(AbsPathView path) const
{
	AbsPath folderPackPath;
	for (const auto& pd : m_packsData)
	{
		if (pd.mountPoint.is_prefix_of(path))
		{
			convertToPackPath(folderPackPath, path, pd.mountPoint);
			if (folderPackPath.empty()) //This is the exact mount point of the pack, which means it is a folder
				return true;

			auto result = pd.pack->isFolder(folderPackPath);
			if (result.has_value())
				return result.value();
			if (result.error().code != Error::code_t::NotFound)
				return result;
		}
	}

	return tl::make_error<Error>(Error::code_t::NotFound, "Path '{}' not found in the filesystem", path);
}

//////////////////////////////////////////////////////////////////////////

MakeFolderResult CustomFilesystem::makeFolder(AbsPathView path)
{
	AbsPath filePackPath;
	for (const auto& pd : m_packsData)
	{
		if (pd.mountPoint.is_prefix_of(path))
		{
			if (const tl::lent_ptr<IWritablePack> pack = tl::dynamic_lent_cast<IWritablePack>(pd.pack))
			{
				convertToPackPath(filePackPath, path, pd.mountPoint);

				MakeFolderResult result = pack->makeFolder(filePackPath);
				if (result.has_value())
					return result;
				if (result.error().code != ErrorCode::NotFound)
					return result;
			}
			else
				return tl::make_error<Error>(ErrorCode::NotAllowed, "The filesystem is read only");
		}
	}

	return tl::make_error<Error>(Error::code_t::SystemError, "Not implemented");
}

//////////////////////////////////////////////////////////////////////////

RemoveResult CustomFilesystem::remove(AbsPathView path)
{
	AbsPath filePackPath;
	for (const auto& pd : m_packsData)
	{
		if (pd.mountPoint.is_prefix_of(path))
		{
			if (const tl::lent_ptr<IWritablePack> pack = tl::dynamic_lent_cast<IWritablePack>(pd.pack))
			{
				convertToPackPath(filePackPath, path, pd.mountPoint);

				auto result = pack->remove(filePackPath);
				if (result.has_value())
					return tl::success();
				if (result.error().code != Error::code_t::NotFound)
					return result;
			}
		}
	}

	return tl::make_error<Error>(ErrorCode::NotFound, "Path '{}' was not found or all mounted packs are read only", path);
}

//////////////////////////////////////////////////////////////////////////

RemoveRecursivelyResult CustomFilesystem::removeRecursively(AbsPathView path)
{
	for (const EnumerateEntry& ee : enumerate(path))
	{
		AbsPath p = path + ee.path;
		if (ee.isFolder)
		{
			OUTCOME_TRY(removeRecursively(p));
		}
		else
		{
			OUTCOME_TRY(remove(p));
		}
	}

	return tl::success();
}

//////////////////////////////////////////////////////////////////////////

ConvertToNativePathResult CustomFilesystem::convertToNativePath(AbsPathView path) const
{
	AbsPath folderPackPath;
	for (const auto& pd : m_packsData)
	{
		if (pd.mountPoint.is_prefix_of(path))
		{
			convertToPackPath(folderPackPath, path, pd.mountPoint);
			auto result = pd.pack->convertToNativePath(folderPackPath);
			if (result.has_value())
				return result.value();
			if (result.error().code != ErrorCode::NotFound)
				return result;
		}
	}

	return tl::make_error<Error>(Error::code_t::NotFound, "Cannot convert '{}' to a native path", path);
}

//////////////////////////////////////////////////////////////////////////

SetWriteTimeResult CustomFilesystem::setWriteTime(AbsPathView path, time_t time)
{
	AbsPath filePackPath;
	for (const auto& pd : m_packsData)
	{
		if (pd.mountPoint.is_prefix_of(path))
		{
			if (const tl::lent_ptr<IWritablePack> pack = tl::dynamic_lent_cast<IWritablePack>(pd.pack))
			{
				convertToPackPath(filePackPath, path, pd.mountPoint);

				SetWriteTimeResult result = pack->setWriteTime(filePackPath, time);
				if (result.has_value())
					return tl::success();
				if (result.error().code != ErrorCode::NotFound)
					return result;
			}
		}
	}

	return tl::make_error<Error>(Error::code_t::NotFound, "Path '{}' was not found or all mounted packs are read only", path);
}

//////////////////////////////////////////////////////////////////////////

GetStatResult CustomFilesystem::getStat(AbsPathView path) const
{
	AbsPath folderPackPath;
	for (const auto& pd : m_packsData)
	{
		if (pd.mountPoint.is_prefix_of(path))
		{
			convertToPackPath(folderPackPath, path, pd.mountPoint);
			auto result = pd.pack->getStat(folderPackPath);
			if (result.has_value())
				return result.value();
			if (result.error().code != ErrorCode::NotFound)
				return result;
		}
	}

	return tl::make_error<Error>(Error::code_t::NotFound, "File {} not found", path);
}

//////////////////////////////////////////////////////////////////////////

cppcoro::generator<EnumerateEntry> CustomFilesystem::enumerate(AbsPath path) const
{
	AbsPath basePackPath;
	for (const auto& pd : m_packsData)
	{
		if (pd.mountPoint.is_prefix_of(path))
		{
			convertToPackPath(basePackPath, path, pd.mountPoint);

			//it's possible that even if the mount point is
			const IsFolderResult isFolderResult = pd.pack->isFolder(basePackPath);
			if (isFolderResult.has_error() || isFolderResult.value() == false)
				continue;

			for (EnumerateEntry ee : pd.pack->enumerate(basePackPath))
				co_yield std::move(ee);
		}
	}
}

//////////////////////////////////////////////////////////////////////////

cppcoro::generator<EnumerateEntry> CustomFilesystem::enumerateRecursively(AbsPath path) const
{
	tl::unordered_set<RelPath> checkedFiles;

	AbsPath basePackPath;
	for (const auto& pd : m_packsData)
	{
		if (pd.mountPoint.is_prefix_of(path))
		{
			convertToPackPath(basePackPath, path, pd.mountPoint);

			if (!basePackPath.empty())
			{
				const IsFolderResult isFolderResult = pd.pack->isFolder(basePackPath);
				if (isFolderResult.has_error() || isFolderResult.value() == false)
					continue;
			}

			for (EnumerateEntry ee: pd.pack->enumerateRecursively(basePackPath))
			{
				if (checkedFiles.insert(ee.path).second)
					co_yield std::move(ee);
			}
		}
		else if (path.is_prefix_of(pd.mountPoint))
		{
			const RelPathView parentPath = path.path_to(pd.mountPoint);

			RelPath p = parentPath;
			for (EnumerateEntry ee : pd.pack->enumerateRecursively(AbsPath(PosixRootTag::value())))
			{
				const size_t eePathSize = ee.path.size();
				p += ee.path;
				ee.path = p;
				p.shrink(eePathSize);
				if (checkedFiles.insert(ee.path).second)
					co_yield std::move(ee);
			}
		}
	}
}

//////////////////////////////////////////////////////////////////////////

void CustomFilesystem::convertToPackPath(AbsPath& packPath, AbsPathView path, AbsPathView mountPoint) const
{
	RelPathView basePath = mountPoint.prefix_path_to(path);

	packPath = PosixRootTag::value();
	packPath.reserve(basePath.size() + 1);
	for (const tl::string& element : basePath)
		packPath.push_back(element);
}

//////////////////////////////////////////////////////////////////////////

tl::result<CustomFilesystem::PackId> CustomFilesystem::mount(AbsPathView mountPoint, tl::unique_ref<IPack> pack, MountPolicy policy)
{

	if (!mountPoint.is_valid())
		return tl::make_generic_error("Invalid mount point");

	auto isFileResult = isFile(mountPoint);
	if (isFileResult.has_value() && isFileResult.value() == true)
		return tl::make_generic_error("Invalid shadowing (mount point shadowing a file)");

	const PackId id(++s_lastId);
	PackData data(std::move(pack), id);
	data.mountPoint = std::move(mountPoint);

	switch (policy)
	{
	case MountPolicy::Front:
		m_packsData.insert(m_packsData.begin(), std::move(data));
		break;
	case MountPolicy::Back:
		m_packsData.push_back(std::move(data));
		break;
	default:
		TL_FAIL();
		break;
	}

	return id;
}

//////////////////////////////////////////////////////////////////////////
}

