/**
 * 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 "surface.h"

#include "spice.h"

#include "common/debug.h"
#include "common/locking.h"

#include <stdlib.h>
#include <string.h>

struct SpiceSurface
{
  LG_RWLock eventsLock;
  LG_Lock   activationLock;
  atomic_uint_fast64_t activationCancel;

  const LG_SwSurfaceEventOps * events;
  void                       * eventOpaque;
  bool                         active;
  bool                         primaryValid;
  int                          hotX;
  int                          hotY;
};

static bool surfaceAttach(LG_Transport * transport,
    const LG_SwSurfaceEventOps * events, void * opaque)
{
  if (!events || !events->configure || !events->destroy ||
      !events->drawFill || !events->drawBitmap || !events->pointer)
    return false;

  SpiceSurface * surface = transport->surface;
  LG_LOCK_EXCLUSIVE(surface->eventsLock);
  surface->events      = events;
  surface->eventOpaque = opaque;
  LG_UNLOCK_EXCLUSIVE(surface->eventsLock);
  return true;
}

static void surfaceDetach(LG_Transport * transport)
{
  SpiceSurface * surface = transport->surface;
  LG_LOCK_EXCLUSIVE(surface->eventsLock);
  surface->events      = NULL;
  surface->eventOpaque = NULL;
  LG_UNLOCK_EXCLUSIVE(surface->eventsLock);
}

static bool surfaceSetActive(LG_Transport * transport, bool active)
{
  SpiceSurface * surface = transport->surface;
  const uint64_t cancel = atomic_load_explicit(
      &surface->activationCancel, memory_order_acquire);
  LG_LOCK(surface->activationLock);

  if (cancel != atomic_load_explicit(
        &surface->activationCancel, memory_order_acquire))
  {
    LG_UNLOCK(surface->activationLock);
    return false;
  }

  if (surface->active == active)
  {
    LG_UNLOCK(surface->activationLock);
    return true;
  }

  bool result = false;
  if (active)
  {
    LG_LOCK_SHARED(surface->eventsLock);
    const bool attached = surface->events != NULL;
    LG_UNLOCK_SHARED(surface->eventsLock);
    if (!attached)
      goto done;

    if (!atomic_load_explicit(
          &transport->sessionValid, memory_order_acquire) ||
        !purespice_hasChannel(PS_CHANNEL_DISPLAY) ||
        !purespice_hasChannel(PS_CHANNEL_CURSOR))
      goto done;

    purespice_beginChannelConnect(PS_CHANNEL_DISPLAY);
    purespice_beginChannelConnect(PS_CHANNEL_CURSOR);
    if (cancel != atomic_load_explicit(
          &surface->activationCancel, memory_order_acquire))
      goto done;
    if (!purespice_connectChannel(PS_CHANNEL_DISPLAY))
      goto done;

    if (!purespice_connectChannel(PS_CHANNEL_CURSOR))
    {
      purespice_disconnectChannel(PS_CHANNEL_DISPLAY);
      goto done;
    }
  }
  else
  {
    if (!purespice_disconnectChannel(PS_CHANNEL_DISPLAY))
      goto done;

    if (!purespice_disconnectChannel(PS_CHANNEL_CURSOR))
    {
      purespice_beginChannelConnect(PS_CHANNEL_DISPLAY);
      if (cancel != atomic_load_explicit(
            &surface->activationCancel, memory_order_acquire))
        goto done;
      purespice_connectChannel(PS_CHANNEL_DISPLAY);
      goto done;
    }
  }

  if (active && cancel != atomic_load_explicit(
        &surface->activationCancel, memory_order_acquire))
  {
    purespice_disconnectChannel(PS_CHANNEL_CURSOR);
    purespice_disconnectChannel(PS_CHANNEL_DISPLAY);
    goto done;
  }

  surface->active = active;
  result = true;

done:
  LG_UNLOCK(surface->activationLock);
  return result;
}

static void surfaceCancelActivation(SpiceSurface * surface)
{
  atomic_fetch_add_explicit(&surface->activationCancel,
      1, memory_order_acq_rel);
  purespice_cancelChannelConnect(PS_CHANNEL_DISPLAY);
  purespice_cancelChannelConnect(PS_CHANNEL_CURSOR);
}

static void surfaceCancelPending(LG_Transport * transport)
{
  surfaceCancelActivation(transport->surface);
}

static const LG_SwSurfaceOps swSurfaceOps =
{
  .attach        = surfaceAttach,
  .detach        = surfaceDetach,
  .setActive     = surfaceSetActive,
  .cancelPending = surfaceCancelPending,
};

static const LG_VideoOps videoOps =
{
  .name      = "SPICE",
  .type      = LG_VIDEO_TYPE_SW_SURFACE,
  .swSurface = &swSurfaceOps,
};

bool spiceSurface_init(SpiceSurface ** result)
{
  if (!result)
    return false;

  SpiceSurface * surface = calloc(1, sizeof(*surface));
  if (!surface)
    return false;

  LG_RWLOCK_INIT(surface->eventsLock);
  LG_LOCK_INIT(surface->activationLock);
  atomic_init(&surface->activationCancel, 0);
  *result = surface;
  return true;
}

void spiceSurface_free(SpiceSurface ** surface)
{
  if (!surface || !*surface)
    return;

  LG_LOCK_FREE((*surface)->activationLock);
  LG_RWLOCK_FREE((*surface)->eventsLock);
  free(*surface);
  *surface = NULL;
}

const LG_VideoOps * spiceSurface_getVideoOps(void)
{
  return &videoOps;
}

void spiceSurface_sessionStopped(SpiceSurface * surface)
{
  surfaceCancelActivation(surface);
  LG_LOCK(surface->activationLock);
  surface->active       = false;
  surface->primaryValid = false;
  LG_UNLOCK(surface->activationLock);
}

#ifdef ENABLE_TESTS
void spiceSurface_testLockActivation(SpiceSurface * surface)
{
  LG_LOCK(surface->activationLock);
}

void spiceSurface_testUnlockActivation(SpiceSurface * surface)
{
  LG_UNLOCK(surface->activationLock);
}
#endif

void spiceSurface_create(SpiceSurface * surface, unsigned int surfaceId,
    PSSurfaceFormat format, unsigned int width, unsigned int height)
{
  if (surfaceId != 0)
  {
    DEBUG_INFO("Ignoring secondary SPICE surface: id: %u, size: %ux%u",
        surfaceId, width, height);
    return;
  }

  switch (format)
  {
    case PS_SURFACE_FMT_32_xRGB:
    case PS_SURFACE_FMT_32_ARGB:
      break;

    default:
      DEBUG_ERROR("Unsupported primary SPICE surface format: %d", format);
      surface->primaryValid = false;
      return;
  }

  DEBUG_INFO("Create primary SPICE surface: id: %u, size: %ux%u",
      surfaceId, width, height);
  surface->primaryValid = true;

  LG_LOCK_SHARED(surface->eventsLock);
  if (surface->events)
    surface->events->configure(surface->eventOpaque, width, height);
  LG_UNLOCK_SHARED(surface->eventsLock);
}

void spiceSurface_destroy(SpiceSurface * surface, unsigned int surfaceId)
{
  if (!surface->primaryValid || surfaceId != 0)
  {
    DEBUG_INFO("Ignoring destruction of inactive or secondary SPICE surface %u",
        surfaceId);
    return;
  }

  DEBUG_INFO("Destroy primary SPICE surface %u", surfaceId);
  surface->primaryValid = false;

  LG_LOCK_SHARED(surface->eventsLock);
  if (surface->events)
    surface->events->destroy(surface->eventOpaque);
  LG_UNLOCK_SHARED(surface->eventsLock);
}

void spiceSurface_drawFill(SpiceSurface * surface, unsigned int surfaceId,
    int x, int y, int width, int height, uint32_t color)
{
  if (!surface->primaryValid || surfaceId != 0)
    return;

  LG_LOCK_SHARED(surface->eventsLock);
  if (surface->events)
    surface->events->drawFill(
        surface->eventOpaque, x, y, width, height, color);
  LG_UNLOCK_SHARED(surface->eventsLock);
}

void spiceSurface_drawBitmap(SpiceSurface * surface, unsigned int surfaceId,
    PSBitmapFormat format, bool topDown, int x, int y,
    int width, int height, int stride, void * data)
{
  if (!surface->primaryValid || surfaceId != 0)
    return;

  switch (format)
  {
    case PS_BITMAP_FMT_32BIT:
    case PS_BITMAP_FMT_RGBA:
      break;

    default:
      DEBUG_ERROR("Unsupported SPICE bitmap format: %d", format);
      return;
  }

  LG_LOCK_SHARED(surface->eventsLock);
  if (surface->events)
    surface->events->drawBitmap(surface->eventOpaque, topDown,
        x, y, width, height, stride, data);
  LG_UNLOCK_SHARED(surface->eventsLock);
}

static void pointerEvent(SpiceSurface * surface,
    const LG_TransportPointer * pointer)
{
  LG_LOCK_SHARED(surface->eventsLock);
  if (surface->events)
    surface->events->pointer(surface->eventOpaque, pointer);
  LG_UNLOCK_SHARED(surface->eventsLock);
}

void spiceSurface_setRGBAImage(SpiceSurface * surface,
    int width, int height, int hx, int hy, const void * data)
{
  surface->hotX = hx;
  surface->hotY = hy;

  const uint8_t * rgba = data;
  uint8_t * bgra = malloc((size_t)width * height * 4);
  if (!bgra)
    return;

  for (int i = 0; i < width * height; ++i)
  {
    bgra[i * 4 + 0] = rgba[i * 4 + 2];
    bgra[i * 4 + 1] = rgba[i * 4 + 1];
    bgra[i * 4 + 2] = rgba[i * 4 + 0];
    bgra[i * 4 + 3] = rgba[i * 4 + 3];
  }

  const LG_TransportPointer pointer =
  {
    .flags  = LG_TRANSPORT_POINTER_SHAPE,
    .type   = CURSOR_TYPE_COLOR,
    .hx     = hx,
    .hy     = hy,
    .width  = width,
    .height = height,
    .pitch  = width * 4,
    .shape  = bgra,
  };
  pointerEvent(surface, &pointer);
  free(bgra);
}

void spiceSurface_setMonoImage(SpiceSurface * surface,
    int width, int height, int hx, int hy,
    const void * xorMask, const void * andMask)
{
  surface->hotX = hx;
  surface->hotY = hy;

  const int stride = (width + 7) / 8;
  uint8_t * buffer = malloc((size_t)stride * height * 2);
  if (!buffer)
    return;

  memcpy(buffer, andMask, (size_t)stride * height);
  memcpy(buffer + (size_t)stride * height,
      xorMask, (size_t)stride * height);

  const LG_TransportPointer pointer =
  {
    .flags  = LG_TRANSPORT_POINTER_SHAPE,
    .type   = CURSOR_TYPE_MONOCHROME,
    .hx     = hx,
    .hy     = hy,
    .width  = width,
    .height = height * 2,
    .pitch  = stride,
    .shape  = buffer,
  };
  pointerEvent(surface, &pointer);
  free(buffer);
}

void spiceSurface_setColorImage(SpiceSurface * surface,
    int width, int height, int hx, int hy,
    const void * data, const void * maskData)
{
  surface->hotX = hx;
  surface->hotY = hy;

  const uint8_t * rgba    = data;
  const uint8_t * andMask = maskData;
  const int maskPitch     = (width + 7) / 8;
  uint8_t * bgra          = malloc((size_t)width * height * 4);
  if (!bgra)
    return;

  for (int y = 0; y < height; ++y)
    for (int x = 0; x < width; ++x)
    {
      const int i = y * width + x;
      bgra[i * 4 + 0] = rgba[i * 4 + 2];
      bgra[i * 4 + 1] = rgba[i * 4 + 1];
      bgra[i * 4 + 2] = rgba[i * 4 + 0];
      bgra[i * 4 + 3] = andMask[y * maskPitch + x / 8] &
        (0x80U >> (x % 8)) ? 255 : 0;
    }

  const LG_TransportPointer pointer =
  {
    .flags  = LG_TRANSPORT_POINTER_SHAPE,
    .type   = CURSOR_TYPE_MASKED_COLOR,
    .hx     = hx,
    .hy     = hy,
    .width  = width,
    .height = height,
    .pitch  = width * 4,
    .shape  = bgra,
  };
  pointerEvent(surface, &pointer);
  free(bgra);
}

void spiceSurface_setPointerState(SpiceSurface * surface,
    bool visible, int x, int y)
{
  const LG_TransportPointer pointer =
  {
    .flags = LG_TRANSPORT_POINTER_POSITION |
      LG_TRANSPORT_POINTER_VISIBLE_VALID |
      (visible ? LG_TRANSPORT_POINTER_VISIBLE : 0),
    .x     = x,
    .y     = y,
    .hx    = surface->hotX,
    .hy    = surface->hotY,
  };
  pointerEvent(surface, &pointer);
}
