/*
 * Filtering Solid Gabor Noise
 * 
 * Example code for the article
 * Lagae, A. and Drettakis, G. 2011. Filtering Solid Gabor Noise.
 * ACM Transactions on Graphics (Proceedings of ACM SIGGRAPH 2011) 30, 4.
 * 
 * Copyright (C) 2011 Ares Lagae (ares.lagae@cs.kuleuven.be) and George
 * Drettakis (george.drettakis@inria.fr)
 * 
 * This work is licensed under a non-commercial license (see LICENSE.txt).
 *
 * Permissions beyond the scope of this license are be available from the
 * authors.
 *
 * If you use this software, you must site the article.
 */

// #############################################################################

#include <cassert>
#include <cerrno>
#ifdef _WIN32
#  define _USE_MATH_DEFINES
#endif
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <ctime>

#include <iostream>

#if defined(_WIN32)
#  include <GL/glew.h>
#  include <GL/glut.h>
#elif defined(linux) || defined(__linux)
#  include <GL/glew.h>
#  include <GL/glut.h>
#elif defined(__APPLE__) && defined(__MACH__)
#  include <GL/glew.h>
#  include <GLUT/glut.h>
#else
#  error
#endif

#include <AntTweakBar.h>

// #############################################################################

float camera_roi = 2.0f;
float camera_rotation_quat[4] = { 0.0f, 0.0f, 0.0f, 1.0f };

enum geometry_type {
  geometry_type_plane,
  geometry_type_cube,
  geometry_type_sphere,
  geometry_type_teapot
};
enum geometry_type geometry_type = geometry_type_cube;

enum noise_type {
  noise_type_anisotropic,
  noise_type_isotropic,
  noise_type_hybrid
};
enum noise_type noise_type = noise_type_anisotropic;
float noise_frequency = 0.03125f;
float noise_orientation[3] = { 1.0f, 0.0f, 0.0f };
float noise_omega[3];
float noise_bandwidth = 1.0f;
float noise_a;
float noise_truncate = 0.02f;
float noise_r;
float noise_impulses = 32.0f;
float noise_lambda;
unsigned noise_seed = 1234u;
bool noise_filter = false;

GLuint program;
GLuint vertex_shader;
GLuint fragment_shader;

// #############################################################################

void TwQuat4fToOpenGLMatrixf(const float* q, GLfloat* m)
{
  float yy2 = 2.0f * q[1] * q[1];
  float xy2 = 2.0f * q[0] * q[1];
  float xz2 = 2.0f * q[0] * q[2];
  float yz2 = 2.0f * q[1] * q[2];
  float zz2 = 2.0f * q[2] * q[2];
  float wz2 = 2.0f * q[3] * q[2];
  float wy2 = 2.0f * q[3] * q[1];
  float wx2 = 2.0f * q[3] * q[0];
  float xx2 = 2.0f * q[0] * q[0];
  m[ 0] = - yy2 - zz2 + 1.0f;
  m[ 1] = xy2 + wz2;
  m[ 2] = xz2 - wy2;
  m[ 3] = 0.0f;
  m[ 4] = xy2 - wz2;
  m[ 5] = - xx2 - zz2 + 1.0f;
  m[ 6] = yz2 + wx2;
  m[ 7] = 0.0f;
  m[ 8] = xz2 + wy2;
  m[ 9] = yz2 - wx2;
  m[10] = - xx2 - yy2 + 1.0f;
  m[11] = 0.0f;
  m[12] = m[13] = m[14] = 0.0f;
  m[15] = 1.0f;
}

GLchar* read_shader_source_from_file(const char* filename)
{
  FILE* fp = std::fopen(filename, "rb");
  if (fp == NULL) {
     std::cerr << " *** error: " << std::strerror(errno) << std::endl;
     std::exit(EXIT_FAILURE);
  }
  std::fseek(fp, 0, SEEK_END);
  std::size_t shader_source_size = std::ftell(fp);
  std::rewind(fp);
  GLchar* shader_source = new GLchar[shader_source_size + 1];
  shader_source[shader_source_size] = 0;
  std::fread(shader_source, 1, shader_source_size, fp);
  std::fclose(fp);
  return shader_source;
}

void shader_source_from_file(GLuint shader, const char* filename)
{
  const GLchar* shader_source = read_shader_source_from_file(filename);
  glShaderSource(shader, 1, &shader_source, NULL);
  delete [] shader_source;
}

void shader_info_log(GLuint shader)
{
  GLint info_log_length;
  glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &info_log_length);
  if (info_log_length > 0) {
    char* info_log = new char[info_log_length];
    glGetShaderInfoLog(shader, info_log_length, NULL, info_log);
    std::cout << info_log;
    delete [] info_log;
  }
}

void compile_shader(GLuint shader)
{
  glCompileShader(shader);
  GLint compile_status;
  glGetShaderiv(shader, GL_COMPILE_STATUS, &compile_status);
  if (compile_status == GL_FALSE) {
    std::cerr << " *** error:" << std::endl;
    shader_info_log(shader);
    std::exit(EXIT_FAILURE);
  }
}

void program_info_log(GLuint program)
{
  GLint info_log_length;
  glGetProgramiv(program, GL_INFO_LOG_LENGTH, &info_log_length);
  if (info_log_length > 0) {
    char* info_log = new char[info_log_length];
    glGetProgramInfoLog(program, info_log_length, NULL, info_log);
    std::cout << info_log;
    delete [] info_log;
  }
}

void link_program(GLuint program)
{
  glLinkProgram(program);
  GLint link_status;
  glGetProgramiv(program, GL_LINK_STATUS, &link_status);
  if (link_status == GL_FALSE) {
    std::cerr << " *** error:" << std::endl;
    program_info_log(program);
    std::exit(EXIT_FAILURE);
  }
}

// #############################################################################

void init()
{
  glEnable(GL_DEPTH_TEST);
  program = glCreateProgram();
  assert(glIsProgram(program) == GL_TRUE);
  vertex_shader = glCreateShader(GL_VERTEX_SHADER);
  glAttachShader(program, vertex_shader);
  if (GLEW_VERSION_3_2) {
    shader_source_from_file(vertex_shader, "filtering_solid_gabor_noise.vert");
  }
  else {
    shader_source_from_file(vertex_shader, "filtering_solid_gabor_noise_glsl120.vert");
  }
  compile_shader(vertex_shader);
  fragment_shader = glCreateShader(GL_FRAGMENT_SHADER);
  glAttachShader(program, fragment_shader);
  if (GLEW_VERSION_3_2) {
    shader_source_from_file(fragment_shader, "filtering_solid_gabor_noise.frag");
  }
  else {
    shader_source_from_file(fragment_shader, "filtering_solid_gabor_noise_glsl120.frag");
  }
  compile_shader(fragment_shader);
  link_program(program);
}

// -----------------------------------------------------------------------------

void reshape(int width, int height)
{
  TwWindowSize(width, height);
  glViewport(0, 0, width, height);
}

// -----------------------------------------------------------------------------

void display()
{
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  float camera_fovy = 45.0f;
  float camera_aspect = (float) glutGet(GLUT_WINDOW_WIDTH) / (float) glutGet(GLUT_WINDOW_HEIGHT);
  float camera_eye_z = camera_roi * (1.0f / std::sin((camera_fovy * (M_PI / 180.0f)) / 2.0f));
  float camera_z_near = camera_eye_z - camera_roi;
  float camera_z_far = camera_eye_z + camera_roi;
  gluPerspective(camera_fovy, camera_aspect, camera_z_near, camera_z_far);  
  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();
  gluLookAt(0.0f, 0.0f, camera_eye_z, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f);       
  GLfloat m[16];
  TwQuat4fToOpenGLMatrixf(camera_rotation_quat, m);
  glMultMatrixf(m);

  glUseProgram(program);

  noise_omega[0] = noise_frequency * noise_orientation[0];
  noise_omega[1] = noise_frequency * noise_orientation[1];
  noise_omega[2] = noise_frequency * noise_orientation[2];
  noise_a = (noise_frequency * ((std::pow(2.0f, noise_bandwidth) - 1.0f) / (std::pow(2.0f, noise_bandwidth) + 1.0f))) * std::sqrt(M_PI / std::log(2.0f));
  noise_r = std::sqrt(-std::log(noise_truncate) / M_PI) / noise_a;
  noise_lambda = noise_impulses / ((4.0f / 3.0f) * M_PI * (noise_r * noise_r * noise_r));
  glUniform1f(glGetUniformLocation(program, "lambda"), noise_lambda);
  glUniform1f(glGetUniformLocation(program, "r"), noise_r);
  if (GLEW_VERSION_3_2) {
    glUniform1ui(glGetUniformLocation(program, "seed"), noise_seed);
  }
  else {
    glUniform1uiEXT(glGetUniformLocation(program, "seed"), noise_seed);
  }
  glUniform3f(glGetUniformLocation(program, "omega"), noise_omega[0], noise_omega[1], noise_omega[2]);
  glUniform1f(glGetUniformLocation(program, "a"), noise_a);
  glUniform1i(glGetUniformLocation(program, "type"), noise_type);
  glUniform1i(glGetUniformLocation(program, "filter"), noise_filter); 

  if (geometry_type == geometry_type_plane) {
    glBegin(GL_QUADS);
      glNormal3f(0.0f, 0.0f, 1.0f);
      glVertex3f(-1.0f, -1.0f, 0.0f);
      glVertex3f(+1.0f, -1.0f, 0.0f);
      glVertex3f(+1.0f, +1.0f, 0.0f);
      glVertex3f(-1.0f, +1.0f, 0.0f);
    glEnd();
  }
  else if (geometry_type == geometry_type_cube) {
    glutSolidCube(2.0f);
  }
  else if (geometry_type == geometry_type_sphere) {
    glutSolidSphere(1.0f, 32, 16);
  }
  else if (geometry_type == geometry_type_teapot) {
    glutSolidTeapot(1.0f);
  }

  glUseProgram(0);

  TwDraw();
  glutSwapBuffers();
}

// -----------------------------------------------------------------------------

void idle()
{
  glutPostRedisplay();
}

// -----------------------------------------------------------------------------

void mouse(int button, int state, int x, int y)
{
  TwEventMouseButtonGLUT(button, state, x, y);
}

// -----------------------------------------------------------------------------

void motion(int x, int y)
{
  TwEventMouseMotionGLUT(x, y);
}

// -----------------------------------------------------------------------------

void passive_motion(int x, int y)
{
  TwEventMouseMotionGLUT(x, y);
}

// -----------------------------------------------------------------------------

void keyboard(unsigned char key, int x, int y)
{
  if (!TwEventKeyboardGLUT(key, x, y)) {
    if (key == 27) {
      std::exit(EXIT_SUCCESS);
    }
  }
}

// -----------------------------------------------------------------------------

void special(int key, int x, int y)
{
  TwEventSpecialGLUT(key, x, y);
}

// -----------------------------------------------------------------------------

void exit()
{
  TwTerminate();
}

// #############################################################################

int main(int argc, char* argv[])
{
  
  // ---------------------------------------------------------------------------
  
  std::cout << 
    "Filtering Solid Gabor Noise"
    " (http://graphics.cs.kuleuven.be/publications/LD11FSGN/) by Ares Lagae"
    " (ares.lagae@cs.kuleuven.be) and George Drettakis (george.drettakis@inria.fr) is"
    " licensed under a non-commercial license (see LICENSE.txt). Permissions beyond"
    " the scope of this license are be available from the authors."
  << std::endl;  
    
  // ---------------------------------------------------------------------------

  glutInit(&argc, argv);
  glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE | GLUT_DEPTH);
  glutInitWindowSize(512, 512);
  glutCreateWindow("Filtering Solid Gabor Noise"); 

  // ---------------------------------------------------------------------------

  std::cout << "info: " << "GL_VENDOR: " << glGetString(GL_VENDOR) << std::endl;
  std::cout << "info: " << "GL_RENDERER: " << glGetString(GL_RENDERER) << std::endl;
  std::cout << "info: " << "GL_VERSION: " << glGetString(GL_VERSION) << std::endl;
  std::cout << "info: " << "GL_SHADING_LANGUAGE_VERSION: " << glGetString(GL_SHADING_LANGUAGE_VERSION) << std::endl;
  //std::cout << "info: " << "GL_EXTENSIONS: " << glGetString(GL_EXTENSIONS) << std::endl;

  // ---------------------------------------------------------------------------

  GLenum return_value = glewInit();
  if (return_value != GLEW_OK) {
    std::cerr << " *** error: " << "glewInit() failed" << std::endl;
    std::exit(EXIT_FAILURE);
  }
  std::cout << "info: " << "GLEW_VERSION: " << glewGetString(GLEW_VERSION) << std::endl;

  // ---------------------------------------------------------------------------
  
  if (GLEW_VERSION_3_2) {
    std::cout << "info: " << "using OpenGL 3.2 with GLSL 1.50" << std::endl;
  }
  else if (GL_VERSION_2_1 && GLEW_EXT_gpu_shader4) {
    std::cout << " *** warning: " << "OpenGL 3.2 with GLSL 1.50 is not available, trying with OpenGL 2.1 with GL_EXT_gpu_shader4 and GLSL 1.20 instead" << std::endl;
  }
  else {
    std::cerr << " *** error: " << "neither OpenGL 3.2 with GLSL 1.50 nor OpenGL 2.1 with GL_EXT_gpu_shader4 and GLSL 1.20 is supported" << std::endl;
    std::exit(EXIT_FAILURE);
  }
  
  // ---------------------------------------------------------------------------

  init();

  // ---------------------------------------------------------------------------

  glutReshapeFunc(reshape);
  glutDisplayFunc(display);  
  glutIdleFunc(idle);
  glutMouseFunc(mouse);  
  glutMotionFunc(motion);
  glutPassiveMotionFunc(passive_motion);
  glutKeyboardFunc(keyboard);
  glutSpecialFunc(special);

  // ---------------------------------------------------------------------------

  std::atexit(exit);

  // ---------------------------------------------------------------------------

  TwInit(TW_OPENGL, NULL);
  TwWindowSize(glutGet(GLUT_WINDOW_WIDTH), glutGet(GLUT_WINDOW_HEIGHT)); 
  TwGLUTModifiersFunc(glutGetModifiers);
  TwBar* bar = TwNewBar("Filtering Solid Gabor Noise");

  TwAddVarRW(bar, "camera_rotation", TW_TYPE_QUAT4F, &camera_rotation_quat, "group='camera' label='tumble'");
  TwAddVarRW(bar, "camera_roi", TW_TYPE_FLOAT, &camera_roi, "group='camera' label='dolly' min='1.0' max='4.00' step='0.01'");

  TwEnumVal geometry_type_enum_val[] = {
    { geometry_type_plane, "plane" },
    { geometry_type_cube, "cube" },
    { geometry_type_sphere, "sphere" },
    { geometry_type_teapot, "teapot" }
  };
  TwType geometry_type_type = TwDefineEnum("geometry_type", geometry_type_enum_val, 4);
  TwAddVarRW(bar, "geometry_type", geometry_type_type, &geometry_type, "group='geometry' label='type'");

  TwEnumVal noise_type_enum_val[] = {
    { noise_type_anisotropic, "anisotropic" },
    { noise_type_isotropic, "isotropic" },
    { noise_type_hybrid, "hybrid" }
  };
  TwType noise_type_type = TwDefineEnum("noise_type", noise_type_enum_val, 3);
  TwAddVarRW(bar, "noise_type", noise_type_type, &noise_type, "group='noise' label='type'");  
  TwAddVarRW(bar, "noise_frequency", TW_TYPE_FLOAT, &noise_frequency, "group='noise' label='frequency' min='0.0000' max='0.5000' step='0.0005'");
  TwAddVarRW(bar, "noise_orientation", TW_TYPE_DIR3F, &noise_orientation, "group='noise' label='orientation_unit_vector'");
  TwAddVarRO(bar, "noise_omega_0", TW_TYPE_FLOAT, &noise_omega[0], "group='noise' label='omega[0]'");
  TwAddVarRO(bar, "noise_omega_1", TW_TYPE_FLOAT, &noise_omega[1], "group='noise' label='omega[1]'");
  TwAddVarRO(bar, "noise_omega_2", TW_TYPE_FLOAT, &noise_omega[2], "group='noise' label='omega[2]'");  
  TwAddVarRW(bar, "noise_bandwidth", TW_TYPE_FLOAT, &noise_bandwidth, "group='noise' label='bandwidth' min='0.0000' max='4.0000' step='0.004'");
  TwAddVarRO(bar, "noise_a", TW_TYPE_FLOAT, &noise_a, "group='noise' label='a'");
  TwAddVarRW(bar, "noise_truncate", TW_TYPE_FLOAT, &noise_truncate, "group='noise' label='truncate' min='0.01' max='0.10' step='0.0001'");
  TwAddVarRO(bar, "noise_r", TW_TYPE_FLOAT, &noise_r, "group='noise' label='r'");  
  TwAddVarRW(bar, "noise_impulses", TW_TYPE_FLOAT, &noise_impulses, "group='noise' label='impulses' min='0' max='256' step='1'");
  TwAddVarRO(bar, "noise_lambda", TW_TYPE_FLOAT, &noise_lambda, "group='noise' label='lambda'");  
  TwAddVarRW(bar, "noise_seed", TW_TYPE_UINT32, &noise_seed, "group='noise' label='seed'");  
  TwAddVarRW(bar, "noise_filter", TW_TYPE_BOOLCPP, &noise_filter, "group='noise' label='filter'");    

  // ---------------------------------------------------------------------------

  glutMainLoop();

  // ---------------------------------------------------------------------------
}

// #############################################################################
