#include <stdio.h>
#include <math.h>
#include <SDL2/SDL.h>
#include <GL/glew.h>

void cmul(double *u, double *v, double x, double y, double a, double b)
{
    *u = x*a - y*b;
    *v = x*b + y*a;
}

void cdiv(double *u, double *v, double x, double y, double a, double b)
{
    double d = a*a + b*b;
    *u = (x*a + y*b)/d;
    *v = (y*a - x*b)/d;
}

void cexp(double *u, double *v, double x, double y, double a, double b)
{
    double m, p, argz, lnz;

    lnz = 0.5*log(x*x + y*y);
    argz = atan2(y, x);
    m = exp(a*lnz - b*argz);
    p = a*argz + b*lnz;

    *u = m*cos(p);
    *v = m*sin(p);
}

void clog(double *u, double *v, double x, double y)
{
    *u = 0.5*log(x*x + y*y);
    *v = atan2(y, x);
}

void csin(double *u, double *v, double x, double y)
{
    *u = sin(x)*cosh(y);
    *v = cos(x)*sinh(y);
}

void ccos(double *u, double *v, double x, double y)
{
    *u = cos(x)*cosh(y);
    *v =-sin(x)*sinh(y);
}

Uint32 hrgb(double xx, double yy)
{
    double x, y, a;
    double r, g, b;

    x = atan2(yy, xx)/(M_PI/4.);
    y = sqrt(xx*xx + yy*yy);

    a = (M_PI + atan2(yy, xx))/(2.*M_PI);
    r =  0.5 - 0.5*sin(2.0*M_PI*a - M_PI/2.0 );
    g = (0.5 + 0.5*sin(2.0*M_PI*a*2.0 - M_PI)); //* float(a < 0.66);
    b = (0.5 + 0.5*sin(2.0*M_PI*a*1.5 + M_PI/2.0 )) * (float)(a > 0.33);

    double m = fabs(y - floor(y));
    double p = fabs(a*16 - floor(a*16));
    double c = pow(fabs(sin(M_PI*xx))*fabs(sin(M_PI*yy)), 0.2);

    r -= 0.15*m + 0.15*p + 0.35*c;
    g -= 0.15*m + 0.15*p + 0.35*c;
    b -= 0.15*m + 0.15*p + 0.35*c;

    if (r <= 0) r = 0;
    if (g <= 0) g = 0;
    if (b <= 0) b = 0;

    return (0xff000000)
         | ((Uint8)(255*r))<<16 
         | ((Uint8)(255*g))<<8 
         | ((Uint8)(255*b));

}

Uint32 checker(double xx, double yy)
{
    double x, y;
    if (1) {
        x = xx;
        y = yy;
    } else {
        x = atan2(yy, xx)/(M_PI/4.);
        y = log(sqrt(xx*xx + yy*yy));
    }

    int z = (int)(fabs(2.*(x - floor(x))))
          ^ (int)(fabs(2.*(y - floor(y))));

    if (z)
        return 0xffffffff;
    else
        return 0x00000000;
}

const GLfloat verts[6][4] = {
    //  x      y      s      t
    { -1.0f, -1.0f,  0.0f,  1.0f }, // BL
    { -1.0f,  1.0f,  0.0f,  0.0f }, // TL
    {  1.0f,  1.0f,  1.0f,  0.0f }, // TR
    {  1.0f, -1.0f,  1.0f,  1.0f }, // BR
};

const GLint indicies[] = {
    0, 1, 2, 0, 2, 3
};

const char *vert_shader_src = "\
#version 150 core                                                            \n\
in vec2 in_Position;                                                         \n\
void main()                                                                  \n\
{                                                                            \n\
    gl_Position = vec4(in_Position, 0.0, 1.0);                               \n\
}                                                                            \n\
";

int main(int argc, char *argv[])
{
    int w = 800, h = 600;
    int dx, dy, mx, my;
    int end = 0, redraw = 1, key, drag = 0, rbutton = 0;

    double zoom = 100; 
    double x0 = 0, y0 = 0, rx = 1, ry = 0;
    double tx, ty, ox0, oy0;

    SDL_Window *window;
    SDL_Surface *surface;
    SDL_Event event;

    //gl -> SDL related parts
    SDL_GLContext context;
    GLuint vao, vbo, ebo, tex;
    GLuint vert_shader;
    GLuint frag_shader;
    GLuint shader_prog;




    SDL_Init(SDL_INIT_EVERYTHING);

    window = SDL_CreateWindow("cplot",
            SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, w, h,
            SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE);


    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3);
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK,
                        SDL_GL_CONTEXT_PROFILE_CORE);
    SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);


    context = SDL_GL_CreateContext(window);
    SDL_GL_SetSwapInterval(1);

    GLenum err;
    glewExperimental = GL_TRUE;
    err = glewInit();

    //gl init shaders
    GLint status;
    char err_buf[512];

    glGenVertexArrays(1, &vao);
    glBindVertexArray(vao);

    vert_shader = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(vert_shader, 1, &vert_shader_src, NULL);
    glCompileShader(vert_shader);
    glGetShaderiv(vert_shader, GL_COMPILE_STATUS, &status);
    if (status != GL_TRUE) {
        glGetShaderInfoLog(vert_shader, sizeof(err_buf), NULL, err_buf);
        err_buf[sizeof(err_buf)-1] = '\0';
        fprintf(stderr, "Vertex shader compilation failed: %s\n", err_buf);
        return 1;
    }

    frag_shader = glCreateShader(GL_FRAGMENT_SHADER);

    //load shader from a file
    FILE *fp = fopen("frag0.glsl", "r");
    size_t n = 0;
    while (getc(fp) != EOF) n++;
    char *frag_shader_src = malloc(n + 1);

    char c;
    fp = fopen("frag0.glsl", "r");
    n = 0;
    while ((c = getc(fp)) != EOF) frag_shader_src[n++] = c;
    frag_shader_src[n] = '\0';

    glShaderSource(frag_shader, 1, &frag_shader_src, NULL);



    glCompileShader(frag_shader);
    glGetShaderiv(frag_shader, GL_COMPILE_STATUS, &status);
    if (status != GL_TRUE) {
        glGetShaderInfoLog(frag_shader, sizeof(err_buf), NULL, err_buf);
        err_buf[sizeof(err_buf)-1] = '\0';
        fprintf(stderr, "Fragment shader compilation failed: %s\n", err_buf);
        return 1;
    }

    shader_prog = glCreateProgram();
    glAttachShader(shader_prog, vert_shader);
    glAttachShader(shader_prog, frag_shader);
    glBindFragDataLocation(shader_prog, 0, "out_Color");
    glLinkProgram(shader_prog);
    glUseProgram(shader_prog);

    //gl init geometry
    glGenBuffers(1, &vbo);
    glBindBuffer(GL_ARRAY_BUFFER, vbo);
    glBufferData(GL_ARRAY_BUFFER, sizeof(verts), verts, GL_STATIC_DRAW);

    glGenBuffers(1, &ebo);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indicies), indicies, GL_STATIC_DRAW);

    GLint pos_attr_loc = glGetAttribLocation(shader_prog, "in_Position");
    glVertexAttribPointer(pos_attr_loc, 2, GL_FLOAT, GL_FALSE, 4*sizeof(GLfloat), (void*)0);
    glEnableVertexAttribArray(pos_attr_loc);

    glUniform2f(glGetUniformLocation(shader_prog, "resolution"), (float)w, (float)h);
    glUniform1f(glGetUniformLocation(shader_prog, "zoom"), (float)zoom);
    glUniform2f(glGetUniformLocation(shader_prog, "z0"), (float)x0, (float)y0);
    glUniform2f(glGetUniformLocation(shader_prog, "r"), (float)rx, (float)ry);

    //surface = SDL_GetWindowSurface(window);

    while(!end) {
        if (redraw) {
            /*
            Uint32 *pixmem = surface->pixels;
            int ptr = 0;
            double x, y, u, v, uu, vv;

            SDL_LockSurface(surface);
            for (int yy=0; yy<h; yy++) {
                y = (h/2 - yy)/zoom + y0;
                for (int xx=0; xx<w; xx++) {
                    x = (xx - w/2)/zoom + x0;

                    cdiv(&u, &v, 1, 0, x - rx, y - ry);
                    cdiv(&u, &v, u, v, x, y);
                    cexp(&u, &v, exp(1), 0, u, v);

                    //pixmem[ptr++] = checker(u, v);
                    pixmem[ptr++] = hrgb(u, v);
                }
            }
            SDL_UnlockSurface(surface);

            SDL_UpdateWindowSurface(window);
            */

            glClearColor(1.0f, 0.0f, 0.0f, 1.0f);
            glClear(GL_COLOR_BUFFER_BIT);
            glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, NULL);
            SDL_GL_SwapWindow(window);

            redraw = 0;
        }

        while (SDL_PollEvent(&event)) {
            switch(event.type) {
            case SDL_QUIT:
                end = 1;
                break;

            case SDL_KEYDOWN:
                key = event.key.keysym.sym;
                if (key == 27) end = 1;
                break;

            case SDL_MOUSEWHEEL:
                SDL_GetMouseState(&mx, &my);
                tx = (mx - w/2)/zoom;
                ty = (h/2 - my)/zoom;

                zoom += 50*event.wheel.y;
                if (zoom < 1) zoom = 1;

                x0 += tx - (mx - w/2)/zoom;
                y0 += ty - (h/2 - my)/zoom;

                glUniform1f(glGetUniformLocation(shader_prog, "zoom"), (float)zoom);
                glUniform2f(glGetUniformLocation(shader_prog, "z0"), (float)x0, (float)y0);

                redraw = 1;
                break;

            case SDL_MOUSEBUTTONUP:
                drag = 0;
                rbutton = 0;
                break; 

            case SDL_MOUSEBUTTONDOWN:
                if (event.button.button == SDL_BUTTON_LEFT) {
                    drag = 1;
                    SDL_GetMouseState(&dx, &dy);
                    ox0 = x0;
                    oy0 = y0;

                } else if (event.button.button == SDL_BUTTON_RIGHT) {
                    rbutton = 1;
                    SDL_GetMouseState(&mx, &my);
                    rx = (mx - w/2)/zoom + x0;
                    ry = (h/2 - my)/zoom + y0;
                    glUniform2f(glGetUniformLocation(shader_prog, "r"), (float)rx, (float)ry);
                    redraw = 1;
                }

                break; 

            case SDL_MOUSEMOTION:
                if (drag) {
                    SDL_GetMouseState(&mx, &my);
                    x0 = ox0 + (dx - mx)/zoom;
                    y0 = oy0 + (my - dy)/zoom;
                    glUniform2f(glGetUniformLocation(shader_prog, "z0"), (float)x0, (float)y0);
                    redraw = 1;
                }

                if (rbutton) {
                    SDL_GetMouseState(&mx, &my);
                    rx = (mx - w/2)/zoom + x0;
                    ry = (h/2 - my)/zoom + y0;
                    glUniform2f(glGetUniformLocation(shader_prog, "r"), (float)rx, (float)ry);
                    redraw = 1;
                }


                //SDL_GetMouseState(&mx, &my);
                //glUniform2f(glGetUniformLocation(shader_prog, "mouse"), (float)mx, (float)(h - my) );
                redraw = 1;

                break; 

            case SDL_WINDOWEVENT:
                SDL_UpdateWindowSurface(window);
                if (event.window.event == SDL_WINDOWEVENT_RESIZED) {
                    w = event.window.data1;
                    h = event.window.data2;
                    //surface = SDL_GetWindowSurface(window);

                    glUniform2f(glGetUniformLocation(shader_prog, "resolution"), (float)w, (float)h);
                    glViewport(0, 0, w, h);
                    redraw = 1;
                }
                break;
            }
        }
    }

    //clean
    glUseProgram(0);
    glDisableVertexAttribArray(0);
    glDetachShader(shader_prog, vert_shader);
    glDetachShader(shader_prog, frag_shader);
    glDeleteProgram(shader_prog);
    glDeleteShader(vert_shader);
    glDeleteShader(frag_shader);
    glDeleteBuffers(1, &ebo);
    glDeleteBuffers(1, &vbo);
    glDeleteVertexArrays(1, &vao);
    SDL_GL_DeleteContext(context);
    SDL_DestroyWindow(window);
    SDL_Quit();

    return 0;
}
