/*
 * Copyright (C) 2021 Pascal Nowack
 *
 * This program 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 2 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
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
 * 02111-1307, USA.
 */

#include "config.h"

#include "grd-rdp-monitor-config.h"

#include <cairo/cairo.h>

static uint32_t
sanitize_value (uint32_t value,
                uint32_t lower_bound,
                uint32_t upper_bound)
{
  g_assert (lower_bound < upper_bound);
  g_assert (lower_bound > 0);

  if (value < lower_bound || value > upper_bound)
    return 0;

  return value;
}

GrdRdpMonitorOrientation
transform_monitor_orientation (uint32_t value)
{
  switch (value)
    {
    case ORIENTATION_LANDSCAPE:
      return GRD_RDP_MONITOR_DIRECTION_LANDSCAPE;
    case ORIENTATION_PORTRAIT:
      return GRD_RDP_MONITOR_DIRECTION_PORTRAIT;
    case ORIENTATION_LANDSCAPE_FLIPPED:
      return GRD_RDP_MONITOR_DIRECTION_LANDSCAPE_FLIPPED;
    case ORIENTATION_PORTRAIT_FLIPPED:
      return GRD_RDP_MONITOR_DIRECTION_PORTRAIT_FLIPPED;
    default:
      return GRD_RDP_MONITOR_DIRECTION_LANDSCAPE;
    }

  g_assert_not_reached ();
}

static gboolean
write_sanitized_monitor_data (GrdRdpVirtualMonitor  *monitor,
                              int32_t                pos_x,
                              int32_t                pos_y,
                              uint32_t               width,
                              uint32_t               height,
                              gboolean               is_primary,
                              uint32_t               physical_width,
                              uint32_t               physical_height,
                              uint32_t               orientation,
                              uint32_t               scale,
                              GError               **error)
{
  if (width % 2 ||
      width < 200 || height < 200 ||
      width > 8192 || height > 8192)
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
                   "Invalid monitor dimensions");
      return FALSE;
    }

  monitor->pos_x = pos_x;
  monitor->pos_y = pos_y;
  monitor->width = width;
  monitor->height = height;
  monitor->is_primary = is_primary;

  monitor->physical_width = sanitize_value (physical_width, 10, 10000);
  monitor->physical_height = sanitize_value (physical_height, 10, 10000);
  if (monitor->physical_width == 0 || monitor->physical_height == 0)
    monitor->physical_width = monitor->physical_height = 0;

  monitor->orientation = transform_monitor_orientation (orientation);
  monitor->scale = sanitize_value (scale, 100, 500);

  return TRUE;
}

/**
 * Create a new monitor config from the Client Core Data data block
 * ([MS-RDPBCGR] 2.2.1.3.2 Client Core Data (TS_UD_CS_CORE))
 */
static GrdRdpMonitorConfig *
create_monitor_config_from_client_core_data (rdpSettings  *rdp_settings,
                                             GError      **error)
{
  g_autoptr (GrdRdpMonitorConfig) monitor_config = NULL;
  GrdRdpVirtualMonitor *virtual_monitor;
  uint32_t desktop_width =
    freerdp_settings_get_uint32 (rdp_settings, FreeRDP_DesktopWidth);
  uint32_t desktop_height =
    freerdp_settings_get_uint32 (rdp_settings, FreeRDP_DesktopHeight);
  uint32_t desktop_physical_width =
    freerdp_settings_get_uint32 (rdp_settings, FreeRDP_DesktopPhysicalWidth);
  uint32_t desktop_physical_height =
    freerdp_settings_get_uint32 (rdp_settings, FreeRDP_DesktopPhysicalHeight);
  uint32_t desktop_orientation =
    freerdp_settings_get_uint16 (rdp_settings, FreeRDP_DesktopOrientation);
  uint32_t desktop_scale_factor =
    freerdp_settings_get_uint32 (rdp_settings, FreeRDP_DesktopScaleFactor);

  monitor_config = g_malloc0 (sizeof (GrdRdpMonitorConfig));
  monitor_config->is_virtual = TRUE;
  monitor_config->monitor_count = 1;
  monitor_config->virtual_monitors = g_new0 (GrdRdpVirtualMonitor, 1);

  virtual_monitor = &monitor_config->virtual_monitors[0];

  /* Ignore the DeviceScaleFactor. It is deprecated (Win 8.1 only) */
  if (!write_sanitized_monitor_data (virtual_monitor,
                                     0, 0,
                                     desktop_width,
                                     desktop_height,
                                     TRUE,
                                     desktop_physical_width,
                                     desktop_physical_height,
                                     desktop_orientation,
                                     desktop_scale_factor,
                                     error))
    return NULL;

  monitor_config->desktop_width = virtual_monitor->width;
  monitor_config->desktop_height = virtual_monitor->height;

  return g_steal_pointer (&monitor_config);
}

static gboolean
determine_primary_monitor (GrdRdpMonitorConfig  *monitor_config,
                           GError              **error)
{
  uint32_t i;

  for (i = 0; i < monitor_config->monitor_count; ++i)
    {
      if (monitor_config->virtual_monitors[i].pos_x == 0 &&
          monitor_config->virtual_monitors[i].pos_y == 0)
        {
          monitor_config->virtual_monitors[i].is_primary = TRUE;
          return TRUE;
        }
    }

  g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
               "No suitable primary monitor in monitor layout");

  return FALSE;
}

static gboolean
verify_monitor_config (GrdRdpMonitorConfig  *monitor_config,
                       GError              **error)
{
  cairo_region_t *region;
  cairo_rectangle_int_t rect;
  uint32_t i;

  /* Check for overlapping monitors */
  region = cairo_region_create ();
  for (i = 0; i < monitor_config->monitor_count; ++i)
    {
      rect.x = monitor_config->virtual_monitors[i].pos_x;
      rect.y = monitor_config->virtual_monitors[i].pos_y;
      rect.width = monitor_config->virtual_monitors[i].width;
      rect.height = monitor_config->virtual_monitors[i].height;

      if (cairo_region_contains_rectangle (region, &rect) != CAIRO_REGION_OVERLAP_OUT)
        {
          g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
                       "Monitor overlaps other monitor in layout");
          cairo_region_destroy (region);
          return FALSE;
        }
      cairo_region_union_rectangle (region, &rect);
    }

  /* Calculate the size of the desktop and Graphics Output Buffer ADM element */
  cairo_region_get_extents (region, &rect);
  monitor_config->desktop_width = rect.width;
  monitor_config->desktop_height = rect.height;

  /* We already checked, that the primary monitor is at (0, 0) */
  g_assert (rect.x <= 0);
  g_assert (rect.y <= 0);

  /* Determine monitor offset in input- and output-region */
  monitor_config->layout_offset_x = rect.x;
  monitor_config->layout_offset_y = rect.y;
  cairo_region_destroy (region);

  return TRUE;
}

/**
 * Create a new monitor config from the Client Monitor Data packet
 * ([MS-RDPBCGR] 2.2.1.3.6 Client Monitor Data (TS_UD_CS_MONITOR))
 */
static GrdRdpMonitorConfig *
create_monitor_config_from_client_monitor_data (rdpSettings  *rdp_settings,
                                                GError      **error)
{
  g_autoptr (GrdRdpMonitorConfig) monitor_config = NULL;
  const rdpMonitor *monitors =
    freerdp_settings_get_pointer_array (rdp_settings, FreeRDP_MonitorDefArray, 0);
  uint32_t monitor_count =
    freerdp_settings_get_uint32 (rdp_settings, FreeRDP_MonitorCount);
  gboolean found_primary_monitor = FALSE;
  uint32_t i;

  g_assert (monitor_count > 0);

  monitor_config = g_malloc0 (sizeof (GrdRdpMonitorConfig));
  monitor_config->is_virtual = TRUE;
  monitor_config->monitor_count = monitor_count;
  monitor_config->virtual_monitors = g_new0 (GrdRdpVirtualMonitor,
                                             monitor_count);

  for (i = 0; i < monitor_count; ++i)
    {
      gboolean has_monitor_attributes =
        freerdp_settings_get_bool (rdp_settings, FreeRDP_HasMonitorAttributes);
      const rdpMonitor *monitor = &monitors[i];
      const MONITOR_ATTRIBUTES *monitor_attributes = &monitor->attributes;
      gboolean is_primary = !!monitor->is_primary;
      uint32_t physical_width = 0;
      uint32_t physical_height = 0;
      uint32_t orientation = 0;
      uint32_t scale = 0;

      if (found_primary_monitor || monitor->x != 0 || monitor->y != 0)
        is_primary = FALSE;
      if (!found_primary_monitor && is_primary)
        found_primary_monitor = TRUE;

      if (has_monitor_attributes)
        {
          physical_width = monitor_attributes->physicalWidth;
          physical_height = monitor_attributes->physicalHeight;
          orientation = monitor_attributes->orientation;
          scale = monitor_attributes->desktopScaleFactor;
        }

      /* Ignore the DeviceScaleFactor. It is deprecated (Win 8.1 only) */
      if (!write_sanitized_monitor_data (&monitor_config->virtual_monitors[i],
                                         monitor->x,
                                         monitor->y,
                                         monitor->width,
                                         monitor->height,
                                         is_primary,
                                         physical_width,
                                         physical_height,
                                         orientation,
                                         scale,
                                         error))
        return NULL;
    }

  if (!found_primary_monitor &&
      !determine_primary_monitor (monitor_config, error))
    return NULL;

  if (!verify_monitor_config (monitor_config, error))
    return NULL;

  return g_steal_pointer (&monitor_config);
}

GrdRdpMonitorConfig *
grd_rdp_monitor_config_new_from_client_data (rdpSettings  *rdp_settings,
                                             uint32_t      max_monitor_count,
                                             GError      **error)
{
  uint32_t monitor_count =
    freerdp_settings_get_uint32 (rdp_settings, FreeRDP_MonitorCount);

  if (monitor_count == 0 ||
      monitor_count > max_monitor_count)
    return create_monitor_config_from_client_core_data (rdp_settings, error);

  return create_monitor_config_from_client_monitor_data (rdp_settings, error);
}

/**
 * Create a new monitor config from DISPLAYCONTROL_MONITOR_LAYOUT PDU
 * ([MS-RDPEDISP] 2.2.2.2.1 DISPLAYCONTROL_MONITOR_LAYOUT)
 */
GrdRdpMonitorConfig *
grd_rdp_monitor_config_new_from_disp_monitor_layout (const DISPLAY_CONTROL_MONITOR_LAYOUT_PDU  *monitor_layout,
                                                     GError                                   **error)
{
  g_autoptr (GrdRdpMonitorConfig) monitor_config = NULL;
  uint32_t monitor_count = monitor_layout->NumMonitors;
  gboolean found_primary_monitor = FALSE;
  uint32_t i;

  if (monitor_count == 0)
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
                   "Monitor Layout PDU contains no monitors");
      return NULL;
    }

  monitor_config = g_malloc0 (sizeof (GrdRdpMonitorConfig));
  monitor_config->is_virtual = TRUE;
  monitor_config->monitor_count = monitor_count;
  monitor_config->virtual_monitors = g_new0 (GrdRdpVirtualMonitor,
                                             monitor_count);

  for (i = 0; i < monitor_count; ++i)
    {
      DISPLAY_CONTROL_MONITOR_LAYOUT *monitor = &monitor_layout->Monitors[i];
      gboolean is_primary;

      is_primary = !!(monitor->Flags & DISPLAY_CONTROL_MONITOR_PRIMARY);
      if (found_primary_monitor || monitor->Left != 0 || monitor->Top != 0)
        is_primary = FALSE;
      if (!found_primary_monitor && is_primary)
        found_primary_monitor = TRUE;

      /* Ignore the DeviceScaleFactor. It is deprecated (Win 8.1 only) */
      if (!write_sanitized_monitor_data (&monitor_config->virtual_monitors[i],
                                         monitor->Left,
                                         monitor->Top,
                                         monitor->Width,
                                         monitor->Height,
                                         is_primary,
                                         monitor->PhysicalWidth,
                                         monitor->PhysicalHeight,
                                         monitor->Orientation,
                                         monitor->DesktopScaleFactor,
                                         error))
        return NULL;
    }

  if (!found_primary_monitor &&
      !determine_primary_monitor (monitor_config, error))
    return NULL;

  if (!verify_monitor_config (monitor_config, error))
    return NULL;

  return g_steal_pointer (&monitor_config);
}

void
grd_rdp_monitor_config_free (GrdRdpMonitorConfig *monitor_config)
{
  g_clear_pointer (&monitor_config->connectors, g_strfreev);
  g_free (monitor_config->virtual_monitors);
  g_free (monitor_config);
}
