#include "renderer.h"

#include <GL/freeglut_std.h>

#include <chrono>
#include <iostream>
#include <stdexcept>

#include "constants.h"
#include "geography.h"
#include "grid.h"

using namespace std;

Renderer *Renderer::window = nullptr;

Renderer::Renderer(int argc, char *argv[]) {
  if (window != nullptr) {
    throw runtime_error("Only one window may exist.");
  }
  window = this;
  Grid::RandomizeBase();

  glutInit(&argc, argv);
  glutInitWindowPosition(10, 10);
  glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH);
  glutInitWindowSize(viewport_width_, viewport_height_);
  glutCreateWindow("Ice Simulator");
  CheckGLError();

  glEnable(GL_DEPTH_TEST);
  glDepthFunc(GL_LESS);
  glEnable(GL_CULL_FACE);
  glFrontFace(GL_CW);
  glCullFace(GL_BACK);
  CheckGLError();

  glutReshapeFunc(ReshapeCB);
  glutDisplayFunc(DisplayCB);
  glutKeyboardFunc(KeyboardCB);
  glutKeyboardUpFunc(KeyboardUpCB);
  glutMotionFunc(MotionCB);
  glutPassiveMotionFunc(PassiveMotionCB);
  CheckGLError();

  const GLenum res = glewInit();
  if (res != GLEW_OK) {
    cerr << "Error in glewInit()";
    throw runtime_error(
        reinterpret_cast<const char *>(glewGetErrorString(res)));
  }
  CheckGLError();

  shader_ = new Shader(R"(
@PHONG_VERT@
)",
                       R"(
@PHONG_FRAG@
)");
  light_ = new PointLight({kGeographyShort * kGeographyCountShort / 2,
                           kGeographyLong * kGeographyCountLong / 2,
                           kHeightMultiplier * kGeographyCountShort / 2});

  using std::chrono::duration_cast;
  using std::chrono::milliseconds;
  const auto start_time = chrono::high_resolution_clock::now();
  for (auto x = 0; x < kGeographyCountShort; ++x) {
    for (auto y = 0; y < kGeographyCountLong; ++y) {
      objects_.push_back(new Geography(x, y));
    }
  }

  const auto end_time = chrono::high_resolution_clock::now();
  cout << "Generation time: "
       << duration_cast<milliseconds>(end_time - start_time).count() << "ms\n";
  cout << "       vertices: "
       << kGeographyCountShort * kGeographyCountLong * kGeographyShort *
              kGeographyLong
       << "\n";
  CheckGLError();

  InitGeom();
  CheckGLError();

  PrintKeyMap();
  TimerCB(0);
  glutMainLoop();
}

Renderer::~Renderer() { delete shader_; }

void Renderer::InitGeom() {
  light_->InitGeom();
  for (const auto geo : objects_) {
    geo->InitGeom();
  }
  CheckGLError();
}

void Renderer::Display() const {
  if (useShadows_ && shadowsChanged_) {
    light_->GenerateCubeMaps(objects_);
  }

  glViewport(0, 0, viewport_width_, viewport_height_);
  glClearColor(0, 0, 0, 1);
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
  glUseProgram(shader_->id());

  camera_.LoadMatrices(shader_);
  light_->LoadData(shader_);
  CheckGLError();

  if (!shader_->CopyDataToUniform(useColor_, "useColor")) {
    cerr << "Shader not considering colour toggle" << endl;
  }
  if (!shader_->CopyDataToUniform(useLight_, "useLight")) {
    cerr << "Shader not considering light toggle" << endl;
  }
  if (!shader_->CopyDataToUniform(useShadows_, "useShadows")) {
    cerr << "Shader not considering light toggle" << endl;
  }
  float minHeight = numeric_limits<float>::infinity();
  float maxHeight = -minHeight;
  for (const auto object : objects_) {
    const auto geo = dynamic_cast<Geography *>(object);
    if (geo != nullptr) {
      const float min = geo->min();
      if (min < minHeight) {
        minHeight = min;
      }
      const float max = geo->max();
      if (max > maxHeight) {
        maxHeight = max;
      }
    }
  }
  if (!shader_->CopyDataToUniform(minHeight, "minHeight")) {
    cerr << "Shader not considering minimum height" << endl;
  }
  if (!shader_->CopyDataToUniform(maxHeight, "maxHeight")) {
    cerr << "Shader not considering maximum height" << endl;
  }

  glActiveTexture(GL_TEXTURE0);
  glBindTexture(GL_TEXTURE_CUBE_MAP, light_->getDepthTexture());
  const auto depthMap = glGetUniformLocation(shader_->id(), "depthMap");
  glUniform1i(depthMap, 0);

  light_->Render(shader_);
  for (const auto geo : objects_) {
    geo->Render(shader_);
  }

  glutSwapBuffers();
  CheckGLError();
}

void Renderer::Reshape(const int new_width, const int new_height) {
  viewport_width_ = new_width;
  viewport_height_ = new_height;

  camera_.set_aspect(new_width, new_height);
  glViewport(0, 0, viewport_width_, viewport_height_);
  glutPostRedisplay();
}

void Renderer::Keyboard(const unsigned char key, const int, const int) {
  switch (key) {
    case 'x':
    case 'X':
    case 'q':
    case 'Q':
    case 27:  // ESC
      exit(0);
    case 'k':
    case 'K':
      setPointLight_ = !setPointLight_;
      break;
    case 'l':
    case 'L':
      useLight_ = !useLight_;
      break;
    case 'm':
    case 'M':
      useShadows_ = !useShadows_;
      break;
    case 'n':
    case 'N':
      useColor_ = !useColor_;
      break;
    case 'p':
    case 'P':
      simulating_ = !simulating_;
      break;
    case 'r':
    case 'R':
      Grid::RandomizeBase();
      for (const auto object : objects_) {
        const auto geo = dynamic_cast<Geography *>(object);
        if (geo != nullptr) {
          geo->Randomize(true);
        }
      }
      shadowsChanged_ = true;
    default:
      break;
  }
  HandleMovementKey(key, true);
  glutPostRedisplay();
}

void Renderer::KeyboardUp(const unsigned char key, const int, const int) {
  HandleMovementKey(key, false);
}

void Renderer::Motion(const int x, const int y) { HandleMouseMove(x, y, true); }

void Renderer::PassiveMotion(const int x, const int y) {
  HandleMouseMove(x, y, false);
}

void Renderer::HandleMouseMove(const int x, const int y, const bool active) {
  if (active) {
    camera_.RelativeRotate(
        {0, static_cast<float>(y - last_mouse_y_) * kRotateDelta,
         static_cast<float>(last_mouse_x_ - x) * kRotateDelta});
    glutPostRedisplay();
  }
  last_mouse_x_ = x;
  last_mouse_y_ = y;
}

void Renderer::HandleMovementKey(const unsigned char key, const bool down) {
  switch (key) {
    case 'w':
    case 'W':
      move_forward_ = down;
      break;
    case 's':
    case 'S':
      move_backward_ = down;
      break;
    case 'a':
    case 'A':
      move_left_ = down;
      break;
    case 'd':
    case 'D':
      move_right_ = down;
      break;
    case 'z':
    case 'Z':
      move_down_ = down;
      break;
    case 'c':
    case 'C':
      move_up_ = down;
      break;
    default:
      break;
  }
}

void Renderer::Tick(const int ticks) {
  bool doneSomething = ticks == 0;
  if (last_mouse_x_ < 0 || last_mouse_x_ > viewport_width_ ||
      last_mouse_y_ < 0 || last_mouse_y_ > viewport_height_) {
    move_forward_ = false;
    move_backward_ = false;
    move_left_ = false;
    move_right_ = false;
    move_up_ = false;
    move_down_ = false;
  }
  if (move_forward_ || move_backward_ || move_left_ || move_right_) {
    camera_.RelativeMove(
        {static_cast<float>((move_forward_ ? 1 : 0) +
                            (move_backward_ ? -1 : 0)) *
             kMoveDelta,
         static_cast<float>((move_left_ ? 1 : 0) + (move_right_ ? -1 : 0)) *
             kMoveDelta,
         0.});
    doneSomething = true;
  }
  if (move_up_ || move_down_) {
    camera_.AbsoluteMove(
        {0., 0.,
         static_cast<float>((move_up_ ? 1 : 0) + (move_down_ ? -1 : 0)) *
             kMoveDelta});
    doneSomething = true;
  }

  if (setPointLight_) {
    light_->setPosition(camera_.getPosition());
    light_->setColors({1, 1, 1});
    doneSomething = true;
    shadowsChanged_ = true;
  } else if (simulating_) {
    const auto lightAngle =
        fmod(static_cast<float>(ticks) * glm::two_pi<float>() / (kFPS * 20),
             glm::pi<float>() * 5 / 4) -
        glm::pi<float>() * 5 / 8;
    const auto lightRotation =
        glm::vec3(0, glm::sin(lightAngle), glm::cos(lightAngle) / 2) *
        static_cast<float>(kGeographyLong * kGeographyCountLong);
    light_->setPosition(lightRotation +
                        glm::vec3(kGeographyShort * kGeographyCountShort / 2,
                                  kGeographyLong + kGeographyCountShort / 2,
                                  0));
    auto baseLightColor = glm::max(0.0f, glm::cos(lightAngle));
    light_->setColors(
        {glm::pow(baseLightColor, 0.8), baseLightColor, baseLightColor});
    doneSomething = true;
    shadowsChanged_ = true;
  }

  if (doneSomething) {
    glutPostRedisplay();
  }
}

void Renderer::DisplayCB() {
  window->Display();
  window->shadowsChanged_ = false;
}

void Renderer::ReshapeCB(const int w, const int h) { window->Reshape(w, h); }

void Renderer::KeyboardCB(const unsigned char key, const int x, const int y) {
  window->Keyboard(key, x, y);
}

void Renderer::KeyboardUpCB(const unsigned char key, const int x, const int y) {
  window->KeyboardUp(key, x, y);
}

void Renderer::MotionCB(const int w, const int h) { window->Motion(w, h); }

void Renderer::PassiveMotionCB(const int w, const int h) {
  window->PassiveMotion(w, h);
}

void Renderer::TimerCB(const int ticks) {
  const auto tickIncrement = window->simulating_ ? 1 : 0;
  glutTimerFunc(1000 / kFPS, TimerCB, ticks + tickIncrement);
  window->Tick(ticks);
}

void Renderer::CheckGLError() {
  for (auto rc = glGetError(); rc != GL_NO_ERROR; rc = glGetError()) {
    PrintOpenGLError(rc);
  }
}

void Renderer::PrintOpenGLError(const GLenum errorCode) {
  switch (errorCode) {
    case GL_INVALID_VALUE:
      cerr << "GL_INVALID_VALUE" << endl;
      break;
    case GL_INVALID_OPERATION:
      cerr << "GL_INVALID_OPERATION" << endl;
      break;
    default:
      cerr << "Unknown OpenGL error" << endl;
  }
}

void Renderer::PrintKeyMap() {
  cout << "\n";
  cout << "Mouse control:\n";
  cout << "\tClick and drag to rotate camera\n";
  cout << "\n";
  cout << "Keyboard control (case-insensitive):\n";
  cout << "\tx, q, [ESC]: Quit program\n";
  cout << "\tr: Regenerate terrain\n";
  cout << "\n";
  cout << "\twasd: Move forward/left/backward/right relative to the camera\n";
  cout << "\tcz: Move up/down relative to the world\n";
  cout << "\n";
  cout << "\tk: Toggle point light following camera\n";
  cout << "\tl: Toggle phong light simulation\n";
  cout << "\tm: Toggle shadows\n";
  cout << "\tn: Toggle ground/normal colour\n";
  cout << "\tp: Toggle day/night cycle\n";
  cout << endl;
}
