/*
 * Copyright (c) 2010-2022 Belledonne Communications SARL.
 *
 * This file is part of mediastreamer2
 * (see https://gitlab.linphone.org/BC/public/mediastreamer2).
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

#include "Vfw.h"
#include "mediastreamer2/msticker.h"
#include "mediastreamer2/msv4l.h"
#include "mediastreamer2/msvideo.h"
#include <Windows.h>
#include <winuser.h>

#include "mediastreamer2/mswebcam.h"
#include "nowebcam.h"

#ifndef AVSTREAMMASTER_NONE
#define AVSTREAMMASTER_NONE 1
#endif

#define AMD_HACK2

typedef struct V4wState {
#ifdef AMD_HACK2
	ms_thread_t thread;
	ms_mutex_t thread_lock;
	ms_cond_t thread_cond;
	bool_t thread_running;
#endif
	char dev[512];
	int devidx;
	HWND capvideo;
	MSVideoSize vsize;
	int pix_fmt;
	mblk_t *mire[10];
	mblk_t *reverted;
	queue_t rq;
	ms_mutex_t mutex;
	int frame_ind;
	int frame_max;
	float fps;
	float start_time;
	int frame_count;
	bool_t running;
	bool_t startwith_yuv_bug; /* avoid bug with USB vimicro cards. */
	bool_t started;
	bool_t autostarted;
	bool_t invert_rgb;
} V4wState;

static void dummy(void *p) {
}

LRESULT CALLBACK VideoStreamCallback(HWND hWnd, LPVIDEOHDR lpVHdr) {
	V4wState *s;
	mblk_t *buf;
	int size;

	s = (V4wState *)capGetUserData(hWnd);
	if (s == NULL) return FALSE;

	size = lpVHdr->dwBufferLength;
	if (size > 0 && s->running) {
		buf = esballoc(lpVHdr->lpData, size, 0, dummy);
		buf->b_wptr += size;

		ms_mutex_lock(&s->mutex);
		putq(&s->rq, buf);
		ms_mutex_unlock(&s->mutex);
	}
	return TRUE;
}

static bool_t try_format(V4wState *s, BITMAPINFO *videoformat, MSPixFmt pixfmt) {
	bool_t ret;
	capGetVideoFormat(s->capvideo, videoformat, sizeof(BITMAPINFO));
	videoformat->bmiHeader.biSizeImage = 0;
	videoformat->bmiHeader.biWidth = s->vsize.width;
	videoformat->bmiHeader.biHeight = s->vsize.height;
	switch (pixfmt) {
		case MS_YUV420P:
			videoformat->bmiHeader.biBitCount = 12;
			videoformat->bmiHeader.biCompression = MAKEFOURCC('I', '4', '2', '0');
			break;
		case MS_YUY2:
			videoformat->bmiHeader.biBitCount = 16;
			videoformat->bmiHeader.biCompression = MAKEFOURCC('Y', 'U', 'Y', '2');
			break;
		case MS_RGB24:
			videoformat->bmiHeader.biBitCount = 24;
			videoformat->bmiHeader.biCompression = BI_RGB;
			break;
		default:
			return FALSE;
	}
	ret = capSetVideoFormat(s->capvideo, videoformat, sizeof(BITMAPINFO));
	if (ret) {
		/*recheck video format */
		capGetVideoFormat(s->capvideo, videoformat, sizeof(BITMAPINFO));
	}
	return ret;
}

static int v4w_open_videodevice(V4wState *s) {
	CAPTUREPARMS capparam;
	BITMAPINFO videoformat;
	char compname[5];
	int i;
	MSPixFmt driver_last;
	char dev[80];
	char ver[80];
	compname[4] = '\0';

	for (i = 0; i < 9; i++) {
		if (capGetDriverDescription(i, dev, sizeof(dev), ver, sizeof(ver))) {
			snprintf(s->dev, sizeof(s->dev), "%s/%s", dev, ver);
			ms_message("v4w: detected %s", s->dev);
			s->devidx = i;
			break;
		}
	}
	if (s->capvideo == NULL) {
		s->capvideo = capCreateCaptureWindow("Capture Window", WS_CHILD /* WS_OVERLAPPED */
		                                     ,
		                                     0, 0, s->vsize.width, s->vsize.height, HWND_MESSAGE, 0);
		if (s->capvideo == NULL) {
			ms_warning("v4w: could not create capture windows");
			return -1;
		}
	}

	if (!capDriverConnect(s->capvideo, s->devidx)) {
		ms_warning("v4w: could not connect to capture driver");
		DestroyWindow(s->capvideo);
		s->capvideo = NULL;
		s->pix_fmt = MS_YUV420P; /* no webcam stuff */
		return -1;
	}
	/*
	capPreviewRate(s->capvideo,s->fps) ;
	if(!capPreview (s->capvideo, 1))
	{
	    ms_warning("v4w: cannot start video preview");
	    capDriverDisconnect(s->capvideo);
	    DestroyWindow(s->capvideo);
	    s->capvideo=NULL;
	    return -1;
	}
	*/
	capCaptureGetSetup(s->capvideo, &capparam, sizeof(capparam));
	capparam.dwRequestMicroSecPerFrame = 100000;
	// detach capture from application
	capparam.fYield = TRUE;
	capparam.fMakeUserHitOKToCapture = FALSE;
	capparam.fAbortLeftMouse = FALSE;
	capparam.fAbortRightMouse = FALSE;
	capparam.wPercentDropForError = 90;
	capparam.fCaptureAudio = FALSE;
	capparam.fAbortRightMouse = FALSE;
	capparam.fAbortLeftMouse = FALSE;
	capparam.AVStreamMaster = AVSTREAMMASTER_NONE;

	if (!capCaptureSetSetup(s->capvideo, &capparam, sizeof(capparam))) {
		ms_error("capCaptureSetSetup failed.");
	}
	capSetUserData(s->capvideo, s);
	capGetVideoFormat(s->capvideo, &videoformat, sizeof(BITMAPINFO));
	/* "orig planes = " disp->videoformat.bmiHeader.biPlanes */
	/* "orig bitcount = " disp->videoformat.bmiHeader.biBitCount */
	/* "orig compression = " disp->videoformat.bmiHeader.biCompression */
	memcpy(compname, &videoformat.bmiHeader.biCompression, 4);
	ms_message("v4w: camera's current format is %s", compname);

	driver_last = ms_fourcc_to_pix_fmt(videoformat.bmiHeader.biCompression);
	if (s->startwith_yuv_bug == TRUE && try_format(s, &videoformat, MS_RGB24)) {
		s->pix_fmt = MS_RGB24;
		ms_message("Using RGB24");
	} else if (driver_last != MS_PIX_FMT_UNKNOWN && try_format(s, &videoformat, driver_last)) {
		ms_message("Using driver last setting");
		s->pix_fmt = driver_last;
	} else if (try_format(s, &videoformat, MS_YUV420P)) {
		s->pix_fmt = MS_YUV420P;
		ms_message("Using YUV420P");
	} else if (try_format(s, &videoformat, MS_RGB24)) {
		s->pix_fmt = MS_RGB24;
		ms_message("Using RGB24");
		s->startwith_yuv_bug = TRUE;
	} else if (try_format(s, &videoformat, MS_YUY2)) {
		s->pix_fmt = MS_YUY2;
		ms_message("Using YUY2");
	} else {
		ms_error("v4w: Failed to set any video format.");
		capDriverDisconnect(s->capvideo);
		DestroyWindow(s->capvideo);
		s->capvideo = NULL;
		return -1;
	}
	if (s->pix_fmt == MS_RGB24) {
		s->invert_rgb = (videoformat.bmiHeader.biHeight > 0);
	} else s->invert_rgb = FALSE;

	if (!capSetCallbackOnVideoStream(s->capvideo, VideoStreamCallback)) {
		ms_error("v4w: fail to set capture callback");
		capDriverDisconnect(s->capvideo);
		DestroyWindow(s->capvideo);
		s->capvideo = NULL;
		return -1;
	}
	if (!capCaptureSequenceNoFile(s->capvideo)) {
		ms_error("v4w: fail to start capture");
		capDriverDisconnect(s->capvideo);
		capSetCallbackOnVideoStream(s->capvideo, NULL);
		DestroyWindow(s->capvideo);
		s->capvideo = NULL;
	}
	return 0;
}

static void v4w_init(MSFilter *f) {
	V4wState *s = (V4wState *)ms_new0(V4wState, 1);
	int idx;
	s->vsize.width = MS_VIDEO_SIZE_CIF_W;
	s->vsize.height = MS_VIDEO_SIZE_CIF_H;
	s->pix_fmt = MS_RGB24;

	s->capvideo = NULL;
	qinit(&s->rq);
	for (idx = 0; idx < 10; idx++) {
		s->mire[idx] = NULL;
	}
	ms_mutex_init(&s->mutex, NULL);
	s->start_time = 0;
	s->frame_count = -1;
	s->fps = 15;
	s->started = FALSE;
	s->autostarted = FALSE;
	s->invert_rgb = FALSE;
	s->reverted = NULL;
#ifdef AMD_HACK2
	/* avoid bug with USB vimicro cards:
	    How can I detect that this problem exist?
	*/
	s->startwith_yuv_bug = FALSE;
#endif

#ifdef AMD_HACK2
	s->thread = NULL;
	ms_mutex_init(&s->thread_lock, NULL);
	ms_cond_init(&s->thread_cond, NULL);
	s->thread_running = FALSE;
#endif
	f->data = s;
}

static int _v4w_start(V4wState *s, void *arg) {
	int i;
	s->frame_count = -1;
	i = v4w_open_videodevice(s);
	if (i == 0 && s->startwith_yuv_bug == TRUE) {
		/* reopen device directly with MS_RGB24 */
		if (s->capvideo) {
			capSetUserData(s->capvideo, (long)0);
			capCaptureStop(s->capvideo);
			capCaptureAbort(s->capvideo);
			capDriverDisconnect(s->capvideo);
			capSetCallbackOnVideoStream(s->capvideo, NULL);
			flushq(&s->rq, 0);
			ms_message("v4w: destroying capture window");
			DestroyWindow(s->capvideo);
			ms_message("v4w: capture window destroyed");
			s->capvideo = NULL;
		}
		i = v4w_open_videodevice(s);
	}
	return i;
}

static int _v4w_stop(V4wState *s, void *arg) {
	s->frame_count = -1;
	if (s->capvideo) {
		capCaptureStop(s->capvideo);
		Sleep(1000);
		// capCaptureAbort(s->capvideo);
		capSetCallbackOnVideoStream(s->capvideo, NULL);
		// SendMessage(s->capvideo, WM_CLOSE, 0, 0);
		capDriverDisconnect(s->capvideo);
		capSetUserData(s->capvideo, (long)0);
		flushq(&s->rq, 0);
		ms_message("v4w: destroying capture window");
		DestroyWindow(s->capvideo);
		ms_message("v4w: capture window destroyed");
		s->capvideo = NULL;
	}
#if 0
	if (s->capvideo){
		CAPSTATUS status;
		capCaptureStop(s->capvideo);
		capDriverDisconnect(s->capvideo);
		capCaptureAbort(s->capvideo);

		capSetCallbackOnVideoStream(s->capvideo, NULL);
		while (1)
		{
			if (capGetStatus(s->capvideo, &status, sizeof(status)))
			{
				if (status.fCapturingNow==FALSE)
					break;
				Sleep(10);
				ms_message("still capturing");
			}
		}
		DestroyWindow(s->capvideo);
		s->capvideo=NULL;
	}
#endif
	return 0;
}

#ifdef AMD_HACK2

void *v4w_thread(void *arg) {
	V4wState *s = (V4wState *)arg;
	MSG msg;

	ms_mutex_lock(&s->thread_lock);
	_v4w_start(s, NULL);
	ms_cond_signal(&s->thread_cond);
	ms_mutex_unlock(&s->thread_lock);

	while (s->thread_running) {
		BOOL fGotMessage;
		if ((fGotMessage = PeekMessage(&msg, (HWND)s->capvideo, 0, 0, PM_REMOVE)) != 0) {
			TranslateMessage(&msg);
			DispatchMessage(&msg);
		} else Sleep(10);
	}

	ms_mutex_lock(&s->thread_lock);
	_v4w_stop(s, NULL);
	ms_cond_signal(&s->thread_cond);
	ms_mutex_unlock(&s->thread_lock);
	ms_thread_exit(NULL);
	return NULL;
}

static int v4w_start(MSFilter *f, void *arg) {
	V4wState *s = (V4wState *)f->data;
	s->thread_running = TRUE;
	ms_thread_create(&s->thread, NULL, v4w_thread, s);
	ms_mutex_lock(&s->thread_lock);
	ms_cond_wait(&s->thread_cond, &s->thread_lock);
	ms_mutex_unlock(&s->thread_lock);
	s->started = TRUE;
	return 0;
}

static int v4w_stop(MSFilter *f, void *arg) {
	V4wState *s = (V4wState *)f->data;
	ms_mutex_lock(&s->thread_lock);
	s->thread_running = FALSE;
	// SendMessage(s->capvideo, WM_CLOSE, 0, 0);
	ms_cond_wait(&s->thread_cond, &s->thread_lock);
	ms_mutex_unlock(&s->thread_lock);
	ms_thread_join(s->thread, NULL);
	s->started = FALSE;
	return 0;
}

#else

static int v4w_start(MSFilter *f, void *arg) {
	V4wState *s = (V4wState *)f->data;
	_v4w_start(s, NULL);
	s->started = TRUE;
	return 0;
}

static int v4w_stop(MSFilter *f, void *arg) {
	V4wState *s = (V4wState *)f->data;
	_v4w_stop(s, NULL);
	s->started = FALSE;
	return 0;
}

#endif

static void v4w_uninit(MSFilter *f) {
	V4wState *s = (V4wState *)f->data;
	int idx;
	flushq(&s->rq, 0);
	ms_mutex_destroy(&s->mutex);
	for (idx = 0; idx < 10; idx++) {
		if (s->mire[idx] == NULL) break;
		freemsg(s->mire[idx]);
	}
	if (s->capvideo != NULL) {
		ms_message("v4w: destroying capture window");
		DestroyWindow(s->capvideo);
		ms_message("v4w: capture window destroyed");
		s->capvideo = NULL;
	}
	if (s->reverted) {
		freemsg(s->reverted);
	}
#ifdef AMD_HACK2
	ms_cond_destroy(&s->thread_cond);
	ms_mutex_destroy(&s->thread_lock);
#endif
	ms_free(s);
}

static mblk_t *v4w_make_nowebcam(MSFilter *f, V4wState *s) {
	int idx;
	int count;
	if (s->mire[0] == NULL && s->frame_ind == 0) {
		/* load several images to fake a movie */
		for (idx = 0; idx < 10; idx++) {
			s->mire[idx] = ms_load_nowebcam(f->factory, &s->vsize, idx);
			if (s->mire[idx] == NULL) break;
		}
		if (idx == 0) s->mire[0] = ms_load_nowebcam(f->factory, &s->vsize, -1);
	}
	for (count = 0; count < 10; count++) {
		if (s->mire[count] == NULL) break;
	}

	s->frame_ind++;
	if (count == 0) return NULL;

	idx = s->frame_ind % count;
	if (s->mire[idx] != NULL) return s->mire[idx];
	return s->mire[0];
}

static void v4w_preprocess(MSFilter *obj) {
	V4wState *s = (V4wState *)obj->data;
	if (!s->started) {
		ms_message("V4W auto-started.");
		v4w_start(obj, NULL);
		s->autostarted = TRUE;
	}
	s->running = TRUE;
	if (s->capvideo == NULL) s->fps = 1;
}

static void v4w_postprocess(MSFilter *obj) {
	V4wState *s = (V4wState *)obj->data;
	s->running = FALSE;
	if (s->autostarted) {
		v4w_stop(obj, NULL);
	}
}

static void v4w_process(MSFilter *obj) {
	V4wState *s = (V4wState *)obj->data;
	mblk_t *m;
	uint32_t timestamp;
	int cur_frame;

	if (s->frame_count == -1) {
		s->start_time = obj->ticker->time;
		s->frame_count = 0;
	}

	cur_frame = ((obj->ticker->time - s->start_time) * s->fps / 1000.0);
	if (cur_frame > s->frame_count) {
		mblk_t *om = NULL;
		ms_mutex_lock(&s->mutex);
		/*keep the most recent frame if several frames have been captured */
		if (s->capvideo != NULL) {
			while ((m = getq(&s->rq)) != NULL) {
				if (om != NULL) freemsg(om);
				om = m;
			}
			if (om != NULL) {
				if (s->invert_rgb) {
					MSVideoSize roi;
					if (s->reverted == NULL) {
						s->reverted = allocb(om->b_wptr - om->b_rptr, 0);
						s->reverted->b_wptr = dblk_lim(s->reverted->b_datap);
					}
					roi = s->vsize;
					rgb24_copy_revert(s->reverted->b_rptr, roi.width * 3, om->b_rptr, roi.width * 3, roi);
					freemsg(om);
					om = dupb(s->reverted);
				}
			}
		} else {
			mblk_t *nowebcam = v4w_make_nowebcam(obj, s);
			if (nowebcam != NULL) {
				om = dupmsg(nowebcam);
				mblk_set_precious_flag(om, 1);
			}
		}
		ms_mutex_unlock(&s->mutex);
		if (om != NULL) {
			timestamp = obj->ticker->time * 90; /* rtp uses a 90000 Hz clockrate for video*/
			mblk_set_timestamp_info(om, timestamp);
			ms_queue_put(obj->outputs[0], om);
		}
		s->frame_count++;
	}
}

static int v4w_set_fps(MSFilter *f, void *arg) {
	V4wState *s = (V4wState *)f->data;
	s->fps = *((float *)arg);
	return 0;
}

static int v4w_get_pix_fmt(MSFilter *f, void *arg) {
	V4wState *s = (V4wState *)f->data;
	if (!s->started) {
		ms_message("V4W auto-started in v4w_get_pix_fmt()");
		v4w_start(f, NULL);
		s->autostarted = TRUE;
	}
	*((MSPixFmt *)arg) = (MSPixFmt)s->pix_fmt;
	return 0;
}

static int v4w_set_vsize(MSFilter *f, void *arg) {
	V4wState *s = (V4wState *)f->data;
	s->vsize = *((MSVideoSize *)arg);
	return 0;
}

static int v4w_get_vsize(MSFilter *f, void *arg) {
	V4wState *s = (V4wState *)f->data;
	MSVideoSize *vs = (MSVideoSize *)arg;
	vs->width = s->vsize.width;
	vs->height = s->vsize.height;
	return 0;
}

static MSFilterMethod methods[] = {{MS_FILTER_SET_FPS, v4w_set_fps},
                                   {MS_FILTER_GET_PIX_FMT, v4w_get_pix_fmt},
                                   {MS_FILTER_SET_VIDEO_SIZE, v4w_set_vsize},
                                   {MS_FILTER_GET_VIDEO_SIZE, v4w_get_vsize},
                                   {MS_V4L_START, v4w_start},
                                   {MS_V4L_STOP, v4w_stop},
                                   {0, NULL}};

#ifdef _MSC_VER

MSFilterDesc ms_v4w_desc = {MS_V4L_ID,
                            "MSV4w",
                            N_("A video4windows compatible source filter to stream pictures."),
                            MS_FILTER_OTHER,
                            NULL,
                            0,
                            1,
                            v4w_init,
                            v4w_preprocess,
                            v4w_process,
                            v4w_postprocess,
                            v4w_uninit,
                            methods};

#else

MSFilterDesc ms_v4w_desc = {.id = MS_V4L_ID,
                            .name = "MSV4w",
                            .text = N_("A video4windows compatible source filter to stream pictures."),
                            .ninputs = 0,
                            .noutputs = 1,
                            .category = MS_FILTER_OTHER,
                            .init = v4w_init,
                            .preprocess = v4w_preprocess,
                            .process = v4w_process,
                            .postprocess = v4w_postprocess,
                            .uninit = v4w_uninit,
                            .methods = methods};

#endif

MS_FILTER_DESC_EXPORT(ms_v4w_desc)

#if 0
static void ms_v4w_detect(MSWebCamManager *obj);

static void ms_v4w_cam_init(MSWebCam *cam){
}


static MSFilter *ms_v4w_create_reader(MSWebCam *obj){
	MSFilter *f= ms_filter_new_from_desc(&ms_v4w_desc);
	V4wState *s=(V4wState*)f->data;
	s->devidx=(int)obj->data;
	return f;
}

MSWebCamDesc ms_v4w_cam_desc={
	"VideoForWindows grabber",
	&ms_v4w_detect,
	&ms_v4w_cam_init,
	&ms_v4w_create_reader,
	NULL
};

static void ms_v4w_detect(MSWebCamManager *obj){
	int i;
	char dev[80];
	char ver[80];
	char name[160];
	MSWebCam *cam;
	for (i = 0; i < 9; i++){
		if (capGetDriverDescription(i, dev, sizeof (dev),
			ver, sizeof (ver))){
			HWND hwnd=capCreateCaptureWindow("Capture Window",WS_CHILD /* WS_OVERLAPPED */
				,0,0,352,288,HWND_MESSAGE, 0) ;
			if (hwnd==NULL) break;
			if(!capDriverConnect(hwnd,i )){
				ms_warning("v4w: could not connect to capture driver, no webcam connected.");
				DestroyWindow(hwnd);
				break;
			}else{
				capGetDriverDescription(i, dev, sizeof (dev),ver, sizeof (ver));
				capDriverDisconnect(hwnd);
				DestroyWindow(hwnd);
			}
			snprintf(name, sizeof(name), "%s/%s",dev,ver);
			cam=ms_web_cam_new(&ms_v4w_cam_desc);
			cam->data=(void*)i;/*store the device index */
			cam->name=ms_strdup(name);
			ms_web_cam_manager_add_cam(obj,cam);
		}
	}
}

#endif
