/**
 * Looking Glass
 * Copyright © 2017-2025 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 "interface/capture.h"
#include "interface/platform.h"
#include "common/util.h"
#include "common/option.h"
#include "common/debug.h"
#include "common/event.h"
#include "common/thread.h"
#include <string.h>
#include <stdlib.h>
#include <inttypes.h>
#include <unistd.h>
#include <xcb/shm.h>
#include <xcb/xfixes.h>
#include <sys/ipc.h>
#include <sys/shm.h>

struct xcb
{
  bool                        initialized;
  bool                        stop;
  xcb_connection_t          * xcb;
  xcb_screen_t              * xcbScreen;
  uint32_t                    seg;
  int                         shmID;
  void                      * data;
  LGEvent                   * frameEvent;

  CaptureGetPointerBuffer     getPointerBufferFn;
  CapturePostPointerBuffer    postPointerBufferFn;
  LGThread                  * pointerThread;

  unsigned int width;
  unsigned int height, dataHeight;
  unsigned int pitch;

  int mouseX, mouseY, mouseHotX, mouseHotY;

  bool                                 hasFrame;
  xcb_shm_get_image_cookie_t           imgC;
  xcb_xfixes_get_cursor_image_cookie_t curC;
};

static struct xcb * this = NULL;

static int pointerThread(void * unused);

// forwards

static bool xcb_deinit(void);

// implementation

static const char * xcb_getName(void)
{
  return "XCB";
}

static void xcb_initOptions(void)
{
  struct Option options[] =
  {
    {0}
  };

  option_register(options);
}

static bool xcb_create(
  CaptureGetPointerBuffer getPointerBufferFn,
  CapturePostPointerBuffer postPointerBufferFn,
  unsigned                 frameBuffers
)
{
  DEBUG_ASSERT(!this);
  this             = calloc(1, sizeof(*this));
  this->shmID      = -1;
  this->data       = (void *)-1;
  this->frameEvent = lgCreateEvent(true, 20);

  this->getPointerBufferFn = getPointerBufferFn;
  this->postPointerBufferFn = postPointerBufferFn;

  if (!this->frameEvent)
  {
    DEBUG_ERROR("Failed to create the frame event");
    free(this);
    return false;
  }

  return true;
}

static bool xcb_init(void * ivshmemBase, unsigned * alignSize)
{
  DEBUG_ASSERT(this);
  DEBUG_ASSERT(!this->initialized);

  lgResetEvent(this->frameEvent);

  this->stop = false;
  this->xcb = xcb_connect(NULL, NULL);
  if (!this->xcb || xcb_connection_has_error(this->xcb))
  {
    DEBUG_ERROR("Unable to open the X display");
    goto fail;
  }

  if (!xcb_get_extension_data(this->xcb, &xcb_shm_id)->present)
  {
    DEBUG_ERROR("Missing the SHM extension");
    goto fail;
  }

  xcb_screen_iterator_t iter;
  iter            = xcb_setup_roots_iterator(xcb_get_setup(this->xcb));
  this->xcbScreen = iter.data;
  this->width     = iter.data->width_in_pixels;
  this->height    = iter.data->height_in_pixels;
  this->pitch     = this->width * 4;
  DEBUG_INFO("Frame Size       : %u x %u", this->width, this->height);

  this->seg   = xcb_generate_id(this->xcb);
  const size_t maxFrameSize = this->width * this->height * 4;
  this->shmID = shmget(IPC_PRIVATE, maxFrameSize, IPC_CREAT | 0777);
  if (this->shmID == -1)
  {
    DEBUG_ERROR("shmget failed");
    goto fail;
  }

  xcb_shm_attach(this->xcb, this->seg ,this->shmID, false);
  this->data = shmat(this->shmID, NULL, 0);
  if ((uintptr_t)this->data == -1)
  {
    DEBUG_ERROR("shmat failed");
    goto fail;
  }
  DEBUG_INFO("Frame Data       : 0x%" PRIXPTR, (uintptr_t)this->data);

  xcb_query_extension_cookie_t extension_cookie =
		xcb_query_extension(this->xcb, strlen("XFIXES"), "XFIXES");
  xcb_query_extension_reply_t * extension_reply =
		xcb_query_extension_reply(this->xcb, extension_cookie, NULL);
  if(!extension_reply)
  {
    DEBUG_ERROR("Extension \"XFIXES\" isn't available");
    free(extension_reply);
    goto fail;
  }
  free(extension_reply);

  xcb_xfixes_query_version_cookie_t version_cookie =
		xcb_xfixes_query_version(this->xcb, XCB_XFIXES_MAJOR_VERSION, XCB_XFIXES_MINOR_VERSION);
  xcb_xfixes_query_version_reply_t * version_reply =
		xcb_xfixes_query_version_reply(this->xcb, version_cookie, NULL);
  if(!version_reply)
  {
    DEBUG_ERROR("Extension \"XFIXES\" isn't available");
    free(version_reply);
    goto fail;
  }
  free(version_reply);

  this->initialized = true;
  return true;
fail:
  xcb_deinit();
  return false;
}

static bool xcb_start(void)
{
  this->stop = false;

  if (!lgCreateThread("XCBPointer", pointerThread, NULL, &this->pointerThread))
  {
    DEBUG_ERROR("Failed to create the XCBPointer thread");
    return false;
  }

  return true;
}

static void xcb_stop(void)
{
  this->stop = true;

  if(this->pointerThread)
  {
    lgJoinThread(this->pointerThread, NULL);
    this->pointerThread = NULL;
  }
}

static bool xcb_deinit(void)
{
  DEBUG_ASSERT(this);

  if ((uintptr_t)this->data != -1)
  {
    shmdt(this->data);
    this->data = (void *)-1;
  }

  if (this->shmID != -1)
  {
    shmctl(this->shmID, IPC_RMID, NULL);
    this->shmID = -1;
  }

  if (this->xcb)
  {
    xcb_disconnect(this->xcb);
    this->xcb = NULL;
  }

  this->initialized = false;
  return true;
}

static void xcb_free(void)
{
  lgFreeEvent(this->frameEvent);
  free(this);
  this = NULL;
}

static CaptureResult xcb_capture(
  unsigned frameBufferIndex,
  FrameBuffer * frame)
{
  DEBUG_ASSERT(this);
  DEBUG_ASSERT(this->initialized);

  if (!this->hasFrame)
  {
    this->imgC = xcb_shm_get_image_unchecked(
        this->xcb,
        this->xcbScreen->root,
        0, 0,
        this->width,
        this->height,
        ~0,
        XCB_IMAGE_FORMAT_Z_PIXMAP,
        this->seg,
        0);

    this->hasFrame  = true;
    lgSignalEvent(this->frameEvent);
  }

  return CAPTURE_RESULT_OK;
}

static CaptureResult xcb_waitFrame(
  unsigned frameBufferIndex,
  CaptureFrame * frame,
  const size_t maxFrameSize)
{
  lgWaitEvent(this->frameEvent, TIMEOUT_INFINITE);

  const unsigned int maxHeight = maxFrameSize / this->pitch;
  this->dataHeight = min(maxHeight, this->height);


  frame->screenWidth  = this->width;
  frame->screenHeight = this->height;
  frame->dataWidth    = this->width;
  frame->dataHeight   = this->dataHeight;
  frame->frameWidth   = this->width;
  frame->frameHeight  = this->height;
  frame->truncated    = maxHeight < this->height;
  frame->pitch        = this->pitch;
  frame->stride       = this->width;
  frame->format       = CAPTURE_FMT_BGRA;
  frame->rotation     = CAPTURE_ROT_0;

  return CAPTURE_RESULT_OK;
}

static CaptureResult xcb_getFrame(
  unsigned frameBufferIndex,
  FrameBuffer  * frame,
  const size_t maxFrameSize)
{
  DEBUG_ASSERT(this);
  DEBUG_ASSERT(this->initialized);

  xcb_shm_get_image_reply_t * img;
  img = xcb_shm_get_image_reply(this->xcb, this->imgC, NULL);
  if (!img)
  {
    DEBUG_ERROR("Failed to get image reply");
    return CAPTURE_RESULT_ERROR;
  }

  framebuffer_write(frame, this->data, this->pitch);
  free(img);

  this->hasFrame = false;
  return CAPTURE_RESULT_OK;
}

static int pointerThread(void * unused)
{
  while (!this->stop)
  {
    if (this->stop)
      break;

    CapturePointer pointer = { 0 };

    this->curC = xcb_xfixes_get_cursor_image_unchecked(this->xcb);

    void * data;
    uint32_t size;
    if (!this->getPointerBufferFn(&data, &size))
    {
      DEBUG_WARN("failed to get a pointer buffer");
      continue;
    }

    xcb_xfixes_get_cursor_image_reply_t * curReply;
    curReply = xcb_xfixes_get_cursor_image_reply(this->xcb, this->curC, NULL);
    if (curReply == NULL)
    {
      DEBUG_WARN("Failed to get cursor reply");
      continue;
    }

    if(curReply->xhot != this->mouseHotX || curReply->yhot != this->mouseHotY)
    {
      pointer.shapeUpdate = true;
      this->mouseHotX = curReply->xhot;
      this->mouseHotY = curReply->yhot;

      uint32_t * src = xcb_xfixes_get_cursor_image_cursor_image(curReply);
      uint32_t * dst = (uint32_t *) (data);
      memcpy(dst, src, curReply->width * curReply->height * sizeof(uint32_t));
    }
    else
      pointer.shapeUpdate = false;

    if(curReply->x != this->mouseX || curReply->y != this->mouseY)
    {
      pointer.positionUpdate = true;
      this->mouseX = curReply->x;
      this->mouseY = curReply->y;
    }
    else
      pointer.positionUpdate = false;

    if(pointer.positionUpdate || pointer.shapeUpdate)
    {
      pointer.hx      = curReply->xhot;
      pointer.hy      = curReply->yhot;
      pointer.visible = true;
      pointer.x       = curReply->x - curReply->xhot;
      pointer.y       = curReply->y - curReply->yhot;
      pointer.format  = CAPTURE_FMT_COLOR;
      pointer.width   = curReply->width;
      pointer.height  = curReply->height;
      pointer.pitch   = curReply->width * 4;

      this->postPointerBufferFn(&pointer);
    }

    free(curReply);
    usleep(1000);
  }

  return 0;
}

struct CaptureInterface Capture_XCB =
{
  .shortName       = "XCB",
  .asyncCapture    = true,
  .initOptions     = xcb_initOptions,
  .getName         = xcb_getName,
  .create          = xcb_create,
  .init            = xcb_init,
  .start           = xcb_start,
  .stop            = xcb_stop,
  .deinit          = xcb_deinit,
  .free            = xcb_free,
  .capture         = xcb_capture,
  .waitFrame       = xcb_waitFrame,
  .getFrame        = xcb_getFrame
};
