/**
 * 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 "texture.h"
#include "texture_buffer.h"
#include "state.h"
#include "util.h"

#include "common/array.h"
#include "egl_dynprocs.h"
#include "egldebug.h"

struct FdImage
{
  int                   fd;
  EGLImage              image;
  GLsync                sync;
  int                   texIndex;
  LG_RendererFrameToken frameToken;
};

typedef struct TexDMABUF
{
  TextureBuffer base;

  EGLDisplay display;

  struct FdImage images[EGL_TEX_BUFFER_MAX];
  int            lastIndex;
  int            renderIndex;

  EGL_PixelFormat pixFmt;
  unsigned        fourcc;
  unsigned        width;
  GLuint          format;
}
TexDMABUF;

EGL_TextureOps EGL_TextureDMABUF;

static bool initDone           = false;
static bool has24BitSupport    = true;
static bool hasImportModifiers = true;

// internal functions

static void egl_texDMABUFCleanup(EGL_Texture * texture)
{
  TextureBuffer * parent = UPCAST(TextureBuffer, texture);
  TexDMABUF     * this   = UPCAST(TexDMABUF    , parent);

  for(int i = 0; i < ARRAY_LENGTH(this->images); ++i)
  {
    if (this->images[i].image != EGL_NO_IMAGE)
    {
      g_egl_dynProcs.eglDestroyImage(this->display, this->images[i].image);
      this->images[i].image = EGL_NO_IMAGE;
    }
    if (this->images[i].sync)
    {
      glDeleteSync(this->images[i].sync);
      this->images[i].sync = 0;
    }
    this->images[i].fd         = -1;
    this->images[i].texIndex   = -1;
    this->images[i].frameToken = LG_RENDERER_FRAME_TOKEN_NONE;
  }

  this->lastIndex        = -1;
  this->renderIndex      = -1;
  parent->rIndex         = -1;
  texture->frameToken    = LG_RENDERER_FRAME_TOKEN_NONE;

  egl_texUtilFreeBuffers(parent->buf, parent->texCount);

  if (parent->tex[0])
  {
    glDeleteTextures(parent->texCount, parent->tex);
    egl_stateInvalidateShared();
  }
}

// dmabuf functions

static bool egl_texDMABUFInit(EGL_Texture ** texture, EGL_TexType type,
    EGLDisplay * display)
{
  TexDMABUF * this = calloc(1, sizeof(*this));
  *texture = &this->base.base;

  for(int i = 0; i < ARRAY_LENGTH(this->images); ++i)
  {
    this->images[i].fd         = -1;
    this->images[i].image      = EGL_NO_IMAGE;
    this->images[i].sync       = 0;
    this->images[i].texIndex   = -1;
    this->images[i].frameToken = LG_RENDERER_FRAME_TOKEN_NONE;
  }
  this->lastIndex   = -1;
  this->renderIndex = -1;

  EGL_Texture * parent = &this->base.base;
  if (!egl_texBufferStreamInit(&parent, type, display))
  {
    free(this);
    *texture = NULL;
    return false;
  }

  this->display = display;

  if (!initDone)
  {
    const char * client_exts = eglQueryString(this->display, EGL_EXTENSIONS);
    hasImportModifiers =
      util_hasGLExt(client_exts, "EGL_EXT_image_dma_buf_import_modifiers");
    initDone = true;
  }

  return true;
}

static void egl_texDMABUFFree(EGL_Texture * texture)
{
  TextureBuffer * parent = UPCAST(TextureBuffer, texture);
  TexDMABUF     * this   = UPCAST(TexDMABUF    , parent);

  egl_texDMABUFCleanup(texture);
  egl_texBufferFree(&parent->base);
  free(this);
}

static bool texDMABUFSetup(EGL_Texture * texture)
{
  TextureBuffer * parent = UPCAST(TextureBuffer, texture);
  TexDMABUF     * this   = UPCAST(TexDMABUF    , parent);

  if (texture->format.pixFmt == EGL_PF_RGB_24 && !has24BitSupport)
  {
    this->pixFmt = EGL_PF_RGB_24_32;
    this->width  = texture->format.pitch / 4;
    this->fourcc = DRM_FORMAT_ARGB8888;
    this->format = GL_BGRA_EXT;
  }

  egl_texDMABUFCleanup(texture);

  glGenTextures(parent->texCount, parent->tex);
  parent->rIndex = -1;

  return true;
}

static bool egl_texDMABUFSetup(EGL_Texture * texture, const EGL_TexSetup * setup)
{
  TextureBuffer * parent = UPCAST(TextureBuffer, texture);
  TexDMABUF     * this   = UPCAST(TexDMABUF    , parent);

  this->pixFmt = texture->format.pixFmt;
  this->width  = texture->format.width;
  this->fourcc = texture->format.fourcc;
  this->format = texture->format.format;

  return texDMABUFSetup(texture);
}

static EGLImage createImage(EGL_Texture * texture, int fd)
{
  TextureBuffer * parent = UPCAST(TextureBuffer, texture);
  TexDMABUF     * this   = UPCAST(TexDMABUF    , parent);

  const uint64_t modifier = DRM_FORMAT_MOD_LINEAR;
  EGLAttrib attribs[] =
  {
    EGL_WIDTH                         , this->width ,
    EGL_HEIGHT                        , texture->format.height,
    EGL_LINUX_DRM_FOURCC_EXT          , this->fourcc,
    EGL_DMA_BUF_PLANE0_FD_EXT         , fd,
    EGL_DMA_BUF_PLANE0_OFFSET_EXT     , 0,
    EGL_DMA_BUF_PLANE0_PITCH_EXT      , texture->format.pitch,
    EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT, (modifier & 0xffffffff),
    EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT, (modifier >> 32),
    EGL_NONE                          , EGL_NONE
  };

  if (!hasImportModifiers)
    attribs[12] = attribs[13] =
    attribs[14] = attribs[15] = EGL_NONE;

  return g_egl_dynProcs.eglCreateImage(
      this->display,
      EGL_NO_CONTEXT,
      EGL_LINUX_DMA_BUF_EXT,
      (EGLClientBuffer)NULL,
      attribs);
}

static bool egl_texDMABUFUpdate(EGL_Texture * texture,
    const EGL_TexUpdate * update)
{
  TextureBuffer * parent = UPCAST(TextureBuffer, texture);
  TexDMABUF     * this   = UPCAST(TexDMABUF    , parent);

  DEBUG_ASSERT(update->type == EGL_TEXTYPE_DMABUF);

  struct FdImage *fdImage = NULL;
  for (int i = 0; i < ARRAY_LENGTH(this->images); ++i)
    if (this->images[i].fd == update->dmaFD)
    {
      fdImage = &this->images[i];
      break;
    }

  if (!fdImage)
    for (int i = 0; i < ARRAY_LENGTH(this->images); ++i)
      if (this->images[i].fd == -1)
      {
        fdImage = &this->images[i];
        break;
      }

  if (!fdImage)
  {
    DEBUG_ERROR("No free DMABUF image slot");
    return false;
  }

  EGLImage image = fdImage->image;
  if (unlikely(image == EGL_NO_IMAGE))
  {
    bool setup = false;
    if (texture->format.pixFmt == EGL_PF_RGB_24 && has24BitSupport)
    {
      image = createImage(texture, update->dmaFD);
      if (image == EGL_NO_IMAGE)
      {
        DEBUG_INFO("Using 24-bit in 32-bit for DMA");
        has24BitSupport = false;
        setup = true;
      }
    }

    if (!has24BitSupport && setup)
      texDMABUFSetup(texture);

    if (image == EGL_NO_IMAGE)
      image = createImage(texture, update->dmaFD);

    if (unlikely(image == EGL_NO_IMAGE))
    {
      DEBUG_EGL_ERROR("Failed to create EGLImage for DMA transfer");
      return false;
    }

    fdImage->fd    = update->dmaFD;
    fdImage->image = image;

    const int slot = (int)(fdImage - this->images);
    fdImage->texIndex = slot;
    GLsync sync = 0;
    INTERLOCKED_SECTION(parent->copyLock,
    {
      egl_stateBindTexture(0, GL_TEXTURE_EXTERNAL_OES, parent->tex[slot]);
      glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
      glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
      glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
      glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
      g_egl_dynProcs.glEGLImageTargetTexture2DOES(
          GL_TEXTURE_EXTERNAL_OES, image);

      sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
      fdImage->sync = sync;
    });

    if (unlikely(!sync))
    {
      DEBUG_GL_ERROR("Failed to create DMABUF sync");
      return false;
    }

    /* Publish the texture attachment to the shared render context once. The
     * backing memory is synchronized separately by framebuffer_wait(), so
     * subsequent frames do not add any GL commands to this context. */
    glFlush();
  }

  INTERLOCKED_SECTION(parent->copyLock,
  {
    fdImage->frameToken = update->frameToken;
    this->lastIndex     = (int)(fdImage - this->images);
  });

  return true;
}

static EGL_TexStatus egl_texDMABUFProcess(EGL_Texture * texture,
    LG_RendererFrameToken frameTokenLimit)
{
  TextureBuffer * parent = UPCAST(TextureBuffer, texture);
  TexDMABUF     * this   = UPCAST(TexDMABUF    , parent);

  int  index     = -1;
  bool haveImage = false;
  bool updated   = false;
  INTERLOCKED_SECTION(parent->copyLock,
  {
    index     = this->lastIndex;
    haveImage = this->renderIndex >= 0;
    if (index >= 0 && egl_textureFrameAllowed(
          this->images[index].frameToken, frameTokenLimit))
    {
      const struct FdImage * pending = &this->images[index];
      if (this->renderIndex != index ||
          texture->frameToken != pending->frameToken)
      {
        this->renderIndex    = index;
        parent->rIndex       = pending->texIndex;
        texture->frameToken = pending->frameToken;
        updated              = true;
        haveImage            = true;
      }
    }
  });

  if (unlikely(!haveImage))
    return EGL_TEX_STATUS_NOTREADY;
  return updated ? EGL_TEX_STATUS_UPDATED : EGL_TEX_STATUS_OK;
}

static EGL_TexStatus egl_texDMABUFGet(EGL_Texture * texture, GLuint * tex,
    EGL_PixelFormat * fmt)
{
  TextureBuffer * parent = UPCAST(TextureBuffer, texture);
  TexDMABUF     * this   = UPCAST(TexDMABUF    , parent);

  int index    = -1;
  int texIndex = -1;
  GLsync sync  = 0;

  INTERLOCKED_SECTION(parent->copyLock,
  {
    index = this->renderIndex;
    if (index >= 0)
    {
      struct FdImage * cur = &this->images[index];
      texIndex = cur->texIndex;
      sync = cur->sync;
      cur->sync = 0;
    }
  });

  if (unlikely(index < 0))
    return EGL_TEX_STATUS_NOTREADY;

  if (sync)
  {
    /* Keep the cross-context dependency on the GPU timeline. Subsequent
     * sampling waits for the import without delaying render submission. */
    glWaitSync(sync, 0, GL_TIMEOUT_IGNORED);
    glDeleteSync(sync);
  }

  *tex = parent->tex[texIndex];

  if (fmt)
    *fmt = this->pixFmt;

  return EGL_TEX_STATUS_OK;
}

static EGL_TexStatus egl_texDMABUFBind(EGL_Texture * texture, GLuint unit)
{
  GLuint tex;
  EGL_TexStatus status;

  if ((status = egl_texDMABUFGet(texture, &tex, NULL)) != EGL_TEX_STATUS_OK)
    return status;

  egl_stateBindTexture(unit, GL_TEXTURE_EXTERNAL_OES, tex);
  return EGL_TEX_STATUS_OK;
}

EGL_TextureOps EGL_TextureDMABUF =
{
  .init        = egl_texDMABUFInit,
  .free        = egl_texDMABUFFree,
  .setup       = egl_texDMABUFSetup,
  .update      = egl_texDMABUFUpdate,
  .process     = egl_texDMABUFProcess,
  .get         = egl_texDMABUFGet,
  .bind        = egl_texDMABUFBind
};
