// GLAD
#include "glad/glad.h"

// GLM
#define GLM_ENABLE_EXPERIMENTAL
#include "glm/ext/matrix_transform.hpp"
#include "glm/ext/vector_float3.hpp"
#include "glm/gtc/type_ptr.hpp"
#include "glm/trigonometric.hpp"
#include "glm/gtx/rotate_vector.hpp"
#include "glm/gtx/string_cast.hpp"

// SDL
#include <SDL2/SDL.h>
#include <SDL2/SDL_timer.h>
#include <SDL2/SDL_stdinc.h>
#include <SDL2/SDL_error.h>
#include <SDL2/SDL_video.h>
#include <SDL2/SDL_events.h>
#include <SDL2/SDL_mouse.h>
#include <SDL2/SDL_keycode.h>

// STDLIB
#include <cstdint>
#include <cstdio>
#include <cmath>
#include <string>
#include <vector>

#define WINDOW_WIDTH       1280
#define WINDOW_HEIGHT      720
#define WINDOW_HALF_WIDTH  640
#define WINDOW_HALF_HEIGHT 360

enum exit_codes : int {
  EXIT_CODE_SUCCESS,
  EXIT_CODE_SDL_INIT_FAILED,
  EXIT_CODE_WINDOW_CREATION_FAILED,
  EXIT_CODE_OPENGL_CONTEXT_FAILED,
  EXIT_CODE_GLAD_LOADER_FAILED,
};

class Shader {
  public:
    Shader(const std::string &vert_file, const std::string &frag_file);
    ~Shader();
    void activate();
    void set_float(const char *name, float value);
    void set_vec3(const char *name, glm::vec3 vector);
    void set_mat4(const char *name, glm::mat4 matrix);
    GLuint program;
  private:
    void link_program(GLuint vert, GLuint frag);
    GLuint load_and_compile_shader(const std::string &filepath, GLenum shader_type);
    std::string load_shader_from_file(const std::string &filepath);
    static const char *get_shader_type_string(GLenum shader_type);
};

int main() {
  if (SDL_Init(SDL_INIT_EVERYTHING) != 0) {
    return EXIT_CODE_SDL_INIT_FAILED;
  }

  SDL_GL_SetAttribute(SDL_GL_BUFFER_SIZE, 24);
  SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
  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_Window *window = SDL_CreateWindow("Window", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
                                        WINDOW_WIDTH, WINDOW_HEIGHT, SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE);
  if (!window) {
    return EXIT_CODE_WINDOW_CREATION_FAILED;
  }

  SDL_GLContext context = SDL_GL_CreateContext(window);
  if (!context) {
    return EXIT_CODE_OPENGL_CONTEXT_FAILED;
  }

  if (gladLoadGLLoader(SDL_GL_GetProcAddress) == 0) {
    return EXIT_CODE_GLAD_LOADER_FAILED;
  }

  SDL_SetRelativeMouseMode(SDL_TRUE);
  SDL_WarpMouseInWindow(window, WINDOW_HALF_WIDTH, WINDOW_HALF_HEIGHT);

  glViewport(0, 0, WINDOW_WIDTH, WINDOW_HEIGHT);

  glEnable(GL_DEPTH_TEST);

  std::vector<GLfloat> vertices = {
     0.5f, -0.5f,  0.5f, // position
    -0.5f,  0.5f,  0.5f, // position
     0.5f,  0.5f,  0.5f, // position
     0.5f, -0.5f,  0.5f, // position
    -0.5f, -0.5f,  0.5f, // position
     0.5f, -0.5f, -0.5f, // position
    -0.5f,  0.5f, -0.5f, // position
    -0.5f,  0.5f, -0.5f, // position
    -0.5f,  0.5f,  0.5f, // position
     0.5f,  0.5f, -0.5f, // position
     0.5f, -0.5f, -0.5f, // position
    -0.5f,  0.5f,  0.5f, // position
     0.5f,  0.5f,  0.5f, // position
    -0.5f, -0.5f, -0.5f, // position
    -0.5f, -0.5f, -0.5f, // position
     0.5f, -0.5f, -0.5f, // position
    -0.5f,  0.5f, -0.5f, // position
  };

  std::vector<GLuint> indices = {
    14, 5, 9,
    9, 7, 14,
    4, 3, 12,
    12, 8, 4,
    1, 6, 13,
    13, 4, 1,
    2, 9, 15,
    15, 0, 2,
    13, 10, 3,
    3, 4, 13,
    7, 9, 2,
    2, 11, 16
  };

  GLuint vao = 0;
  glGenVertexArrays(1, &vao);
  glBindVertexArray(vao);

  GLuint ebo = 0;
  glGenBuffers(1, &ebo);
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
  glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(GLuint), indices.data(), GL_STATIC_DRAW);

  GLuint vbo = 0;
  glGenBuffers(1, &vbo);
  glBindBuffer(GL_ARRAY_BUFFER, vbo);
  glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(GLfloat), vertices.data(), GL_STATIC_DRAW);

  glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (void *)0);
  glEnableVertexAttribArray(0);

  GLuint light_vao = 0;
  glGenVertexArrays(1, &light_vao);
  glBindVertexArray(light_vao);
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
  glBindBuffer(GL_ARRAY_BUFFER, vbo);

  glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (void *)0);
  glEnableVertexAttribArray(0);

  glBindVertexArray(0);
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
  glBindBuffer(GL_ARRAY_BUFFER, 0);

  Shader main_shader {"shaders/vert.glsl", "shaders/frag.glsl"};
  Shader light_shader {"shaders/vert.glsl", "shaders/light_frag.glsl"};

  main_shader.set_vec3("object_color", glm::vec3(1.0f, 0.5f, 0.31f));
  main_shader.set_vec3("light_color", glm::vec3(1.0f, 1.0f, 1.0f));

  const float camera_speed   = 25.0f;
  glm::vec3 camera_position  = glm::vec3(0.0f, 0.0f, 4.0f);
  glm::vec3 camera_forward   = glm::vec3(0.0f);
  glm::vec3 world_up         = glm::vec3(0.0f, 1.0f, 0.0f);

  float yaw   = -90.0f;
  float pitch = 0.0f;

  glm::mat4 model      = glm::mat4(1.0f);
  glm::mat4 view       = glm::mat4(1.0f);
  glm::mat4 projection = glm::perspective(glm::radians(45.0f), (float)WINDOW_WIDTH / (float)WINDOW_HEIGHT, 0.1f, 100.0f);

  main_shader.set_mat4("projection", projection);
  light_shader.set_mat4("projection", projection);

  const float sensitivity = 0.1f;
  int last_mouse_x        = WINDOW_HALF_WIDTH;
  int last_mouse_y        = WINDOW_HALF_HEIGHT;
  uint32_t last_frame     = SDL_GetTicks();
  float delta             = 0.0f;
  bool running            = true;
  SDL_Event event         = {};

  while (running) {
    uint32_t ticks = SDL_GetTicks();
    delta          = (ticks - last_frame) * 0.001f;
    last_frame     = ticks;

    while (SDL_PollEvent(&event)) {
      switch (event.type) {
        case SDL_QUIT:
          running = false;
          break;
        case SDL_KEYDOWN:
          if (event.key.keysym.sym == SDLK_ESCAPE) {
            running = false;
          } else if (event.key.keysym.sym == SDLK_w) {
            camera_position += camera_speed * delta * camera_forward;
          } else if (event.key.keysym.sym == SDLK_s) {
            camera_position -= camera_speed * delta * camera_forward;
          } else if (event.key.keysym.sym == SDLK_d) {
            camera_position += camera_speed * delta * glm::normalize(glm::cross(camera_forward, world_up));
          } else if (event.key.keysym.sym == SDLK_a) {
            camera_position -= camera_speed * delta * glm::normalize(glm::cross(camera_forward, world_up));
          }
          break;
        case SDL_MOUSEMOTION: {
            float x_offset = event.motion.xrel;
            float y_offset = -event.motion.yrel;

            last_mouse_x = last_mouse_x + event.motion.xrel;
            last_mouse_y = last_mouse_y + event.motion.yrel;

            x_offset *= sensitivity;
            y_offset *= sensitivity;

            yaw   += x_offset;
            pitch += y_offset;

            if(pitch > 89.0f) {
              pitch =  89.0f;
            }

            if(pitch < -89.0f) {
              pitch = -89.0f;
            }
          }
          break;
        case SDL_WINDOWEVENT:
          if (event.window.event == SDL_WINDOWEVENT_RESIZED) {
            SDL_Window *wnd = SDL_GetWindowFromID(event.window.windowID);
            if (!wnd) {
              continue;
            }
            int w, h;
            SDL_GL_GetDrawableSize(wnd, &w, &h);
            glViewport(0, 0, w, h);
            SDL_WarpMouseInWindow(wnd, (int)(w * 0.5f), (int)(h * 0.5f));
          }
          break;
      }
    }

    camera_forward.x = cos(glm::radians(yaw)) * cos(glm::radians(pitch));
    camera_forward.y = sin(glm::radians(pitch));
    camera_forward.z = sin(glm::radians(yaw)) * cos(glm::radians(pitch));
    camera_forward   = glm::normalize(camera_forward);

    view = glm::lookAt(camera_position, camera_position + camera_forward, world_up);
    main_shader.set_mat4("view", view);
    light_shader.set_mat4("view", view);

    glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    model = glm::translate(glm::mat4(1.0f), glm::vec3(0.0f,  0.0f,  0.0f));
    main_shader.activate();
    main_shader.set_mat4("model", model);
    glBindVertexArray(vao);
    glUniformMatrix4fv(glGetUniformLocation(main_shader.program, "model"), 1, GL_FALSE, glm::value_ptr(model));
    glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_INT, (void *)0);

    model = glm::translate(glm::mat4(1.0f), glm::vec3(1.2f,  1.0f, 2.0f));
    model = glm::scale(model, glm::vec3(0.2f));
    light_shader.activate();
    light_shader.set_mat4("model", model);
    glBindVertexArray(light_vao);
    glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_INT, (void *)0);

    SDL_GL_SwapWindow(window);
  }

  SDL_GL_DeleteContext(context);
  SDL_DestroyWindow(window);
  SDL_Quit();

  return EXIT_CODE_SUCCESS;
}


Shader::Shader(const std::string &vert_file, const std::string &frag_file) {
  GLuint vert = load_and_compile_shader(vert_file, GL_VERTEX_SHADER);
  GLuint frag = load_and_compile_shader(frag_file, GL_FRAGMENT_SHADER);
  link_program(vert, frag);
  glDeleteShader(vert);
  glDeleteShader(frag);
}

Shader::~Shader() {
  if (program > 0) {
    glDeleteProgram(program);
  }
}

void Shader::activate() {
  if (program > 0) {
    glUseProgram(program);
  }
}

void Shader::set_float(const char *name, float value) {
  activate();
  glUniform1f(glGetUniformLocation(program, name), value);
}

void Shader::set_vec3(const char *name, glm::vec3 vector) {
  activate();
  glUniform3f(glGetUniformLocation(program, name), vector.x, vector.y, vector.z);
}

void Shader::set_mat4(const char *name, glm::mat4 matrix) {
  activate();
  glUniformMatrix4fv(glGetUniformLocation(program, name), 1, GL_FALSE, glm::value_ptr(matrix));
}

void Shader::link_program(GLuint vert, GLuint frag) {
  program = glCreateProgram();
  glAttachShader(program, vert);
  glAttachShader(program, frag);

  glLinkProgram(program);
  GLint program_linked;
  glGetProgramiv(program, GL_LINK_STATUS, &program_linked);
  if (program_linked != GL_TRUE)
  {
      GLsizei log_length = 0;
      GLchar message[1024];
      glGetProgramInfoLog(program, 1024, &log_length, message);
      printf("Failed to link program: %s\n", message);
      program = 0;
  }
}

GLuint Shader::load_and_compile_shader(const std::string &filepath, GLenum shader_type) {
  std::string src = load_shader_from_file(filepath);
  const char *shader_src = src.c_str();

  GLuint shader = glCreateShader(shader_type);
  glShaderSource(shader, 1, &shader_src, NULL);
  glCompileShader(shader);

  GLint shader_compiled;
  glGetShaderiv(shader, GL_COMPILE_STATUS, &shader_compiled);
  if (shader_compiled != GL_TRUE)
  {
      GLsizei log_length = 0;
      GLchar message[1024];
      glGetShaderInfoLog(shader, 1024, &log_length, message);
      printf("Failed to compile %s shader: %s\n", get_shader_type_string(shader_type), message);
      return 0;
  }

  return shader;
}

std::string Shader::load_shader_from_file(const std::string &filepath) {
  FILE *fp = fopen(filepath.c_str(), "r");
  if (!fp) {
    return "";
  }

  std::string output = {};

  char buf[1024] = {0};
  while (fgets(buf, sizeof(buf), fp)) {
    output += buf;
  }

  return output;
}

const char *Shader::get_shader_type_string(GLenum shader_type) {
  const char *output;

  switch (shader_type) {
    case GL_COMPUTE_SHADER:
      output = "compute";
      break;
    case GL_VERTEX_SHADER:
      output = "vertex";
      break;
    case GL_TESS_CONTROL_SHADER:
      output = "tess_control";
      break;
    case GL_TESS_EVALUATION_SHADER:
      output = "tess_evaluation";
      break;
    case GL_GEOMETRY_SHADER:
      output = "geometry";
      break;
    case GL_FRAGMENT_SHADER:
      output = "fragment";
      break;
    default:
      output = "UNKNOWN";
      break;
  }

  return output;
}