/**
 * FreeRDP: A Remote Desktop Protocol Implementation
 *
 * Copyright 2011-2014 Marc-Andre Moreau <marcandre.moreau@gmail.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 <winpr/crt.h>
#include <winpr/synch.h>
#include <winpr/input.h>
#include <winpr/sysinfo.h>

#include <freerdp/server/server-common.h>
#include <freerdp/codec/color.h>
#include <freerdp/codec/region.h>
#include <freerdp/log.h>

#include "mac_shadow.h"

#define TAG SERVER_TAG("shadow.mac")

static macShadowSubsystem* g_Subsystem = nullptr;

static BOOL mac_shadow_input_synchronize_event(rdpShadowSubsystem* subsystem,
                                               rdpShadowClient* client, UINT32 flags)
{
	if (!subsystem || !client)
		return FALSE;

	return TRUE;
}

static BOOL mac_shadow_input_keyboard_event(rdpShadowSubsystem* subsystem, rdpShadowClient* client,
                                            UINT16 flags, UINT8 code)
{
	DWORD vkcode;
	DWORD keycode;
	BOOL extended;
	CGEventRef kbdEvent;
	CGEventSourceRef source;
	extended = (flags & KBD_FLAGS_EXTENDED) ? TRUE : FALSE;

	if (!subsystem || !client)
		return FALSE;

	if (extended)
		code |= KBDEXT;

	vkcode = GetVirtualKeyCodeFromVirtualScanCode(code, 4);

	if (extended)
		vkcode |= KBDEXT;

	keycode = GetKeycodeFromVirtualKeyCode(vkcode, WINPR_KEYCODE_TYPE_APPLE);

	source = CGEventSourceCreate(kCGEventSourceStateHIDSystemState);

	if (flags & KBD_FLAGS_DOWN)
	{
		kbdEvent = CGEventCreateKeyboardEvent(source, (CGKeyCode)keycode, TRUE);
		CGEventPost(kCGHIDEventTap, kbdEvent);
		CFRelease(kbdEvent);
	}
	else if (flags & KBD_FLAGS_RELEASE)
	{
		kbdEvent = CGEventCreateKeyboardEvent(source, (CGKeyCode)keycode, FALSE);
		CGEventPost(kCGHIDEventTap, kbdEvent);
		CFRelease(kbdEvent);
	}

	CFRelease(source);
	return TRUE;
}

static BOOL mac_shadow_input_unicode_keyboard_event(rdpShadowSubsystem* subsystem,
                                                    rdpShadowClient* client, UINT16 flags,
                                                    UINT16 code)
{
	if (!subsystem || !client)
		return FALSE;

	return TRUE;
}

static BOOL mac_shadow_input_mouse_event(rdpShadowSubsystem* subsystem, rdpShadowClient* client,
                                         UINT16 flags, UINT16 x, UINT16 y)
{
	macShadowSubsystem* mac = (macShadowSubsystem*)subsystem;
	UINT32 scrollX = 0;
	UINT32 scrollY = 0;
	CGWheelCount wheelCount = 2;

	if (!subsystem || !client)
		return FALSE;

	if (flags & PTR_FLAGS_WHEEL)
	{
		scrollY = flags & WheelRotationMask;

		if (flags & PTR_FLAGS_WHEEL_NEGATIVE)
		{
			scrollY = -(flags & WheelRotationMask) / 392;
		}
		else
		{
			scrollY = (flags & WheelRotationMask) / 120;
		}

		CGEventSourceRef source = CGEventSourceCreate(kCGEventSourceStateHIDSystemState);
		CGEventRef scroll = CGEventCreateScrollWheelEvent(source, kCGScrollEventUnitLine,
		                                                  wheelCount, scrollY, scrollX);
		CGEventPost(kCGHIDEventTap, scroll);
		CFRelease(scroll);
		CFRelease(source);
	}
	else
	{
		CGEventSourceRef source = CGEventSourceCreate(kCGEventSourceStateHIDSystemState);
		CGEventType mouseType = kCGEventNull;
		CGMouseButton mouseButton = kCGMouseButtonLeft;

		if (flags & PTR_FLAGS_MOVE)
		{
			if (mac->mouseDownLeft)
				mouseType = kCGEventLeftMouseDragged;
			else if (mac->mouseDownRight)
				mouseType = kCGEventRightMouseDragged;
			else if (mac->mouseDownOther)
				mouseType = kCGEventOtherMouseDragged;
			else
				mouseType = kCGEventMouseMoved;

			CGEventRef move =
			    CGEventCreateMouseEvent(source, mouseType, CGPointMake(x, y), mouseButton);
			CGEventPost(kCGHIDEventTap, move);
			CFRelease(move);
		}

		if (flags & PTR_FLAGS_BUTTON1)
		{
			mouseButton = kCGMouseButtonLeft;

			if (flags & PTR_FLAGS_DOWN)
			{
				mouseType = kCGEventLeftMouseDown;
				mac->mouseDownLeft = TRUE;
			}
			else
			{
				mouseType = kCGEventLeftMouseUp;
				mac->mouseDownLeft = FALSE;
			}
		}
		else if (flags & PTR_FLAGS_BUTTON2)
		{
			mouseButton = kCGMouseButtonRight;

			if (flags & PTR_FLAGS_DOWN)
			{
				mouseType = kCGEventRightMouseDown;
				mac->mouseDownRight = TRUE;
			}
			else
			{
				mouseType = kCGEventRightMouseUp;
				mac->mouseDownRight = FALSE;
			}
		}
		else if (flags & PTR_FLAGS_BUTTON3)
		{
			mouseButton = kCGMouseButtonCenter;

			if (flags & PTR_FLAGS_DOWN)
			{
				mouseType = kCGEventOtherMouseDown;
				mac->mouseDownOther = TRUE;
			}
			else
			{
				mouseType = kCGEventOtherMouseUp;
				mac->mouseDownOther = FALSE;
			}
		}

		CGEventRef mouseEvent =
		    CGEventCreateMouseEvent(source, mouseType, CGPointMake(x, y), mouseButton);
		CGEventPost(kCGHIDEventTap, mouseEvent);
		CFRelease(mouseEvent);
		CFRelease(source);
	}

	return TRUE;
}

static BOOL mac_shadow_input_extended_mouse_event(rdpShadowSubsystem* subsystem,
                                                  rdpShadowClient* client, UINT16 flags, UINT16 x,
                                                  UINT16 y)
{
	if (!subsystem || !client)
		return FALSE;

	return TRUE;
}

static int mac_shadow_detect_monitors(macShadowSubsystem* subsystem)
{
	size_t wide, high;
	MONITOR_DEF* monitor;
	CGDirectDisplayID displayId;
	displayId = CGMainDisplayID();
	CGDisplayModeRef mode = CGDisplayCopyDisplayMode(displayId);
	subsystem->pixelWidth = CGDisplayModeGetPixelWidth(mode);
	subsystem->pixelHeight = CGDisplayModeGetPixelHeight(mode);
	wide = CGDisplayPixelsWide(displayId);
	high = CGDisplayPixelsHigh(displayId);
	CGDisplayModeRelease(mode);
	subsystem->retina = ((subsystem->pixelWidth / wide) == 2) ? TRUE : FALSE;

	if (subsystem->retina)
	{
		subsystem->width = wide;
		subsystem->height = high;
	}
	else
	{
		subsystem->width = subsystem->pixelWidth;
		subsystem->height = subsystem->pixelHeight;
	}

	subsystem->common.numMonitors = 1;
	monitor = &(subsystem->common.monitors[0]);
	monitor->left = 0;
	monitor->top = 0;
	monitor->right = subsystem->width;
	monitor->bottom = subsystem->height;
	monitor->flags = 1;
	return 1;
}

static int mac_shadow_capture_start(macShadowSubsystem* subsystem)
{
	CGError err;
	err = CGDisplayStreamStart(subsystem->stream);

	if (err != kCGErrorSuccess)
		return -1;

	return 1;
}

static int mac_shadow_capture_stop(macShadowSubsystem* subsystem)
{
	CGError err;
	err = CGDisplayStreamStop(subsystem->stream);

	if (err != kCGErrorSuccess)
		return -1;

	return 1;
}

static int mac_shadow_capture_get_dirty_region(macShadowSubsystem* subsystem)
{
	size_t numRects;
	const CGRect* rects;
	RECTANGLE_16 invalidRect;
	rdpShadowSurface* surface = subsystem->common.server->surface;
	rects = CGDisplayStreamUpdateGetRects(subsystem->lastUpdate, kCGDisplayStreamUpdateDirtyRects,
	                                      &numRects);

	if (!numRects)
		return -1;

	for (size_t index = 0; index < numRects; index++)
	{
		invalidRect.left = (UINT16)rects[index].origin.x;
		invalidRect.top = (UINT16)rects[index].origin.y;
		invalidRect.right = invalidRect.left + (UINT16)rects[index].size.width;
		invalidRect.bottom = invalidRect.top + (UINT16)rects[index].size.height;

		if (subsystem->retina)
		{
			/* scale invalid rect */
			invalidRect.left /= 2;
			invalidRect.top /= 2;
			invalidRect.right /= 2;
			invalidRect.bottom /= 2;
		}

		region16_union_rect(&(surface->invalidRegion), &(surface->invalidRegion), &invalidRect);
	}

	return 0;
}

static int freerdp_image_copy_from_retina(BYTE* pDstData, DWORD DstFormat, int nDstStep, int nXDst,
                                          int nYDst, int nWidth, int nHeight, BYTE* pSrcData,
                                          int nSrcStep, int nXSrc, int nYSrc)
{
	BYTE* pSrcPixel;
	BYTE* pDstPixel;
	int nSrcPad;
	int nDstPad;
	int srcBitsPerPixel;
	int srcBytesPerPixel;
	int dstBitsPerPixel;
	int dstBytesPerPixel;
	srcBitsPerPixel = 24;
	srcBytesPerPixel = 8;

	if (nSrcStep < 0)
		nSrcStep = srcBytesPerPixel * nWidth;

	dstBitsPerPixel = FreeRDPGetBitsPerPixel(DstFormat);
	dstBytesPerPixel = FreeRDPGetBytesPerPixel(DstFormat);

	if (nDstStep < 0)
		nDstStep = dstBytesPerPixel * nWidth;

	nSrcPad = (nSrcStep - (nWidth * srcBytesPerPixel));
	nDstPad = (nDstStep - (nWidth * dstBytesPerPixel));
	pSrcPixel = &pSrcData[(nYSrc * nSrcStep) + (nXSrc * 4)];
	pDstPixel = &pDstData[(nYDst * nDstStep) + (nXDst * 4)];

	for (int y = 0; y < nHeight; y++)
	{
		for (int x = 0; x < nWidth; x++)
		{
			UINT32 R, G, B;
			UINT32 color;
			/* simple box filter scaling, could be improved with better algorithm */
			B = pSrcPixel[0] + pSrcPixel[4] + pSrcPixel[nSrcStep + 0] + pSrcPixel[nSrcStep + 4];
			G = pSrcPixel[1] + pSrcPixel[5] + pSrcPixel[nSrcStep + 1] + pSrcPixel[nSrcStep + 5];
			R = pSrcPixel[2] + pSrcPixel[6] + pSrcPixel[nSrcStep + 2] + pSrcPixel[nSrcStep + 6];
			pSrcPixel += 8;
			color = FreeRDPGetColor(DstFormat, R >> 2, G >> 2, B >> 2, 0xFF);
			FreeRDPWriteColor(pDstPixel, DstFormat, color);
			pDstPixel += dstBytesPerPixel;
		}

		pSrcPixel = &pSrcPixel[nSrcPad + nSrcStep];
		pDstPixel = &pDstPixel[nDstPad];
	}

	return 1;
}

static void (^mac_capture_stream_handler)(
    CGDisplayStreamFrameStatus, uint64_t, IOSurfaceRef,
    CGDisplayStreamUpdateRef) = ^(CGDisplayStreamFrameStatus status, uint64_t displayTime,
                                  IOSurfaceRef frameSurface, CGDisplayStreamUpdateRef updateRef) {
  int x, y;
  int count;
  int width;
  int height;
  int nSrcStep;
  BOOL empty;
  BYTE* pSrcData;
  RECTANGLE_16 surfaceRect;
  const RECTANGLE_16* extents;
  macShadowSubsystem* subsystem = g_Subsystem;
  rdpShadowServer* server = subsystem->common.server;
  rdpShadowSurface* surface = server->surface;
  count = ArrayList_Count(server->clients);

  if (count < 1)
	  return;

  EnterCriticalSection(&(surface->lock));
  mac_shadow_capture_get_dirty_region(subsystem);
  surfaceRect.left = 0;
  surfaceRect.top = 0;
  surfaceRect.right = surface->width;
  surfaceRect.bottom = surface->height;
  region16_intersect_rect(&(surface->invalidRegion), &(surface->invalidRegion), &surfaceRect);
  empty = region16_is_empty(&(surface->invalidRegion));
  LeaveCriticalSection(&(surface->lock));

  if (!empty)
  {
	  extents = region16_extents(&(surface->invalidRegion));
	  x = extents->left;
	  y = extents->top;
	  width = extents->right - extents->left;
	  height = extents->bottom - extents->top;
	  IOSurfaceLock(frameSurface, kIOSurfaceLockReadOnly, nullptr);
	  pSrcData = (BYTE*)IOSurfaceGetBaseAddress(frameSurface);
	  nSrcStep = (int)IOSurfaceGetBytesPerRow(frameSurface);

	  if (subsystem->retina)
	  {
		  freerdp_image_copy_from_retina(surface->data, surface->format, surface->scanline, x, y,
			                             width, height, pSrcData, nSrcStep, x, y);
	  }
	  else
	  {
		  freerdp_image_copy_no_overlap(surface->data, surface->format, surface->scanline, x, y,
			                            width, height, pSrcData, PIXEL_FORMAT_BGRX32, nSrcStep, x,
			                            y, nullptr, FREERDP_FLIP_NONE);
	  }
	  LeaveCriticalSection(&(surface->lock));

	  IOSurfaceUnlock(frameSurface, kIOSurfaceLockReadOnly, nullptr);
	  ArrayList_Lock(server->clients);
	  count = ArrayList_Count(server->clients);
	  shadow_subsystem_frame_update(&subsystem->common);

	  if (count == 1)
	  {
		  rdpShadowClient* client;
		  client = (rdpShadowClient*)ArrayList_GetItem(server->clients, 0);

		  if (client)
		  {
			  subsystem->common.captureFrameRate = shadow_encoder_preferred_fps(client->encoder);
		  }
	  }

	  ArrayList_Unlock(server->clients);
	  EnterCriticalSection(&(surface->lock));
	  region16_clear(&(surface->invalidRegion));
	  LeaveCriticalSection(&(surface->lock));
  }

  if (status != kCGDisplayStreamFrameStatusFrameComplete)
  {
	  switch (status)
	  {
		  case kCGDisplayStreamFrameStatusFrameIdle:
			  break;

		  case kCGDisplayStreamFrameStatusStopped:
			  break;

		  case kCGDisplayStreamFrameStatusFrameBlank:
			  break;

		  default:
			  break;
	  }
  }
  else if (!subsystem->lastUpdate)
  {
	  CFRetain(updateRef);
	  subsystem->lastUpdate = updateRef;
  }
  else
  {
	  CGDisplayStreamUpdateRef tmpRef = subsystem->lastUpdate;
	  subsystem->lastUpdate = CGDisplayStreamUpdateCreateMergedUpdate(tmpRef, updateRef);
	  CFRelease(tmpRef);
  }
};

static int mac_shadow_capture_init(macShadowSubsystem* subsystem)
{
	void* keys[2];
	void* values[2];
	CFDictionaryRef opts;
	CGDirectDisplayID displayId;
	displayId = CGMainDisplayID();
	subsystem->captureQueue = dispatch_queue_create("mac.shadow.capture", nullptr);
	keys[0] = (void*)kCGDisplayStreamShowCursor;
	values[0] = (void*)kCFBooleanFalse;
	opts = CFDictionaryCreate(kCFAllocatorDefault, (const void**)keys, (const void**)values, 1,
	                          nullptr, nullptr);
	subsystem->stream = CGDisplayStreamCreateWithDispatchQueue(
	    displayId, subsystem->pixelWidth, subsystem->pixelHeight, 'BGRA', opts,
	    subsystem->captureQueue, mac_capture_stream_handler);
	CFRelease(opts);
	return 1;
}

static int mac_shadow_screen_grab(macShadowSubsystem* subsystem)
{
	return 1;
}

static int mac_shadow_subsystem_process_message(macShadowSubsystem* subsystem, wMessage* message)
{
	rdpShadowServer* server = subsystem->common.server;
	rdpShadowSurface* surface = server->surface;

	switch (message->id)
	{
		case SHADOW_MSG_IN_REFRESH_REQUEST_ID:
			EnterCriticalSection(&(surface->lock));
			shadow_subsystem_frame_update((rdpShadowSubsystem*)subsystem);
			LeaveCriticalSection(&(surface->lock));
			break;

		default:
			WLog_ERR(TAG, "Unknown message id: %" PRIu32 "", message->id);
			break;
	}

	if (message->Free)
		message->Free(message);

	return 1;
}

static DWORD WINAPI mac_shadow_subsystem_thread(LPVOID arg)
{
	macShadowSubsystem* subsystem = (macShadowSubsystem*)arg;
	DWORD status;
	DWORD nCount;
	UINT64 cTime;
	DWORD dwTimeout;
	DWORD dwInterval;
	UINT64 frameTime;
	HANDLE events[32];
	wMessage message;
	wMessagePipe* MsgPipe;
	MsgPipe = subsystem->common.MsgPipe;
	nCount = 0;
	events[nCount++] = MessageQueue_Event(MsgPipe->In);
	subsystem->common.captureFrameRate = 16;
	dwInterval = 1000 / subsystem->common.captureFrameRate;
	frameTime = GetTickCount64() + dwInterval;

	while (1)
	{
		cTime = GetTickCount64();
		dwTimeout = (cTime > frameTime) ? 0 : frameTime - cTime;
		status = WaitForMultipleObjects(nCount, events, FALSE, dwTimeout);

		if (WaitForSingleObject(MessageQueue_Event(MsgPipe->In), 0) == WAIT_OBJECT_0)
		{
			if (MessageQueue_Peek(MsgPipe->In, &message, TRUE))
			{
				if (message.id == WMQ_QUIT)
					break;

				mac_shadow_subsystem_process_message(subsystem, &message);
			}
		}

		if ((status == WAIT_TIMEOUT) || (GetTickCount64() > frameTime))
		{
			mac_shadow_screen_grab(subsystem);
			dwInterval = 1000 / subsystem->common.captureFrameRate;
			frameTime += dwInterval;
		}
	}

	ExitThread(0);
	return 0;
}

static UINT32 mac_shadow_enum_monitors(MONITOR_DEF* monitors, UINT32 maxMonitors)
{
	int index;
	size_t wide, high;
	UINT32 numMonitors = 0;
	MONITOR_DEF* monitor;
	CGDirectDisplayID displayId;
	displayId = CGMainDisplayID();
	CGDisplayModeRef mode = CGDisplayCopyDisplayMode(displayId);
	wide = CGDisplayPixelsWide(displayId);
	high = CGDisplayPixelsHigh(displayId);
	CGDisplayModeRelease(mode);
	index = 0;
	numMonitors = 1;
	monitor = &monitors[index];
	monitor->left = 0;
	monitor->top = 0;
	monitor->right = (int)wide;
	monitor->bottom = (int)high;
	monitor->flags = 1;
	return numMonitors;
}

static int mac_shadow_subsystem_init(rdpShadowSubsystem* rdpsubsystem)
{
	macShadowSubsystem* subsystem = (macShadowSubsystem*)rdpsubsystem;
	g_Subsystem = subsystem;

	mac_shadow_detect_monitors(subsystem);
	mac_shadow_capture_init(subsystem);
	return 1;
}

static int mac_shadow_subsystem_uninit(rdpShadowSubsystem* rdpsubsystem)
{
	macShadowSubsystem* subsystem = (macShadowSubsystem*)rdpsubsystem;
	if (!subsystem)
		return -1;

	if (subsystem->lastUpdate)
	{
		CFRelease(subsystem->lastUpdate);
		subsystem->lastUpdate = nullptr;
	}

	return 1;
}

static int mac_shadow_subsystem_start(rdpShadowSubsystem* rdpsubsystem)
{
	macShadowSubsystem* subsystem = (macShadowSubsystem*)rdpsubsystem;
	HANDLE thread;

	if (!subsystem)
		return -1;

	mac_shadow_capture_start(subsystem);

	if (!(thread =
	          CreateThread(nullptr, 0, mac_shadow_subsystem_thread, (void*)subsystem, 0, nullptr)))
	{
		WLog_ERR(TAG, "Failed to create thread");
		return -1;
	}

	return 1;
}

static int mac_shadow_subsystem_stop(rdpShadowSubsystem* subsystem)
{
	if (!subsystem)
		return -1;

	return 1;
}

static void mac_shadow_subsystem_free(rdpShadowSubsystem* subsystem)
{
	if (!subsystem)
		return;

	mac_shadow_subsystem_uninit(subsystem);
	free(subsystem);
}

static rdpShadowSubsystem* mac_shadow_subsystem_new(void)
{
	macShadowSubsystem* subsystem = calloc(1, sizeof(macShadowSubsystem));

	if (!subsystem)
		return nullptr;

	subsystem->common.SynchronizeEvent = mac_shadow_input_synchronize_event;
	subsystem->common.KeyboardEvent = mac_shadow_input_keyboard_event;
	subsystem->common.UnicodeKeyboardEvent = mac_shadow_input_unicode_keyboard_event;
	subsystem->common.MouseEvent = mac_shadow_input_mouse_event;
	subsystem->common.ExtendedMouseEvent = mac_shadow_input_extended_mouse_event;
	return &subsystem->common;
}

FREERDP_API const char* ShadowSubsystemName(void)
{
	return "Mac";
}

FREERDP_API int ShadowSubsystemEntry(RDP_SHADOW_ENTRY_POINTS* pEntryPoints)
{
	char name[] = "mac shadow subsystem";
	char* arg[] = { name };

	freerdp_server_warn_unmaintained(ARRAYSIZE(arg), arg);
	pEntryPoints->New = mac_shadow_subsystem_new;
	pEntryPoints->Free = mac_shadow_subsystem_free;
	pEntryPoints->Init = mac_shadow_subsystem_init;
	pEntryPoints->Uninit = mac_shadow_subsystem_uninit;
	pEntryPoints->Start = mac_shadow_subsystem_start;
	pEntryPoints->Stop = mac_shadow_subsystem_stop;
	pEntryPoints->EnumMonitors = mac_shadow_enum_monitors;
	return 1;
}
