/**
 * 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 "shader.h"
#include "state.h"
#include "common/debug.h"
#include "common/stringutils.h"
#include "util.h"

#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>

struct EGL_Uniform
{
  char  * name;
  GLint   location;

  enum EGL_UniformType type;
  GLsizei              count;
  GLboolean            transpose;
  bool                 initialized;
  bool                 dirty;

  void * data;
  size_t size;
  size_t capacity;

  EGL_Shader         * shader;
  struct EGL_Uniform * next;
  struct EGL_Uniform * dirtyNext;
};

struct EGL_Shader
{
  bool   hasShader;
  GLuint shader;

  EGL_Uniform * uniforms;
  EGL_Uniform * uniformsTail;
  EGL_Uniform * dirtyUniforms;
  EGL_Uniform * dirtyUniformsTail;
};

bool egl_shaderInit(EGL_Shader ** this)
{
  *this = calloc(1, sizeof(EGL_Shader));
  if (!*this)
  {
    DEBUG_ERROR("Failed to malloc EGL_Shader");
    return false;
  }

  return true;
}

void egl_shaderFree(EGL_Shader ** shader)
{
  EGL_Shader * this = *shader;
  if (!this)
    return;

  if (this->hasShader)
  {
    glDeleteProgram(this->shader);
    egl_stateInvalidateShared();
  }

  EGL_Uniform * uniform = this->uniforms;
  while (uniform)
  {
    EGL_Uniform * next = uniform->next;
    free(uniform->name);
    free(uniform->data);
    free(uniform);
    uniform = next;
  }

  free(this);
  *shader = NULL;
}

bool egl_shaderLoad(EGL_Shader * this,
    const char * vertex_file, const char * fragment_file, bool useDMA,
    const EGL_ShaderDefine * defines)
{
  char   * vertex_code, * fragment_code;
  size_t   vertex_size,   fragment_size;

  if (!util_fileGetContents(vertex_file, &vertex_code, &vertex_size))
  {
    DEBUG_ERROR("Failed to read vertex shader");
    return false;
  }

  DEBUG_INFO("Loaded vertex shader: %s", vertex_file);

  if (!util_fileGetContents(fragment_file, &fragment_code, &fragment_size))
  {
    DEBUG_ERROR("Failed to read fragment shader");
    free(vertex_code);
    return false;
  }

  DEBUG_INFO("Loaded fragment shader: %s", fragment_file);

  bool ret = egl_shaderCompile(this,
      vertex_code, vertex_size, fragment_code, fragment_size,
      useDMA, defines);

  free(vertex_code);
  free(fragment_code);
  return ret;
}

static void uniformMarkDirty(EGL_Uniform * uniform)
{
  if (uniform->dirty)
    return;

  uniform->dirty = true;
  if (uniform->shader->dirtyUniformsTail)
    uniform->shader->dirtyUniformsTail->dirtyNext = uniform;
  else
    uniform->shader->dirtyUniforms = uniform;
  uniform->shader->dirtyUniformsTail = uniform;
}

static void shaderResolveUniforms(EGL_Shader * this)
{
  this->dirtyUniforms     = NULL;
  this->dirtyUniformsTail = NULL;

  for(EGL_Uniform * uniform = this->uniforms; uniform; uniform = uniform->next)
  {
    uniform->location  = glGetUniformLocation(this->shader, uniform->name);
    uniform->dirty     = false;
    uniform->dirtyNext = NULL;
    if (uniform->initialized)
      uniformMarkDirty(uniform);
  }
}

static bool shaderCompile(EGL_Shader * this, const char * vertex_code,
    size_t vertex_size, const char * fragment_code, size_t fragment_size)
{
  if (this->hasShader)
  {
    glDeleteProgram(this->shader);
    egl_stateInvalidateShared();
    this->hasShader = false;
  }

  GLint  length;
  GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);

  length = vertex_size;
  glShaderSource(vertexShader, 1, (const char**)&vertex_code, &length);
  glCompileShader(vertexShader);

  GLint result = GL_FALSE;
  glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &result);
  if (result == GL_FALSE)
  {
    DEBUG_ERROR("Failed to compile vertex shader");

    int logLength;
    glGetShaderiv(vertexShader, GL_INFO_LOG_LENGTH, &logLength);
    if (logLength > 0)
    {
      char *log = malloc(logLength + 1);
      if (!log)
        DEBUG_ERROR("out of memory");
      else
      {
        glGetShaderInfoLog(vertexShader, logLength, NULL, log);
        log[logLength] = 0;
        DEBUG_ERROR("%s", log);
        free(log);
      }
    }

    glDeleteShader(vertexShader);
    return false;
  }

  GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);

  length = fragment_size;
  glShaderSource(fragmentShader, 1, (const char**)&fragment_code, &length);
  glCompileShader(fragmentShader);

  glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &result);
  if (result == GL_FALSE)
  {
    DEBUG_ERROR("Failed to compile fragment shader");

    int logLength;
    glGetShaderiv(fragmentShader, GL_INFO_LOG_LENGTH, &logLength);
    if (logLength > 0)
    {
      char *log = malloc(logLength + 1);
      if (!log)
        DEBUG_ERROR("out of memory");
      else
      {
        glGetShaderInfoLog(fragmentShader, logLength, NULL, log);
        log[logLength] = 0;
        DEBUG_ERROR("%s", log);
        free(log);
      }
    }

    glDeleteShader(fragmentShader);
    glDeleteShader(vertexShader  );
    return false;
  }

  this->shader = glCreateProgram();
  glAttachShader(this->shader, vertexShader  );
  glAttachShader(this->shader, fragmentShader);
  glLinkProgram(this->shader);

  glGetProgramiv(this->shader, GL_LINK_STATUS, &result);
  if (result == GL_FALSE)
  {
    DEBUG_ERROR("Failed to link shader program");

    int logLength;
    glGetProgramiv(this->shader, GL_INFO_LOG_LENGTH, &logLength);
    if (logLength > 0)
    {
      char *log = malloc(logLength + 1);
      if (!log)
        DEBUG_ERROR("out of memory");
      else
      {
        glGetProgramInfoLog(this->shader, logLength, NULL, log);
        log[logLength] = 0;
        DEBUG_ERROR("%s", log);
        free(log);
      }
    }

    glDetachShader(this->shader, vertexShader  );
    glDetachShader(this->shader, fragmentShader);
    glDeleteShader(fragmentShader);
    glDeleteShader(vertexShader  );
    glDeleteProgram(this->shader );
    this->shader = 0;
    return false;
  }

  glDetachShader(this->shader, vertexShader  );
  glDetachShader(this->shader, fragmentShader);
  glDeleteShader(fragmentShader);
  glDeleteShader(vertexShader  );

  this->hasShader = true;
  shaderResolveUniforms(this);
  return true;
}

bool egl_shaderCompile(EGL_Shader * this, const char * vertex_code,
    size_t vertex_size, const char * fragment_code, size_t fragment_size,
    bool useDMA, const EGL_ShaderDefine * defines)
{
  bool result      = false;
  char * processed = NULL;
  char * newCode   = NULL;

  if (useDMA)
  {
    const char search[]  = "sampler2D";
    const char replace[] = "samplerExternalOES";

    const char * offset = NULL;
    int instances = 0;

    while((offset = memsearch(
      fragment_code, fragment_size,
      search       , sizeof(search)-1,
      offset)))
    {
      ++instances;
      offset += sizeof(search)-1;
    }

    const int diff   = (sizeof(replace) - sizeof(search)) * instances;
    const int newLen = fragment_size + diff;
    newCode = malloc(newLen + 1);
    if (!newCode)
    {
      DEBUG_ERROR("Out of memory");
      goto exit;
    }

    const char * src = fragment_code;
    char * dst = newCode;
    for(int i = 0; i < instances; ++i)
    {
      const char * pos = strstr(src, search);
      const int offset = pos - src;

      memcpy(dst, src, offset);
      dst += offset;
      src  = pos + sizeof(search)-1;

      memcpy(dst, replace, sizeof(replace)-1);
      dst += sizeof(replace)-1;
    }

    const int final = fragment_size - (src - fragment_code);
    memcpy(dst, src, final);
    dst[final] = '\0';

    fragment_code = newCode;
    fragment_size = newLen;
  }

  if (defines)
  {
    // find the end of any existing lines starting with #
    bool newLine   = true;
    bool skip      = false;
    int  insertPos = 0;
    for(int i = 0; i < fragment_size; ++i)
    {
      if (skip)
      {
        if (fragment_code[i] == '\n')
          skip = false;
        continue;
      }

      switch(fragment_code[i])
      {
        case '\n':
          newLine = true;
          continue;

        case ' ':
        case '\t':
        case '\r':
          continue;

        case '#':
          if (newLine)
          {
            skip = true;
            continue;
          }
          //fallthrough

        default:
          newLine = false;
          break;
      }

      if (!newLine)
      {
        insertPos = i;
        if (insertPos > 0)
          --insertPos;
        break;
      }
    }

    int processedLen = fragment_size;
    const char * defineFormat = "#define %s %s\n";
    for(const EGL_ShaderDefine * define = defines; define->name; ++define)
      processedLen += snprintf(NULL, 0, defineFormat, define->name, define->value);

    processed = malloc(processedLen);
    if (!processed)
    {
      DEBUG_ERROR("Out of memory");
      goto exit;
    }

    memcpy(processed, fragment_code, insertPos);

    int offset = insertPos;
    for(const EGL_ShaderDefine * define = defines; define->name; ++define)
      offset += sprintf(processed + offset, defineFormat,
          define->name, define->value);

    memcpy(
        processed       + offset,
        fragment_code + insertPos,
        fragment_size - insertPos);

    fragment_code = processed;
    fragment_size = processedLen;
  }

  result = shaderCompile(this,
      vertex_code  , vertex_size,
      fragment_code, fragment_size);

exit:
  free(processed);
  free(newCode);
  return result;
}

static bool uniformLayout(enum EGL_UniformType type, size_t * scalarSize,
    size_t * components)
{
  switch(type)
  {
    case EGL_UNIFORM_TYPE_1F:
    case EGL_UNIFORM_TYPE_1FV:
      *scalarSize = sizeof(GLfloat); *components = 1; break;
    case EGL_UNIFORM_TYPE_2F:
    case EGL_UNIFORM_TYPE_2FV:
      *scalarSize = sizeof(GLfloat); *components = 2; break;
    case EGL_UNIFORM_TYPE_3F:
    case EGL_UNIFORM_TYPE_3FV:
      *scalarSize = sizeof(GLfloat); *components = 3; break;
    case EGL_UNIFORM_TYPE_4F:
    case EGL_UNIFORM_TYPE_4FV:
      *scalarSize = sizeof(GLfloat); *components = 4; break;

    case EGL_UNIFORM_TYPE_1I:
    case EGL_UNIFORM_TYPE_1IV:
      *scalarSize = sizeof(GLint); *components = 1; break;
    case EGL_UNIFORM_TYPE_2I:
    case EGL_UNIFORM_TYPE_2IV:
      *scalarSize = sizeof(GLint); *components = 2; break;
    case EGL_UNIFORM_TYPE_3I:
    case EGL_UNIFORM_TYPE_3IV:
      *scalarSize = sizeof(GLint); *components = 3; break;
    case EGL_UNIFORM_TYPE_4I:
    case EGL_UNIFORM_TYPE_4IV:
      *scalarSize = sizeof(GLint); *components = 4; break;

    case EGL_UNIFORM_TYPE_1UI:
    case EGL_UNIFORM_TYPE_1UIV:
      *scalarSize = sizeof(GLuint); *components = 1; break;
    case EGL_UNIFORM_TYPE_2UI:
    case EGL_UNIFORM_TYPE_2UIV:
      *scalarSize = sizeof(GLuint); *components = 2; break;
    case EGL_UNIFORM_TYPE_3UI:
    case EGL_UNIFORM_TYPE_3UIV:
      *scalarSize = sizeof(GLuint); *components = 3; break;
    case EGL_UNIFORM_TYPE_4UI:
    case EGL_UNIFORM_TYPE_4UIV:
      *scalarSize = sizeof(GLuint); *components = 4; break;

    case EGL_UNIFORM_TYPE_M2FV:
      *scalarSize = sizeof(GLfloat); *components = 4; break;
    case EGL_UNIFORM_TYPE_M3FV:
      *scalarSize = sizeof(GLfloat); *components = 9; break;
    case EGL_UNIFORM_TYPE_M4FV:
      *scalarSize = sizeof(GLfloat); *components = 16; break;
    case EGL_UNIFORM_TYPE_M2x3FV:
    case EGL_UNIFORM_TYPE_M3x2FV:
      *scalarSize = sizeof(GLfloat); *components = 6; break;
    case EGL_UNIFORM_TYPE_M2x4FV:
    case EGL_UNIFORM_TYPE_M4x2FV:
      *scalarSize = sizeof(GLfloat); *components = 8; break;
    case EGL_UNIFORM_TYPE_M3x4FV:
    case EGL_UNIFORM_TYPE_M4x3FV:
      *scalarSize = sizeof(GLfloat); *components = 12; break;

    default:
      return false;
  }
  return true;

  return false;
}

EGL_Uniform * egl_shaderGetUniform(EGL_Shader * this, const char * name)
{
  for(EGL_Uniform * uniform = this->uniforms; uniform; uniform = uniform->next)
    if (!strcmp(uniform->name, name))
      return uniform;

  EGL_Uniform * uniform = calloc(1, sizeof(*uniform));
  if (!uniform)
  {
    DEBUG_ERROR("Failed to allocate uniform `%s`", name);
    return NULL;
  }

  const size_t nameSize = strlen(name) + 1;
  uniform->name = malloc(nameSize);
  if (!uniform->name)
  {
    DEBUG_ERROR("Failed to allocate uniform name `%s`", name);
    free(uniform);
    return NULL;
  }

  memcpy(uniform->name, name, nameSize);
  uniform->shader = this;
  uniform->location = this->hasShader ?
    glGetUniformLocation(this->shader, name) : -1;

  if (this->uniformsTail)
    this->uniformsTail->next = uniform;
  else
    this->uniforms = uniform;
  this->uniformsTail = uniform;

  return uniform;
}

bool egl_uniformSet(EGL_Uniform * uniform, enum EGL_UniformType type,
    GLsizei count, GLboolean transpose, const void * data)
{
  if (!uniform || !data || count < 1)
    return false;

  size_t scalarSize, components;
  if (!uniformLayout(type, &scalarSize, &components) ||
      components > SIZE_MAX / scalarSize ||
      (size_t)count > SIZE_MAX / (scalarSize * components))
    return false;

  const size_t size = scalarSize * components * count;
  const bool changed = !uniform->initialized ||
    uniform->type      != type      ||
    uniform->count     != count     ||
    uniform->transpose != transpose ||
    uniform->size      != size      ||
    memcmp(uniform->data, data, size);

  if (!changed)
    return true;

  if (size > uniform->capacity)
  {
    void * newData = realloc(uniform->data, size);
    if (!newData)
    {
      DEBUG_ERROR("Failed to allocate data for uniform `%s`", uniform->name);
      return false;
    }

    uniform->data = newData;
    uniform->capacity = size;
  }

  memcpy(uniform->data, data, size);
  uniform->type      = type;
  uniform->count     = count;
  uniform->transpose = transpose;
  uniform->size      = size;
  uniform->initialized = true;
  uniformMarkDirty(uniform);
  return true;
}

bool egl_uniform1f(EGL_Uniform * uniform, GLfloat x)
{
  return egl_uniformSet(uniform, EGL_UNIFORM_TYPE_1F, 1, GL_FALSE, &x);
}

bool egl_uniform2f(EGL_Uniform * uniform, GLfloat x, GLfloat y)
{
  const GLfloat value[] = { x, y };
  return egl_uniformSet(uniform, EGL_UNIFORM_TYPE_2F, 1, GL_FALSE, value);
}

bool egl_uniform3f(EGL_Uniform * uniform, GLfloat x, GLfloat y, GLfloat z)
{
  const GLfloat value[] = { x, y, z };
  return egl_uniformSet(uniform, EGL_UNIFORM_TYPE_3F, 1, GL_FALSE, value);
}

bool egl_uniform4f(EGL_Uniform * uniform, GLfloat x, GLfloat y, GLfloat z,
    GLfloat w)
{
  const GLfloat value[] = { x, y, z, w };
  return egl_uniformSet(uniform, EGL_UNIFORM_TYPE_4F, 1, GL_FALSE, value);
}

bool egl_uniform1i(EGL_Uniform * uniform, GLint x)
{
  return egl_uniformSet(uniform, EGL_UNIFORM_TYPE_1I, 1, GL_FALSE, &x);
}

bool egl_uniform1ui(EGL_Uniform * uniform, GLuint x)
{
  return egl_uniformSet(uniform, EGL_UNIFORM_TYPE_1UI, 1, GL_FALSE, &x);
}

bool egl_uniform4ui(EGL_Uniform * uniform, GLuint x, GLuint y, GLuint z,
    GLuint w)
{
  const GLuint value[] = { x, y, z, w };
  return egl_uniformSet(uniform, EGL_UNIFORM_TYPE_4UI, 1, GL_FALSE, value);
}

bool egl_uniform4uiv(EGL_Uniform * uniform, GLsizei count,
    const GLuint * value)
{
  return egl_uniformSet(uniform, EGL_UNIFORM_TYPE_4UIV, count, GL_FALSE,
      value);
}

bool egl_uniformMatrix3x2fv(EGL_Uniform * uniform, GLsizei count,
    GLboolean transpose, const GLfloat * value)
{
  return egl_uniformSet(uniform, EGL_UNIFORM_TYPE_M3x2FV, count, transpose,
      value);
}

static void uniformUpload(EGL_Uniform * uniform)
{
  switch(uniform->type)
  {
    case EGL_UNIFORM_TYPE_1F:
      glUniform1fv(uniform->location, uniform->count, uniform->data); break;
    case EGL_UNIFORM_TYPE_2F:
      glUniform2fv(uniform->location, uniform->count, uniform->data); break;
    case EGL_UNIFORM_TYPE_3F:
      glUniform3fv(uniform->location, uniform->count, uniform->data); break;
    case EGL_UNIFORM_TYPE_4F:
      glUniform4fv(uniform->location, uniform->count, uniform->data); break;
    case EGL_UNIFORM_TYPE_1I:
      glUniform1iv(uniform->location, uniform->count, uniform->data); break;
    case EGL_UNIFORM_TYPE_2I:
      glUniform2iv(uniform->location, uniform->count, uniform->data); break;
    case EGL_UNIFORM_TYPE_3I:
      glUniform3iv(uniform->location, uniform->count, uniform->data); break;
    case EGL_UNIFORM_TYPE_4I:
      glUniform4iv(uniform->location, uniform->count, uniform->data); break;
    case EGL_UNIFORM_TYPE_1UI:
      glUniform1uiv(uniform->location, uniform->count, uniform->data); break;
    case EGL_UNIFORM_TYPE_2UI:
      glUniform2uiv(uniform->location, uniform->count, uniform->data); break;
    case EGL_UNIFORM_TYPE_3UI:
      glUniform3uiv(uniform->location, uniform->count, uniform->data); break;
    case EGL_UNIFORM_TYPE_4UI:
      glUniform4uiv(uniform->location, uniform->count, uniform->data); break;

    case EGL_UNIFORM_TYPE_1FV:
      glUniform1fv(uniform->location, uniform->count, uniform->data); break;
    case EGL_UNIFORM_TYPE_2FV:
      glUniform2fv(uniform->location, uniform->count, uniform->data); break;
    case EGL_UNIFORM_TYPE_3FV:
      glUniform3fv(uniform->location, uniform->count, uniform->data); break;
    case EGL_UNIFORM_TYPE_4FV:
      glUniform4fv(uniform->location, uniform->count, uniform->data); break;
    case EGL_UNIFORM_TYPE_1IV:
      glUniform1iv(uniform->location, uniform->count, uniform->data); break;
    case EGL_UNIFORM_TYPE_2IV:
      glUniform2iv(uniform->location, uniform->count, uniform->data); break;
    case EGL_UNIFORM_TYPE_3IV:
      glUniform3iv(uniform->location, uniform->count, uniform->data); break;
    case EGL_UNIFORM_TYPE_4IV:
      glUniform4iv(uniform->location, uniform->count, uniform->data); break;
    case EGL_UNIFORM_TYPE_1UIV:
      glUniform1uiv(uniform->location, uniform->count, uniform->data); break;
    case EGL_UNIFORM_TYPE_2UIV:
      glUniform2uiv(uniform->location, uniform->count, uniform->data); break;
    case EGL_UNIFORM_TYPE_3UIV:
      glUniform3uiv(uniform->location, uniform->count, uniform->data); break;
    case EGL_UNIFORM_TYPE_4UIV:
      glUniform4uiv(uniform->location, uniform->count, uniform->data); break;

    case EGL_UNIFORM_TYPE_M2FV:
      glUniformMatrix2fv(uniform->location, uniform->count,
          uniform->transpose, uniform->data); break;
    case EGL_UNIFORM_TYPE_M3FV:
      glUniformMatrix3fv(uniform->location, uniform->count,
          uniform->transpose, uniform->data); break;
    case EGL_UNIFORM_TYPE_M4FV:
      glUniformMatrix4fv(uniform->location, uniform->count,
          uniform->transpose, uniform->data); break;
    case EGL_UNIFORM_TYPE_M2x3FV:
      glUniformMatrix2x3fv(uniform->location, uniform->count,
          uniform->transpose, uniform->data); break;
    case EGL_UNIFORM_TYPE_M3x2FV:
      glUniformMatrix3x2fv(uniform->location, uniform->count,
          uniform->transpose, uniform->data); break;
    case EGL_UNIFORM_TYPE_M2x4FV:
      glUniformMatrix2x4fv(uniform->location, uniform->count,
          uniform->transpose, uniform->data); break;
    case EGL_UNIFORM_TYPE_M4x2FV:
      glUniformMatrix4x2fv(uniform->location, uniform->count,
          uniform->transpose, uniform->data); break;
    case EGL_UNIFORM_TYPE_M3x4FV:
      glUniformMatrix3x4fv(uniform->location, uniform->count,
          uniform->transpose, uniform->data); break;
    case EGL_UNIFORM_TYPE_M4x3FV:
      glUniformMatrix4x3fv(uniform->location, uniform->count,
          uniform->transpose, uniform->data); break;
  }
}

void egl_shaderUse(EGL_Shader * this)
{
  if (!this->hasShader)
  {
    DEBUG_ERROR("Shader program has not been compiled");
    return;
  }

  egl_stateUseProgram(this->shader);

  while(this->dirtyUniforms)
  {
    EGL_Uniform * uniform = this->dirtyUniforms;
    this->dirtyUniforms = uniform->dirtyNext;
    uniform->dirtyNext = NULL;
    uniform->dirty = false;

    if (uniform->location != -1)
      uniformUpload(uniform);
  }

  this->dirtyUniformsTail = NULL;
}

void egl_shaderAssocTextures(EGL_Shader * this, int count)
{
  char name[32];
  for(int i = 0; i < count; ++i)
  {
    snprintf(name, sizeof(name), "sampler%d", i + 1);
    egl_uniform1i(egl_shaderGetUniform(this, name), i);
  }
}
