#include <boostAsioLinkageFix.h>

#include <lcameraBuffInternal.h>
#include <lcameraBuffParams.h>
#include <yuvStimProducer.h>
#include <algorithm>
#include <cassert>
#include <dlfcn.h>
#include <iostream>
#include <stdexcept>

namespace smo::stim_buff::lcamera_buff {

const SmoCallbacks *lcameraBuffSmoHooksPtr = nullptr;
SmoThreadingModelDesc lcameraBuffThreadingModelDesc;
LcameraDevDllState lcameraDevDll;
std::vector<std::shared_ptr<YuvStimProducer>> managedStimulusProducers;

void addManagedStimulusProducer(
	const std::shared_ptr<YuvStimProducer>& producer)
{
	managedStimulusProducers.push_back(producer);
	producer->start();
}

sscl::co::ViralNonPostingInvoker<void> removeManagedStimulusProducerIfUnused(
	const std::shared_ptr<YuvStimProducer>& producer)
{
	if (!producer) {
		co_return;
	}

	if (!producer->attachedStimulusBuffers.empty()) {
		co_return;
	}

	producer->stop();

	managedStimulusProducers.erase(
		std::remove_if(
			managedStimulusProducers.begin(),
			managedStimulusProducers.end(),
			[&producer](const std::shared_ptr<YuvStimProducer>& candidate)
			{
				return candidate == producer;
			}),
		managedStimulusProducers.end());

	const std::shared_ptr<lcamera_dev::CameraSession> deviceSession =
		producer->deviceSession;

	if (deviceSession) {
		co_await (*lcameraDevDll.lcameraDev_releaseDeviceCReq)(deviceSession);
	}

	producer->deviceSession.reset();
}

void LcameraDevDllState::DlCloser::operator()(void *handle)
{
	if (handle) {
		dlclose(handle);
	}
}

LcameraDevDllState::LcameraDevDllState()
:	dlopenHandle(nullptr, DlCloser{}),
	lcameraDev_main(nullptr),
	lcameraDev_exit(nullptr),
	lcameraDev_getOrCreateDeviceCReq(nullptr),
	lcameraDev_resolveDeviceSelectorCReq(nullptr),
	lcameraDev_releaseDeviceCReq(nullptr),
	lcameraDev_configureSessionModeCReq(nullptr)
{}

std::shared_ptr<YuvStimProducer> findStimProducerByCameraId(
	const std::string& resolvedCameraId)
{
	for (const std::shared_ptr<YuvStimProducer>& producer :
		managedStimulusProducers)
	{
		assert(producer != nullptr);
		if (producer->resolvedCameraId == resolvedCameraId) {
			return producer;
		}
	}

	return nullptr;
}

bool validateAttachRequest(
	const std::shared_ptr<device::DeviceAttachmentSpec>& spec)
{
	if (!spec)
	{
		std::cerr << __func__ << ": null DeviceAttachmentSpec\n";
		return false;
	}

	if (spec->stimBuffApi != "lcameraBuff")
	{
		std::cerr << __func__ << ": stimBuffApi must be lcameraBuff, got '"
			<< spec->stimBuffApi << "'\n";
		return false;
	}

	if (spec->provider != "lcameraDev")
	{
		std::cerr << __func__ << ": provider must be lcameraDev(), got '"
			<< spec->provider << "'\n";
		return false;
	}

	if (!YuvStimProducer::supportsQualeIfaceApi(spec->qualeIfaceApi))
	{
		std::cerr << __func__ << ": unsupported qualeIfaceApi '"
			<< spec->qualeIfaceApi << "'\n";
		return false;
	}

	return true;
}

namespace {

void stopAllManagedProducersBeforeFinalize()
{
	for (const std::shared_ptr<YuvStimProducer>& producer :
		managedStimulusProducers)
	{
		assert(producer != nullptr);
		producer->stop();
	}
}

void loadLcameraDevSymbols()
{
	lcameraDevDll.lcameraDev_main = reinterpret_cast<lcameraDev_mainFn *>(
		dlsym(lcameraDevDll.dlopenHandle.get(), "lcameraDev_main"));
	lcameraDevDll.lcameraDev_exit = reinterpret_cast<lcameraDev_exitFn *>(
		dlsym(lcameraDevDll.dlopenHandle.get(), "lcameraDev_exit"));
	lcameraDevDll.lcameraDev_getOrCreateDeviceCReq = reinterpret_cast<
		lcameraDev_getOrCreateDeviceCReqFn *>(
			dlsym(
				lcameraDevDll.dlopenHandle.get(),
				"lcameraDev_getOrCreateDeviceCReq"));
	lcameraDevDll.lcameraDev_resolveDeviceSelectorCReq = reinterpret_cast<
		lcameraDev_resolveDeviceSelectorCReqFn *>(
			dlsym(
				lcameraDevDll.dlopenHandle.get(),
				"lcameraDev_resolveDeviceSelectorCReq"));
	lcameraDevDll.lcameraDev_releaseDeviceCReq = reinterpret_cast<
		lcameraDev_releaseDeviceCReqFn *>(
			dlsym(
				lcameraDevDll.dlopenHandle.get(),
				"lcameraDev_releaseDeviceCReq"));
	lcameraDevDll.lcameraDev_configureSessionModeCReq = reinterpret_cast<
		lcameraDev_configureSessionModeCReqFn *>(
			dlsym(
				lcameraDevDll.dlopenHandle.get(),
				"lcameraDev_configureSessionModeCReq"));

	if (!lcameraDevDll.lcameraDev_main
		|| !lcameraDevDll.lcameraDev_exit
		|| !lcameraDevDll.lcameraDev_getOrCreateDeviceCReq
		|| !lcameraDevDll.lcameraDev_resolveDeviceSelectorCReq
		|| !lcameraDevDll.lcameraDev_releaseDeviceCReq
		|| !lcameraDevDll.lcameraDev_configureSessionModeCReq)
	{
		throw std::runtime_error(
			std::string(__func__)
			+ ": failed to resolve lcameraDev library symbols");
	}
}

sscl::co::ViralNonPostingInvoker<StimBuffDeviceOpResult>
attachChannelBufferToProducer(
	const std::shared_ptr<device::DeviceAttachmentSpec>& desc,
	const std::shared_ptr<YuvStimProducer>& producer)
{
	producer->getOrCreateAttachedStimulusBuffer(desc);
	co_return StimBuffDeviceOpResult{true, desc};
}

sscl::co::ViralNonPostingInvoker<StimBuffDeviceOpResult>
attachToExistingProducer(
	const std::shared_ptr<device::DeviceAttachmentSpec>& desc,
	const std::shared_ptr<YuvStimProducer>& producer)
{
	const LcameraBuffParsedParams parsedParams = parseLcameraBuffParams(*desc);
	const lcamera_dev::LcameraDevCameraModeRequest modeRequest =
		toCameraModeRequest(parsedParams);

	co_await (*lcameraDevDll.lcameraDev_configureSessionModeCReq)(
		producer->deviceSession,
		modeRequest);

	co_return co_await attachChannelBufferToProducer(desc, producer);
}

sscl::co::ViralNonPostingInvoker<StimBuffDeviceOpResult>
attachByCreatingProducer(
	const std::shared_ptr<device::DeviceAttachmentSpec>& desc,
	const std::shared_ptr<sscl::ComponentThread>& componentThread,
	const lcamera_dev::LcameraDevGetOrCreateResult& createResult)
{
	const LcameraBuffParsedParams parsedParams = parseLcameraBuffParams(*desc);
	const lcamera_dev::LcameraDevCameraModeRequest modeRequest =
		toCameraModeRequest(parsedParams);

	const lcamera_dev::LcameraDevConfiguredCameraMode configuredMode =
		co_await (*lcameraDevDll.lcameraDev_configureSessionModeCReq)(
			createResult.deviceSession,
			modeRequest);

	auto producer = std::make_shared<YuvStimProducer>(
		desc,
		componentThread->getIoContext(),
		createResult.deviceSession,
		createResult.resolvedIdentity,
		parsedParams,
		configuredMode);

	addManagedStimulusProducer(producer);

	co_return co_await attachChannelBufferToProducer(desc, producer);
}

} // namespace

sscl::co::ViralNonPostingInvoker<int> lcameraBuff_initializeCInd()
{
	if (!lcameraBuffSmoHooksPtr)
	{
		throw std::runtime_error(
			std::string(__func__) + ": SMO hooks not initialized");
	}

	const std::optional<std::string> libPath =
		lcameraBuffSmoHooksPtr->searchForLibInSmoSearchPaths(
			"liblcameraDev.so.0");

	lcameraDevDll.dlopenHandle.reset(
		dlopen(
			libPath.value_or("liblcameraDev.so.0").c_str(),
			RTLD_LAZY));

	if (!lcameraDevDll.dlopenHandle)
	{
		throw std::runtime_error(
			std::string(__func__) + ": failed to load lcameraDev library: "
			+ (dlerror() ? dlerror() : "unknown error"));
	}

	loadLcameraDevSymbols();

	(*lcameraDevDll.lcameraDev_main)(
		lcameraBuffThreadingModelDesc.componentThread);

	co_return 0;
}

sscl::co::ViralNonPostingInvoker<int> lcameraBuff_finalizeCInd()
{
	stopAllManagedProducersBeforeFinalize();
	managedStimulusProducers.clear();

	if (lcameraDevDll.lcameraDev_exit) {
		(*lcameraDevDll.lcameraDev_exit)();
	}

	lcameraDevDll.dlopenHandle.reset(nullptr);
	lcameraDevDll = LcameraDevDllState();
	co_return 0;
}

sscl::co::DynamicViralPostingInvoker<StimBuffDeviceOpResult>
lcameraBuff_attachDeviceCReq(
	sscl::co::ExplicitPostTarget,
	const std::shared_ptr<device::DeviceAttachmentSpec>& desc,
	const std::shared_ptr<sscl::ComponentThread>& componentThread)
{
	if (!validateAttachRequest(desc)) {
		co_return StimBuffDeviceOpResult{false, desc};
	}

	const lcamera_dev::LcameraDevGetOrCreateResult createResult =
		co_await (*lcameraDevDll.lcameraDev_getOrCreateDeviceCReq)(
			desc->deviceSelector);

	const std::string resolvedCameraId = createResult.resolvedIdentity.id;

	auto existingProducer = findStimProducerByCameraId(resolvedCameraId);
	if (existingProducer) {
		co_return co_await attachToExistingProducer(desc, existingProducer);
	}

	co_return co_await attachByCreatingProducer(
		desc, componentThread, createResult);
}

sscl::co::DynamicViralPostingInvoker<StimBuffDeviceOpResult>
lcameraBuff_detachDeviceCReq(
	[[maybe_unused]] sscl::co::ExplicitPostTarget postTarget,
	const std::shared_ptr<device::DeviceAttachmentSpec>& desc)
{
	(void)postTarget;

	if (!validateAttachRequest(desc)) {
		co_return StimBuffDeviceOpResult{false, desc};
	}

	const lcamera_dev::CameraIdentityRecord resolvedIdentity =
		co_await (*lcameraDevDll.lcameraDev_resolveDeviceSelectorCReq)(
			desc->deviceSelector);

	auto producer = findStimProducerByCameraId(resolvedIdentity.id);
	if (!producer) {
		co_return StimBuffDeviceOpResult{true, desc};
	}

	auto stimBuffer = producer->getAttachedStimulusBufferByAttachIdentity(
		desc->deviceIdentifier, desc->qualeIfaceApi);

	if (!stimBuffer) {
		co_return StimBuffDeviceOpResult{true, desc};
	}

	producer->destroyAttachedStimulusBuffer(stimBuffer);

	if (!producer->attachedStimulusBuffers.empty()) {
		co_return StimBuffDeviceOpResult{true, desc};
	}

	co_await removeManagedStimulusProducerIfUnused(producer);

	co_return StimBuffDeviceOpResult{true, desc};
}

} // namespace smo::stim_buff::lcamera_buff

extern "C" smo::stim_buff::SMO_GET_STIM_BUFF_API_DESC_FN_TYPEDEF
	SMO_GET_STIM_BUFF_API_DESC_FN_NAME;

namespace {

static const smo::stim_buff::StimBuffApiDesc lcameraBuffApiDesc = {
	.name = "lcameraBuff",
	.exportedQualeIfaceApis =
	{
		{.name = "colour-yuv-y"},
		{.name = "colour-yuv-u"},
		{.name = "colour-yuv-v"},
	},
	.sal_mgmt_libOps =
	{
		.initializeCInd =
			smo::stim_buff::lcamera_buff::lcameraBuff_initializeCInd,
		.finalizeCInd =
			smo::stim_buff::lcamera_buff::lcameraBuff_finalizeCInd,
		.attachDeviceCReq =
			smo::stim_buff::lcamera_buff::lcameraBuff_attachDeviceCReq,
		.detachDeviceCReq =
			smo::stim_buff::lcamera_buff::lcameraBuff_detachDeviceCReq,
	},
};

} // namespace

const smo::stim_buff::StimBuffApiDesc& SMO_GET_STIM_BUFF_API_DESC_FN_NAME(
	const smo::stim_buff::SmoCallbacks& callbacks,
	const smo::stim_buff::SmoThreadingModelDesc& threadingModel)
{
	smo::stim_buff::lcamera_buff::lcameraBuffSmoHooksPtr = &callbacks;
	smo::stim_buff::lcamera_buff::lcameraBuffThreadingModelDesc = threadingModel;
	return lcameraBuffApiDesc;
}
