/*
 * Copyright © 2020-2026 Pierrick Caillon <pierrick.caillon+zipdirfs@megami.fr>
 */
#include "ZipDirFs/Components/ZipDirectoryEnumerator.h"
#include "StateReporter.h"
#include "ZipDirFs/Zip/Exception.h"
#include "ZipDirFs/Zip/Factory.h"
#include <filesystem>
#include <condition_variable>
#include <mutex>
#include <set>
#include <thread>
#include <tuple>

namespace ZipDirFs::Components
{
	using namespace ZipDirFs::Zip;

	namespace
	{
		struct Holder : ZipDirectoryEnumerator::HolderBase
		{
			Holder(Archive::iterator&& currentIt, Archive::iterator&&);
			Archive::iterator currentIt, endIt;
		};

		struct less;

		struct Cleaner
		{
			typedef std::tuple<std::time_t, std::unique_ptr<Holder>, ZipDirectoryEnumerator*>
				value_type;
			typedef std::unique_lock<std::mutex> lock_type;
			Cleaner(std::time_t);
			~Cleaner();
			static void CollectFor(ZipDirectoryEnumerator const*);
			static void CollectAllKeepSome();
			static void Add(std::unique_ptr<Holder>&& holder, ZipDirectoryEnumerator*);

		private:
			struct less
			{
				bool operator()(const value_type&, const value_type&) const;
			};
			const std::time_t waitTime;
			std::condition_variable wakeup;
			std::set<value_type, less> cleanQueue;
			bool stop;
			bool stopWhenEmpty;
			std::thread thread;
			void doWork();
			void CollectExpired();
			static std::tuple<lock_type, std::shared_ptr<Cleaner>> GetCleaner(
				bool, StateReporter::Lock&);
			static void ReleaseCleaner(lock_type lock);
			static std::shared_ptr<Cleaner> instance;
			static std::mutex access;
		};
		std::shared_ptr<Cleaner> Cleaner::instance;
		std::mutex Cleaner::access;

		std::shared_ptr<Archive> getArchiveRetry(const std::filesystem::path& path)
		{
			while (true)
			{
				try
				{
					return Factory::getInstance().get(path);
				}
				catch (Exception const& zipErr)
				{
					const auto systemCode = zipSystemCode(zipErr.code());
					if (systemCode == EMFILE || systemCode == ENFILE)
					{
						Cleaner::CollectAllKeepSome();
						// TODO: Replace random generation
						std::this_thread::sleep_for(
							std::chrono::milliseconds((std::rand() % 100) * 10));
					}
					else
					{
						throw;
					}
				}
			}
		}
	} // namespace

	extern std::time_t zipDirectoryEnumeratorDelay;
	std::time_t zipDirectoryEnumeratorDelay = 60;

	Holder::Holder(Archive::iterator&& currentIt, Archive::iterator&& endIt) :
		currentIt(currentIt), endIt(endIt)
	{
	}

	Cleaner::Cleaner(const std::time_t waitTime) :
		waitTime(waitTime), stop(false), stopWhenEmpty(false),
		thread([=] { this->doWork(); })
	{
	}
	Cleaner::~Cleaner()
	{
		{
			StateReporter::Lock rl("clean");
			rl.init();
			lock_type lock(access);
			rl.set();
			stop = true;
			wakeup.notify_all();
		}
		thread.join();
	}
	void Cleaner::CollectFor(ZipDirectoryEnumerator const* parent)
	{
		StateReporter::Lock rl("clean");
		auto const cleanerLock = GetCleaner(false, rl);
		if (std::get<1>(cleanerLock) == nullptr)
		{
			return;
		}
		auto& cleaner = *std::get<1>(cleanerLock);
		auto it = cleaner.cleanQueue.begin();
		auto const endIt = cleaner.cleanQueue.end();
		while (it != endIt)
		{
			if (std::get<2>(*it) == parent)
			{
				it = cleaner.cleanQueue.erase(it);
			}
			else
			{
				++it;
			}
		}
		cleaner.stopWhenEmpty = true;
		cleaner.wakeup.notify_all();
	}
	void Cleaner::CollectAllKeepSome()
	{
		StateReporter::Lock rl("clean");
		auto const cleanerLock = GetCleaner(false, rl);
		if (std::get<1>(cleanerLock) == nullptr)
		{
			return;
		}
		auto& cleaner = *std::get<1>(cleanerLock);
		auto it = cleaner.cleanQueue.begin();
		while (cleaner.cleanQueue.size() > 16)
		{
			it = cleaner.cleanQueue.erase(it);
		}
		cleaner.stopWhenEmpty = true;
		cleaner.wakeup.notify_all();
	}
	void Cleaner::Add(std::unique_ptr<Holder>&& holder, ZipDirectoryEnumerator* parent)
	{
		if (holder != nullptr)
		{
			StateReporter::Lock rl("clean");
			auto const cleanerLock = GetCleaner(true, rl);
			auto& cleaner = *std::get<1>(cleanerLock);
			cleaner.cleanQueue.emplace(
				std::time(nullptr) + cleaner.waitTime, std::move(holder), parent);
			cleaner.wakeup.notify_all();
		}
	}
	bool Cleaner::less::operator()(
		const value_type& lhs, const value_type& rhs) const
	{
		auto const first = std::get<0>(lhs) - std::get<0>(rhs);
		return first != 0 ? (first < 0) : (std::get<1>(lhs) < std::get<1>(rhs));
	}
	void Cleaner::doWork()
	{
		reportAction("--cleaner--");
		StateReporter::Lock rl("clean");
		rl.init();
		lock_type lock(access);
		rl.set();
		std::time_t lastTime = std::time(nullptr);
		while (!stop)
		{
			if (cleanQueue.empty())
			{
				if (!stopWhenEmpty)
				{
					rl.condStart();
					wakeup.wait(lock);
					rl.set();
				}
				else
				{
					stopWhenEmpty = false;
					std::thread(Cleaner::ReleaseCleaner, std::move(lock)).detach();
					rl.unset();
					rl.init();
					lock = lock_type(access);
					rl.set();
				}
			}
			else
			{
				std::time_t now = std::time(nullptr);
				std::time_t top = std::get<0>(*cleanQueue.begin());
				rl.condStart();
				if (top <= now
					|| condEnd(rl,
						   now == lastTime ?
								 wakeup.wait_for(lock, std::chrono::milliseconds(500)) :
								 wakeup.wait_until(lock, std::chrono::system_clock::from_time_t(top)))
						== std::cv_status::timeout)
				{
					CollectExpired();
				}
				rl.set();
				lastTime = now;
			}
		}
	}
	void Cleaner::CollectExpired()
	{
		auto it = cleanQueue.begin(), endIt = cleanQueue.end();
		auto now = std::time(nullptr);
		while (it != endIt && std::get<0>(*it) <= now)
		{
			it = cleanQueue.erase(it);
		}
	}
	std::tuple<Cleaner::lock_type, std::shared_ptr<Cleaner>> Cleaner::GetCleaner(
		bool create, StateReporter::Lock& rl)
	{
		rl.init();
		lock_type lock(access);
		rl.set();
		if (instance == nullptr && create)
		{
			instance = std::make_shared<Cleaner>(zipDirectoryEnumeratorDelay);
		}
		return std::make_tuple(std::move(lock), instance);
	}
	void Cleaner::ReleaseCleaner(lock_type lock)
	{
		StateReporter::Lock rl("clean");
		rl.set();
		{
			auto old = std::move(instance);
			lock.unlock();
			rl.unset();
		}
		reportDone();
	}

	ZipDirectoryEnumerator::ZipDirectoryEnumerator(
		const std::filesystem::path& path, const std::string& item) :
		path(path),
		item(item), holder(std::unique_ptr<HolderBase>(std::unique_ptr<Holder>(nullptr))),
		atEnd(false)
	{
	}

	ZipDirectoryEnumerator::~ZipDirectoryEnumerator() { Cleaner::CollectFor(this); }

	void ZipDirectoryEnumerator::reset()
	{
		Cleaner::Add(std::unique_ptr<Holder>(dynamic_cast<Holder*>(holder.release())), this);
		atEnd = false;
		auto archive = getArchiveRetry(path);
		auto beginIt{archive->begin(item)}; // Keep for order of operations (begin have side eff
		auto instance = new Holder(std::move(beginIt), archive->end());
		holder = std::unique_ptr<HolderBase>(std::unique_ptr<Holder>(instance));
		if (instance->currentIt == instance->endIt)
		{
			Cleaner::Add(std::unique_ptr<Holder>(dynamic_cast<Holder*>(holder.release())), this);
			atEnd = true;
		}
	}
	void ZipDirectoryEnumerator::next()
	{
		if (holder == nullptr && !atEnd)
		{
			reset();
		}
		if (holder != nullptr)
		{
			auto instance = dynamic_cast<Holder*>(holder.get());
			++instance->currentIt;
			if (instance->currentIt == instance->endIt)
			{
				Cleaner::Add(
					std::unique_ptr<Holder>(dynamic_cast<Holder*>(holder.release())), this);
				atEnd = true;
			}
		}
	}
	bool ZipDirectoryEnumerator::valid()
	{
		if (holder == nullptr && !atEnd)
		{
			reset();
		}
		return !atEnd;
	}
	const std::string& ZipDirectoryEnumerator::current()
	{
		if (holder == nullptr && !atEnd)
		{
			reset();
		}
		return *dynamic_cast<Holder*>(holder.get())->currentIt;
	}
} // namespace ZipDirFs::Components
