/**
 * Looking Glass
 * Copyright © 2017-2026 The Looking Glass Authors
 * https://looking-glass.io
 *
 * 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 "render_queue.h"

#include <stdint.h>
#include <string.h>

#include "common/debug.h"
#include "common/ll.h"
#include "main.h"
#include "overlays.h"

struct ll * l_renderQueue = NULL;
static bool l_showSpice;
static bool l_surfaceFormatValid;
static bool l_rendererSupportsNativeHDR;
static LG_RendererFormat l_surfaceFormat;

static void updateSurfaceFormat(void)
{
  if (!g_state.ds->setHDRImageDesc)
    return;

  LG_RendererFormat format = {};
  if (l_surfaceFormatValid)
    format = l_surfaceFormat;

  if (l_showSpice)
  {
    // The SPICE display is always 8-bit SDR, regardless of the last format
    // received from the Looking Glass host.
    format.hdr         = false;
    format.hdrPQ       = false;
    format.hdrMetadata = false;
    g_state.ds->setHDRImageDesc(&format);
    atomic_store(&g_state.hdrDescFailed, false);
    return;
  }

  if (!l_surfaceFormatValid)
    return;

  if (format.hdr && !l_rendererSupportsNativeHDR)
  {
    format.hdr         = false;
    format.hdrPQ       = false;
    format.hdrMetadata = false;
    g_state.ds->setHDRImageDesc(&format);
    DEBUG_WARN("Renderer surface cannot represent the native HDR encoding; "
        "using software HDR mapping");
    atomic_store(&g_state.hdrDescFailed, true);
  }
  else if (!g_state.ds->setHDRImageDesc(&format))
  {
    DEBUG_WARN("Display server failed to apply HDR image description");
    atomic_store(&g_state.hdrDescFailed, true);
  }
  else
    atomic_store(&g_state.hdrDescFailed, false);
}

void renderQueue_init(void)
{
  l_renderQueue               = ll_new();
  l_showSpice                 = false;
  l_surfaceFormatValid        = false;
  l_rendererSupportsNativeHDR = false;
  memset(&l_surfaceFormat, 0, sizeof(l_surfaceFormat));
}

void renderQueue_free(void)
{
  if (!l_renderQueue)
    return;

  renderQueue_clear();
  ll_free(l_renderQueue);
}

void renderQueue_clear(void)
{
  RenderCommand * cmd;
  while(ll_shift(l_renderQueue, (void **)&cmd))
  {
    if (cmd->op == SPICE_OP_DRAW_BITMAP)
      free(cmd->spiceDrawBitmap.data);
    free(cmd);
  }
}

void renderQueue_spiceConfigure(int width, int height)
{
  RenderCommand * cmd = malloc(sizeof(*cmd));
  cmd->op                    = SPICE_OP_CONFIGURE;
  cmd->spiceConfigure.width  = width;
  cmd->spiceConfigure.height = height;
  ll_push(l_renderQueue, cmd);
  app_invalidateWindow(true);
}

void renderQueue_spiceDrawFill(int x, int y, int width, int height,
    uint32_t color)
{
  RenderCommand * cmd = malloc(sizeof(*cmd));
  cmd->op                   = SPICE_OP_DRAW_FILL;
  cmd->spiceFillRect.x      = x;
  cmd->spiceFillRect.y      = y;
  cmd->spiceFillRect.width  = width;
  cmd->spiceFillRect.height = height;
  cmd->spiceFillRect.color  = color;
  ll_push(l_renderQueue, cmd);
  app_invalidateWindow(true);
}

void renderQueue_spiceDrawBitmap(int x, int y, int width, int height, int stride,
    void * data, bool topDown)
{
  if (width <= 0 || height <= 0 || stride <= 0)
  {
    if (width < 0 || height < 0 || stride < 0)
      DEBUG_ERROR("Invalid SPICE bitmap dimensions: %dx%d, stride: %d",
          width, height, stride);
    return;
  }

  if (!data)
  {
    DEBUG_ERROR("SPICE bitmap data is NULL");
    return;
  }

  if ((size_t)height > SIZE_MAX / (size_t)stride)
  {
    DEBUG_ERROR("SPICE bitmap size overflows: height: %d, stride: %d",
        height, stride);
    return;
  }

  const size_t size = (size_t)height * (size_t)stride;
  RenderCommand * cmd = malloc(sizeof(*cmd));
  if (!cmd)
  {
    DEBUG_ERROR("Failed to allocate SPICE bitmap command");
    return;
  }

  uint8_t * copy = malloc(size);
  if (!copy)
  {
    DEBUG_ERROR("Failed to allocate %zu bytes for SPICE bitmap", size);
    free(cmd);
    return;
  }

  memcpy(copy, data, size);

  cmd->op                      = SPICE_OP_DRAW_BITMAP;
  cmd->spiceDrawBitmap.x       = x;
  cmd->spiceDrawBitmap.y       = y;
  cmd->spiceDrawBitmap.width   = width;
  cmd->spiceDrawBitmap.height  = height;
  cmd->spiceDrawBitmap.stride  = stride;
  cmd->spiceDrawBitmap.data    = copy;
  cmd->spiceDrawBitmap.topDown = topDown;

  if (!ll_push(l_renderQueue, cmd))
  {
    free(copy);
    free(cmd);
    return;
  }

  app_invalidateWindow(true);
}

void renderQueue_spiceShow(bool show)
{
  RenderCommand * cmd = malloc(sizeof(*cmd));
  cmd->op             = SPICE_OP_SHOW;
  cmd->spiceShow.show = show;
  ll_push(l_renderQueue, cmd);
  app_invalidateWindow(true);
}

void renderQueue_surfaceFormat(const LG_RendererFormat format,
    bool rendererSupportsNativeHDR)
{
  RenderCommand * cmd = malloc(sizeof(*cmd));
  cmd->op = SURFACE_OP_FORMAT;
  cmd->surfaceFormat.format = format;
  cmd->surfaceFormat.rendererSupportsNativeHDR =
    rendererSupportsNativeHDR;
  ll_push(l_renderQueue, cmd);
  app_invalidateWindow(true);
}

void renderQueue_cursorState(bool visible, int x, int y, int hx, int hy)
{
  RenderCommand * cmd      = malloc(sizeof(*cmd));
  cmd->op                  = CURSOR_OP_STATE;
  cmd->cursorState.visible = visible;
  cmd->cursorState.x       = x;
  cmd->cursorState.y       = y;
  cmd->cursorState.hx      = hx;
  cmd->cursorState.hy      = hy;
  ll_push(l_renderQueue, cmd);
}

void renderQueue_cursorImage(LG_RendererCursor type,
    int width, int height, int pitch, uint8_t * data)
{
  RenderCommand * cmd     = malloc(sizeof(*cmd));
  cmd->op                 = CURSOR_OP_IMAGE;
  cmd->cursorImage.type   = type;
  cmd->cursorImage.width  = width;
  cmd->cursorImage.height = height;
  cmd->cursorImage.pitch  = pitch;
  cmd->cursorImage.data   = data;
  ll_push(l_renderQueue, cmd);
}

void renderQueue_process(void)
{
  RenderCommand * cmd;
  while(ll_shift(l_renderQueue, (void **)&cmd))
  {
    switch(cmd->op)
    {
      case SPICE_OP_CONFIGURE:
        RENDERER(spiceConfigure,
            cmd->spiceConfigure.width, cmd->spiceConfigure.height);
        break;

      case SPICE_OP_DRAW_FILL:
        RENDERER(spiceDrawFill,
            cmd->spiceFillRect.x    , cmd->spiceFillRect.y,
            cmd->spiceFillRect.width, cmd->spiceFillRect.height,
            cmd->spiceFillRect.color);
        break;

      case SPICE_OP_DRAW_BITMAP:
        RENDERER(spiceDrawBitmap,
            cmd->spiceDrawBitmap.x     , cmd->spiceDrawBitmap.y,
            cmd->spiceDrawBitmap.width , cmd->spiceDrawBitmap.height,
            cmd->spiceDrawBitmap.stride, cmd->spiceDrawBitmap.data,
            cmd->spiceDrawBitmap.topDown);
        free(cmd->spiceDrawBitmap.data);
        break;

      case SPICE_OP_SHOW:
        l_showSpice = cmd->spiceShow.show;
        RENDERER(spiceShow, cmd->spiceShow.show);
        updateSurfaceFormat();
        if (cmd->spiceShow.show)
          overlaySplash_show(false);
        break;

      case SURFACE_OP_FORMAT:
        l_surfaceFormat = cmd->surfaceFormat.format;
        l_surfaceFormatValid = true;
        l_rendererSupportsNativeHDR =
          cmd->surfaceFormat.rendererSupportsNativeHDR;
        updateSurfaceFormat();
        break;

      case CURSOR_OP_STATE:
        RENDERER(onMouseEvent, cmd->cursorState.visible, cmd->cursorState.x,
            cmd->cursorState.y, cmd->cursorState.hx, cmd->cursorState.hy);
        break;

      case CURSOR_OP_IMAGE:
        RENDERER(onMouseShape,
            cmd->cursorImage.type,
            cmd->cursorImage.width, cmd->cursorImage.height,
            cmd->cursorImage.pitch, cmd->cursorImage.data);
        free(cmd->cursorImage.data);
    }
    free(cmd);
  }
}
