#if HAVE_CRT
#define _CRTDBG_MAP_ALLOC 
#include <stdlib.h> 
#include <crtdbg.h>
#endif //HAVE_CRT
/*
* Copyright (C) 2020, University of the Basque Country (UPV/EHU)
* Contact for licensing options: <licensing-mcpttclient(at)mcopenplatform(dot)com>
*
* The original file was part of Open Source Doubango Framework
* Copyright (C) 2010-2011 Mamadou Diop.
* Copyright (C) 2012 Doubango Telecom <http://doubango.org>
*
* This file is part of Open Source Doubango Framework.
*
* DOUBANGO is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* DOUBANGO 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with DOUBANGO.
*
*/

#include "tinydav/video/gdi/tdav_consumer_video_gdi.h"

#if TDAV_UNDER_WINDOWS && !TDAV_UNDER_WINDOWS_RT

#include <windows.h>

#include "tsk_memory.h"
#include "tsk_string.h"
#include "tsk_safeobj.h"
#include "tsk_debug.h"

#define CHECK_HR(x) { HRESULT __hr__ = (x); if (FAILED(__hr__)) { TSK_DEBUG_ERROR("Operation Failed (%08x)", __hr__); goto bail; } }

static HRESULT HookWindow(struct tdav_consumer_video_gdi_s *p_gdi, HWND hWnd, BOOL bFullScreenWindow);
static HRESULT UnhookWindow(struct tdav_consumer_video_gdi_s *p_gdi, BOOL bFullScreenWindow);
static HRESULT SetFullscreen(struct tdav_consumer_video_gdi_s *p_gdi, BOOL bFullScreen);
static HWND CreateFullScreenWindow(struct tdav_consumer_video_gdi_s *p_gdi);
static LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

typedef struct tdav_consumer_video_gdi_s
{
	TMEDIA_DECLARE_CONSUMER;

	BOOL bStarted, bPrepared, bPaused, bFullScreen, bWindowHooked, bWindowHookedFullScreen;
	HWND hWindow;
	WNDPROC wndProc;
	HWND hWindowFullScreen;
	WNDPROC wndProcFullScreen;
	BITMAPINFO bitmapInfo;
	void* pBuffer;

	TSK_DECLARE_SAFEOBJ;
}
tdav_consumer_video_gdi_t;



/* ============ Media Consumer Interface ================= */
static int tdav_consumer_video_gdi_set(tmedia_consumer_t *self, const tmedia_param_t* param)
{
	int ret = 0;
	tdav_consumer_video_gdi_t* p_gdi = (tdav_consumer_video_gdi_t*)self;
	HRESULT hr = S_OK;

	if (!self || !param) {
		TSK_DEBUG_ERROR("Invalid parameter");
		CHECK_HR(hr = E_POINTER);
	}

	if (param->value_type == tmedia_pvt_int64) {
		if (tsk_striequals(param->key, "remote-hwnd")) {
			HWND hWnd = ((HWND)*((int64_t*)param->value));
			if (hWnd != p_gdi->hWindow) {
				tsk_safeobj_lock(p_gdi); // block consumer thread
				UnhookWindow(p_gdi, FALSE);
				p_gdi->hWindow = hWnd;
				tsk_safeobj_unlock(p_gdi); // unblock consumer thread
			}			
		}
	}
	else if(param->value_type == tmedia_pvt_int32) {
		if(tsk_striequals(param->key, "fullscreen")) {
			BOOL bFullScreen = !!*((int32_t*)param->value);
			TSK_DEBUG_INFO("[GDI video consumer] Full Screen = %d", bFullScreen);
		    CHECK_HR(hr = SetFullscreen(p_gdi, bFullScreen));
		}
	}

	CHECK_HR(hr);

bail:
	return SUCCEEDED(hr) ?  0 : -1;
}


static int tdav_consumer_video_gdi_prepare(tmedia_consumer_t* self, const tmedia_codec_t* codec)
{
	tdav_consumer_video_gdi_t* p_gdi = (tdav_consumer_video_gdi_t*)self;

	if (!p_gdi || !codec && codec->plugin) {
		TSK_DEBUG_ERROR("Invalid parameter");
		return -1;
	}
	
	TMEDIA_CONSUMER(p_gdi)->video.fps = TMEDIA_CODEC_VIDEO(codec)->in.fps;
	TMEDIA_CONSUMER(p_gdi)->video.in.width = TMEDIA_CODEC_VIDEO(codec)->in.width;
	TMEDIA_CONSUMER(p_gdi)->video.in.height = TMEDIA_CODEC_VIDEO(codec)->in.height;

	if (!TMEDIA_CONSUMER(p_gdi)->video.display.width) {
		TMEDIA_CONSUMER(p_gdi)->video.display.width = TMEDIA_CONSUMER(p_gdi)->video.in.width;
	}
	if (!TMEDIA_CONSUMER(p_gdi)->video.display.height) {
		TMEDIA_CONSUMER(p_gdi)->video.display.height = TMEDIA_CONSUMER(p_gdi)->video.in.height;
	}

	ZeroMemory(&p_gdi->bitmapInfo, sizeof(p_gdi->bitmapInfo));
	p_gdi->bitmapInfo.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
	p_gdi->bitmapInfo.bmiHeader.biPlanes = 1;
	p_gdi->bitmapInfo.bmiHeader.biBitCount = 24; // RGB24
	p_gdi->bitmapInfo.bmiHeader.biCompression = BI_RGB;
	p_gdi->bitmapInfo.bmiHeader.biWidth = (LONG)TMEDIA_CONSUMER(p_gdi)->video.in.width;
	p_gdi->bitmapInfo.bmiHeader.biHeight = (LONG)(TMEDIA_CONSUMER(p_gdi)->video.in.height * -1);
	p_gdi->bitmapInfo.bmiHeader.biSizeImage = (DWORD)(TMEDIA_CONSUMER(p_gdi)->video.in.width * abs((int)TMEDIA_CONSUMER(p_gdi)->video.in.height) *
		(p_gdi->bitmapInfo.bmiHeader.biBitCount >> 3));

	return 0;
}

static int tdav_consumer_video_gdi_start(tmedia_consumer_t* self)
{
	tdav_consumer_video_gdi_t* p_gdi = (tdav_consumer_video_gdi_t*)self;

	if (!p_gdi) {
		TSK_DEBUG_ERROR("Invalid parameter");
		return -1;
	}
	
	tsk_safeobj_lock(p_gdi);

	p_gdi->bPaused = FALSE;
	p_gdi->bStarted = TRUE;

	tsk_safeobj_unlock(p_gdi);

	return 0;
}

static int tdav_consumer_video_gdi_consume(tmedia_consumer_t* self, const void* buffer, tsk_size_t size, const tsk_object_t* proto_hdr)
{
	tdav_consumer_video_gdi_t* p_gdi = (tdav_consumer_video_gdi_t*)self;
	int ret = 0;
	HWND* p_Window;
	BOOL* p_bWindowHooked;

	if (!p_gdi) {
		TSK_DEBUG_ERROR("Invalid parameter");
		return -1;
	}

	tsk_safeobj_lock(p_gdi);

	if (!p_gdi->bStarted || p_gdi->bPaused) {
		TSK_DEBUG_INFO("GDI consumer stopped or paused");
		goto bail;
	}

	if (p_gdi->bitmapInfo.bmiHeader.biSizeImage != size) {
		tsk_size_t xNewSize = TMEDIA_CONSUMER(p_gdi)->video.in.width * TMEDIA_CONSUMER(p_gdi)->video.in.height * (p_gdi->bitmapInfo.bmiHeader.biBitCount >> 3);
		TSK_DEBUG_INFO("GDI input size changed: %u->%u", p_gdi->bitmapInfo.bmiHeader.biSizeImage, size);
		if (xNewSize != size) {
			TSK_DEBUG_ERROR("GDI consumer: chroma issue?");
			ret = -1;
			goto bail;
		}
		p_gdi->bitmapInfo.bmiHeader.biWidth = (LONG)TMEDIA_CONSUMER(p_gdi)->video.in.width;
		p_gdi->bitmapInfo.bmiHeader.biHeight = (LONG)TMEDIA_CONSUMER(p_gdi)->video.in.height * -1;
		p_gdi->bitmapInfo.bmiHeader.biSizeImage = (DWORD)xNewSize;
		p_gdi->pBuffer = tsk_realloc(p_gdi->pBuffer, p_gdi->bitmapInfo.bmiHeader.biSizeImage);
	}

	p_Window = p_gdi->bFullScreen ? &p_gdi->hWindowFullScreen : &p_gdi->hWindow;
	p_bWindowHooked = p_gdi->bFullScreen ? &p_gdi->bWindowHookedFullScreen : &p_gdi->bWindowHooked;

	if (*p_Window) {
		if (!*p_bWindowHooked) {
			// Do not hook "hWnd" as it could be the fullscreen handle which is always hooked.
			CHECK_HR(HookWindow(p_gdi, *p_Window, p_gdi->bFullScreen));
		}
		if (!p_gdi->pBuffer) {
			p_gdi->pBuffer = tsk_realloc(p_gdi->pBuffer, p_gdi->bitmapInfo.bmiHeader.biSizeImage);
		}
		if (p_gdi->pBuffer) {
			memcpy(p_gdi->pBuffer, buffer, size);
			InvalidateRect(*p_Window, NULL, TRUE);
		}
	}

bail:
	tsk_safeobj_unlock(p_gdi);
	return ret;
}

static int tdav_consumer_video_gdi_pause(tmedia_consumer_t* self)
{
	tdav_consumer_video_gdi_t* p_gdi = (tdav_consumer_video_gdi_t*)self;

	if (!p_gdi) {
		TSK_DEBUG_ERROR("Invalid parameter");
		return -1;
	}

	tsk_safeobj_lock(p_gdi);

	p_gdi->bPaused = TRUE;

	tsk_safeobj_unlock(p_gdi);

	return 0;
}

static int tdav_consumer_video_gdi_stop(tmedia_consumer_t* self)
{
	tdav_consumer_video_gdi_t* p_gdi = (tdav_consumer_video_gdi_t*)self;

	if (!p_gdi) {
		TSK_DEBUG_ERROR("Invalid parameter");
		return -1;
	}

	tsk_safeobj_lock(p_gdi);
	
	p_gdi->bStarted = FALSE;
	p_gdi->bPaused = FALSE;
	SetFullscreen(p_gdi, FALSE);
	UnhookWindow(p_gdi, TRUE);
	UnhookWindow(p_gdi, FALSE);
	
	tsk_safeobj_unlock(p_gdi);

	return 0;
}


static LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	switch(uMsg)
	{
		case WM_CREATE:
		case WM_SIZE:
		case WM_MOVE:
			{
				struct tdav_consumer_video_gdi_s* p_gdi = ((struct tdav_consumer_video_gdi_s*)GetProp(hWnd, TEXT("Self")));
				if (p_gdi) {					
					
				}
				break;
			}

		case WM_PAINT:
			{
				struct tdav_consumer_video_gdi_s* p_gdi = ((struct tdav_consumer_video_gdi_s*)GetProp(hWnd, TEXT("Self")));
				if (p_gdi) {					
					tsk_safeobj_lock(p_gdi);

					if (p_gdi->bStarted && !p_gdi->bPaused && p_gdi->pBuffer) {
						PAINTSTRUCT ps;
						HDC hdc;
						RECT rc, logical_rect;
						int height, width, i, x, y;
						HDC dc_mem, all_dc[2];
						HBITMAP bmp_mem;
						HGDIOBJ bmp_old;
						POINT logical_area;
						HBRUSH brush;

						if (!(hdc = BeginPaint(hWnd, &ps))) {
							goto paint_done;
						}
						
						if (!GetClientRect(hWnd, &rc)) {
							EndPaint(hWnd, &ps);
							goto paint_done;
						}

						height = abs(p_gdi->bitmapInfo.bmiHeader.biHeight);
						width = p_gdi->bitmapInfo.bmiHeader.biWidth;

						dc_mem = CreateCompatibleDC(ps.hdc);
						SetStretchBltMode(dc_mem, HALFTONE);
						
						// Set the map mode so that the ratio will be maintained for us.
						all_dc[0] = ps.hdc, all_dc[1] = dc_mem;
						for (i = 0; i < sizeof(all_dc)/sizeof(all_dc[0]); ++i) {
#if !TDAV_UNDER_WINDOWS_CE
							SetMapMode(all_dc[i], MM_ISOTROPIC);
							SetWindowExtEx(all_dc[i], width, height, NULL);
							SetViewportExtEx(all_dc[i], rc.right, rc.bottom, NULL);
#endif
						}

						bmp_mem = CreateCompatibleBitmap(ps.hdc, rc.right, rc.bottom);
						bmp_old = SelectObject(dc_mem, bmp_mem);

						logical_area.x = rc.right, logical_area.y = rc.bottom;
#if !TDAV_UNDER_WINDOWS_CE
						DPtoLP(ps.hdc, &logical_area, 1);
#endif

						brush = CreateSolidBrush(RGB(0, 0, 0));
						logical_rect.left = 0, logical_rect.top = 0, logical_rect.right = logical_area.x, logical_rect.bottom = logical_area.y;
						FillRect(dc_mem, &logical_rect, brush);
						DeleteObject(brush);

						x = (logical_area.x / 2) - (width / 2);
						y = (logical_area.y / 2) - (height / 2);

						StretchDIBits(dc_mem, x, y, width, height,
							0, 0, width, height, p_gdi->pBuffer, &p_gdi->bitmapInfo, DIB_RGB_COLORS, SRCCOPY);

						BitBlt(ps.hdc, 0, 0, logical_area.x, logical_area.y,
							dc_mem, 0, 0, SRCCOPY);

						// Cleanup.
						SelectObject(dc_mem, bmp_old);
						DeleteObject(bmp_mem);
						DeleteDC(dc_mem);

						EndPaint(hWnd, &ps);
					}
paint_done:
					tsk_safeobj_unlock(p_gdi);
				}
				break;
			}

		case WM_ERASEBKGND:
			{
				return TRUE; // avoid background erasing.
			}

		case WM_CHAR:
		case WM_KEYUP:
			{
				struct tdav_consumer_video_gdi_s* p_gdi = ((struct tdav_consumer_video_gdi_s*)GetProp(hWnd, TEXT("Self")));
				if (p_gdi) {	
					SetFullscreen(p_gdi, FALSE);
				}
				
				break;
			}
	}

	return DefWindowProc(hWnd, uMsg, wParam, lParam);
}

static HRESULT HookWindow(struct tdav_consumer_video_gdi_s *p_gdi, HWND hWnd, BOOL bFullScreenWindow)
{
	HRESULT hr = S_OK;
	HWND* p_Window = bFullScreenWindow ? &p_gdi->hWindowFullScreen : &p_gdi->hWindow;
	WNDPROC* p_wndProc = bFullScreenWindow ? &p_gdi->wndProcFullScreen : &p_gdi->wndProc;
	BOOL* p_bWindowHooked = bFullScreenWindow ? &p_gdi->bWindowHookedFullScreen : &p_gdi->bWindowHooked;

	tsk_safeobj_lock(p_gdi);

	CHECK_HR(hr = UnhookWindow(p_gdi, bFullScreenWindow));

	if ((*p_Window = hWnd)) {
#if TDAV_UNDER_WINDOWS_CE
		*p_wndProc = (WNDPROC)SetWindowLong(hWnd, GWL_WNDPROC, (LONG)WndProc);
#else
		*p_wndProc = (WNDPROC)SetWindowLongPtr(hWnd, GWLP_WNDPROC, (LONG_PTR)WndProc);
#endif
		if (!*p_wndProc) {
			TSK_DEBUG_ERROR("HookWindowLongPtr() failed with errcode=%d", GetLastError());
			CHECK_HR(hr = E_FAIL);
		}
		*p_bWindowHooked = TRUE;
		SetProp(*p_Window, TEXT("Self"), p_gdi);
	}
bail:
	tsk_safeobj_unlock(p_gdi);
	return S_OK;
}

static HRESULT UnhookWindow(struct tdav_consumer_video_gdi_s *p_gdi, BOOL bFullScreenWindow)
{
	HWND* p_Window = bFullScreenWindow ? &p_gdi->hWindowFullScreen : &p_gdi->hWindow;
	WNDPROC* p_wndProc = bFullScreenWindow ? &p_gdi->wndProcFullScreen : &p_gdi->wndProc;
	BOOL* p_bWindowHooked = bFullScreenWindow ? &p_gdi->bWindowHookedFullScreen : &p_gdi->bWindowHooked;

	tsk_safeobj_lock(p_gdi);
	if (*p_Window && *p_wndProc) {
#if TDAV_UNDER_WINDOWS_CE
		SetWindowLong(*p_Window, GWL_WNDPROC, (LONG)*p_wndProc);
#else
		SetWindowLongPtr(*p_Window, GWLP_WNDPROC, (LONG_PTR)*p_wndProc);
#endif
		*p_wndProc = NULL;
	}
	if (*p_Window) {
		if (p_gdi->pBuffer) {
			memset(p_gdi->pBuffer, 0, p_gdi->bitmapInfo.bmiHeader.biSizeImage);
		}
		InvalidateRect(*p_Window, NULL, FALSE);
	}
	*p_bWindowHooked = FALSE;
	tsk_safeobj_unlock(p_gdi);
	return S_OK;
}

static HRESULT SetFullscreen(struct tdav_consumer_video_gdi_s *p_gdi, BOOL bFullScreen)
{
	HRESULT hr = S_OK;
	if (!p_gdi) {
		CHECK_HR(hr = E_POINTER);
	}

	if (p_gdi->bFullScreen != bFullScreen) {
		tsk_safeobj_lock(p_gdi);
		if (bFullScreen) {
			HWND hWnd = CreateFullScreenWindow(p_gdi);
			if (hWnd) {
#if TDAV_UNDER_WINDOWS_CE
				ShowWindow(hWnd, SW_SHOWNORMAL);
#else
				ShowWindow(hWnd, SW_SHOWDEFAULT);
#endif
				UpdateWindow(hWnd);
				HookWindow(p_gdi, hWnd, TRUE);
			}
		}
		else if(p_gdi->hWindowFullScreen) {
			ShowWindow(p_gdi->hWindowFullScreen, SW_HIDE);
			UnhookWindow(p_gdi, TRUE);
		}
		p_gdi->bFullScreen = bFullScreen;
		tsk_safeobj_unlock(p_gdi);

		CHECK_HR(hr);
	}

bail:
	return hr;
}

static HWND CreateFullScreenWindow(struct tdav_consumer_video_gdi_s *p_gdi)
{
	HRESULT hr = S_OK;
	
	if(!p_gdi) {
		return NULL;
	}

	if (!p_gdi->hWindowFullScreen) {
		WNDCLASS wc = {0};

		wc.lpfnWndProc   = WndProc;
		wc.hInstance     = GetModuleHandle(NULL);
		wc.hCursor       = LoadCursor(NULL, IDC_ARROW);
		wc.lpszClassName =  L"WindowClass";
		RegisterClass(&wc);
		p_gdi->hWindowFullScreen = CreateWindowEx(
				0,
				wc.lpszClassName, 
				L"Doubango's Video Consumer Fullscreen",
				WS_EX_TOPMOST | WS_POPUP,
				0, 0,
				GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN),
				NULL,
				NULL,
				GetModuleHandle(NULL),
				NULL);

		SetProp(p_gdi->hWindowFullScreen, TEXT("Self"), p_gdi);
	}
	return p_gdi->hWindowFullScreen;
}

//
//	GDI video consumer object definition
//
/* constructor */
static tsk_object_t* tdav_consumer_video_gdi_ctor(tsk_object_t * self, va_list * app)
{
	tdav_consumer_video_gdi_t *p_gdi = (tdav_consumer_video_gdi_t *)self;
	if (p_gdi) {
		/* init base */
		tmedia_consumer_init(TMEDIA_CONSUMER(p_gdi));
		TMEDIA_CONSUMER(p_gdi)->video.display.chroma = tmedia_chroma_bgr24;

		/* init self */
		TMEDIA_CONSUMER(p_gdi)->video.fps = 15;
		TMEDIA_CONSUMER(p_gdi)->video.display.width = 352;
		TMEDIA_CONSUMER(p_gdi)->video.display.height = 288;
		TMEDIA_CONSUMER(p_gdi)->video.display.auto_resize = tsk_true;
		tsk_safeobj_init(p_gdi);
	}
	return self;
}
/* destructor */
static tsk_object_t* tdav_consumer_video_gdi_dtor(tsk_object_t * self)
{ 
	tdav_consumer_video_gdi_t *p_gdi = (tdav_consumer_video_gdi_t *)self;
	if (p_gdi) {
		/* stop */
		tdav_consumer_video_gdi_stop((tmedia_consumer_t*)self);

		/* deinit base */
		tmedia_consumer_deinit(TMEDIA_CONSUMER(p_gdi));
		/* deinit self */
		TSK_FREE(p_gdi->pBuffer);
		tsk_safeobj_deinit(p_gdi);
	}

	return self;
}
/* object definition */
static const tsk_object_def_t tdav_consumer_video_gdi_def_s = 
{
	sizeof(tdav_consumer_video_gdi_t),
	tdav_consumer_video_gdi_ctor, 
	tdav_consumer_video_gdi_dtor,
	tsk_null, 
};
/* plugin definition*/
static const tmedia_consumer_plugin_def_t tdav_consumer_video_gdi_plugin_def_s = 
{
	&tdav_consumer_video_gdi_def_s,
	
	tmedia_video,
	"Microsoft GDI consumer (using custom source)",
	
	tdav_consumer_video_gdi_set,
	tdav_consumer_video_gdi_prepare,
	tdav_consumer_video_gdi_start,
	tdav_consumer_video_gdi_consume,
	tdav_consumer_video_gdi_pause,
	tdav_consumer_video_gdi_stop
};
const tmedia_consumer_plugin_def_t *tdav_consumer_video_gdi_plugin_def_t = &tdav_consumer_video_gdi_plugin_def_s;

#endif /* TDAV_UNDER_WINDOWS && !TDAV_UNDER_WINDOWS_RT */