#include <lcameraBuffParams.h>
#include <sstream>
#include <stdexcept>
#include <unordered_map>
#include <vector>

namespace smo {
namespace stim_buff {
namespace lcamera_buff {

namespace {

constexpr const char *mutuallyExclusiveResolutionMessage =
	"lcameraBuff: explicit dimension width/height params cannot be combined "
	"with vertical-resolution shorthand params";

constexpr const char *missingResolutionMessage =
	"lcameraBuff: specify either explicit dimension width and height, or a "
	"vertical-resolution shorthand (v-res, vres, etc.)";

constexpr const char *incompleteExplicitResolutionMessage =
	"lcameraBuff: both dimension width and height are required when using "
	"explicit dimension params";

const std::vector<std::string> widthParamSynonyms = {
	"frame-width",
	"frame-w",
	"dimension-width",
	"dimension-w",
	"dim-width",
	"dim-w",
};

const std::vector<std::string> heightParamSynonyms = {
	"frame-height",
	"frame-h",
	"dimension-height",
	"dimension-h",
	"dim-height",
	"dim-h",
};

const std::vector<std::string> verticalResolutionParamSynonyms = {
	"vertical-resolution",
	"v-resolution",
	"v-res",
	"vres",
};

const std::vector<std::string> colourSpaceParamSynonyms = {
	"colorspace",
	"color-space",
	"colourspace",
	"colour-space",
};

const std::vector<std::string> optPlanarParamSynonyms = {
	"full-planar-is-optional",
	"opt-planar",
};

std::string normalizeVerticalResolutionToken(const std::string& token)
{
	std::string normalizedToken =
		device::DeviceAttachmentSpec::normalizeParamToken(token);

	if (normalizedToken.empty())
	{
		throw std::runtime_error(
			"lcameraBuff: vertical-resolution value is empty");
	}

	if (normalizedToken.back() == 'p')
	{
		normalizedToken.pop_back();
	}

	if (normalizedToken.empty())
	{
		throw std::runtime_error(
			"lcameraBuff: vertical-resolution value is empty after "
			"normalization");
	}

	return normalizedToken;
}

struct VerticalResolutionPreset
{
	unsigned width;
	unsigned height;
};

const std::unordered_map<std::string, VerticalResolutionPreset>&
verticalResolutionPresetTable()
{
	static const std::unordered_map<std::string, VerticalResolutionPreset>
		presets = {
			{"360", {640, 360}},
			{"480", {640, 480}},
			{"720", {1280, 720}},
			{"1080", {1920, 1080}},
		};

	return presets;
}

VerticalResolutionPreset resolveVerticalResolutionPreset(
	const std::string& token)
{
	const std::string normalizedToken =
		normalizeVerticalResolutionToken(token);

	const std::unordered_map<std::string, VerticalResolutionPreset>& presets =
		verticalResolutionPresetTable();

	const auto presetIt = presets.find(normalizedToken);
	if (presetIt == presets.end())
	{
		std::ostringstream supportedStream;
		bool first = true;
		for (const auto& entry : presets)
		{
			if (!first) {
				supportedStream << ", ";
			}

			supportedStream << entry.first << "p";
			first = false;
		}

		throw std::runtime_error(
			"lcameraBuff: unsupported vertical-resolution '"
			+ token + "'; supported values: "
			+ supportedStream.str());
	}

	return presetIt->second;
}

lcamera_dev::LcameraDevColourSpace parseColourSpaceValue(
	const std::string& value)
{
	const std::string lowered =
		device::DeviceAttachmentSpec::normalizeParamToken(value);
	if (lowered == "yuv") {
		return lcamera_dev::LcameraDevColourSpace::Yuv;
	}

	throw std::runtime_error(
		"lcameraBuff: unsupported colour-space '" + value
		+ "'; supported values: yuv");
}

void applyExplicitDimensions(
	const std::vector<std::pair<std::string, std::string>>& params,
	LcameraBuffParsedParams& parsedParams)
{
	const std::optional<std::pair<std::string, std::string>> widthParam =
		device::DeviceAttachmentSpec::findOptionalParamWithSynonyms(
			params, widthParamSynonyms);
	const std::optional<std::pair<std::string, std::string>> heightParam =
		device::DeviceAttachmentSpec::findOptionalParamWithSynonyms(
			params, heightParamSynonyms);

	if (!widthParam && !heightParam) { return; }

	const std::optional<std::pair<std::string, std::string>>
		verticalResolutionParam =
			device::DeviceAttachmentSpec::findOptionalParamWithSynonyms(
				params, verticalResolutionParamSynonyms);

	if (verticalResolutionParam) {
		throw std::runtime_error(mutuallyExclusiveResolutionMessage);
	}

	if (!widthParam || !heightParam) {
		throw std::runtime_error(incompleteExplicitResolutionMessage);
	}

	parsedParams.width = static_cast<unsigned>(
		device::DeviceAttachmentSpec::parseParamValueAsPositiveInt(
			widthParam->first, widthParam->second));
	parsedParams.height = static_cast<unsigned>(
		device::DeviceAttachmentSpec::parseParamValueAsPositiveInt(
			heightParam->first, heightParam->second));
}

void applyVerticalResolutionShorthand(
	const std::vector<std::pair<std::string, std::string>>& params,
	LcameraBuffParsedParams& parsedParams)
{
	const std::optional<std::string> verticalResolutionValue =
		device::DeviceAttachmentSpec::parseOptionalParamValueWithSynonyms(
			params,
			verticalResolutionParamSynonyms,
			"lcameraBuff: vertical-resolution param requires a value "
			"(e.g. v-res=480p)");

	if (!verticalResolutionValue) { return; }

	const VerticalResolutionPreset preset =
		resolveVerticalResolutionPreset(*verticalResolutionValue);
	parsedParams.width = preset.width;
	parsedParams.height = preset.height;
}

void applyColourSpaceParam(
	const std::vector<std::pair<std::string, std::string>>& params,
	LcameraBuffParsedParams& parsedParams)
{
	const std::optional<std::string> colourSpaceValue =
		device::DeviceAttachmentSpec::parseOptionalParamValueWithSynonyms(
			params,
			colourSpaceParamSynonyms,
			"lcameraBuff: colour-space param requires a value "
			"(e.g. colour-space=yuv)");

	if (!colourSpaceValue) { return; }

	parsedParams.colourSpace = parseColourSpaceValue(*colourSpaceValue);
}

void finalizeCheckResolutionValues(const LcameraBuffParsedParams& parsedParams)
{
	if (parsedParams.width == 0 || parsedParams.height == 0)
	{
		throw std::runtime_error(missingResolutionMessage);
	}
}

} // namespace

LcameraBuffParsedParams parseLcameraBuffParams(
	const device::DeviceAttachmentSpec& spec)
{
	const auto& params = spec.stimBuffApiParams;

	device::DeviceAttachmentSpec::rejectUnknownParams(
		params,
		"lcameraBuff: unknown stimbuff-api param '",
		widthParamSynonyms,
		heightParamSynonyms,
		verticalResolutionParamSynonyms,
		colourSpaceParamSynonyms,
		optPlanarParamSynonyms);

	LcameraBuffParsedParams parsedParams;

	applyExplicitDimensions(params, parsedParams);
	applyVerticalResolutionShorthand(params, parsedParams);
	applyColourSpaceParam(params, parsedParams);

	/* Presence flag: opt-planar or opt-planar= with no value means true. */
	parsedParams.fullPlanarIsOptional =
		device::DeviceAttachmentSpec::parseOptionalParamAsBoolWithSynonyms(
			params,
			optPlanarParamSynonyms,
			/*defaultValue=*/false,
			/*emptyMeansTrue=*/true);

	finalizeCheckResolutionValues(parsedParams);

	return parsedParams;
}

lcamera_dev::LcameraDevCameraModeRequest toCameraModeRequest(
	const LcameraBuffParsedParams& parsedParams)
{
	lcamera_dev::LcameraDevCameraModeRequest request;
	request.width = parsedParams.width;
	request.height = parsedParams.height;
	request.colourSpace = parsedParams.colourSpace;
	request.fullPlanarIsOptional = parsedParams.fullPlanarIsOptional;
	return request;
}

bool lcameraBuffParamsEqual(
	const LcameraBuffParsedParams& left,
	const LcameraBuffParsedParams& right)
{
	return left.width == right.width
		&& left.height == right.height
		&& left.colourSpace == right.colourSpace
		&& left.fullPlanarIsOptional == right.fullPlanarIsOptional;
}

} // namespace lcamera_buff
} // namespace stim_buff
} // namespace smo
