#include <boostAsioLinkageFix.h>

#include <cameraIdentity.h>
#include <cameraManagerState.h>
#include <selectorResolve.h>
#include <algorithm>
#include <stdexcept>

namespace lcamera_dev {

namespace {

static LcameraDevState lcameraDevState;

void startCameraManager(CameraManagerResources& resources)
{
	resources.cameraManager = std::make_unique<libcamera::CameraManager>();

	if (resources.cameraManager->start())
	{
		resources.cameraManager.reset();
		throw std::runtime_error(
			"lcameraDev: failed to start libcamera CameraManager");
	}
}

void stopCameraManager(CameraManagerResources& resources)
{
	if (!resources.cameraManager) {
		return;
	}

	resources.cameraManager->stop();
	resources.cameraManager.reset();
}

std::shared_ptr<libcamera::Camera> findCameraById(
	const std::vector<std::shared_ptr<libcamera::Camera>>& cameras,
	const std::string& cameraId)
{
	for (const std::shared_ptr<libcamera::Camera>& camera : cameras)
	{
		if (camera && camera->id() == cameraId) {
			return camera;
		}
	}

	return nullptr;
}

struct LiveCameraSnapshot
{
	std::vector<std::shared_ptr<libcamera::Camera>> cameras;
	std::vector<CameraIdentityRecord> identityRecords;
};

LiveCameraSnapshot snapshotLiveCameras(CameraManagerResources& resources)
{
	LiveCameraSnapshot snapshot;
	snapshot.cameras = resources.cameraManager->cameras();
	snapshot.identityRecords = buildIdentityRecords(snapshot.cameras);
	return snapshot;
}

CameraIdentityRecord resolveDeviceSelectorForLiveCameras(
	const std::string& deviceSelector,
	CameraManagerResources& resources)
{
	const LiveCameraSnapshot snapshot = snapshotLiveCameras(resources);
	return resolveDeviceSelectorAgainstRecords(
		deviceSelector, snapshot.identityRecords);
}

void requireNonEmptyDeviceSelector(
	const std::string& deviceSelector,
	const char *apiName)
{
	if (deviceSelector.empty())
	{
		throw std::runtime_error(
			std::string(apiName) + ": deviceSelector is empty");
	}
}

} // namespace

LcameraDevState& getLcameraDevState()
{
	return lcameraDevState;
}

std::vector<std::shared_ptr<libcamera::Camera>> listLibcameraCameras()
{
	LcameraDevState& state = getLcameraDevState();
	if (!state.isInitialized || !state.managerState.rsrc.cameraManager) {
		return {};
	}

	return state.managerState.rsrc.cameraManager->cameras();
}

void lcameraDevMain(
	const std::shared_ptr<sscl::ComponentThread>& componentThread)
{
	LcameraDevState& state = getLcameraDevState();
	if (state.isInitialized) {
		return;
	}

	if (!componentThread)
	{
		throw std::runtime_error(
			"lcameraDev_main: componentThread must be non-null");
	}

	startCameraManager(state.managerState.rsrc);
	state.componentThread = componentThread;
	state.isInitialized = true;
}

void lcameraDevExit()
{
	LcameraDevState& state = getLcameraDevState();
	if (!state.isInitialized) {
		return;
	}

	CameraManagerResources& resources = state.managerState.rsrc;
	for (auto& entry : resources.sessionsByCameraId)
	{
		std::shared_ptr<CameraSession> session = entry.second;
		if (!session || !session->s.rsrc.camera) {
			continue;
		}

		session->s.rsrc.camera->release();
	}

	resources.sessionsByCameraId.clear();
	stopCameraManager(resources);

	state.componentThread.reset();
	state.isInitialized = false;
}

sscl::co::ViralNonPostingInvoker<CameraIdentityRecord>
resolveDeviceSelectorCReq(const std::string& deviceSelector)
{
	requireNonEmptyDeviceSelector(
		deviceSelector, "lcameraDev_resolveDeviceSelectorCReq");

	LcameraDevState& state = getLcameraDevState();

	sscl::co::CoQutex::ReleaseHandle managerGuard =
		co_await state.managerState.lock.getAcquireInvocationAndSuspensionPolicy();

	co_return resolveDeviceSelectorForLiveCameras(
		deviceSelector, state.managerState.rsrc);
}

sscl::co::ViralNonPostingInvoker<LcameraDevGetOrCreateResult>
getOrCreateDeviceSessionCReq(const std::string& deviceSelector)
{
	requireNonEmptyDeviceSelector(
		deviceSelector, "lcameraDev_getOrCreateDeviceCReq");

	LcameraDevState& state = getLcameraDevState();

	sscl::co::CoQutex::ReleaseHandle managerGuard =
		co_await state.managerState.lock.getAcquireInvocationAndSuspensionPolicy();

	const LiveCameraSnapshot snapshot =
		snapshotLiveCameras(state.managerState.rsrc);

	const CameraIdentityRecord resolvedRecord =
		resolveDeviceSelectorAgainstRecords(
			deviceSelector, snapshot.identityRecords);
	const std::string& resolvedCameraId = resolvedRecord.id;

	auto sessionIt =
		state.managerState.rsrc.sessionsByCameraId.find(resolvedCameraId);

	if (sessionIt != state.managerState.rsrc.sessionsByCameraId.end())
	{
		std::shared_ptr<CameraSession> session = sessionIt->second;
		sscl::co::CoQutex::ReleaseHandle sessionGuard =
			co_await session->s.lock.getAcquireInvocationAndSuspensionPolicy();

		session->incrementRefcount();

		LcameraDevGetOrCreateResult result;
		result.deviceSession = session;
		result.resolvedIdentity = session->getIdentityRecord();
		co_return result;
	}

	std::shared_ptr<libcamera::Camera> camera =
		findCameraById(snapshot.cameras, resolvedCameraId);
	if (!camera)
	{
		throw std::runtime_error(
			"lcameraDev: resolved camera is no longer available: "
			+ resolvedCameraId);
	}

	if (camera->acquire())
	{
		throw std::runtime_error(
			"lcameraDev: failed to acquire camera: " + resolvedCameraId);
	}

	std::shared_ptr<CameraSession> session =
		std::make_shared<CameraSession>(resolvedRecord, camera);

	session->incrementRefcount();
	state.managerState.rsrc.sessionsByCameraId.emplace(
		resolvedCameraId, session);

	LcameraDevGetOrCreateResult result;
	result.deviceSession = session;
	result.resolvedIdentity = session->getIdentityRecord();
	co_return result;
}

sscl::co::ViralNonPostingInvoker<void>
releaseDeviceSessionCReq(
	const std::shared_ptr<CameraSession>& deviceSession)
{
	if (!deviceSession) { co_return; }

	LcameraDevState& state = getLcameraDevState();
	sscl::co::CoQutex::ReleaseHandle managerGuard =
		co_await state.managerState.lock.getAcquireInvocationAndSuspensionPolicy();

	const auto sessionIt = std::find_if(
		state.managerState.rsrc.sessionsByCameraId.begin(),
		state.managerState.rsrc.sessionsByCameraId.end(),
		[&deviceSession](const auto& entry) {
			return entry.second == deviceSession;
		});

	if (sessionIt == state.managerState.rsrc.sessionsByCameraId.end()) {
		co_return;
	}

	bool shouldDestroy = false;
	{
		sscl::co::CoQutex::ReleaseHandle sessionGuard =
			co_await deviceSession->s.lock.getAcquireInvocationAndSuspensionPolicy();

		shouldDestroy = deviceSession->decrementRefcount();
	}

	if (!shouldDestroy) {
		co_return;
	}

	if (deviceSession->s.rsrc.camera) {
		deviceSession->s.rsrc.camera->release();
	}

	state.managerState.rsrc.sessionsByCameraId.erase(sessionIt);
	co_return;
}

sscl::co::ViralNonPostingInvoker<std::vector<LcameraDevCameraInfo>>
enumerateCamerasCReq()
{
	LcameraDevState& state = getLcameraDevState();
	sscl::co::CoQutex::ReleaseHandle managerGuard =
		co_await state.managerState.lock.getAcquireInvocationAndSuspensionPolicy();

	const LiveCameraSnapshot snapshot =
		snapshotLiveCameras(state.managerState.rsrc);

	std::vector<LcameraDevCameraInfo> cameraInfos;
	cameraInfos.reserve(snapshot.identityRecords.size());

	for (const CameraIdentityRecord& record : snapshot.identityRecords)
	{
		cameraInfos.push_back(LcameraDevCameraInfo{
			record.id,
			record.model,
			record.locationLabel
		});
	}

	co_return cameraInfos;
}

} // namespace lcamera_dev
