#include <GLFW/glfw3.h>
#ifdef __APPLE__
#include <OpenGL/glu.h>
#include <OpenGL/gl.h>
// Tell Xcode IDE to not gripe about OpenGL deprecation
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
#else
#include <GL/glu.h>
#include <GL/gl.h>
#endif
#include <math.h>
#include <stdio.h>
#include <stdlib.h>

#include "steve_data.h"
#include "main.h"
#include "draw_steve.h"

#define GL_PI 3.1415927f
#define Cos(th) cos(GL_PI / 180 * (th))
#define Sin(th) sin(GL_PI / 180 * (th))

#define ORTHO 0
#define PERSP 1

#define RENDER_FROM_HAND_DRAWN 0
#define RENDER_FROM_RAW_PIXEL_DATA 1

// Flag for projection mode (default: orthographic)
GLenum projection = ORTHO;

GLenum render_method = RENDER_FROM_HAND_DRAWN;

/* arm/leg sway rate */
GLfloat r_roc = 0.5f;

// Rotation amounts
GLfloat xRot = 0.0f;
GLfloat yRot = 0.0f;

// Orthographic projection range
GLfloat range = 100.0f;

// Perspective projection field of view
GLfloat fov = 55.0f;

// Framebuffer width and height
GLint fb_width;
GLint fb_height;

void updateProjection(GLfloat w, GLfloat h)
{

    // Switch to projection matrix
    glMatrixMode(GL_PROJECTION);
    // Reset the matrix
    glLoadIdentity();

    // Get the aspect ratio of the current window
    GLfloat aspectRatio = w / h;

    switch (projection)
    {

    // Perspective projection
    case PERSP:
        gluPerspective(fov, aspectRatio, range / 4, range * 4);
        break;

    // Orthographic projection
    case ORTHO:
        if (w <= h)
            glOrtho(-range, range,
                    -range / aspectRatio, range / aspectRatio,
                    -range, range);
        else
            glOrtho(-range * aspectRatio, range * aspectRatio,
                    -range, range,
                    -range, range);
        break;
    }

    // Switch to modelview matrix
    glMatrixMode(GL_MODELVIEW);
    // Reset the matrix
    glLoadIdentity();
}

void changeSize(GLFWwindow *window, int w, int h)
{

    // Set height to 1 in case it's 0 to avoid divide-by-zero
    if (h == 0)
        h = 1;

    // Set the viewport to match the framebuffer size
    glViewport(0, 0, w, h);

    // Update framebuffer size
    fb_height = h;
    fb_width = w;

    // Update projection
    updateProjection(w, h);
}

void drawAxes(float length)
{
    glBegin(GL_LINES);

    // X axis (Red)
    glColor3f(1.0f, 0.0f, 0.0f);
    glVertex3f(0, 0, 0);
    glVertex3f(length, 0, 0);

    // Y axis (Green)
    glColor3f(0.0f, 1.0f, 0.0f);
    glVertex3f(0, 0, 0);
    glVertex3f(0, length, 0);

    // Z axis (Blue)
    glColor3f(0.0f, 0.0f, 1.0f);
    glVertex3f(0, 0, 0);
    glVertex3f(0, 0, length);

    glEnd();
}

void keyCallback(GLFWwindow *window, int key, int scancode, int action, int mods)
{
    if (action == GLFW_PRESS || action == GLFW_REPEAT)
    {
        switch (key)
        {
        case GLFW_KEY_UP:
            xRot -= 5.0f;
            break;
        case GLFW_KEY_DOWN:
            xRot += 5.0f;
            break;
        case GLFW_KEY_LEFT:
            yRot -= 5.0f;
            break;
        case GLFW_KEY_RIGHT:
            yRot += 5.0f;
            break;
        case GLFW_KEY_PAGE_UP:
            range -= 5.0f;
            break;
        case GLFW_KEY_PAGE_DOWN:
            range += 5.0f;
            break;
        case GLFW_KEY_LEFT_BRACKET:
            fov--;
            break;
        case GLFW_KEY_RIGHT_BRACKET:
            fov++;
            break;

        case GLFW_KEY_P:
            projection = 1 - projection;
            break;

        case GLFW_KEY_R:
            render_method = 1 - render_method;
            break;

        case GLFW_KEY_W:
            r_roc += 0.1f;
            /* 64 arbitrarily chosen (minecraft stack limit) */
            if (r_roc > 64.0f ) { r_roc = 64.0f; }
            break;
        case GLFW_KEY_S:
            r_roc -= 0.1f;
            if (r_roc < 0.0f) { r_roc = 0.0f; }
            break;

        case GLFW_KEY_ESCAPE:
        case GLFW_KEY_Q:
            glfwSetWindowShouldClose(window, 1);
            break;
        }

        // Print parameters
        printf(" xRot  = %6.2f |"
               " yRot  = %6.2f |"
               " range = %6.2f |"
               " FOV   = %6.2f\n"
               " r_roc = %6.2f\n"
               " Render method = %s\n"
               " --------------\n",
               xRot, yRot, range, fov, r_roc,
               render_method == RENDER_FROM_HAND_DRAWN ? "\"Hand-drawn\"" : "from raw pixel data");

        // Make sure range is not negative (better: greater than 5)
        if (range < 5.0f)
            range = 5.0f;

        // Make sure FOV is between 1 -- 179 degrees
        if (fov < 1.0f)
            fov = 1.0f;
        else if (fov > 179.0f)
            fov = 179.0f;

        // Make sure rotation amount is smaller than 360 degrees
        xRot = (GLint)xRot % 360;
        yRot = (GLint)yRot % 360;

        // Update projection
        updateProjection(fb_width, fb_height);
    }
}

void printMenu()
{
    puts(("Key bindings:\n"
          " - up/down:      Rotate around X-axis;\n"
          " - left/right:   Rotate around Y-axis;\n"
          " - page up/down: Zoom in/out (change range);\n"
          " - [ / ]:        Decrease/increase field of view (perspective);\n"
          " - P:            Toggle between orthographic and perspective;\n"
          " - R:            Toggle between hand-drawn and raw pixel data rendering;\n"
          " - W/S:          Increase/decrease arm/leg sway rate;\n"
          " - Q / esc:      Exit.\n"));
}

void drawCube(GLfloat x, GLfloat y, GLfloat z,
              GLfloat sx, GLfloat sy, GLfloat sz,
              GLfloat th, Color color)
{

    // Push current matrix to save transformation
    glPushMatrix();

    // Translate amount x, y, z
    glTranslatef(x, y, z);
    // Rotate around Y-axis for th degrees
    glRotatef(th, 0.0f, 1.0f, 0.0f);
    // Scale the object by sx, sy, sz
    glScalef(sx, sy, sz);

    // Draw cube
    glBegin(GL_QUADS);
    // Front face
    glColor3f(color.r / 255.0f, color.g / 255.0f, color.b / 255.0f);
    glVertex3f(-1, -1, 1);
    glVertex3f(1, -1, 1);
    glVertex3f(1, 1, 1);
    glVertex3f(-1, 1, 1);
    // Back face
    glColor3f(color.r / 255.0f, color.g / 255.0f, color.b / 255.0f);
    glVertex3f(-1, -1, -1);
    glVertex3f(-1, 1, -1);
    glVertex3f(1, 1, -1);
    glVertex3f(1, -1, -1);
    // Top face
    glColor3f(color.r / 255.0f, color.g / 255.0f, color.b / 255.0f);
    glVertex3f(-1, 1, -1);
    glVertex3f(-1, 1, 1);
    glVertex3f(1, 1, 1);
    glVertex3f(1, 1, -1);
    // Bottom face
    glColor3f(color.r / 255.0f, color.g / 255.0f, color.b / 255.0f);
    glVertex3f(-1, -1, -1);
    glVertex3f(1, -1, -1);
    glVertex3f(1, -1, 1);
    glVertex3f(-1, -1, 1);
    // Right face
    glColor3f(color.r / 255.0f, color.g / 255.0f, color.b / 255.0f);
    glVertex3f(1, -1, -1);
    glVertex3f(1, 1, -1);
    glVertex3f(1, 1, 1);
    glVertex3f(1, -1, 1);
    // Left face
    glColor3f(color.r / 255.0f, color.g / 255.0f, color.b / 255.0f);
    glVertex3f(-1, -1, -1);
    glVertex3f(-1, -1, 1);
    glVertex3f(-1, 1, 1);
    glVertex3f(-1, 1, -1);
    glEnd();

    // Pop matrix to restore previous transformation
    glPopMatrix();
}

void render()
{

    // Clear the color buffer and depth buffer
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    // Push current matrix to save transformation
    // from previous frame
    glPushMatrix();

    if (projection == ORTHO)
    {
        // Rotate xRot degrees around X-axis
        glRotatef(xRot, 1.0f, 0.0f, 0.0f);
        // Rotate yRot degrees around Y-axis
        glRotatef(yRot, 0.0f, 1.0f, 0.0f);
    }
    else if (projection == PERSP)
    {
        GLdouble Ex = range * Cos(xRot) * Sin(yRot);
        GLdouble Ey = range * Sin(xRot);
        GLdouble Ez = range * Cos(xRot) * Cos(yRot);
        GLdouble up = (Cos(xRot) >= 0) ? 1.0 : -1.0;
        gluLookAt(Ex, Ey, Ez, // Eye position
                  0, 0, 0,    // Looking into origin (0,0,0)
                  0, up, 0    // The up vector
        );
    }

    draw_steve();

    // Pop matrix to restore previous transformation
    glPopMatrix();
}

void draw_steve()
{

    /* render from hand-drawn geometry */
    if (render_method == RENDER_FROM_HAND_DRAWN)
    {
        /* start the arms and legs at 45 degrees */
        static float rotation = -45.0f;
        static float dir = 1.0f; 

        rotation += dir * r_roc;

        if (rotation >= 45.0f)
        {
            rotation = 45.0f;
            dir = -1.0f;
        }
        else if (rotation <= -45.0f)
        {
            rotation = -45.0f;
            dir = 1.0f;
        }

        float legsX = STEVE_BASE + OFFSET_UNIT * 16;
        float hipY = STEVE_BASE + OFFSET_UNIT * S_LEG_ROWS;

        draw_leg(legsX, hipY, STEVE_BASE, rotation);
        draw_leg(legsX + OFFSET_UNIT * S_LEG_WIDTH, hipY, STEVE_BASE, -rotation);

        float chestX = legsX;
        float chestY = hipY + OFFSET_UNIT;
        /* y of the chest's top row */
        float shoulderY = chestY + (S_CHEST_ROWS - 1) * OFFSET_UNIT;

        draw_chest(chestX, chestY, STEVE_BASE);

        /* left arm*/
        draw_arm(chestX - S_ARM_WIDTH * OFFSET_UNIT, shoulderY, STEVE_BASE, -rotation);
        /* right */
        draw_arm(chestX + S_TOROSO_WIDTH * OFFSET_UNIT, shoulderY, STEVE_BASE, rotation);

        draw_head(chestX, shoulderY + OFFSET_UNIT, STEVE_BASE);

        return;
    }

    /* render from raw pixel data  */
    for (int i = 0; i < STEVE_H; i++)
    {
        for (int j = 0; j < STEVE_W; j++)
        {
            Pixel pixel = steve[i * STEVE_W + j];
            if (pixel.r == 0 && pixel.g == 0 && pixel.b == 0)
            {
                continue; /* Skip drawing if the pixel is black because we didn't load an alpha channel */
            }

            /* depth is 4 units whereas head is 8 units */
            int depth = i + 8 >= STEVE_H ? 16 : 8;

            drawCube(STEVE_BASE + (OFFSET_UNIT * j), STEVE_BASE + (OFFSET_UNIT * i), STEVE_BASE, depth, SIZE_UNIT, SIZE_UNIT, 90.0f, (Color){pixel.r, pixel.g, pixel.b});
        }
    }
}

void setUp()
{
    // Enable depth testC
    glEnable(GL_DEPTH_TEST);
}

int main()
{
    // Initialize GLFW
    if (!glfwInit())
    {
        return EXIT_FAILURE;
    }

    // Create window
    GLFWwindow *window = glfwCreateWindow(800, 600, "I am... Steve 🤨", NULL, NULL);
    if (!window)
    {
        glfwTerminate();
        return EXIT_FAILURE;
    }

    // Make context current
    glfwMakeContextCurrent(window);

    // Set key callback
    glfwSetKeyCallback(window, keyCallback);

    // Set window size change callback
    glfwSetFramebufferSizeCallback(window, changeSize);

    // Make the window size consistent with framebuffer size
    glfwGetFramebufferSize(window, &fb_width, &fb_height);
    changeSize(window, fb_width, fb_height);

    // Print the menu for key bindings
    printMenu();

    // Set up
    setUp();

    // The main loop
    while (!glfwWindowShouldClose(window))
    {

        // Draw the scene in the framebuffer
        render();

        // Swap out framebuffer to the screen
        glfwSwapBuffers(window);

        // To process window evens
        glfwPollEvents();
    }

    // Cleanup
    glfwDestroyWindow(window);
    glfwTerminate();
    return EXIT_SUCCESS;
}