/**
 * FreeRDP: A Remote Desktop Protocol Implementation
 * MS-RDPECAM Implementation, V4L Interface
 *
 * Copyright 2024 Oleg Turovski <oleg2104@hotmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <errno.h>
#include <fcntl.h>
#include <poll.h>
#include <sys/ioctl.h>
#include <sys/mman.h>

/* v4l includes */
#include <linux/videodev2.h>

#include "camera_v4l.h"
#include "uvc_h264.h"

#define TAG CHANNELS_TAG("rdpecam-v4l.client")

#define CAM_V4L2_BUFFERS_COUNT 4
#define CAM_V4L2_CAPTURE_THREAD_SLEEP_MS 1000

#define CAM_V4L2_FRAMERATE_NUMERATOR_DEFAULT 30
#define CAM_V4L2_FRAMERATE_DENOMINATOR_DEFAULT 1

typedef struct
{
	ICamHal iHal;

	wHashTable* streams; /* Index: deviceId, Value: CamV4lStream */

} CamV4lHal;

static CamV4lStream* cam_v4l_stream_create(const char* deviceId, size_t streamIndex);
static void cam_v4l_stream_free(void* obj);
static void cam_v4l_stream_close_device(CamV4lStream* stream);
static CAM_ERROR_CODE cam_v4l_stream_stop(CamV4lStream* stream);

/**
 * Function description
 *
 * @return \0-terminated fourcc string
 */
static const char* cam_v4l_get_fourcc_str(unsigned int fourcc, char* buffer, size_t size)
{
	if (size < 5)
		return nullptr;

	buffer[0] = (char)(fourcc & 0xFF);
	buffer[1] = (char)((fourcc >> 8) & 0xFF);
	buffer[2] = (char)((fourcc >> 16) & 0xFF);
	buffer[3] = (char)((fourcc >> 24) & 0xFF);
	buffer[4] = '\0';
	return buffer;
}

/**
 * Function description
 *
 * @return one of V4L2_PIX_FMT
 */
static UINT32 ecamToV4L2PixFormat(CAM_MEDIA_FORMAT ecamFormat)
{
	switch (ecamFormat)
	{
		case CAM_MEDIA_FORMAT_H264:
			return V4L2_PIX_FMT_H264;
		case CAM_MEDIA_FORMAT_MJPG:
			return V4L2_PIX_FMT_MJPEG;
		case CAM_MEDIA_FORMAT_YUY2:
			return V4L2_PIX_FMT_YUYV;
		case CAM_MEDIA_FORMAT_NV12:
			return V4L2_PIX_FMT_NV12;
		case CAM_MEDIA_FORMAT_I420:
			return V4L2_PIX_FMT_YUV420;
		case CAM_MEDIA_FORMAT_RGB24:
			return V4L2_PIX_FMT_RGB24;
		case CAM_MEDIA_FORMAT_RGB32:
			return V4L2_PIX_FMT_RGB32;
		default:
			WLog_ERR(TAG, "Unsupported CAM_MEDIA_FORMAT %u", ecamFormat);
			return 0;
	}
}

/**
 * Function description
 *
 * @return TRUE or FALSE
 */
static BOOL cam_v4l_format_supported(int fd, UINT32 format)
{
	struct v4l2_fmtdesc fmtdesc = WINPR_C_ARRAY_INIT;
	fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;

	for (fmtdesc.index = 0; ioctl(fd, VIDIOC_ENUM_FMT, &fmtdesc) == 0; fmtdesc.index++)
	{
		if (fmtdesc.pixelformat == format)
			return TRUE;
	}
	return FALSE;
}

/**
 * Function description
 *
 * @return file descriptor
 */
static int cam_v4l_open_device(const char* deviceId, int flags)
{
	char device[20] = WINPR_C_ARRAY_INIT;
	int fd = -1;
	struct v4l2_capability cap = WINPR_C_ARRAY_INIT;

	if (!deviceId)
		return -1;

	if (0 == strncmp(deviceId, "/dev/video", 10))
		return open(deviceId, flags);

	for (UINT n = 0; n < 64; n++)
	{
		(void)_snprintf(device, sizeof(device), "/dev/video%" PRIu32, n);
		if ((fd = open(device, flags)) == -1)
			continue;

		/* query device capabilities and make sure this is a video capture device */
		if (ioctl(fd, VIDIOC_QUERYCAP, &cap) < 0 || !(cap.device_caps & V4L2_CAP_VIDEO_CAPTURE))
		{
			close(fd);
			continue;
		}

		if (cap.bus_info[0] != 0 && 0 == strcmp((const char*)cap.bus_info, deviceId))
			return fd;

		close(fd);
	}

	return fd;
}

static BOOL cam_v4l_activate(ICamHal* ihal, const char* deviceId, CAM_ERROR_CODE* errorCode)
{
	WINPR_UNUSED(ihal);
	WINPR_UNUSED(deviceId);

	*errorCode = CAM_ERROR_CODE_None;
	return TRUE;
}

static BOOL cam_v4l_deactivate(ICamHal* ihal, const char* deviceId, CAM_ERROR_CODE* errorCode)
{
	WINPR_UNUSED(ihal);
	WINPR_UNUSED(deviceId);

	*errorCode = CAM_ERROR_CODE_None;
	return TRUE;
}

/**
 * Function description
 *
 * @return -1 if error, otherwise index of supportedFormats array and mediaTypes/nMediaTypes filled
 * in
 */
static INT16 cam_v4l_get_media_type_descriptions(ICamHal* ihal, const char* deviceId,
                                                 size_t streamIndex,
                                                 const CAM_MEDIA_FORMAT_INFO* supportedFormats,
                                                 size_t nSupportedFormats,
                                                 CAM_MEDIA_TYPE_DESCRIPTION* mediaTypes,
                                                 size_t* nMediaTypes)
{
	CamV4lHal* hal = (CamV4lHal*)ihal;
	size_t maxMediaTypes = *nMediaTypes;
	size_t nTypes = 0;
	BOOL formatFound = FALSE;

	CamV4lStream* stream = (CamV4lStream*)HashTable_GetItemValue(hal->streams, deviceId);

	if (!stream)
	{
		stream = cam_v4l_stream_create(deviceId, streamIndex);
		if (!stream)
			return CAM_ERROR_CODE_OutOfMemory;

		if (!HashTable_Insert(hal->streams, deviceId, stream))
		{
			cam_v4l_stream_free(stream);
			return CAM_ERROR_CODE_UnexpectedError;
		}
	}

	int fd = cam_v4l_open_device(deviceId, O_RDONLY);
	if (fd == -1)
	{
		WLog_ERR(TAG, "Unable to open device %s", deviceId);
		return -1;
	}

	size_t formatIndex = 0;
	for (; formatIndex < nSupportedFormats; formatIndex++)
	{
		const UINT32 pixelFormat = ecamToV4L2PixFormat(supportedFormats[formatIndex].inputFormat);

		WINPR_ASSERT(pixelFormat != 0);
		struct v4l2_frmsizeenum frmsize = WINPR_C_ARRAY_INIT;

		if (!cam_v4l_format_supported(fd, pixelFormat))
			continue;

		frmsize.pixel_format = pixelFormat;
		for (frmsize.index = 0; ioctl(fd, VIDIOC_ENUM_FRAMESIZES, &frmsize) == 0; frmsize.index++)
		{
			struct v4l2_frmivalenum frmival = WINPR_C_ARRAY_INIT;

			if (frmsize.type != V4L2_FRMSIZE_TYPE_DISCRETE)
				break; /* don't support size types other than discrete */

			formatFound = TRUE;
			mediaTypes->Width = frmsize.discrete.width;
			mediaTypes->Height = frmsize.discrete.height;
			mediaTypes->Format = supportedFormats[formatIndex].inputFormat;

			/* query frame rate (1st is highest fps supported) */
			frmival.index = 0;
			frmival.pixel_format = pixelFormat;
			frmival.width = frmsize.discrete.width;
			frmival.height = frmsize.discrete.height;
			if (ioctl(fd, VIDIOC_ENUM_FRAMEINTERVALS, &frmival) == 0 &&
			    frmival.type == V4L2_FRMIVAL_TYPE_DISCRETE)
			{
				/* inverse of a fraction */
				mediaTypes->FrameRateNumerator = frmival.discrete.denominator;
				mediaTypes->FrameRateDenominator = frmival.discrete.numerator;
			}
			else
			{
				WLog_DBG(TAG, "VIDIOC_ENUM_FRAMEINTERVALS failed, using default framerate");
				mediaTypes->FrameRateNumerator = CAM_V4L2_FRAMERATE_NUMERATOR_DEFAULT;
				mediaTypes->FrameRateDenominator = CAM_V4L2_FRAMERATE_DENOMINATOR_DEFAULT;
			}

			mediaTypes->PixelAspectRatioNumerator = mediaTypes->PixelAspectRatioDenominator = 1;

			char fourccstr[5] = WINPR_C_ARRAY_INIT;
			WLog_DBG(TAG, "Camera format: %s, width: %u, height: %u, fps: %u/%u",
			         cam_v4l_get_fourcc_str(pixelFormat, fourccstr, ARRAYSIZE(fourccstr)),
			         mediaTypes->Width, mediaTypes->Height, mediaTypes->FrameRateNumerator,
			         mediaTypes->FrameRateDenominator);

			mediaTypes++;
			nTypes++;

			if (nTypes == maxMediaTypes)
			{
				WLog_ERR(TAG, "Media types reached buffer maximum %" PRIuz "", maxMediaTypes);
				goto error;
			}
		}

		if (formatFound)
		{
			/* we are interested in 1st supported format only, with all supported sizes */
			break;
		}
	}

error:

	*nMediaTypes = nTypes;
	close(fd);
	if (formatIndex > INT16_MAX)
		return -1;
	return (INT16)formatIndex;
}

/**
 * Function description
 *
 * @return number of video capture devices
 */
static UINT cam_v4l_enumerate(WINPR_ATTR_UNUSED ICamHal* ihal, ICamHalEnumCallback callback,
                              CameraPlugin* ecam, GENERIC_CHANNEL_CALLBACK* hchannel)
{
	UINT count = 0;

	for (UINT n = 0; n < 64; n++)
	{
		char device[20] = WINPR_C_ARRAY_INIT;
		struct v4l2_capability cap = WINPR_C_ARRAY_INIT;
		(void)_snprintf(device, sizeof(device), "/dev/video%" PRIu32, n);
		int fd = open(device, O_RDONLY);
		if (fd == -1)
			continue;

		/* query device capabilities and make sure this is a video capture device */
		if (ioctl(fd, VIDIOC_QUERYCAP, &cap) < 0 || !(cap.device_caps & V4L2_CAP_VIDEO_CAPTURE))
		{
			close(fd);
			continue;
		}
		count++;

		const char* deviceName = (char*)cap.card;
		const char* deviceId = device;
		if (cap.bus_info[0] != 0) /* may not be available in all drivers */
			deviceId = (char*)cap.bus_info;

		IFCALL(callback, ecam, hchannel, deviceId, deviceName);

		close(fd);
	}

	return count;
}

static void cam_v4l_stream_free_buffers(CamV4lStream* stream)
{
	if (!stream || !stream->buffers)
		return;

	/* unmap buffers */
	for (size_t i = 0; i < stream->nBuffers; i++)
	{
		if (stream->buffers[i].length && stream->buffers[i].start != MAP_FAILED)
		{
			munmap(stream->buffers[i].start, stream->buffers[i].length);
		}
	}

	free(stream->buffers);
	stream->buffers = nullptr;
	stream->nBuffers = 0;
}

/**
 * Function description
 *
 * @return 0 on failure, otherwise allocated buffer size
 */
static size_t cam_v4l_stream_alloc_buffers(CamV4lStream* stream)
{
	struct v4l2_requestbuffers rbuffer = WINPR_C_ARRAY_INIT;

	rbuffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
	rbuffer.memory = V4L2_MEMORY_MMAP;
	rbuffer.count = CAM_V4L2_BUFFERS_COUNT;

	if (ioctl(stream->fd, VIDIOC_REQBUFS, &rbuffer) < 0 || rbuffer.count == 0)
	{
		char buffer[64] = WINPR_C_ARRAY_INIT;
		WLog_ERR(TAG, "Failure in VIDIOC_REQBUFS, errno  %s [%d], count %u",
		         winpr_strerror(errno, buffer, sizeof(buffer)), errno, rbuffer.count);
		return 0;
	}

	stream->nBuffers = rbuffer.count;

	/* Map the buffers */
	stream->buffers = (CamV4lBuffer*)calloc(rbuffer.count, sizeof(CamV4lBuffer));
	if (!stream->buffers)
	{
		WLog_ERR(TAG, "Failure in calloc");
		return 0;
	}

	for (unsigned int i = 0; i < rbuffer.count; i++)
	{
		struct v4l2_buffer vbuffer = WINPR_C_ARRAY_INIT;
		vbuffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
		vbuffer.memory = V4L2_MEMORY_MMAP;
		vbuffer.index = i;

		if (ioctl(stream->fd, VIDIOC_QUERYBUF, &vbuffer) < 0)
		{
			char buffer[64] = WINPR_C_ARRAY_INIT;
			WLog_ERR(TAG, "Failure in VIDIOC_QUERYBUF, errno %s [%d]",
			         winpr_strerror(errno, buffer, sizeof(buffer)), errno);
			cam_v4l_stream_free_buffers(stream);
			return 0;
		}

		stream->buffers[i].start = mmap(nullptr, vbuffer.length, PROT_READ | PROT_WRITE, MAP_SHARED,
		                                stream->fd, vbuffer.m.offset);

		if (MAP_FAILED == stream->buffers[i].start)
		{
			char buffer[64] = WINPR_C_ARRAY_INIT;
			WLog_ERR(TAG, "Failure in mmap, errno %s [%d]",
			         winpr_strerror(errno, buffer, sizeof(buffer)), errno);
			cam_v4l_stream_free_buffers(stream);
			return 0;
		}

		stream->buffers[i].length = vbuffer.length;

		WLog_DBG(TAG, "Buffer %u mapped, size: %u", i, vbuffer.length);

		if (ioctl(stream->fd, VIDIOC_QBUF, &vbuffer) < 0)
		{
			char buffer[64] = WINPR_C_ARRAY_INIT;
			WLog_ERR(TAG, "Failure in VIDIOC_QBUF, errno %s [%d]",
			         winpr_strerror(errno, buffer, sizeof(buffer)), errno);
			cam_v4l_stream_free_buffers(stream);
			return 0;
		}
	}

	return stream->buffers[0].length;
}

/**
 * Function description
 *
 * @return 0 on success, otherwise a Win32 error code
 */
static DWORD WINAPI cam_v4l_stream_capture_thread(LPVOID param)
{
	CamV4lStream* stream = (CamV4lStream*)param;
	WINPR_ASSERT(stream);

	int fd = stream->fd;

	do
	{
		int retVal = 0;
		struct pollfd pfd = WINPR_C_ARRAY_INIT;

		pfd.fd = fd;
		pfd.events = POLLIN;

		retVal = poll(&pfd, 1, CAM_V4L2_CAPTURE_THREAD_SLEEP_MS);

		if (retVal == 0)
		{
			/* poll timed out */
			continue;
		}
		else if (retVal < 0)
		{
			char buffer[64] = WINPR_C_ARRAY_INIT;
			WLog_DBG(TAG, "Failure in poll, errno %s [%d]",
			         winpr_strerror(errno, buffer, sizeof(buffer)), errno);
			Sleep(CAM_V4L2_CAPTURE_THREAD_SLEEP_MS); /* trying to recover */
			continue;
		}
		else if (!(pfd.revents & POLLIN))
		{
			WLog_DBG(TAG, "poll reported non-read event %d", pfd.revents);
			Sleep(CAM_V4L2_CAPTURE_THREAD_SLEEP_MS); /* also trying to recover */
			continue;
		}

		EnterCriticalSection(&stream->lock);
		if (stream->streaming)
		{
			struct v4l2_buffer buf = WINPR_C_ARRAY_INIT;
			buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
			buf.memory = V4L2_MEMORY_MMAP;

			/* dequeue buffers until empty */
			while (ioctl(fd, VIDIOC_DQBUF, &buf) != -1)
			{
				const UINT error =
				    stream->sampleCallback(stream->dev, stream->streamIndex,
				                           stream->buffers[buf.index].start, buf.bytesused);
				if (error != CHANNEL_RC_OK)
					WLog_ERR(TAG, "Failure in sampleCallback: %" PRIu32, error);

				/* enqueue buffer back */
				if (ioctl(fd, VIDIOC_QBUF, &buf) == -1)
				{
					char buffer[64] = WINPR_C_ARRAY_INIT;
					WLog_ERR(TAG, "Failure in VIDIOC_QBUF, errno %s [%d]",
					         winpr_strerror(errno, buffer, sizeof(buffer)), errno);
				}
			}
		}
		LeaveCriticalSection(&stream->lock);

	} while (stream->streaming);

	return CHANNEL_RC_OK;
}

void cam_v4l_stream_close_device(CamV4lStream* stream)
{
	if (stream->fd != -1)
	{
		close(stream->fd);
		stream->fd = -1;
	}
}

/**
 * Function description
 *
 * @return Null on failure, otherwise pointer to new CamV4lStream
 */
WINPR_ATTR_MALLOC(cam_v4l_stream_free, 1)
CamV4lStream* cam_v4l_stream_create(const char* deviceId, size_t streamIndex)
{
	CamV4lStream* stream = calloc(1, sizeof(CamV4lStream));

	if (!stream)
	{
		WLog_ERR(TAG, "Failure in calloc");
		return nullptr;
	}
	stream->streamIndex = streamIndex;
	stream->fd = -1;
	stream->h264UnitId = get_uvc_h624_unit_id(deviceId);

	if (!InitializeCriticalSectionEx(&stream->lock, 0, 0))
	{
		WLog_ERR(TAG, "Failure in calloc");
		free(stream);
		return nullptr;
	}

	return stream;
}

/**
 * Function description
 *
 * @return 0 on success, otherwise a Win32 error code
 */
CAM_ERROR_CODE cam_v4l_stream_stop(CamV4lStream* stream)
{
	if (!stream || !stream->streaming)
		return CAM_ERROR_CODE_None;

	stream->streaming = FALSE; /* this will terminate capture thread */

	if (stream->captureThread)
	{
		(void)WaitForSingleObject(stream->captureThread, INFINITE);
		(void)CloseHandle(stream->captureThread);
		stream->captureThread = nullptr;
	}

	EnterCriticalSection(&stream->lock);

	/* stop streaming */
	enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
	if (ioctl(stream->fd, VIDIOC_STREAMOFF, &type) < 0)
	{
		char buffer[64] = WINPR_C_ARRAY_INIT;
		WLog_ERR(TAG, "Failure in VIDIOC_STREAMOFF, errno %s [%d]",
		         winpr_strerror(errno, buffer, sizeof(buffer)), errno);
	}

	cam_v4l_stream_free_buffers(stream);
	cam_v4l_stream_close_device(stream);

	LeaveCriticalSection(&stream->lock);

	return CAM_ERROR_CODE_None;
}

static CAM_ERROR_CODE cam_v4l_stream_start(ICamHal* ihal, CameraDevice* dev, size_t streamIndex,
                                           const CAM_MEDIA_TYPE_DESCRIPTION* mediaType,
                                           ICamHalSampleCapturedCallback callback)
{
	CamV4lHal* hal = (CamV4lHal*)ihal;
	WINPR_ASSERT(hal);

	CamV4lStream* stream = (CamV4lStream*)HashTable_GetItemValue(hal->streams, dev->deviceId);

	if (!stream)
	{
		WLog_ERR(TAG, "Unable to find stream, device %s, streamIndex %" PRIuz, dev->deviceId,
		         streamIndex);
		return CAM_ERROR_CODE_UnexpectedError;
	}

	if (stream->streaming)
	{
		WLog_ERR(TAG, "Streaming already in progress, device %s, streamIndex %" PRIuz,
		         dev->deviceId, streamIndex);
		return CAM_ERROR_CODE_UnexpectedError;
	}

	stream->dev = dev;
	stream->sampleCallback = callback;

	if ((stream->fd = cam_v4l_open_device(dev->deviceId, O_RDWR | O_NONBLOCK)) == -1)
	{
		WLog_ERR(TAG, "Unable to open device %s", dev->deviceId);
		return CAM_ERROR_CODE_UnexpectedError;
	}

	struct v4l2_format video_fmt = WINPR_C_ARRAY_INIT;
	video_fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;

	const UINT32 pixelFormat = ecamToV4L2PixFormat(mediaType->Format);
	video_fmt.fmt.pix.width = mediaType->Width;
	video_fmt.fmt.pix.height = mediaType->Height;

	if (pixelFormat == 0)
	{
		cam_v4l_stream_close_device(stream);
		return CAM_ERROR_CODE_InvalidMediaType;
	}

	video_fmt.fmt.pix.pixelformat = pixelFormat;

	/* set format and frame size */
	if (ioctl(stream->fd, VIDIOC_S_FMT, &video_fmt) < 0)
	{
		char buffer[64] = WINPR_C_ARRAY_INIT;
		WLog_ERR(TAG, "Failure in VIDIOC_S_FMT, errno %s [%d]",
		         winpr_strerror(errno, buffer, sizeof(buffer)), errno);
		cam_v4l_stream_close_device(stream);
		return CAM_ERROR_CODE_InvalidMediaType;
	}

	/* trying to set frame rate, if driver supports it */
	struct v4l2_streamparm sp1 = WINPR_C_ARRAY_INIT;
	struct v4l2_streamparm sp2 = WINPR_C_ARRAY_INIT;
	sp1.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
	if (ioctl(stream->fd, VIDIOC_G_PARM, &sp1) < 0 ||
	    !(sp1.parm.capture.capability & V4L2_CAP_TIMEPERFRAME))
	{
		WLog_INFO(TAG, "Driver doesn't support setting framerate");
	}
	else
	{
		sp2.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;

		/* inverse of a fraction */
		sp2.parm.capture.timeperframe.numerator = mediaType->FrameRateDenominator;
		sp2.parm.capture.timeperframe.denominator = mediaType->FrameRateNumerator;

		if (ioctl(stream->fd, VIDIOC_S_PARM, &sp2) < 0)
		{
			char buffer[64] = WINPR_C_ARRAY_INIT;
			WLog_INFO(TAG, "Failed to set the framerate, errno %s [%d]",
			          winpr_strerror(errno, buffer, sizeof(buffer)), errno);
		}
	}

	size_t maxSample = cam_v4l_stream_alloc_buffers(stream);
	if (maxSample == 0)
	{
		WLog_ERR(TAG, "Failure to allocate video buffers");
		cam_v4l_stream_close_device(stream);
		return CAM_ERROR_CODE_OutOfMemory;
	}

	stream->streaming = TRUE;

	/* start streaming */
	enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
	if (ioctl(stream->fd, VIDIOC_STREAMON, &type) < 0)
	{
		char buffer[64] = WINPR_C_ARRAY_INIT;
		WLog_ERR(TAG, "Failure in VIDIOC_STREAMON, errno %s [%d]",
		         winpr_strerror(errno, buffer, sizeof(buffer)), errno);
		cam_v4l_stream_stop(stream);
		return CAM_ERROR_CODE_UnexpectedError;
	}

	stream->captureThread =
	    CreateThread(nullptr, 0, cam_v4l_stream_capture_thread, stream, 0, nullptr);
	if (!stream->captureThread)
	{
		WLog_ERR(TAG, "CreateThread failure");
		cam_v4l_stream_stop(stream);
		return CAM_ERROR_CODE_OutOfMemory;
	}

	char fourccstr[16] = WINPR_C_ARRAY_INIT;
	cam_v4l_get_fourcc_str(pixelFormat, fourccstr, ARRAYSIZE(fourccstr));

	WLog_INFO(TAG, "Camera format: %s, width: %u, height: %u, fps: %u/%u", fourccstr,
	          mediaType->Width, mediaType->Height, mediaType->FrameRateNumerator,
	          mediaType->FrameRateDenominator);

	return CAM_ERROR_CODE_None;
}

/**
 * Function description
 *
 * @return 0 on success, otherwise a Win32 error code
 */
static CAM_ERROR_CODE cam_v4l_stream_stop_by_device_id(ICamHal* ihal, const char* deviceId,
                                                       WINPR_ATTR_UNUSED size_t streamIndex)
{
	CamV4lHal* hal = (CamV4lHal*)ihal;

	CamV4lStream* stream = (CamV4lStream*)HashTable_GetItemValue(hal->streams, deviceId);

	if (!stream)
		return CAM_ERROR_CODE_NotInitialized;

	return cam_v4l_stream_stop(stream);
}

/**
 * Function description
 *
 * OBJECT_FREE_FN for streams hash table value
 *
 */
void cam_v4l_stream_free(void* obj)
{
	CamV4lStream* stream = (CamV4lStream*)obj;
	if (!stream)
		return;

	cam_v4l_stream_stop(stream);

	DeleteCriticalSection(&stream->lock);
	free(stream);
}

/**
 * Function description
 *
 * @return 0 on success, otherwise a Win32 error code
 */
static CAM_ERROR_CODE cam_v4l_free(ICamHal* ihal)
{
	CamV4lHal* hal = (CamV4lHal*)ihal;

	if (hal == nullptr)
		return CAM_ERROR_CODE_NotInitialized;

	HashTable_Free(hal->streams);

	free(hal);

	return CAM_ERROR_CODE_None;
}

/**
 * Function description
 *
 * @return 0 on success, otherwise a Win32 error code
 */
FREERDP_ENTRY_POINT(UINT VCAPITYPE v4l_freerdp_rdpecam_client_subsystem_entry(
    PFREERDP_CAMERA_HAL_ENTRY_POINTS pEntryPoints))
{
	UINT ret = ERROR_INTERNAL_ERROR;
	WINPR_ASSERT(pEntryPoints);

	CamV4lHal* hal = (CamV4lHal*)calloc(1, sizeof(CamV4lHal));

	if (hal == nullptr)
		return CHANNEL_RC_NO_MEMORY;

	hal->iHal.Enumerate = cam_v4l_enumerate;
	hal->iHal.GetMediaTypeDescriptions = cam_v4l_get_media_type_descriptions;
	hal->iHal.Activate = cam_v4l_activate;
	hal->iHal.Deactivate = cam_v4l_deactivate;
	hal->iHal.StartStream = cam_v4l_stream_start;
	hal->iHal.StopStream = cam_v4l_stream_stop_by_device_id;
	hal->iHal.Free = cam_v4l_free;

	hal->streams = HashTable_New(FALSE);
	if (!hal->streams)
		goto error;

	if (!HashTable_SetupForStringData(hal->streams, FALSE))
		goto error;

	wObject* obj = HashTable_ValueObject(hal->streams);
	WINPR_ASSERT(obj);
	obj->fnObjectFree = cam_v4l_stream_free;

	ret = pEntryPoints->pRegisterCameraHal(pEntryPoints->plugin, &hal->iHal);
	if (ret != CHANNEL_RC_OK)
	{
		WLog_ERR(TAG, "RegisterCameraHal failed with error %" PRIu32 "", ret);
		goto error;
	}

	return ret;

error:
	cam_v4l_free(&hal->iHal);
	return ret;
}
