#include "opengles_ctx.h"

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

#include <GLES2/gl2.h>
#include <GLES3/gl3.h>

#define log_trace printf
#define log_debug printf
#define log_info printf
#define log_warn printf
#define log_error printf
#define log_fatal printf

const char g_indices[] =
    {0, 3, 2, 0, 2, 1};

const char g_vertextShader[] =
    {"attribute vec4 aPosition;\n"
     "attribute vec2 aTextureCoord;\n"
     "varying vec2 vTextureCoord;\n"
     "void main() {\n"
     "  gl_Position = aPosition;\n"
     "  vTextureCoord = aTextureCoord;\n"
     "}\n"};

const char g_fragmentShader[] =
    {"precision mediump float;\n"
     "uniform sampler2D Ytex;\n"
     "uniform sampler2D Utex,Vtex;\n"
     "varying vec2 vTextureCoord;\n"
     "void main(void) {\n"
     "  float nx,ny,r,g,b,y,u,v;\n"
     "  mediump vec4 txl,ux,vx;"
     "  nx=vTextureCoord[0];\n"
     "  ny=vTextureCoord[1];\n"
     "  y=texture2D(Ytex,vec2(nx,ny)).r;\n"
     "  u=texture2D(Utex,vec2(nx,ny)).r;\n"
     "  v=texture2D(Vtex,vec2(nx,ny)).r;\n"

     "  y=1.1643*(y-0.0625);\n"
     "  u=u-0.5;\n"
     "  v=v-0.5;\n"

     "  r=y+1.5958*v;\n"
     "  g=y-0.39173*u-0.81290*v;\n"
     "  b=y+2.017*u;\n"
     "  gl_FragColor=vec4(r,g,b,1.0);\n"
     "}\n"};

struct opengles_ctx
{
  int32_t id;
  GLuint textureIds[3]; // Texture id of Y,U and V texture.
  GLuint program;
  GLsizei width;
  GLsizei height;
  GLfloat vertices[20];
};

static void glesPrintInfo(const char *name, GLenum s)
{
  const char *v = (const char *)glGetString(s);
  log_debug("GL %s = %s\n", name, v);
}

static void glesCheckStatus(const char *op)
{
  for (GLint error = glGetError(); error; error = glGetError())
  {
    log_debug("Call %s() glError (0x%x)\n", op, error);
  }
}

static void glesConfigTexture(int name, int id, int width, int height)
{
  glActiveTexture(name);
  glBindTexture(GL_TEXTURE_2D, id);
  glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
  glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
  glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
  glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, width, height, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, NULL);
}

static void glesCreateTextures(opengles_ctx_t *gles, GLsizei width, GLsizei height)
{
  glGenTextures(3, gles->textureIds); // Generate  the Y, U and V texture
  glesConfigTexture(GL_TEXTURE0, gles->textureIds[0], width, height);
  glesConfigTexture(GL_TEXTURE1, gles->textureIds[1], width / 2, height / 2);
  glesConfigTexture(GL_TEXTURE2, gles->textureIds[2], width / 2, height / 2);
}

// Uploads a plane of pixel data, accounting for stride != width*bpp.
static void glesUpdateImage(GLsizei width, GLsizei height, int stride, const char *plane)
{
  glPixelStorei(GL_UNPACK_ROW_LENGTH, stride);
  // Yay!  We can upload the entire plane in a single GL call.
  glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_LUMINANCE,
                  GL_UNSIGNED_BYTE, (const GLvoid *)(plane));
}

static void glesUpdateTextures(opengles_ctx_t *gles, GLsizei width, GLsizei height,
                               const char *y, const char *u, const char *v,
                               int y_stride, int u_stride, int v_stride)
{

  glActiveTexture(GL_TEXTURE0);
  glBindTexture(GL_TEXTURE_2D, gles->textureIds[0]);
  glesUpdateImage(width, height, y_stride, y);

  glActiveTexture(GL_TEXTURE1);
  glBindTexture(GL_TEXTURE_2D, gles->textureIds[1]);
  glesUpdateImage(width / 2, height / 2, u_stride, u);

  glActiveTexture(GL_TEXTURE2);
  glBindTexture(GL_TEXTURE_2D, gles->textureIds[2]);
  glesUpdateImage(width / 2, height / 2, v_stride, v);
}

static GLuint glesLoadShader(GLenum shaderType, const char *pSource)
{
  GLuint shader = glCreateShader(shaderType);
  if (shader)
  {
    glShaderSource(shader, 1, &pSource, NULL);
    glCompileShader(shader);
    GLint compiled = 0;
    glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
    if (!compiled)
    {
      GLint infoLen = 0;
      glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &infoLen);
      if (infoLen)
      {
        char *buf = (char *)malloc(infoLen);
        if (buf)
        {
          glGetShaderInfoLog(shader, infoLen, NULL, buf);
          log_debug("Could not compile shader %d: %s", shaderType, buf);
          free(buf);
        }
        glDeleteShader(shader);
        shader = 0;
      }
    }
  }
  return shader;
}

static GLuint glesCreateProgram(const char *pVertexSource,
                                const char *pFragmentSource)
{
  GLuint vertexShader = glesLoadShader(GL_VERTEX_SHADER, pVertexSource);
  if (!vertexShader)
  {
    return 0;
  }

  GLuint pixelShader = glesLoadShader(GL_FRAGMENT_SHADER, pFragmentSource);
  if (!pixelShader)
  {
    return 0;
  }

  GLuint program = glCreateProgram();
  if (program)
  {
    glAttachShader(program, vertexShader);
    glesCheckStatus("glAttachShader");
    glAttachShader(program, pixelShader);
    glesCheckStatus("glAttachShader");
    glLinkProgram(program);
    GLint linkStatus = GL_FALSE;
    glGetProgramiv(program, GL_LINK_STATUS, &linkStatus);
    if (linkStatus != GL_TRUE)
    {
      GLint bufLength = 0;
      glGetProgramiv(program, GL_INFO_LOG_LENGTH, &bufLength);
      if (bufLength)
      {
        char *buf = (char *)malloc(bufLength);
        if (buf)
        {
          glGetProgramInfoLog(program, bufLength, NULL, buf);
          log_debug("Could not link program: %s", buf);
          free(buf);
        }
      }
      glDeleteProgram(program);
      program = 0;
    }
  }
  return program;
}

static int32_t glesInitialize(opengles_ctx_t *gles)
{
  glesPrintInfo("Version", GL_VERSION);
  glesPrintInfo("Vendor", GL_VENDOR);
  glesPrintInfo("Renderer", GL_RENDERER);
  // glesPrintInfo("Extensions", GL_EXTENSIONS);

  int maxTextureImageUnits[2];
  int maxTextureSize[2];
  glGetIntegerv(GL_MAX_TEXTURE_IMAGE_UNITS, maxTextureImageUnits);
  glGetIntegerv(GL_MAX_TEXTURE_SIZE, maxTextureSize);

  gles->program = glesCreateProgram(g_vertextShader, g_fragmentShader);
  if (!gles->program)
  {
    log_debug("Could not create program");
    return -1;
  }

  int positionHandle = glGetAttribLocation(gles->program, "aPosition");
  glesCheckStatus("glGetAttribLocation aPosition");

  int textureHandle = glGetAttribLocation(gles->program, "aTextureCoord");
  glesCheckStatus("glGetAttribLocation aTextureCoord");

  // set the vertices array in the shader
  // _vertices contains 4 vertices with 5 coordinates.
  // 3 for (xyz) for the vertices and 2 for the texture
  glVertexAttribPointer(positionHandle, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), gles->vertices);
  glesCheckStatus("glVertexAttribPointer aPosition");

  glEnableVertexAttribArray(positionHandle);
  glesCheckStatus("glEnableVertexAttribArray positionHandle");

  // set the texture coordinate array in the shader
  // _vertices contains 4 vertices with 5 coordinates.
  // 3 for (xyz) for the vertices and 2 for the texture
  glVertexAttribPointer(textureHandle, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), &gles->vertices[3]);
  glesCheckStatus("glVertexAttribPointer maTextureHandle");
  glEnableVertexAttribArray(textureHandle);
  glesCheckStatus("glEnableVertexAttribArray textureHandle");

  glUseProgram(gles->program);
  int i = glGetUniformLocation(gles->program, "Ytex");
  glesCheckStatus("glGetUniformLocation");
  glUniform1i(i, 0); /* Bind Ytex to texture unit 0 */
  glesCheckStatus("glUniform1i Ytex");

  i = glGetUniformLocation(gles->program, "Utex");
  glesCheckStatus("glGetUniformLocation Utex");
  glUniform1i(i, 1); /* Bind Utex to texture unit 1 */
  glesCheckStatus("glUniform1i Utex");

  i = glGetUniformLocation(gles->program, "Vtex");
  glesCheckStatus("glGetUniformLocation");
  glUniform1i(i, 2); /* Bind Vtex to texture unit 2 */
  glesCheckStatus("glUniform1i");

  return 0;
}

int opengles_ctx_create(opengles_ctx_t **pp_ctx)
{
  opengles_ctx_t *ctx = (opengles_ctx_t *)malloc(sizeof(opengles_ctx_t));
  if (ctx == NULL)
  {
    log_error("Call malloc failed.\n");
    return -1;
  }

  const GLfloat vertices[20] = {
      // X, Y, Z, U, V
      -1, -1, 0, 0, 1, // Bottom Left
      1, -1, 0, 1, 1,  // Bottom Right
      1, 1, 0, 1, 0,   // Top Right
      -1, 1, 0, 0, 0   // Top Left
  };

  memcpy(ctx->vertices, vertices, sizeof(ctx->vertices));

  if (glesInitialize(ctx) != 0)
  {
    log_error("Call glesInitialize(ctx) failed.\n");
    free(ctx);
    return -1;
  }

  *pp_ctx = ctx;

  return 0;
}

void opengles_ctx_release(opengles_ctx_t **pp_ctx)
{
  if (pp_ctx && *pp_ctx)
  {
    opengles_ctx_t *ctx = *pp_ctx;
    *pp_ctx = NULL;
    glDeleteProgram(ctx->program);
    free(ctx);
  }
}

int opengles_ctx_draw_yuv420p(opengles_ctx_t *ctx, int width, int height,
                              const char *y, const char *u, const char *v,
                              int y_stride, int u_stride, int v_stride)
{
  glUseProgram(ctx->program);
  glesCheckStatus("glUseProgram");

  if (ctx->width != (GLsizei)width || ctx->height != (GLsizei)height)
  {
    ctx->width = width;
    ctx->height = height;
    glesCreateTextures(ctx, ctx->width, ctx->height);
  }

  glesUpdateTextures(ctx, ctx->width, ctx->height, y, u, v, y_stride, u_stride, v_stride);

  glClear(GL_COLOR_BUFFER_BIT);
  glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_BYTE, g_indices);
  glesCheckStatus("glDrawArrays");

  return 0;
}
