#include <boostAsioLinkageFix.h>

#include <cameraSession.h>
#include <lcameraDev.h>
#include <probeRunner.h>
#include <iostream>
#include <spinscale/co/nonViralTaskNursery.h>
#include <stdexcept>
#include <string>

namespace {

constexpr const char *optPlanarFlag = "--opt-planar";
constexpr const char *fullPlanarOptionalFlag = "--full-planar-is-optional";
constexpr const char *reconfigureTwiceFlag = "--reconfigure-twice";
constexpr const char *colourSpacePrefix = "--colour-space=";
constexpr const char *colorSpacePrefix = "--color-space=";

struct ConfigureProbeArgs
{
	std::string deviceSelector;
	unsigned width = 0;
	unsigned height = 0;
	lcamera_dev::LcameraDevColourSpace colourSpace =
		lcamera_dev::LcameraDevColourSpace::Yuv;
	bool fullPlanarIsOptional = false;
	bool reconfigureTwice = false;
};

void printUsage(std::ostream& stream)
{
	stream <<
		"Usage: lcameraDev_configure_probe <deviceSelector> <width> <height> "
		"[options]\n"
		"Options:\n"
		"  --colour-space=yuv       Semantic colour-space (only yuv today)\n"
		"  --opt-planar             Set fullPlanarIsOptional=true on request\n"
		"  --full-planar-is-optional  Same as --opt-planar\n"
		"  --reconfigure-twice      Configure twice with identical request "
		"(no-op check)\n"
		"Examples:\n"
		"  lcameraDev_configure_probe model-substr:Integrated 640 480\n"
		"  lcameraDev_configure_probe model-substr:HDMI 1280 720 --opt-planar\n"
		"  lcameraDev_configure_probe index:0 640 480 --reconfigure-twice\n";
}

std::string colourSpaceToString(lcamera_dev::LcameraDevColourSpace colourSpace)
{
	switch (colourSpace)
	{
	case lcamera_dev::LcameraDevColourSpace::Yuv:
		return "yuv";
	default:
		return "unknown";
	}
}

bool parseColourSpaceValue(const std::string& value)
{
	if (value == "yuv" || value == "YUV") {
		return true;
	}

	throw std::runtime_error(
		"unsupported colour-space \"" + value + "\" (only yuv is supported)");
}

unsigned parseDimensionArg(const char *arg, const char *label)
{
	try {
		const unsigned long parsed = std::stoul(arg);
		if (parsed == 0) {
			throw std::runtime_error(
				std::string(label) + " must be non-zero");
		}

		return static_cast<unsigned>(parsed);
	}
	catch (const std::exception&)
	{
		throw std::runtime_error(
			std::string("invalid ") + label + " \"" + arg + "\"");
	}
}

ConfigureProbeArgs parseConfigureProbeArgs(int argc, char *argv[])
{
	if (argc < 4) {
		throw std::runtime_error("missing required arguments");
	}

	ConfigureProbeArgs args;
	args.deviceSelector = argv[1];
	args.width = parseDimensionArg(argv[2], "width");
	args.height = parseDimensionArg(argv[3], "height");

	for (int i = 4; i < argc; ++i)
	{
		const std::string token = argv[i];

		if (token == optPlanarFlag || token == fullPlanarOptionalFlag) {
			args.fullPlanarIsOptional = true;
			continue;
		}

		if (token == reconfigureTwiceFlag) {
			args.reconfigureTwice = true;
			continue;
		}

		const std::string colourSpacePrefixStr = colourSpacePrefix;
		const std::string colorSpacePrefixStr = colorSpacePrefix;

		if (token.rfind(colourSpacePrefixStr, 0) == 0) {
			parseColourSpaceValue(token.substr(colourSpacePrefixStr.size()));
			continue;
		}

		if (token.rfind(colorSpacePrefixStr, 0) == 0) {
			parseColourSpaceValue(token.substr(colorSpacePrefixStr.size()));
			continue;
		}

		throw std::runtime_error("unknown option \"" + token + "\"");
	}

	return args;
}

void printRequest(const lcamera_dev::LcameraDevCameraModeRequest& request)
{
	std::cout << "request:"
		<< " width=" << request.width
		<< " height=" << request.height
		<< " colour-space=" << colourSpaceToString(request.colourSpace)
		<< " fullPlanarIsOptional="
		<< (request.fullPlanarIsOptional ? "true" : "false")
		<< '\n';
}

void printConfiguredMode(
	const lcamera_dev::LcameraDevConfiguredCameraMode& configuredMode)
{
	std::cout << "configured:"
		<< " width=" << configuredMode.width
		<< " height=" << configuredMode.height
		<< " colour-space=" << colourSpaceToString(configuredMode.colourSpace)
		<< " pixelFormat=" << configuredMode.pixelFormatName
		<< " isFullyPlanar="
		<< (configuredMode.isFullyPlanar ? "true" : "false")
		<< " planeCount=" << configuredMode.planeCount
		<< '\n';
}

void runNurseryRethrowing(
	const std::shared_ptr<sscl::ComponentThread>& componentThread,
	const std::function<sscl::co::NonViralNonPostingInvoker(
		sscl::co::NonViralTaskNursery::Slot::Lease&)>& invokerFactory)
{
	std::exception_ptr slotException;

	sscl::co::NonViralTaskNursery nursery;
	nursery.openAdmission();
	nursery.launch(
		invokerFactory,
		[&slotException](std::exception_ptr& exceptionPtr)
		{
			slotException = exceptionPtr;
		});
	nursery.closeAdmission();
	nursery.syncAwaitAllSettlements(componentThread->getIoContext());

	if (slotException) {
		std::rethrow_exception(slotException);
	}
}

sscl::co::NonViralNonPostingInvoker getOrCreateCInd(
	std::exception_ptr& exceptionStorage,
	std::function<void()> callerLambda,
	const std::string& deviceSelector,
	lcamera_dev::LcameraDevGetOrCreateResult& createResult)
{
	(void)callerLambda;

	createResult = co_await lcameraDev_getOrCreateDeviceCReq(deviceSelector);

	if (exceptionStorage) {
		std::rethrow_exception(exceptionStorage);
	}

	co_return;
}

sscl::co::NonViralNonPostingInvoker configureProbeCInd(
	std::exception_ptr& exceptionStorage,
	std::function<void()> callerLambda,
	const std::shared_ptr<lcamera_dev::CameraSession>& deviceSession,
	const lcamera_dev::LcameraDevCameraModeRequest& request,
	lcamera_dev::LcameraDevConfiguredCameraMode& configuredMode)
{
	(void)callerLambda;

	configuredMode =
		co_await lcameraDev_configureSessionModeCReq(deviceSession, request);

	if (exceptionStorage) {
		std::rethrow_exception(exceptionStorage);
	}

	co_return;
}

sscl::co::NonViralNonPostingInvoker releaseCInd(
	std::exception_ptr& exceptionStorage,
	std::function<void()> callerLambda,
	const std::shared_ptr<lcamera_dev::CameraSession>& deviceSession)
{
	(void)exceptionStorage;
	(void)callerLambda;

	co_await lcameraDev_releaseDeviceCReq(deviceSession);
	co_return;
}

void releaseSessionAndExit(
	const std::shared_ptr<sscl::ComponentThread>& componentThread,
	std::shared_ptr<lcamera_dev::CameraSession>& deviceSession)
{
	if (!deviceSession) {
		lcameraDev_exit();
		return;
	}

	runNurseryRethrowing(
		componentThread,
		[&deviceSession](sscl::co::NonViralTaskNursery::Slot::Lease& lease)
		{
			return releaseCInd(
				lease.getExceptionStorage(),
				lease.getCallerLambda(),
				deviceSession);
		});

	std::cout << "lcameraDev_configure_probe: released session\n";

	/** Drop the session before stopping CameraManager so libcamera does not
	 * tear down media devices while a Camera handle is still alive. */
	deviceSession.reset();

	lcameraDev_exit();
}

int runConfigureProbe(
	const std::shared_ptr<sscl::ComponentThread>& componentThread,
	const ConfigureProbeArgs& args)
{
	lcameraDev_main(componentThread);

	lcamera_dev::LcameraDevGetOrCreateResult createResult;

	runNurseryRethrowing(
		componentThread,
		[&args, &createResult](sscl::co::NonViralTaskNursery::Slot::Lease& lease)
		{
			return getOrCreateCInd(
				lease.getExceptionStorage(),
				lease.getCallerLambda(),
				args.deviceSelector,
				createResult);
		});

	std::cout << "lcameraDev_configure_probe: opened session for camera id="
		<< createResult.resolvedIdentity.id << '\n';

	lcamera_dev::LcameraDevCameraModeRequest request;
	request.width = args.width;
	request.height = args.height;
	request.colourSpace = args.colourSpace;
	request.fullPlanarIsOptional = args.fullPlanarIsOptional;

	printRequest(request);

	lcamera_dev::LcameraDevConfiguredCameraMode firstMode;

	try {
		runNurseryRethrowing(
			componentThread,
			[&createResult, &request, &firstMode](
				sscl::co::NonViralTaskNursery::Slot::Lease& lease)
			{
				return configureProbeCInd(
					lease.getExceptionStorage(),
					lease.getCallerLambda(),
					createResult.deviceSession,
					request,
					firstMode);
			});

		printConfiguredMode(firstMode);

		const int configureCallCountAfterFirst =
			createResult.deviceSession->getLibcameraConfigureCallCount();
		std::cout << "libcameraConfigureCallCount="
			<< configureCallCountAfterFirst << '\n';

		if (args.reconfigureTwice)
		{
			lcamera_dev::LcameraDevConfiguredCameraMode secondMode;

			runNurseryRethrowing(
				componentThread,
				[&createResult, &request, &secondMode](
					sscl::co::NonViralTaskNursery::Slot::Lease& lease)
				{
					return configureProbeCInd(
						lease.getExceptionStorage(),
						lease.getCallerLambda(),
						createResult.deviceSession,
						request,
						secondMode);
				});

			printConfiguredMode(secondMode);
			std::cout << "libcameraConfigureCallCount="
				<< createResult.deviceSession->getLibcameraConfigureCallCount()
				<< " (after identical reconfigure)\n";
		}
	}
	catch (const std::exception& configureException)
	{
		std::cerr << "lcameraDev_configure_probe: configure failed: "
			<< configureException.what() << '\n';

		releaseSessionAndExit(
			componentThread,
			createResult.deviceSession);
		return 1;
	}

	releaseSessionAndExit(
		componentThread,
		createResult.deviceSession);
	return 0;
}

} // namespace

int main(int argc, char *argv[])
{
	try {
		const ConfigureProbeArgs args = parseConfigureProbeArgs(argc, argv);

		int exitCode = 0;

		lcamera_dev_probe::runOnComponentThread(
			[&args, &exitCode](
				const std::shared_ptr<sscl::ComponentThread>& componentThread)
			{
				exitCode = runConfigureProbe(componentThread, args);
			});

		return exitCode;
	}
	catch (const std::runtime_error& exception)
	{
		std::cerr << "lcameraDev_configure_probe: " << exception.what() << '\n';
		printUsage(std::cerr);
		return 1;
	}
	catch (const std::exception& exception)
	{
		std::cerr << "lcameraDev_configure_probe: " << exception.what() << '\n';
		return 1;
	}
}
