/*
Looking Glass - KVM FrameRelay (KVMFR) Client
Copyright (C) 2017-2019 Geoffrey McRae <geoff@hostfission.com>
https://looking-glass.hostfission.com

This program is free software; you can redistribute it and/or modify it under
cahe 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 "utils.h"

#include "texture.h"
#include "shader.h"
#include "model.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"

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

  // cursor state
  bool              visible;
  float             x, y, w, h;

  // textures
  struct EGL_Texture * texture;
  struct EGL_Texture * textureMono;

  // shaders
  struct EGL_Shader  * shader;
  struct EGL_Shader  * shaderMono;

  // uniforms
  GLuint uMousePos;
  GLuint uMousePosMono;

  // model
  struct EGL_Model   * model;
};

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

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

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

  if (!egl_texture_init(&(*cursor)->textureMono))
  {
    DEBUG_ERROR("Failed to initialize the cursor mono texture");
    return false;
  }

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

  if (!egl_shader_init(&(*cursor)->shaderMono))
  {
    DEBUG_ERROR("Failed to initialize the cursor mono shader");
    return false;
  }

  if (!egl_shader_compile(
        (*cursor)->shader,
        b_shader_cursor_vert    , b_shader_cursor_vert_size,
        b_shader_cursor_rgb_frag, b_shader_cursor_rgb_frag_size))
  {
    DEBUG_ERROR("Failed to compile the cursor shader");
    return false;
  }

  if (!egl_shader_compile(
        (*cursor)->shaderMono,
        b_shader_cursor_vert     , b_shader_cursor_vert_size,
        b_shader_cursor_mono_frag, b_shader_cursor_mono_frag_size))
  {
    DEBUG_ERROR("Failed to compile the cursor mono shader");
    return false;
  }

  (*cursor)->uMousePos     = egl_shader_get_uniform_location((*cursor)->shader    , "mouse");
  (*cursor)->uMousePosMono = egl_shader_get_uniform_location((*cursor)->shaderMono, "mouse");

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

  egl_model_set_default((*cursor)->model);
  return true;
}

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

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

  egl_texture_free(&(*cursor)->texture    );
  egl_texture_free(&(*cursor)->textureMono);
  egl_shader_free (&(*cursor)->shader     );
  egl_shader_free (&(*cursor)->shaderMono );
  egl_model_free  (&(*cursor)->model      );

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

bool egl_cursor_set_shape(EGL_Cursor * cursor, const LG_RendererCursor type, const int width, const int height, const int stride, const uint8_t * data)
{
  LG_LOCK(cursor->lock);

  cursor->type   = type;
  cursor->width  = width;
  cursor->height = (type == LG_CURSOR_MONOCHROME ? height / 2 : height);
  cursor->stride  = stride;

  const size_t size = height * stride;
  if (size > cursor->dataSize)
  {
    if (cursor->data)
      free(cursor->data);

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

    cursor->dataSize = size;
  }

  memcpy(cursor->data, data, size);
  cursor->update = true;

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

void egl_cursor_set_size(EGL_Cursor * cursor, const float w, const float h)
{
  cursor->w = w;
  cursor->h = h;
}

void egl_cursor_set_state(EGL_Cursor * cursor, const bool visible, const float x, const float y)
{
  cursor->visible = visible;
  cursor->x       = x;
  cursor->y       = y;
}

void egl_cursor_render(EGL_Cursor * cursor)
{
  if (!cursor->visible)
    return;

  if (cursor->update)
  {
    LG_LOCK(cursor->lock);
    cursor->update = false;

    uint8_t * data = cursor->data;

    switch(cursor->type)
    {
      case LG_CURSOR_MASKED_COLOR:
        // fall through

      case LG_CURSOR_COLOR:
      {
        egl_texture_setup(cursor->texture, EGL_PF_BGRA, cursor->width, cursor->height, cursor->stride, false);
        egl_texture_update(cursor->texture, data);
        egl_model_set_texture(cursor->model, cursor->texture);
        break;
      }

      case LG_CURSOR_MONOCHROME:
      {
        uint32_t and[cursor->width * cursor->height];
        uint32_t xor[cursor->width * cursor->height];

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

            and[y * cursor->width + x] = andMask;
            xor[y * cursor->width + x] = xorMask;
          }

        egl_texture_setup (cursor->texture    , EGL_PF_BGRA, cursor->width, cursor->height, cursor->width * 4, false);
        egl_texture_setup (cursor->textureMono, EGL_PF_BGRA, cursor->width, cursor->height, cursor->width * 4, false);
        egl_texture_update(cursor->texture    , (uint8_t *)and);
        egl_texture_update(cursor->textureMono, (uint8_t *)xor);
        break;
      }
    }
    LG_UNLOCK(cursor->lock);
  }

  glEnable(GL_BLEND);
  switch(cursor->type)
  {
    case LG_CURSOR_MONOCHROME:
    {
      egl_shader_use(cursor->shader);
      glUniform4f(cursor->uMousePos, cursor->x, cursor->y, cursor->w, cursor->h / 2);
      glBlendFunc(GL_ZERO, GL_SRC_COLOR);
      egl_model_set_texture(cursor->model, cursor->texture);
      egl_model_render(cursor->model);

      egl_shader_use(cursor->shaderMono);
      glUniform4f(cursor->uMousePosMono, cursor->x, cursor->y, cursor->w, cursor->h / 2);
      glBlendFunc(GL_ONE_MINUS_DST_COLOR, GL_ZERO);
      egl_model_set_texture(cursor->model, cursor->textureMono);
      egl_model_render(cursor->model);
      break;
    }

    case LG_CURSOR_COLOR:
    {
      egl_shader_use(cursor->shader);
      glUniform4f(cursor->uMousePos, cursor->x, cursor->y, cursor->w, cursor->h);
      glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
      egl_model_render(cursor->model);
      break;
    }

    case LG_CURSOR_MASKED_COLOR:
    {
      egl_shader_use(cursor->shaderMono);
      glUniform4f(cursor->uMousePos, cursor->x, cursor->y, cursor->w, cursor->h);
      glBlendFunc(GL_ONE_MINUS_DST_COLOR, GL_ZERO);
      egl_model_render(cursor->model);
      break;
    }
  }
  glDisable(GL_BLEND);
}