/**
 * FreeRDP: A Remote Desktop Protocol Implementation
 * X11 Monitor Handling
 *
 * Copyright 2011 Marc-Andre Moreau <marcandre.moreau@gmail.com>
 * Copyright 2017 David Fort <contact@hardening-consulting.com>
 * Copyright 2018 Kai Harms <kharms@rangee.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.
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <X11/Xlib.h>
#include <X11/Xutil.h>

#include <winpr/crt.h>

#include <freerdp/log.h>

#define TAG CLIENT_TAG("x11")

#ifdef WITH_XINERAMA
#include <X11/extensions/Xinerama.h>
#endif

#ifdef WITH_XRANDR
#include <X11/extensions/Xrandr.h>
#include <X11/extensions/randr.h>

#if (RANDR_MAJOR * 100 + RANDR_MINOR) >= 105
#	define USABLE_XRANDR
#endif

#endif

#include "xf_monitor.h"

/* See MSDN Section on Multiple Display Monitors: http://msdn.microsoft.com/en-us/library/dd145071 */

int xf_list_monitors(xfContext* xfc)
{
	Display* display;
	int major, minor;
	int i, nmonitors = 0;
	display = XOpenDisplay(NULL);

	if (!display)
	{
		WLog_ERR(TAG, "failed to open X display");
		return -1;
	}

#if defined(USABLE_XRANDR)

	if (XRRQueryExtension(xfc->display, &major, &minor) &&
	    (XRRQueryVersion(xfc->display, &major, &minor) == True) &&
	    (major * 100 + minor >= 105))
	{
		XRRMonitorInfo* monitors = XRRGetMonitors(xfc->display, DefaultRootWindow(xfc->display), 1,
		                           &nmonitors);

		for (i = 0; i < nmonitors; i++)
		{
			printf("      %s [%d] %dx%d\t+%d+%d\n",
			       monitors[i].primary ? "*" : " ", i,
			       monitors[i].width, monitors[i].height,
			       monitors[i].x, monitors[i].y);
		}

		XRRFreeMonitors(monitors);
	}
	else
#endif
#ifdef WITH_XINERAMA
		if (XineramaQueryExtension(display, &major, &minor))
		{
			if (XineramaIsActive(display))
			{
				XineramaScreenInfo* screen = XineramaQueryScreens(display, &nmonitors);

				for (i = 0; i < nmonitors; i++)
				{
					printf("      %s [%d] %hdx%hd\t+%hd+%hd\n",
					       (i == 0) ? "*" : " ", i,
					       screen[i].width, screen[i].height,
					       screen[i].x_org, screen[i].y_org);
				}

				XFree(screen);
			}
		}
		else
#else
	{
		Screen* screen = ScreenOfDisplay(display, DefaultScreen(display));
		printf("      * [0] %dx%d\t+0+0\n", WidthOfScreen(screen), HeightOfScreen(screen));
	}

#endif
			XCloseDisplay(display);
	return 0;
}

static BOOL xf_is_monitor_id_active(xfContext* xfc, UINT32 id)
{
	int index;
	rdpSettings* settings = xfc->context.settings;

	if (!settings->NumMonitorIds)
		return TRUE;

	for (index = 0; index < settings->NumMonitorIds; index++)
	{
		if (settings->MonitorIds[index] == id)
			return TRUE;
	}

	return FALSE;
}

BOOL xf_detect_monitors(xfContext* xfc, UINT32* pMaxWidth, UINT32* pMaxHeight)
{
	UINT32 i;
	int nmonitors = 0;
	int monitor_index = 0;
	BOOL primaryMonitorFound = FALSE;
	VIRTUAL_SCREEN* vscreen;
	rdpSettings* settings;
	int mouse_x, mouse_y, _dummy_i;
	Window _dummy_w;
	int current_monitor = 0;
	Screen* screen;
	MONITOR_INFO* monitor;
#if defined WITH_XINERAMA || defined WITH_XRANDR
	int major, minor;
#endif
#if defined(USABLE_XRANDR)
	XRRMonitorInfo* rrmonitors = NULL;
	BOOL useXRandr = FALSE;
#endif
	
	if (!xfc || !pMaxWidth || !pMaxHeight || !xfc->context.settings)
		return FALSE;

	settings = xfc->context.settings;
	vscreen = &xfc->vscreen;
	*pMaxWidth = settings->DesktopWidth;
	*pMaxHeight = settings->DesktopHeight;

	/* get mouse location */
	if (!XQueryPointer(xfc->display, DefaultRootWindow(xfc->display),
	                   &_dummy_w, &_dummy_w, &mouse_x, &mouse_y,
	                   &_dummy_i, &_dummy_i, (void*) &_dummy_i))
		mouse_x = mouse_y = 0;

#if defined(USABLE_XRANDR)

	if (XRRQueryExtension(xfc->display, &major, &minor) &&
	    (XRRQueryVersion(xfc->display, &major, &minor) == True) &&
	    (major * 100 + minor >= 105))
	{
		XRRMonitorInfo* rrmonitors = XRRGetMonitors(xfc->display, DefaultRootWindow(xfc->display), 1,
		                             &vscreen->nmonitors);

		if (vscreen->nmonitors > 16)
			vscreen->nmonitors = 0;

		if (vscreen->nmonitors)
		{
			for (i = 0; i < vscreen->nmonitors; i++)
			{
				vscreen->monitors[i].area.left = rrmonitors[i].x;
				vscreen->monitors[i].area.top = rrmonitors[i].y;
				vscreen->monitors[i].area.right = rrmonitors[i].x + rrmonitors[i].width - 1;
				vscreen->monitors[i].area.bottom = rrmonitors[i].y + rrmonitors[i].height - 1;
				vscreen->monitors[i].primary = rrmonitors[i].primary > 0;
			}
		}

		XRRFreeMonitors(rrmonitors);
		useXRandr = TRUE;
	}
	else
#endif
#ifdef WITH_XINERAMA
		if (XineramaQueryExtension(xfc->display, &major, &minor) && XineramaIsActive(xfc->display))
		{
			XineramaScreenInfo* screenInfo = XineramaQueryScreens(xfc->display, &vscreen->nmonitors);

			if (vscreen->nmonitors > 16)
				vscreen->nmonitors = 0;

			if (vscreen->nmonitors)
			{
				for (i = 0; i < vscreen->nmonitors; i++)
				{
					vscreen->monitors[i].area.left = screenInfo[i].x_org;
					vscreen->monitors[i].area.top = screenInfo[i].y_org;
					vscreen->monitors[i].area.right = screenInfo[i].x_org + screenInfo[i].width - 1;
					vscreen->monitors[i].area.bottom = screenInfo[i].y_org + screenInfo[i].height - 1;
				}
			}

			XFree(screenInfo);
		}

#endif
	xfc->fullscreenMonitors.top = xfc->fullscreenMonitors.bottom =
	                                  xfc->fullscreenMonitors.left = xfc->fullscreenMonitors.right = 0;

	/* Determine which monitor that the mouse cursor is on */
	if (vscreen->monitors)
	{
		for (i = 0; i < vscreen->nmonitors; i++)
		{
			if ((mouse_x >= vscreen->monitors[i].area.left) &&
			    (mouse_x <= vscreen->monitors[i].area.right) &&
			    (mouse_y >= vscreen->monitors[i].area.top) &&
			    (mouse_y <= vscreen->monitors[i].area.bottom))
			{
				current_monitor = i;
				break;
			}
		}
	}

	/*
	   Even for a single monitor, we need to calculate the virtual screen to support
	   window managers that do not implement all X window state hints.

	   If the user did not request multiple monitor or is using workarea
	   without remote app, we force the number of monitors be 1 so later
	   the rest of the client don't end up using more monitors than the user desires.
	 */
	if ((!settings->UseMultimon && !settings->SpanMonitors) ||
	    (settings->Workarea && !settings->RemoteApplicationMode))
	{
		/* If no monitors were specified on the command-line then set the current monitor as active */
		if (!settings->NumMonitorIds)
		{
			settings->MonitorIds[0] = current_monitor;
		}

		/* Always sets number of monitors from command-line to just 1.
		 * If the monitor is invalid then we will default back to current monitor
		 * later as a fallback. So, there is no need to validate command-line entry here.
		 */
		settings->NumMonitorIds = 1;
	}

	/* WORKAROUND: With Remote Application Mode - using NET_WM_WORKAREA
	 * causes issues with the ability to fully size the window vertically
	 * (the bottom of the window area is never updated). So, we just set
	 * the workArea to match the full Screen width/height.
	 */
	if (settings->RemoteApplicationMode || !xf_GetWorkArea(xfc))
	{
		/*
		   if only 1 monitor is enabled, use monitor area
		   this is required in case of a screen composed of more than one monitor
		   but user did not enable multimonitor
		*/
		if (settings->NumMonitorIds == 1)
		{
			monitor = vscreen->monitors + current_monitor;
			if (!monitor)
				return FALSE;

			xfc->workArea.x = monitor->area.left;
			xfc->workArea.y = monitor->area.top;
			xfc->workArea.width = monitor->area.right - monitor->area.left + 1;
			xfc->workArea.height = monitor->area.bottom - monitor->area.top + 1;
		}
		else
		{
			xfc->workArea.x = 0;
			xfc->workArea.y = 0;
			xfc->workArea.width = WidthOfScreen(xfc->screen);
			xfc->workArea.height = HeightOfScreen(xfc->screen);
		}
	}

	if (settings->Fullscreen)
	{
		*pMaxWidth = WidthOfScreen(xfc->screen);
		*pMaxHeight = HeightOfScreen(xfc->screen);
	}
	else if (settings->Workarea)
	{
		*pMaxWidth = xfc->workArea.width;
		*pMaxHeight = xfc->workArea.height;
	}
	else if (settings->PercentScreen)
	{
		/* If we have specific monitor information then limit the PercentScreen value
		 * to only affect the current monitor vs. the entire desktop
		 */
		if (vscreen->nmonitors > 0)
		{
			if (!vscreen->monitors)
				return FALSE;

			*pMaxWidth = vscreen->monitors[current_monitor].area.right -
			             vscreen->monitors[current_monitor].area.left + 1;
			*pMaxHeight = vscreen->monitors[current_monitor].area.bottom -
			              vscreen->monitors[current_monitor].area.top + 1;

			if (settings->PercentScreenUseWidth)
				*pMaxWidth = ((vscreen->monitors[current_monitor].area.right -
				               vscreen->monitors[current_monitor].area.left + 1) * settings->PercentScreen) /
				             100;

			if (settings->PercentScreenUseHeight)
				*pMaxHeight = ((vscreen->monitors[current_monitor].area.bottom -
				                vscreen->monitors[current_monitor].area.top + 1) * settings->PercentScreen) /
				              100;
		}
		else
		{
			*pMaxWidth = xfc->workArea.width;
			*pMaxHeight = xfc->workArea.height;

			if (settings->PercentScreenUseWidth)
				*pMaxWidth = (xfc->workArea.width * settings->PercentScreen) / 100;

			if (settings->PercentScreenUseHeight)
				*pMaxHeight = (xfc->workArea.height * settings->PercentScreen) / 100;
		}
	}
	else if (settings->DesktopWidth && settings->DesktopHeight)
	{
		*pMaxWidth = settings->DesktopWidth;
		*pMaxHeight = settings->DesktopHeight;
	}

	/* Create array of all active monitors by taking into account monitors requested on the command-line */
	for (i = 0; i < vscreen->nmonitors; i++)
	{
		MONITOR_ATTRIBUTES* attrs;

		if (!xf_is_monitor_id_active(xfc, i))
			continue;

		if (!vscreen->monitors)
			return FALSE;

		settings->MonitorDefArray[nmonitors].x = vscreen->monitors[i].area.left;
		settings->MonitorDefArray[nmonitors].y = vscreen->monitors[i].area.top;
		settings->MonitorDefArray[nmonitors].width =
		    vscreen->monitors[i].area.right - vscreen->monitors[i].area.left + 1;
		settings->MonitorDefArray[nmonitors].height =
		    vscreen->monitors[i].area.bottom - vscreen->monitors[i].area.top + 1;
		settings->MonitorDefArray[nmonitors].orig_screen = i;
#ifdef USABLE_XRANDR

		if (useXRandr && rrmonitors)
		{
			Rotation rot, ret;
			attrs = &settings->MonitorDefArray[nmonitors].attributes;
			attrs->physicalWidth = rrmonitors[i].mwidth;
			attrs->physicalHeight = rrmonitors[i].mheight;
			ret = XRRRotations(xfc->display, i, &rot);
			attrs->orientation = rot;
		}

#endif

		if (i == settings->MonitorIds[0])
		{
			settings->MonitorDefArray[nmonitors].is_primary = TRUE;
			settings->MonitorLocalShiftX = settings->MonitorDefArray[nmonitors].x;
			settings->MonitorLocalShiftY = settings->MonitorDefArray[nmonitors].y;
			primaryMonitorFound = TRUE;
		}

		nmonitors++;
	}

	/* If no monitor is active(bogus command-line monitor specification) - then lets try to fallback to go fullscreen on the current monitor only */
	if (nmonitors == 0 && vscreen->nmonitors > 0)
	{	
		if (!vscreen->monitors)
			return FALSE;

		settings->MonitorDefArray[0].x = vscreen->monitors[current_monitor].area.left;
		settings->MonitorDefArray[0].y = vscreen->monitors[current_monitor].area.top;
		settings->MonitorDefArray[0].width = MIN(
		        vscreen->monitors[current_monitor].area.right -
		        vscreen->monitors[current_monitor].area.left + 1, *pMaxWidth);
		settings->MonitorDefArray[0].height = MIN(
		        vscreen->monitors[current_monitor].area.bottom -
		        vscreen->monitors[current_monitor].area.top + 1, *pMaxHeight);
		settings->MonitorDefArray[0].orig_screen = current_monitor;
		nmonitors = 1;
	}

	settings->MonitorCount = nmonitors;

	/* If we have specific monitor information */
	if (settings->MonitorCount)
	{
		/* Initialize bounding rectangle for all monitors */
		int vX = settings->MonitorDefArray[0].x;
		int vY = settings->MonitorDefArray[0].y;
		int vR = vX + settings->MonitorDefArray[0].width;
		int vB = vY + settings->MonitorDefArray[0].height;
		xfc->fullscreenMonitors.top = xfc->fullscreenMonitors.bottom =
		                                  xfc->fullscreenMonitors.left = xfc->fullscreenMonitors.right =
		                                          settings->MonitorDefArray[0].orig_screen;

		/* Calculate bounding rectangle around all monitors to be used AND
		 * also set the Xinerama indices which define left/top/right/bottom monitors.
		 */
		for (i = 1; i < settings->MonitorCount; i++)
		{
			/* does the same as gdk_rectangle_union */
			int destX = MIN(vX, settings->MonitorDefArray[i].x);
			int destY = MIN(vY, settings->MonitorDefArray[i].y);
			int destR = MAX(vR, settings->MonitorDefArray[i].x +
			                settings->MonitorDefArray[i].width);
			int destB = MAX(vB, settings->MonitorDefArray[i].y +
			                settings->MonitorDefArray[i].height);

			if (vX != destX)
				xfc->fullscreenMonitors.left = settings->MonitorDefArray[i].orig_screen;

			if (vY != destY)
				xfc->fullscreenMonitors.top = settings->MonitorDefArray[i].orig_screen;

			if (vR != destR)
				xfc->fullscreenMonitors.right = settings->MonitorDefArray[i].orig_screen;

			if (vB != destB)
				xfc->fullscreenMonitors.bottom = settings->MonitorDefArray[i].orig_screen;

			vX = destX;
			vY = destY;
			vR = destR;
			vB = destB;
		}

		vscreen->area.left = 0;
		vscreen->area.right = vR - vX - 1;
		vscreen->area.top = 0;
		vscreen->area.bottom = vB - vY - 1;

		if (settings->Workarea)
		{
			vscreen->area.top = xfc->workArea.y;
			vscreen->area.bottom = xfc->workArea.height + xfc->workArea.y - 1;
		}

		if (!primaryMonitorFound)
		{
			/* If we have a command line setting we should use it */
			if (settings->NumMonitorIds)
			{
				/* The first monitor is the first in the setting which should be used */
				monitor_index =  settings->MonitorIds[0];
			}
			else
			{
				/* This is the same as when we would trust the Xinerama results..
				   and set the monitor index to zero.
				   The monitor listed with /monitor-list on index zero is always the primary
				*/
				screen = DefaultScreenOfDisplay(xfc->display);
				monitor_index = XScreenNumberOfScreen(screen);
			}

			int j = monitor_index;

			/* If the "default" monitor is not 0,0 use it */
			if (settings->MonitorDefArray[j].x != 0 || settings->MonitorDefArray[j].y != 0)
			{
				settings->MonitorDefArray[j].is_primary = TRUE;
				settings->MonitorLocalShiftX = settings->MonitorDefArray[j].x;
				settings->MonitorLocalShiftY = settings->MonitorDefArray[j].y;
				primaryMonitorFound = TRUE;
			}
			else
			{
				/* Lets try to see if there is a monitor with a 0,0 coordinate and use it as a fallback*/
				for (i = 0; i < settings->MonitorCount; i++)
				{
					if (!primaryMonitorFound && settings->MonitorDefArray[i].x == 0
					    && settings->MonitorDefArray[i].y == 0)
					{
						settings->MonitorDefArray[i].is_primary = TRUE;
						settings->MonitorLocalShiftX = settings->MonitorDefArray[i].x;
						settings->MonitorLocalShiftY = settings->MonitorDefArray[i].y;
						primaryMonitorFound = TRUE;
					}
				}
			}
		}

		/* Subtract monitor shift from monitor variables for server-side use.
		 * We maintain monitor shift value as Window requires the primary monitor to have a coordinate of 0,0
		 * In some X configurations, no monitor may have a coordinate of 0,0. This can also be happen if the user
		 * requests specific monitors from the command-line as well. So, we make sure to translate our primary monitor's
		 * upper-left corner to 0,0 on the server.
		 */
		for (i = 0; i < settings->MonitorCount; i++)
		{
			settings->MonitorDefArray[i].x = settings->MonitorDefArray[i].x -
			                                 settings->MonitorLocalShiftX;
			settings->MonitorDefArray[i].y = settings->MonitorDefArray[i].y -
			                                 settings->MonitorLocalShiftY;
		}

		/* Set the desktop width and height according to the bounding rectangle around the active monitors */
		*pMaxWidth = MIN(*pMaxWidth, vscreen->area.right - vscreen->area.left + 1);
		*pMaxHeight = MIN(*pMaxHeight, vscreen->area.bottom - vscreen->area.top + 1);
	}

	/* some 2008 server freeze at logon if we announce support for monitor layout PDU with
	 * #monitors < 2. So let's announce it only if we have more than 1 monitor.
	 */
	if (settings->MonitorCount)
		settings->SupportMonitorLayoutPdu = TRUE;

#ifdef USABLE_XRANDR

	if (rrmonitors)
		XRRFreeMonitors(rrmonitors);

#endif
	return TRUE;
}
