/**
 * 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 "cursor.h"
#include "common/debug.h"
#include "common/locking.h"
#include "common/option.h"

#include "texture.h"
#include "shader.h"
#include "state.h"
#include "model.h"
#include "util.h"

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

// these headers are auto generated by cmake
#include "cursor.vert.h"
#include "cursor_rgb.frag.h"
#include "cursor_mono.frag.h"

enum CursorTransfer
{
  CURSOR_TRANSFER_SRGB  = 0,
  CURSOR_TRANSFER_SCRGB = 1,
  CURSOR_TRANSFER_PQ    = 2,
};

struct CursorTex
{
  struct EGL_Texture * texture;
  struct EGL_Shader  * shader;
  EGL_Uniform * uMousePos;
  EGL_Uniform * uScale;
  EGL_Uniform * uRotate;
  EGL_Uniform * uCBMode;
  EGL_Uniform * uSourceTransfer;
  EGL_Uniform * uSDRWhiteLevel;
  EGL_Uniform * uMapHDRtoSDR;
  EGL_Uniform * uMapHDRGain;
  EGL_Uniform * uMapHDRContentPeak;
  EGL_Uniform * uColorTransformFlags;
  EGL_Uniform * uColorMatrix;
  EGL_Uniform * uColorScalar;
  EGL_Uniform * uLinearComposition;
};

struct CursorPos
{
  float x, y;
};

struct CursorSize
{
  float w, h;
};

struct EGL_Cursor
{
  LG_Lock           lock;
  LG_RendererCursor type;
  int               width;
  int               height;
  int               stride;
  uint8_t *         data;
  size_t            dataSize;
  uint32_t *        normData;
  uint32_t *        monoData;
  size_t            decodedCapacity;
  bool              update;

  // cursor state
  bool              visible;
  LG_RendererRotate rotate;
  int               cbMode;

  _Atomic(struct CursorPos)    pos;
  _Atomic(struct CursorPos)    hs;
  _Atomic(struct CursorSize)   size;
  _Atomic(float)               scale;
  _Atomic(enum CursorTransfer) sourceTransfer;
  _Atomic(float)               sdrWhiteLevel;
  _Atomic(bool)                mapHDRtoSDR;
  _Atomic(float)               mapHDRGain;
  _Atomic(float)               mapHDRContentPeak;

  bool                colorTransformUpdate;
  LGColorTransform pendingColorTransform;
  LGColorTransform activeColorTransform;
  GLuint              colorLUT;

  struct CursorTex norm;
  struct CursorTex mono;
  struct EGL_Model * model;
};

static bool cursorTexInit(struct CursorTex * t,
    const char * vertex_code  , size_t vertex_size,
    const char * fragment_code, size_t fragment_size)
{
  if (!egl_textureInit(&t->texture, NULL, EGL_TEXTYPE_BUFFER))
  {
    DEBUG_ERROR("Failed to initialize the cursor texture");
    return false;
  }

  if (!egl_shaderInit(&t->shader))
  {
    DEBUG_ERROR("Failed to initialize the cursor shader");
    return false;
  }

  if (!egl_shaderCompile(t->shader,
        vertex_code, vertex_size, fragment_code, fragment_size, false, NULL))
  {
    DEBUG_ERROR("Failed to compile the cursor shader");
    return false;
  }

  t->uMousePos             = egl_shaderGetUniform(t->shader, "mouse"        );
  t->uScale                = egl_shaderGetUniform(t->shader, "scale"        );
  t->uRotate               = egl_shaderGetUniform(t->shader, "rotate"       );
  t->uCBMode               = egl_shaderGetUniform(t->shader, "cbMode"       );
  t->uSourceTransfer       = egl_shaderGetUniform(t->shader, "sourceTransfer");
  t->uSDRWhiteLevel        = egl_shaderGetUniform(t->shader, "sdrWhiteLevel");
  t->uMapHDRtoSDR          = egl_shaderGetUniform(t->shader, "mapHDRtoSDR"  );
  t->uMapHDRGain           = egl_shaderGetUniform(t->shader, "mapHDRGain"   );
  t->uMapHDRContentPeak    =
    egl_shaderGetUniform(t->shader, "mapHDRContentPeak");
  t->uColorTransformFlags  =
    egl_shaderGetUniform(t->shader, "colorTransformFlags");
  t->uColorMatrix          = egl_shaderGetUniform(t->shader, "colorMatrix[0]");
  t->uColorScalar          =
    egl_shaderGetUniform(t->shader, "colorTransformScalar");
  t->uLinearComposition    =
    egl_shaderGetUniform(t->shader, "linearComposition");
  egl_shaderAssocTextures(t->shader, 2);

  return true;
}

static inline void setCursorTexUniforms(EGL_Cursor * cursor,
    struct CursorTex * t, bool mono, float x, float y,
    float w, float h, float scale, bool linearComposition)
{
  const enum CursorTransfer sourceTransfer = mono ? CURSOR_TRANSFER_SRGB :
    atomic_load(&cursor->sourceTransfer);
  egl_uniform4f (t->uMousePos            , x, y, w, mono ? h / 2 : h);
  egl_uniform1f (t->uScale               , scale);
  egl_uniform1i (t->uRotate              , cursor->rotate);
  // The colour-blind transform is defined for SDR signals. The desktop
  // disables it while passing native HDR through, so keep the cursor on the
  // same path rather than altering encoded PQ or linear scRGB independently.
  egl_uniform1i (t->uCBMode              ,
      (sourceTransfer == CURSOR_TRANSFER_SRGB ||
       atomic_load(&cursor->mapHDRtoSDR)) ?
        cursor->cbMode : 0);
  egl_uniform1i (t->uSourceTransfer      , sourceTransfer);
  egl_uniform1f (t->uSDRWhiteLevel       ,
      atomic_load(&cursor->sdrWhiteLevel));
  egl_uniform1i (t->uMapHDRtoSDR         ,
      !mono && atomic_load(&cursor->mapHDRtoSDR));
  egl_uniform1f (t->uMapHDRGain          ,
      atomic_load(&cursor->mapHDRGain));
  egl_uniform1f (t->uMapHDRContentPeak   ,
      atomic_load(&cursor->mapHDRContentPeak));
  egl_uniform1ui(t->uColorTransformFlags ,
      mono ? 0 : cursor->activeColorTransform.flags);
  egl_uniformSet (t->uColorMatrix        , EGL_UNIFORM_TYPE_4FV, 3, GL_FALSE,
      cursor->activeColorTransform.matrix);
  egl_uniform1f (t->uColorScalar         ,
      cursor->activeColorTransform.scalar);
  egl_uniform1i (t->uLinearComposition   ,
      !mono && linearComposition);
  if (!mono && cursor->colorLUT)
    egl_stateBindTexture(1, GL_TEXTURE_2D, cursor->colorLUT);
}

static void cursorTexFree(struct CursorTex * t)
{
  egl_textureFree(&t->texture);
  egl_shaderFree (&t->shader );
};

bool egl_cursorInit(EGL_Cursor ** cursor)
{
  *cursor = malloc(sizeof(**cursor));
  if (!*cursor)
  {
    DEBUG_ERROR("Failed to malloc EGL_Cursor");
    return false;
  }

  memset(*cursor, 0, sizeof(**cursor));
  LG_LOCK_INIT((*cursor)->lock);

  if (!cursorTexInit(&(*cursor)->norm,
      b_shader_cursor_vert    , b_shader_cursor_vert_size,
      b_shader_cursor_rgb_frag, b_shader_cursor_rgb_frag_size))
    return false;

  if (!cursorTexInit(&(*cursor)->mono,
      b_shader_cursor_vert     , b_shader_cursor_vert_size,
      b_shader_cursor_mono_frag, b_shader_cursor_mono_frag_size))
    return false;

  if (!egl_modelInit(&(*cursor)->model))
  {
    DEBUG_ERROR("Failed to initialize the cursor model");
    return false;
  }

  egl_modelSetDefault((*cursor)->model, true);

  (*cursor)->cbMode = option_get_int("egl", "cbMode");

  struct CursorPos  pos  = { .x = 0, .y = 0 };
  struct CursorPos  hs   = { .x = 0, .y = 0 };
  struct CursorSize size = { .w = 0, .h = 0 };
  atomic_init(&(*cursor)->pos              , pos  );
  atomic_init(&(*cursor)->hs               , hs   );
  atomic_init(&(*cursor)->size             , size );
  atomic_init(&(*cursor)->scale            , 1.0f );
  atomic_init(&(*cursor)->sourceTransfer   , CURSOR_TRANSFER_SRGB);
  atomic_init(&(*cursor)->sdrWhiteLevel    ,
      LG_SDR_WHITE_LEVEL_DEFAULT);
  atomic_init(&(*cursor)->mapHDRtoSDR      , false);
  atomic_init(&(*cursor)->mapHDRGain       , 1.0f );
  atomic_init(&(*cursor)->mapHDRContentPeak, 1.0f );

  return true;
}

void egl_cursorFree(EGL_Cursor ** cursor)
{
  if (!*cursor)
    return;

  LG_LOCK_FREE((*cursor)->lock);
  if ((*cursor)->data)
    free((*cursor)->data);
  free((*cursor)->normData);
  free((*cursor)->monoData);

  cursorTexFree(&(*cursor)->norm);
  cursorTexFree(&(*cursor)->mono);
  if ((*cursor)->colorLUT)
    glDeleteTextures(1, &(*cursor)->colorLUT);
  egl_modelFree(&(*cursor)->model);

  free(*cursor);
  *cursor = NULL;
}

static bool cursorEnsureDecoded(EGL_Cursor * cursor, size_t pixels)
{
  if (pixels <= cursor->decodedCapacity)
    return true;

  const size_t size = pixels * sizeof(*cursor->normData);
  uint32_t * normData = malloc(size);
  uint32_t * monoData = malloc(size);
  if (!normData || !monoData)
  {
    free(normData);
    free(monoData);
    DEBUG_ERROR("Failed to allocate cursor conversion buffers");
    return false;
  }

  free(cursor->normData);
  free(cursor->monoData);
  cursor->normData        = normData;
  cursor->monoData        = monoData;
  cursor->decodedCapacity = pixels;
  return true;
}

bool egl_cursorSetShape(EGL_Cursor * cursor, const LG_RendererCursor type,
    const int width, const int height, const int stride, const uint8_t * data)
{
  size_t size;
  if (!data || !lg_rendererCursorValidate(
        type, width, height, stride, &size))
  {
    DEBUG_ERROR("Invalid cursor shape geometry");
    return false;
  }

  const int decodedHeight =
    type == LG_CURSOR_MONOCHROME ? height / 2 : height;
  const size_t pixels = (size_t)width * (size_t)decodedHeight;

  LG_LOCK(cursor->lock);

  if (!cursorEnsureDecoded(cursor, pixels))
  {
    LG_UNLOCK(cursor->lock);
    return false;
  }

  if (size > cursor->dataSize)
  {
    uint8_t * resized = realloc(cursor->data, size);
    if (!resized)
    {
      DEBUG_ERROR("Failed to malloc buffer for cursor shape");
      LG_UNLOCK(cursor->lock);
      return false;
    }

    cursor->data     = resized;
    cursor->dataSize = size;
  }

  memcpy(cursor->data, data, size);
  cursor->type   = type;
  cursor->width  = width;
  cursor->height = decodedHeight;
  cursor->stride = stride;
  cursor->update = true;

  LG_UNLOCK(cursor->lock);
  return true;
}

void egl_cursorSetColorTransform(EGL_Cursor * cursor,
    const LGColorTransform * transform)
{
  LG_LOCK(cursor->lock);
  cursor->pendingColorTransform = *transform;
  cursor->colorTransformUpdate = true;
  LG_UNLOCK(cursor->lock);
}

void egl_cursorSetSize(EGL_Cursor * cursor, const float w, const float h)
{
  struct CursorSize size = { .w = w, .h = h };
  atomic_store(&cursor->size, size);
}

void egl_cursorSetScale(EGL_Cursor * cursor, const float scale)
{
  atomic_store(&cursor->scale, scale);
}

void egl_cursorSetState(EGL_Cursor * cursor, const bool visible,
    const float x, const float y, const float hx, const float hy)
{
  cursor->visible = visible;
  struct CursorPos pos = { .x = x , .y = y  };
  struct CursorPos hs  = { .x = hx, .y = hy };
  atomic_store(&cursor->pos, pos);
  atomic_store(&cursor->hs , hs);
}

struct CursorState egl_cursorRender(EGL_Cursor * cursor,
    LG_RendererRotate rotate, int width, int height,
    bool linearComposition, bool * logicalDeferred)
{
  if (logicalDeferred)
    *logicalDeferred = false;

  if (!cursor->visible)
    return (struct CursorState) { .visible = false };

  if (cursor->update || cursor->colorTransformUpdate)
  {
    LG_LOCK(cursor->lock);
    const bool updateShape = cursor->update;
    cursor->update = false;

    if (cursor->colorTransformUpdate)
    {
      cursor->activeColorTransform = cursor->pendingColorTransform;
      cursor->colorTransformUpdate = false;

      if (cursor->activeColorTransform.flags & LG_COLOR_TRANSFORM_LUT)
      {
        if (!cursor->colorLUT)
          glGenTextures(1, &cursor->colorLUT);
        egl_stateBindTexture(1, GL_TEXTURE_2D, cursor->colorLUT);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, 4096, 1, 0,
            GL_RGBA, GL_FLOAT, cursor->activeColorTransform.lut);
      }
    }

    if (updateShape)
    {
      const uint8_t * data = cursor->data;
      switch(cursor->type)
      {
      case LG_CURSOR_MASKED_COLOR:
      {
        for(int y = 0; y < cursor->height; ++y)
        {
          const uint8_t * row = data + (size_t)cursor->stride * (size_t)y;
          for(int x = 0; x < cursor->width; ++x)
          {
            uint32_t src;
            memcpy(&src, row + (size_t)x * sizeof(src), sizeof(src));
            const size_t index =
              (size_t)y * (size_t)cursor->width + (size_t)x;
            const bool masked = (src & 0xFF000000) != 0;
            if (masked)
              cursor->normData[index] = cursor->monoData[index] =
                src & 0x00FFFFFF;
            else
            {
              cursor->normData[index] = src | 0xFF000000;
              cursor->monoData[index] = 0xFF000000;
            }
          }
        }

        egl_textureSetup(cursor->mono.texture, EGL_PF_BGRA,
            cursor->width, cursor->height, cursor->width,
            (size_t)cursor->width * sizeof(*cursor->monoData));
        egl_textureUpdate(cursor->mono.texture,
            (const uint8_t *)cursor->monoData, true);
        egl_textureSetup(cursor->norm.texture, EGL_PF_BGRA,
            cursor->width, cursor->height, cursor->width,
            (size_t)cursor->width * sizeof(*cursor->normData));
        egl_textureUpdate(cursor->norm.texture,
            (const uint8_t *)cursor->normData, true);
        break;
      }

      case LG_CURSOR_COLOR:
        for(int y = 0; y < cursor->height; ++y)
          memcpy(cursor->normData + (size_t)y * (size_t)cursor->width,
              data + (size_t)y * (size_t)cursor->stride,
              (size_t)cursor->width * sizeof(*cursor->normData));

        egl_textureSetup(cursor->norm.texture, EGL_PF_BGRA,
            cursor->width, cursor->height, cursor->width,
            (size_t)cursor->width * sizeof(*cursor->normData));
        egl_textureUpdate(cursor->norm.texture,
            (const uint8_t *)cursor->normData, true);
        break;

      case LG_CURSOR_MONOCHROME:
      {
        for(int y = 0; y < cursor->height; ++y)
        {
          const uint8_t * srcAnd =
            data + (size_t)cursor->stride * (size_t)y;
          const uint8_t * srcXor = data +
            (size_t)cursor->stride * (size_t)(y + cursor->height);
          for(int x = 0; x < cursor->width; ++x)
          {
            const uint8_t    mask    = 0x80 >> (x % 8);
            const uint32_t   andMask =
              (srcAnd[x / 8] & mask) ? 0xFFFFFFFF : 0xFF000000;
            const uint32_t   xorMask =
              (srcXor[x / 8] & mask) ? 0x00FFFFFF : 0x00000000;
            const size_t index =
              (size_t)y * (size_t)cursor->width + (size_t)x;

            cursor->normData[index] = andMask;
            cursor->monoData[index] = xorMask;
          }
        }

        // Monochrome cursors use AND/XOR mask textures - never convert to PQ
        egl_textureSetup(cursor->norm.texture, EGL_PF_BGRA,
            cursor->width, cursor->height, cursor->width,
            (size_t)cursor->width * sizeof(*cursor->normData));
        egl_textureSetup(cursor->mono.texture, EGL_PF_BGRA,
            cursor->width, cursor->height, cursor->width,
            (size_t)cursor->width * sizeof(*cursor->monoData));
        egl_textureUpdate(cursor->norm.texture,
            (const uint8_t *)cursor->normData, true);
        egl_textureUpdate(cursor->mono.texture,
            (const uint8_t *)cursor->monoData, true);
        break;
      }
      }
    }
    LG_UNLOCK(cursor->lock);
  }

  cursor->rotate = rotate;

  struct CursorPos  pos   = atomic_load(&cursor->pos  );
  float             scale = atomic_load(&cursor->scale);
  struct CursorPos  hs    = atomic_load(&cursor->hs   );
  struct CursorSize size  = atomic_load(&cursor->size );

  pos.x  -= hs.x * scale;
  pos.y  -= hs.y * scale;
  size.w *= scale;
  size.h *= scale;

  struct CursorState state = {
    .visible = true,
  };

  switch (rotate)
  {
    case LG_ROTATE_0:
      state.rect.x = (pos.x * width + width) / 2;
      state.rect.y = (-pos.y * height + height) / 2 - size.h * height;
      state.rect.w = size.w * width + 3;
      state.rect.h = size.h * height + 3;
      break;

    case LG_ROTATE_90:
      state.rect.x = (-pos.y * width + width) / 2 - size.h * width;
      state.rect.y = (-pos.x * height + height) / 2 - size.w * height;
      state.rect.w = size.h * width + 3;
      state.rect.h = size.w * height + 3;
      break;

    case LG_ROTATE_180:
      state.rect.x = (-pos.x * width + width) / 2 - size.w * width;
      state.rect.y = (pos.y * height + height) / 2;
      state.rect.w = size.w * width + 3;
      state.rect.h = size.h * height + 3;
      break;

    case LG_ROTATE_270:
      state.rect.x = (pos.y * width + width) / 2;
      state.rect.y = (pos.x * height + height) / 2;
      state.rect.w = size.h * width + 3;
      state.rect.h = size.w * height + 3;
      break;

    default:
      DEBUG_UNREACHABLE();
  }

  state.rect.x = max(0, state.rect.x - 1);
  state.rect.y = max(0, state.rect.y - 1);

  if (linearComposition && cursor->type != LG_CURSOR_COLOR)
  {
    if (logicalDeferred)
      *logicalDeferred = true;
    return state;
  }

  egl_stateBlend(true);
  switch(cursor->type)
  {
    case LG_CURSOR_MONOCHROME:
    {
      setCursorTexUniforms(cursor, &cursor->norm, true, pos.x, pos.y,
          size.w, size.h, scale, false);
      egl_shaderUse(cursor->norm.shader);
      egl_stateBlendFunc(GL_ZERO, GL_SRC_COLOR);
      egl_modelSetTexture(cursor->model, cursor->norm.texture);
      egl_modelRender(cursor->model);

      setCursorTexUniforms(cursor, &cursor->mono, true, pos.x, pos.y,
          size.w, size.h, scale, false);
      egl_shaderUse(cursor->mono.shader);
      egl_stateBlendFunc(GL_ONE_MINUS_DST_COLOR, GL_ZERO);
      egl_modelSetTexture(cursor->model, cursor->mono.texture);
      egl_modelRender(cursor->model);
      break;
    }

    case LG_CURSOR_MASKED_COLOR:
    {
      setCursorTexUniforms(cursor, &cursor->norm, false, pos.x, pos.y,
          size.w, size.h, scale, linearComposition);
      egl_shaderUse(cursor->norm.shader);
      egl_stateBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
      egl_modelSetTexture(cursor->model, cursor->norm.texture);
      egl_modelRender(cursor->model);

      setCursorTexUniforms(cursor, &cursor->mono, false, pos.x, pos.y,
          size.w, size.h, scale, linearComposition);
      egl_shaderUse(cursor->mono.shader);
      egl_stateBlendFunc(GL_ONE_MINUS_DST_COLOR, GL_ZERO);
      egl_modelSetTexture(cursor->model, cursor->mono.texture);
      egl_modelRender(cursor->model);
      break;
    }

    case LG_CURSOR_COLOR:
    {
      setCursorTexUniforms(cursor, &cursor->norm, false, pos.x, pos.y,
          size.w, size.h, scale, linearComposition);
      egl_shaderUse(cursor->norm.shader);
      egl_stateBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
      egl_modelSetTexture(cursor->model, cursor->norm.texture);
      egl_modelRender(cursor->model);
      break;
    }
  }
  egl_stateBlend(false);

  return state;
}

void egl_cursorSetHDRState(EGL_Cursor * cursor, bool sourceHDR,
    bool nativeHDR, bool mapHDRtoSDR, bool hdrPQ,
    float mapGain, float mapContentPeak)
{
  // The cursor's white level is supplied independently by the pointer queue.
  // Do not replace it with the frame-level fallback during every render.
  atomic_store(&cursor->sourceTransfer,
      !sourceHDR ? CURSOR_TRANSFER_SRGB :
        (hdrPQ ? CURSOR_TRANSFER_PQ : CURSOR_TRANSFER_SCRGB));
  atomic_store(&cursor->mapHDRtoSDR,
      sourceHDR && !nativeHDR && mapHDRtoSDR);
  atomic_store(&cursor->mapHDRGain, mapGain);
  atomic_store(&cursor->mapHDRContentPeak, mapContentPeak);
}

void egl_cursorSetSDRWhiteLevel(EGL_Cursor * cursor, float sdrWhiteLevel)
{
  atomic_store(&cursor->sdrWhiteLevel, sdrWhiteLevel > 0.0f ?
      sdrWhiteLevel : LG_SDR_WHITE_LEVEL_DEFAULT);
}
