/**
 * Convolution with GLSL.
 * Note: uses GL_ARB_shader_objects, GL_ARB_vertex_shader, GL_ARB_fragment_shader,
 * not the OpenGL 2.0 shader API.
 * Author: Zack Rusin
 */

#include <glad/glad.h>

#define GL_GLEXT_PROTOTYPES
#include "readtex.h"

#include "glut_wrap.h"
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <math.h>

enum Filter {
   GAUSSIAN_BLUR,
   SHARPEN,
   MEAN_REMOVAL,
   EMBOSS,
   EDGE_DETECT,
   NO_FILTER,
   LAST
};
#define QUIT LAST

struct BoundingBox {
   float minx, miny, minz;
   float maxx, maxy, maxz;
};
struct Texture {
   GLuint id;
   GLfloat x;
   GLfloat y;
   GLint width;
   GLint height;
   GLenum format;
};

static const char *textureLocation = DEMOS_DATA_DIR "girl2.rgb";

static GLfloat viewRotx = 0.0, viewRoty = 0.0, viewRotz = 0.0;
static struct BoundingBox box;
static struct Texture texture;
static GLuint program;
static GLint menuId;
static enum Filter filter = GAUSSIAN_BLUR;


static void checkError(int line)
{
   GLenum err = glGetError();
   if (err) {
      printf("GL Error %s (0x%x) at line %d\n",
             gluErrorString(err), (int) err, line);
   }
}

static void loadAndCompileShader(GLuint shader, const char *text)
{
   GLint stat;

   glShaderSource(shader, 1, (const GLchar **) &text, NULL);

   glCompileShader(shader);

   glGetShaderiv(shader, GL_COMPILE_STATUS, &stat);
   if (!stat) {
      GLchar log[1000];
      GLsizei len;
      glGetShaderInfoLog(shader, 1000, &len, log);
      fprintf(stderr, "Problem compiling shader: %s\n", log);
      exit(1);
   }
   else {
      printf("Shader compiled OK\n");
   }
}

static void readShader(GLuint shader, const char *filename)
{
   const int max = 100*1000;
   int n;
   char *buffer = (char*) malloc(max);
   FILE *f = fopen(filename, "r");
   if (!f) {
      fprintf(stderr, "Unable to open shader file %s\n", filename);
      exit(1);
   }

   n = fread(buffer, 1, max, f);
   printf("Read %d bytes from shader file %s\n", n, filename);
   if (n > 0) {
      buffer[n] = 0;
      loadAndCompileShader(shader, buffer);
   }

   fclose(f);
   free(buffer);
}


static void
checkLink(GLuint prog)
{
   GLint stat;
   glGetProgramiv(prog, GL_LINK_STATUS, &stat);
   if (!stat) {
      GLchar log[1000];
      GLsizei len;
      glGetProgramInfoLog(prog, 1000, &len, log);
      fprintf(stderr, "Linker error:\n%s\n", log);
   }
   else {
      fprintf(stderr, "Link success!\n");
   }
}

static void fillConvolution(GLint *k,
                            GLfloat *scale,
                            GLfloat *color)
{
   switch(filter) {
   case GAUSSIAN_BLUR:
      k[0] = 1; k[1] = 2; k[2] = 1;
      k[3] = 2; k[4] = 4; k[5] = 2;
      k[6] = 1; k[7] = 2; k[8] = 1;

      *scale = 1./16.;
      break;
   case SHARPEN:
      k[0] =  0; k[1] = -2; k[2] =  0;
      k[3] = -2; k[4] = 11; k[5] = -2;
      k[6] =  0; k[7] = -2; k[8] =  0;

      *scale = 1./3.;
      break;
   case MEAN_REMOVAL:
      k[0] = -1; k[1] = -1; k[2] = -1;
      k[3] = -1; k[4] =  9; k[5] = -1;
      k[6] = -1; k[7] = -1; k[8] = -1;

      *scale = 1./1.;
      break;
   case EMBOSS:
      k[0] = -1; k[1] =  0; k[2] = -1;
      k[3] =  0; k[4] =  4; k[5] =  0;
      k[6] = -1; k[7] =  0; k[8] = -1;

      *scale = 1./1.;
      color[0] = 0.5;
      color[1] = 0.5;
      color[2] = 0.5;
      color[3] = 0.5;
      break;
   case EDGE_DETECT:
      k[0] =  1; k[1] =  1; k[2] =  1;
      k[3] =  0; k[4] =  0; k[5] =  0;
      k[6] = -1; k[7] = -1; k[8] = -1;

      *scale = 1.;
      color[0] = 0.5;
      color[1] = 0.5;
      color[2] = 0.5;
      color[3] = 0.5;
      break;
   case NO_FILTER:
      k[0] =  0; k[1] =  0; k[2] =  0;
      k[3] =  0; k[4] =  1; k[5] =  0;
      k[6] =  0; k[7] =  0; k[8] =  0;

      *scale = 1.;
      break;
   default:
      assert(!"Unhandled switch value");
   }
}

static void setupConvolution(void)
{
   GLint *kernel = (GLint*)malloc(sizeof(GLint) * 9);
   GLfloat scale = 0.0;
   GLfloat *vecKer = (GLfloat*)malloc(sizeof(GLfloat) * 9 * 4);
   GLuint loc;
   GLuint i;
   GLfloat baseColor[4];
   baseColor[0] = 0;
   baseColor[1] = 0;
   baseColor[2] = 0;
   baseColor[3] = 0;

   fillConvolution(kernel, &scale, baseColor);
   /*vector of 4*/
   for (i = 0; i < 9; ++i) {
      vecKer[i*4 + 0] = kernel[i];
      vecKer[i*4 + 1] = kernel[i];
      vecKer[i*4 + 2] = kernel[i];
      vecKer[i*4 + 3] = kernel[i];
   }

   loc = glGetUniformLocationARB(program, "KernelValue");
   glUniform4fv(loc, 9, vecKer);
   loc = glGetUniformLocationARB(program, "ScaleFactor");
   glUniform4f(loc, scale, scale, scale, scale);
   loc = glGetUniformLocationARB(program, "BaseColor");
   glUniform4f(loc, baseColor[0], baseColor[1],
               baseColor[2], baseColor[3]);

   free(vecKer);
   free(kernel);
}

static void createProgram(const char *vertProgFile,
                          const char *fragProgFile)
{
   GLuint fragShader = 0, vertShader = 0;

   program = glCreateProgram();
   if (vertProgFile) {
      vertShader = glCreateShader(GL_VERTEX_SHADER);
      readShader(vertShader, vertProgFile);
      glAttachShader(program, vertShader);
   }

   if (fragProgFile) {
      fragShader = glCreateShader(GL_FRAGMENT_SHADER);
      readShader(fragShader, fragProgFile);
      glAttachShader(program, fragShader);
   }

   glLinkProgram(program);
   checkLink(program);

   glUseProgram(program);

   /*
   assert(glIsProgram(program));
   assert(glIsShader(fragShader));
   assert(glIsShader(vertShader));
   */

   checkError(__LINE__);
   {/*texture*/
      GLuint texLoc = glGetUniformLocationARB(program, "srcTex");
      glUniform1iARB(texLoc, 0);
   }
   {/*setup offsets */
      float offsets[] = { 1.0 / texture.width,  1.0 / texture.height,
                          0.0                ,  1.0 / texture.height,
                          -1.0 / texture.width,  1.0 / texture.height,
                          1.0 / texture.width,  0.0,
                          0.0                ,  0.0,
                          -1.0 / texture.width,  0.0,
                          1.0 / texture.width, -1.0 / texture.height,
                          0.0                , -1.0 / texture.height,
                          -1.0 / texture.width, -1.0 / texture.height };
      GLuint offsetLoc = glGetUniformLocationARB(program, "Offset");
      glUniform2fv(offsetLoc, 9, offsets);
   }
   setupConvolution();

   checkError(__LINE__);
}


static void readTexture(const char *filename)
{
   GLubyte *data;

   texture.x = 0;
   texture.y = 0;

   glGenTextures(1, &texture.id);
   glBindTexture(GL_TEXTURE_2D, texture.id);
   glTexParameteri(GL_TEXTURE_2D,
                   GL_TEXTURE_MIN_FILTER, GL_NEAREST);
   glTexParameteri(GL_TEXTURE_2D,
                   GL_TEXTURE_MAG_FILTER, GL_NEAREST);
   glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
   glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
   glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
   data = LoadRGBImage(filename, &texture.width, &texture.height,
                       &texture.format);
   if (!data) {
      printf("Error: couldn't load texture image '%s'\n", filename);
      exit(1);
   }
   printf("Texture %s (%d x %d)\n",
          filename, texture.width, texture.height);
   glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB,
                texture.width, texture.height, 0, texture.format,
                GL_UNSIGNED_BYTE, data);
}

static void menuSelected(int entry)
{
   switch (entry) {
   case QUIT:
      exit(0);
      break;
   default:
      filter = (enum Filter)entry;
   }
   setupConvolution();

   glutPostRedisplay();
}

static void menuInit(void)
{
   menuId = glutCreateMenu(menuSelected);

   glutAddMenuEntry("Gaussian blur", GAUSSIAN_BLUR);
   glutAddMenuEntry("Sharpen", SHARPEN);
   glutAddMenuEntry("Mean removal", MEAN_REMOVAL);
   glutAddMenuEntry("Emboss", EMBOSS);
   glutAddMenuEntry("Edge detect", EDGE_DETECT);
   glutAddMenuEntry("None", NO_FILTER);

   glutAddMenuEntry("Quit", QUIT);

   glutAttachMenu(GLUT_RIGHT_BUTTON);
}

static void init(void)
{
   if (!glutExtensionSupported("GL_ARB_shader_objects") ||
       !glutExtensionSupported("GL_ARB_vertex_shader") ||
       !glutExtensionSupported("GL_ARB_fragment_shader")) {
      fprintf(stderr, "Sorry, this program requires GL_ARB_shader_objects, GL_ARB_vertex_shader, and GL_ARB_fragment_shader\n");
      exit(1);
   }

   fprintf(stderr, "GL_RENDERER   = %s\n", (char *) glGetString(GL_RENDERER));
   fprintf(stderr, "GL_VERSION    = %s\n", (char *) glGetString(GL_VERSION));
   fprintf(stderr, "GL_VENDOR     = %s\n", (char *) glGetString(GL_VENDOR));

   menuInit();
   readTexture(textureLocation);
   createProgram("convolution.vert", "convolution.frag");

   glEnable(GL_TEXTURE_2D);
   glClearColor(1.0, 1.0, 1.0, 1.0);
   /*glShadeModel(GL_SMOOTH);*/
   glShadeModel(GL_FLAT);
}

static void reshape(int width, int height)
{
   glViewport(0, 0, width, height);
   glMatrixMode(GL_PROJECTION);
   glLoadIdentity();
   box.minx = 0;
   box.maxx = width;
   box.miny = 0;
   box.maxy = height;
   box.minz = 0;
   box.maxz = 1;
   glOrtho(box.minx, box.maxx, box.miny, box.maxy, -999999, 999999);
   glMatrixMode(GL_MODELVIEW);
}

static void keyPress(unsigned char key, int x, int y)
{
   switch(key) {
   case 27:
      exit(0);
   default:
      break;
   }
   glutPostRedisplay();
}

static void
special(int k, int x, int y)
{
   switch (k) {
   case GLUT_KEY_UP:
      viewRotx += 2.0;
      break;
   case GLUT_KEY_DOWN:
      viewRotx -= 2.0;
      break;
   case GLUT_KEY_LEFT:
      viewRoty += 2.0;
      break;
   case GLUT_KEY_RIGHT:
      viewRoty -= 2.0;
      break;
   default:
      return;
   }
   glutPostRedisplay();
}


static void draw(void)
{
   GLfloat center[2];
   GLfloat anchor[2];

   glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

   glLoadIdentity();
   glPushMatrix();

   center[0] = box.maxx/2;
   center[1] = box.maxy/2;
   anchor[0] = center[0] - texture.width/2;
   anchor[1] = center[1] - texture.height/2;

   glTranslatef(center[0], center[1], 0);
   glRotatef(viewRotx, 1.0, 0.0, 0.0);
   glRotatef(viewRoty, 0.0, 1.0, 0.0);
   glRotatef(viewRotz, 0.0, 0.0, 1.0);
   glTranslatef(-center[0], -center[1], 0);

   glTranslatef(anchor[0], anchor[1], 0);
   glBegin(GL_TRIANGLE_STRIP);
   {
      glColor3f(1., 0., 0.);
      glTexCoord2f(0, 0);
      glVertex3f(0, 0, 0);

      glColor3f(0., 1., 0.);
      glTexCoord2f(0, 1.0);
      glVertex3f(0, texture.height, 0);

      glColor3f(1., 0., 0.);
      glTexCoord2f(1.0, 0);
      glVertex3f(texture.width, 0, 0);

      glColor3f(0., 1., 0.);
      glTexCoord2f(1, 1);
      glVertex3f(texture.width, texture.height, 0);
   }
   glEnd();

   glPopMatrix();

   glutSwapBuffers();
}

int main(int argc, char **argv)
{
   glutInit(&argc, argv);

   glutInitWindowSize(400, 400);
   glutInitDisplayMode(GLUT_RGB | GLUT_ALPHA | GLUT_DOUBLE);

   if (!glutCreateWindow("Image Convolutions")) {
      fprintf(stderr, "Couldn't create window!\n");
      exit(1);
   }

   gladLoadGL();
   init();

   glutReshapeFunc(reshape);
   glutKeyboardFunc(keyPress);
   glutSpecialFunc(special);
   glutDisplayFunc(draw);


   glutMainLoop();
   return 0;
}
