#include <QtGui>
#ifdef NEEDGLEE
  #include "GLee.h"
#endif
#include "openglcanvas.h"
#include <cmath>
#ifdef WINDOWS
  #include <direct.h>
  #define GET_WORKDIR _getcwd
#else
  #include <unistd.h>
  #define GET_WORKDIR getcwd
#endif
#include <sys/types.h> // for fstat
#include <sys/stat.h>  // for fstat

#define PROGNAME "pano_interface"
#define INPUT_IMAGE_FILE "input_images/image_4_input.pnm"
#define VERT_SHADER_FILE "shaders/test_vertex_shader.vert"
#define FRAG_SHADER_FILE "shaders/fragment_shader.frag"

OpenGLCanvas::OpenGLCanvas(QWidget *parent) :
    QGLWidget(parent)
{
    setFormat(QGL::DoubleBuffer | QGL::DepthBuffer);
    fov = 60.f;
    scale = 1.0f;
    center_lambda = 0.f;
    center_phi = 0.f;
    fov_scale_relation = "Square Root";
    visualization = "Perspective";

    time_frames = 0;
    time_timer.setInterval(0);
    connect(&time_timer, SIGNAL(timeout()), this, SLOT(slotTimer()));
    time_start = time_time.time();
}

void OpenGLCanvas::slotTimer(void) {
    updateGL();
}

void OpenGLCanvas::change_fov(double f){

    if (fov!=f && f>=1.f && f<=360.f) fov = f;
    if (fov<60.f) scale = 1.f;
    else if (fov>295.f) scale = 0.02f;
    else {
        if (fov_scale_relation == "Square Root") scale = sqrt((300.f-fov)/240.f);
        else if (fov_scale_relation == "Linear") scale = (300.f-fov)/240.f;
        else if (fov_scale_relation == "Square Power") scale = powf((300.f-fov)/240.f,2);
        else if (fov_scale_relation == "Cubic Power") scale = powf((300.f-fov)/240.f,3);
        else if (fov_scale_relation == "Logarithm") scale = log(exp(1.f) + (1.f-exp(1.f))*(fov-60.f)/240.f);
    }

//    scale = 0.3f;

    updateGL();

}

//void OpenGLCanvas::change_scale(double s){

//    if (scale!=s && s>=0.0 && s<=1.0) scale = s;
//    updateGL();

//}

void OpenGLCanvas::change_center_lambda(double lambda){

    if (center_lambda!=lambda && lambda>=-3.14f && lambda<=3.14f) {
        center_lambda = lambda;
        updateGL();
    }

}

void OpenGLCanvas::change_center_phi(double phi){

    if (center_phi!=phi && phi>=-1.57f && phi<=1.57f) {
        center_phi = phi;
        updateGL();
    }

}

void OpenGLCanvas::change_fov_scale_relation(QString name){

   fov_scale_relation = name;
   if (fov<60.f) scale = 1.f;
   else if (fov>295.f) scale = 0.01f;
   else{
       if (fov_scale_relation == "Square Root") scale = sqrt((300.f-fov)/240.f);
       else if (fov_scale_relation == "Linear") scale = (300.f-fov)/240.f;
       else if (fov_scale_relation == "Square Power") scale = powf((300.f-fov)/240.f,2);
       else if (fov_scale_relation == "Cubic Power") scale = powf((300.f-fov)/240.f,3);
       else if (fov_scale_relation == "Logarithm") scale = log(exp(1.f) + (1.f-exp(1.f))*(fov-60.f)/240.f);
   }
   updateGL();

}

void OpenGLCanvas::change_visualization(QString name){

    visualization = name;
    updateGL();

}

void OpenGLCanvas::load_image(const char *new_image){
    const char * const progname=(char*)(PROGNAME);
    int textureSize=getTextureSize(progname,new_image);
    unsigned char * textureBytes=(unsigned char*)malloc(textureSize);
    int width,height;
    readTextureBytes(progname,new_image,textureBytes,&width,&height);
    glPixelStorei(GL_UNPACK_ALIGNMENT,1);
    GLuint tex;
    glGenTextures(1,&tex);
    glBindTexture(GL_TEXTURE_2D,tex);
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
    glTexImage2D(GL_TEXTURE_2D,0, GL_RGB, width,height,0,GL_RGB,GL_UNSIGNED_BYTE,textureBytes);
}

void OpenGLCanvas::change_input_image(){
    load_image(QFileDialog::getOpenFileName(this,tr("Choose Panorama File")).toStdString().c_str());
    updateGL();
}

void OpenGLCanvas::initializeGL(){
    glShadeModel(GL_SMOOTH);
    glClearColor(1.0f,1.0f,1.0f,0.0f);
    glClearDepth(1.0f);
    glEnable(GL_DEPTH_TEST);
    glDepthFunc(GL_LEQUAL);
    glHint(GL_PERSPECTIVE_CORRECTION_HINT,GL_NICEST);

    char *input_image=(char*)malloc(512*sizeof(char*));
    if(!GET_WORKDIR(input_image,512)){
        fprintf(stderr,"too long pathname\n");
        exit(-1);
    };
    strcat(input_image,"/\0");
    strcat(input_image,INPUT_IMAGE_FILE);
#ifdef __APPLE__
    const char * const progname = "PROJ_ROOT_DIR";
    //const char * input_image = INPUT_IMAGE_FILE;
#else
    // progname is a file name or a path???
    const char * const progname = (char*)(PROGNAME);
    //const char * input_image = (char*)(INPUT_IMAGE_FILE);
#endif

    fprintf(stderr,"progname=%s\n",progname);
    fprintf(stderr,"default image=%s\n",input_image);
    // what to do if the input file does not exist
    struct stat testbuf;
    if(stat(input_image,&testbuf)){
        fprintf(stderr,"the default image file does not exist!\n");
        free(input_image);
        load_image(QFileDialog::getOpenFileName(this,tr("Choose Panorama File")).toStdString().c_str());
    }else{
        load_image(input_image);
    }

    // mesh resolution
    int m,n;
    m = n = 100;

    //defining texture coordinates
    int meshNumTexCoord = m*n;
    float *texCoord = (float *)malloc(2*meshNumTexCoord*sizeof(float));
    if (texCoord == NULL){
        printf("problem allocating memory for texture coordinates \n");
    }
    define_texture_coordinates(texCoord, m, n, -1.57f, 1.57f, -3.14f, 3.14f);

    //defining positions of the sphere vertices
    int meshNumVertices = m*n;
    float* positions = (float *)malloc(3*meshNumVertices*sizeof(float));
    if (positions == NULL){
        printf("problem allocating memory for positions \n");
    }
//    vertex_transformation(positions, m, n, center_lambda, center_phi, fov_rads, scale); //passar pelo vertex shader
    load_sphere_mesh(positions, m, n); //colocar essa e funcoes para textura e triangulos no initializeGL

    //defining triagle indices
    unsigned int meshNumFaces = 2*(m-1)*(n-1);
    unsigned int meshNumIndices = 3*meshNumFaces;
    unsigned int * indices = (unsigned int *)malloc(meshNumIndices*sizeof(unsigned int));
    define_triangle_indices(indices, m, n);

    // draw setup
    verticesPositions = positions;
    textureCoordinates = texCoord;
    numberOfIndices = meshNumIndices;
    triangleIndices = indices;

    free(input_image);
    setShaders();

}

void OpenGLCanvas::define_texture_coordinates(float *texCoord, int m, int n, float min_phi, float max_phi, float min_lambda, float max_lambda){

    float delta_lambda = (max_lambda-min_lambda)/(1.0*(n-1));
    float delta_phi = (max_phi-min_phi)/(1.0*(m-1));

    for (int i = 0; i<m; i++){
        for (int j = 0; j<n; j++){
            texCoord[2*(j+i*n)] = (min_lambda+delta_lambda*j)/(6.2832) + 0.5;
            texCoord[2*(j+i*n)+1] = (min_phi+delta_phi*i)/(3.1416) + 0.5;
        }
    }

}

void OpenGLCanvas::vertex_transformation(float *positions, int m, int n, float center_lambda, float center_phi, float fov_rads, float scale){

    float min_lambda = -3.1415f;
    float max_lambda = 3.1415f;
    float min_phi = -1.5708f;
    float max_phi = 1.5708f;

    float delta_lambda = (max_lambda-min_lambda)/(1.0*(n-1));
    float delta_phi = (max_phi-min_phi)/(1.0*(m-1));

    float lambda, phi, x, y, z, u, v, r, theta;

    //calculating the extent of the projection for the given FOV
    lambda = fov_rads;
    phi = 0.f;
    // OpenGL: x is the vertical axes pointg downwards, and y is horizontal axes
    y = sin(phi);
    x = -sin(lambda)*cos(phi);
    z = -cos(lambda)*cos(phi);
    u = 2.f*x/(-z+1.f);
    v = 2.f*y/(-z+1.f);
    r = sqrt(u*u+v*v);
    theta = atan2(u,v);
    r *= scale;
    u = -r*sin(theta);
    v = r*cos(theta);
    x = (4.f*u)/(u*u+v*v+4.f);
    y = (4.f*v)/(u*u+v*v+4.f);
    z = (u*u+v*v-4.f)/(u*u+v*v+4.f);
    u = x/(-z);
    v = y/(-z);
    float extent = u;

    for (int i = 0; i<m; i++){
        for (int j = 0; j<n; j++){

            lambda = (min_lambda+delta_lambda*j);
            phi = (min_phi+delta_phi*i);

            // OpenGL: x is the vertical axes pointg downwards, and y is horizontal axes
            y = sin(phi);
            x = -sin(lambda)*cos(phi);
            z = -cos(lambda)*cos(phi);

            //Rotation 1: (-center_lambda)-rotation on the xz-plane
            float x_copy = x;
            x = cos(-center_lambda)*x - sin(-center_lambda)*z;
            y = 1.f*y;
            z = sin(-center_lambda)*x_copy + cos(-center_lambda)*z;

            //Rotation 2: (-center_phi)-rotation on the yz-plane
            float y_copy = y;
            x = 1.f*x;
            y = cos(-center_phi)*y - sin(-center_phi)*z;
            z = sin(-center_phi)*y_copy + cos(-center_phi)*z;


            u = 2.f*x/(-z+1.f);
            v = 2.f*y/(-z+1.f);

            r = sqrt(u*u+v*v);
            theta = atan2(u,v);

            // scaling the complex plane according to scale specified in the interface (relate it to FOV)
            r *= scale;

            u = -r*sin(theta);
            v = r*cos(theta);

            x = (4.f*u)/(u*u+v*v+4.f);
            y = (4.f*v)/(u*u+v*v+4.f);
            z = (u*u+v*v-4.f)/(u*u+v*v+4.f);

            lambda = atan2(x,-z)/3.1415;
            phi = asin(y)/1.5708;

            u = x/(-z);
            v = y/(-z);

            if (visualization=="Perspective"){
                positions[3*(j+i*n)] = u/extent;
                positions[3*(j+i*n)+1] = v/extent;
                positions[3*(j+i*n)+2] = z;
            }

            if (visualization=="3D Sphere"){
                positions[3*(j+i*n)] = 0.9f*x;
                positions[3*(j+i*n)+1] = 0.9f*y;
                positions[3*(j+i*n)+2] = z;
            }

            if (visualization=="Equi-Rectangular"){
                positions[3*(j+i*n)] = lambda;
                positions[3*(j+i*n)+1] = phi;
                positions[3*(j+i*n)+2] = z;
            }

        }
    }

}

void OpenGLCanvas::load_sphere_mesh(float *positions, int m, int n){

    float min_lambda = -3.1415f;
    float max_lambda = 3.1415f;
    float min_phi = -1.5708f;
    float max_phi = 1.5708f;

    float delta_lambda = (max_lambda-min_lambda)/(1.0*(n-1));
    float delta_phi = (max_phi-min_phi)/(1.0*(m-1));

    float lambda, phi, x, y, z;

    for (int i = 0; i<m; i++){
        for (int j = 0; j<n; j++){

            lambda = (min_lambda+delta_lambda*j);
            phi = (min_phi+delta_phi*i);

            // OpenGL: x is the vertical axes pointg downwards, and y is horizontal axes
            y = sin(phi);
            x = -sin(lambda)*cos(phi);
            z = -cos(lambda)*cos(phi);

            positions[3*(j+i*n)] = x;
            positions[3*(j+i*n)+1] = y;
            positions[3*(j+i*n)+2] = z;

        }
    }

}

float OpenGLCanvas::calculate_extent(float fov_rads){

    float lambda, phi, x, y, z, u, v, r, theta;
    //calculating the extent of the projection for the given FOV
    lambda = fov_rads;
    phi = 0.f;
    // OpenGL: x is the vertical axes pointg downwards, and y is horizontal axes
    y = sin(phi);
    x = -sin(lambda)*cos(phi);
    z = -cos(lambda)*cos(phi);
    u = 2.f*x/(-z+1.f);
    v = 2.f*y/(-z+1.f);
    r = sqrt(u*u+v*v);
    theta = atan2(u,v);
    r *= scale;
    u = -r*sin(theta);
    v = r*cos(theta);
    x = (4.f*u)/(u*u+v*v+4.f);
    y = (4.f*v)/(u*u+v*v+4.f);
    z = (u*u+v*v-4.f)/(u*u+v*v+4.f);
    u = x/(-z);
    v = y/(-z);
    return u;
}

void OpenGLCanvas::define_triangle_indices(unsigned int * indices, int m, int n){

    for (int i = 0; i<m-1; i++){
        for (int j = 0; j<n-1; j++){

            unsigned int index = (j+i*n);

            indices[3*(2*(j+i*(n-1)))] = index;
            indices[3*(2*(j+i*(n-1)))+1] = index+1;
            indices[3*(2*(j+i*(n-1)))+2] = index+n;

            indices[3*(2*(j+i*(n-1))+1)] = index+1;
            indices[3*(2*(j+i*(n-1))+1)+1] = index+n+1;
            indices[3*(2*(j+i*(n-1))+1)+2] = index+n;

        }
    }

}

int OpenGLCanvas::getTextureSize(const char * const progname, const char * texturePath)
{
    struct pam inpam;
    pm_init(progname, 0);
    FILE * in_file = fopen(texturePath,"r");
    if (in_file==NULL){
        fprintf(stderr,"ERROR in getTextureSize: unable to open specified file\n");
        return -1;
    }

#ifdef PAM_STRUCT_SIZE
    pnm_readpaminit(in_file,&inpam,PAM_STRUCT_SIZE(tuple_type));
#else
    pnm_readpaminit(in_file,&inpam,sizeof(struct pam));
#endif

    int size = inpam.width*inpam.height*inpam.depth*inpam.bytes_per_sample;
    pm_close(in_file);

    //fprintf(stderr,"size=%d\n",size);
    return size;
}

void OpenGLCanvas::readTextureBytes(const char * const progname,
                                          const char * texturePath,
                                          unsigned char * textureBytes,
                                          int * outImageWidth,
                                          int * outImageHeight)
{
        struct pam inpam;
        tuple * tuplerow;
        int row;

        pm_init(progname, 0);
        FILE * in_file = fopen(texturePath, "r");

#ifdef PAM_STRUCT_SIZE
        pnm_readpaminit(in_file,&inpam,PAM_STRUCT_SIZE(tuple_type));
#else
        pnm_readpaminit(in_file,&inpam,sizeof(struct pam));
#endif

        tuplerow = pnm_allocpamrow(&inpam);

        for (row = 0; row < inpam.height; row++) {
                int column;
                pnm_readpamrow(&inpam, tuplerow);
                for (column = 0; column < inpam.width; ++column) {
                        unsigned int plane;
                        for (plane = 0; plane < inpam.depth; ++plane) {
                                textureBytes[(inpam.height-row-1)*3*inpam.width+3*column+plane] = tuplerow[column][plane];
                        }
                }
        }

        pnm_freepamrow(tuplerow);

        *outImageWidth = inpam.width;
        *outImageHeight = inpam.height;

        pm_close(in_file);
}

void OpenGLCanvas::resizeGL(int w, int h){
    glViewport(0,0,w,h);

}

char * OpenGLCanvas::textFileRead(char *fn) {

    FILE *fp;
    char *content = NULL;
    int f, count;
    f = open(fn, O_RDONLY);
    count = lseek(f, 0, SEEK_END);
//    close(f);
    if (fn != NULL) {
        fp = fopen(fn,"rt");
        if (fp != NULL) {
            if (count > 0) {
                content = (char *)malloc(sizeof(char) * (count+1));
                count = fread(content,sizeof(char),count,fp);
                content[count] = '\0';
            }
            fclose(fp);
        }
    }
    return content;
}

void OpenGLCanvas::setShaders() {

    char *vs,*fs;

#ifdef GLEW_VERSION_1_5
    GLenum err=glewInit();
    if(err!=GLEW_OK){
        fprintf(stderr,"error in GLEW initialization: %s\n",glewGetString(err));
        exit(-1);
    }
#endif

    GLuint v = glCreateShader(GL_VERTEX_SHADER);
    GLuint f = glCreateShader(GL_FRAGMENT_SHADER);

    char *vs_file=(char*)malloc(512*sizeof(char*));
    if(!GET_WORKDIR(vs_file,512)){
        fprintf(stderr,"too long pathname\n");
        exit(-1);
    };
    strcat(vs_file,"/\0");
    strcat(vs_file,VERT_SHADER_FILE);
    fprintf(stderr,"vs_file=%s\n",vs_file);

    char *fs_file=(char*)malloc(512*sizeof(char*));
    if(!GET_WORKDIR(fs_file,512)){
        fprintf(stderr,"too long pathname\n");
        exit(-1);
    };
    strcat(fs_file,"/\0");
    strcat(fs_file,FRAG_SHADER_FILE);
    fprintf(stderr,"fs_file=%s\n",fs_file);

    struct stat vs_testbuf,fs_testbuf;
    if(stat(vs_file,&vs_testbuf)||stat(fs_file,&fs_testbuf)){
        fprintf(stderr,"a shader file does not exist!\n");
        free(vs_file);
        free(fs_file);
        exit(-1);
    }

//#ifdef __APPLE__
    // in Mac
    //vs = textFileRead(VERT_SHADER_FILE);
    //fs = textFileRead(FRAG_SHADER_FILE);
//#else
    // in Linux (I think this would also work in mac, I just need to try)
    vs=textFileRead(vs_file);
    fs=textFileRead(fs_file);
//#endif

    const char * vv = vs;
    const char * ff = fs;

    glShaderSource(v, 1, &vv,NULL);
    glShaderSource(f, 1, &ff,NULL);

    free(vs);free(fs);
    free(vs_file);free(fs_file);

    glCompileShader(v);
    glCompileShader(f);

    GLuint p = glCreateProgram();

    glAttachShader(p,v);
    glAttachShader(p,f);

    glLinkProgram(p);
    glUseProgram(p);

}

void OpenGLCanvas::paintGL(){

    float fov_rads = (fov/180.f)*1.5708f;

//    // changing scale to generate the figures for the paper (remove it after)
//    scale = 0.8;


    // defining transformation parameters (that will be passed to the vertex shader)
    float extent = calculate_extent(fov_rads);
    float vis_mode=.0;
    if (visualization=="Perspective") vis_mode=1.0;
    else if (visualization=="3D Sphere") vis_mode=2.0;
    else if (visualization=="Equi-Rectangular") vis_mode=3.0;
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glOrtho(0.0, 2.0/extent, 0.0, 2.0/scale, 0.0, -2.0/vis_mode);

    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
    glOrtho(0.0, 2.0/center_lambda, 0.0, 2.0/center_phi, -1.0, 1.0);

    // drawing the mesh
    glClearColor(1.0, 1.0, 1.0, 1.0);

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glEnable(GL_TEXTURE_2D);
    glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);

    glColor3f(1, 0, 0);

    glEnableClientState(GL_VERTEX_ARRAY);
    glVertexPointer(3, GL_FLOAT, 0, verticesPositions);
    glEnableClientState(GL_TEXTURE_COORD_ARRAY);
    glTexCoordPointer(2, GL_FLOAT, 0, textureCoordinates);

    glDrawElements(GL_TRIANGLES, numberOfIndices, GL_UNSIGNED_INT, triangleIndices);
    glDisableClientState(GL_TEXTURE_COORD_ARRAY);
    glDisableClientState(GL_VERTEX_ARRAY);

    time_frames++;
//    if (time_frames > 0) {
        double dt = time_time.elapsed();
//        if (dt > 0.5) {
            time_fps = time_frames/dt;
            time_frames = 0;
            time_time.reset();
            emit fps(QString("%1 fps").arg((int)(time_fps+0.5)));
            printf("fps = %d ", (int)(time_fps+0.5));
//        }
//    }

}
