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

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

#include "common/debug.h"
#include "common/rects.h"
#include "common/util.h"

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

// these headers are auto generated by cmake
#include "hdr_overlay.vert.h"
#include "hdr_overlay.frag.h"

struct EGL_HDROverlay
{
  EGL_Framebuffer * framebuffer;
  EGL_Shader      * shader;
  EGL_Model       * model;

  EGL_Uniform * uPQ;
  EGL_Uniform * uReferenceWhiteLevel;

  unsigned int width;
  unsigned int height;
  bool configured;

  _Atomic(bool)  active;
  _Atomic(bool)  pq;
  _Atomic(float) referenceWhiteLevel;

  bool  renderPQ;
  float renderReferenceWhiteLevel;
};

bool egl_hdrOverlayInit(EGL_HDROverlay ** overlay)
{
  *overlay = calloc(1, sizeof(**overlay));
  if (!*overlay)
  {
    DEBUG_ERROR("Failed to allocate the HDR overlay");
    return false;
  }

  EGL_HDROverlay * this = *overlay;
  if (!egl_framebufferInit(&this->framebuffer) ||
      !egl_shaderInit(&this->shader) ||
      !egl_shaderCompile(this->shader,
        b_shader_hdr_overlay_vert, b_shader_hdr_overlay_vert_size,
        b_shader_hdr_overlay_frag, b_shader_hdr_overlay_frag_size,
        false, NULL) ||
      !egl_modelInit(&this->model))
  {
    DEBUG_ERROR("Failed to initialize the HDR overlay");
    egl_hdrOverlayFree(overlay);
    return false;
  }

  this->uPQ = egl_shaderGetUniform(this->shader, "pq");
  this->uReferenceWhiteLevel =
    egl_shaderGetUniform(this->shader, "referenceWhiteLevel");

  egl_modelSetDefault(this->model, false);
  egl_modelSetShader(this->model, this->shader);
  egl_modelSetTexture(this->model,
      egl_framebufferGetTexture(this->framebuffer));

  /* The conversion pass is a 1:1 copy; filtering can sample stale pixels just
   * outside a partial-damage region. */
  EGL_Texture * texture = egl_framebufferGetTexture(this->framebuffer);
  glSamplerParameteri(texture->sampler, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
  glSamplerParameteri(texture->sampler, GL_TEXTURE_MAG_FILTER, GL_NEAREST);

  atomic_init(&this->active, false);
  atomic_init(&this->pq, false);
  atomic_init(&this->referenceWhiteLevel, 80.0f);
  return true;
}

void egl_hdrOverlayFree(EGL_HDROverlay ** overlay)
{
  if (!*overlay)
    return;

  EGL_HDROverlay * this = *overlay;
  egl_modelFree(&this->model);
  egl_shaderFree(&this->shader);
  egl_framebufferFree(&this->framebuffer);
  free(this);
  *overlay = NULL;
}

bool egl_hdrOverlayResize(EGL_HDROverlay * this, unsigned int width,
    unsigned int height)
{
  this->width      = width;
  this->height     = height;
  this->configured = false;

  if (!width || !height)
    return true;

  this->configured = egl_framebufferSetup(this->framebuffer, EGL_PF_RGBA,
      width, height);
  return this->configured;
}

void egl_hdrOverlaySetState(EGL_HDROverlay * this, bool active, bool pq,
    float referenceWhiteLevel)
{
  atomic_store(&this->referenceWhiteLevel,
      referenceWhiteLevel > 0.0f ? referenceWhiteLevel : 80.0f);
  atomic_store(&this->pq, pq);
  atomic_store(&this->active, active);
}

static int convertDamage(EGL_HDROverlay * this, const struct Rect * damage,
    int damageCount, FrameDamageRect * output)
{
  int count = 0;
  for (int i = 0; i < damageCount; ++i)
  {
    const int x1 = clamp(damage[i].x, 0, (int)this->width);
    const int y1 = clamp(damage[i].y, 0, (int)this->height);
    const int x2 = clamp(damage[i].x + damage[i].w, 0, (int)this->width);
    const int y2 = clamp(damage[i].y + damage[i].h, 0, (int)this->height);
    if (x2 <= x1 || y2 <= y1)
      continue;

    output[count++] = (FrameDamageRect)
    {
      .x      = x1,
      .y      = this->height - y2,
      .width  = x2 - x1,
      .height = y2 - y1,
    };
  }

  return rectsMergeOverlapping(output, count);
}

bool egl_hdrOverlayBegin(EGL_HDROverlay * this,
    const struct Rect * damage, int damageCount)
{
  if (!this->configured || !atomic_load(&this->active))
    return false;

  this->renderPQ = atomic_load(&this->pq);
  this->renderReferenceWhiteLevel =
    atomic_load(&this->referenceWhiteLevel);

  egl_framebufferBind(this->framebuffer);
  egl_stateBlend(false);
  glClearColor(0.0f, 0.0f, 0.0f, 0.0f);

  if (damageCount < 0)
  {
    egl_stateScissor(false);
    glClear(GL_COLOR_BUFFER_BIT);
  }
  else
  {
    FrameDamageRect rects[damageCount];
    const int count = convertDamage(this, damage, damageCount, rects);

    egl_stateScissor(true);
    for (int i = 0; i < count; ++i)
    {
      glScissor(rects[i].x, rects[i].y,
          rects[i].width, rects[i].height);
      glClear(GL_COLOR_BUFFER_BIT);
    }
    egl_stateScissor(false);
  }

  return true;
}

void egl_hdrOverlayEnd(EGL_HDROverlay * this,
    const struct Rect * damage, int damageCount,
    EGL_Framebuffer * target)
{
  if (target)
    egl_framebufferBind(target);
  else
  {
    egl_stateBindFramebuffer(0);
    egl_stateViewport(0, 0, this->width, this->height);
  }

  egl_uniform1i(this->uPQ, this->renderPQ);
  egl_uniform1f(this->uReferenceWhiteLevel,
      this->renderReferenceWhiteLevel);

  egl_stateBlend(true);
  egl_stateBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);

  if (damageCount < 0)
  {
    egl_stateScissor(false);
    egl_modelRender(this->model);
  }
  else
  {
    FrameDamageRect rects[damageCount];
    const int count = convertDamage(this, damage, damageCount, rects);

    egl_stateScissor(true);
    for (int i = 0; i < count; ++i)
    {
      glScissor(rects[i].x, rects[i].y,
          rects[i].width, rects[i].height);
      egl_modelRender(this->model);
    }
    egl_stateScissor(false);
  }

  egl_stateBlend(false);
}
