// Copyright 2006-2015 Tobias Sargeant (tobias.sargeant@gmail.com).
//
// This file is part of the Carve CSG Library (http://carve-csg.com/)
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
// files (the "Software"), to deal in the Software without
// restriction, including without limitation the rights to use, copy,
// modify, merge, publish, distribute, sublicense, and/or sell copies
// of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
// BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
// ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

#if defined(HAVE_CONFIG_H)
#include <carve_config.h>
#endif

#include "scene.hpp"

#include <carve/geom3d.hpp>
#include <carve/matrix.hpp>

#include <GL/glui.h>

static int lastx = 0, lasty = 0;
static unsigned buttons;
static int lastbutton = 0;

static Scene* g_scene = nullptr;
static int g_mainWindow = 0;
static GLUI* g_rightPanel = nullptr;
static double near_plane = 0.2;
static double far_plane = 200;
carve::math::Matrix g_projection, g_modelview;

void Scene::updateDisplay() {
  if (CAM_ELEVATION < -90.0) {
    CAM_ELEVATION = -90;
  }
  if (CAM_ELEVATION > 90.0) {
    CAM_ELEVATION = 90;
  }
  if (CAM_DIST < 0.05) {
    CAM_DIST = 0.05;
  }
  glutPostWindowRedisplay(g_mainWindow);
  glutPostRedisplay();
}

carve::geom3d::Vector rotateWithVector(const carve::geom3d::Vector& x,
                                       carve::geom3d::Vector u, float ang) {
  carve::geom3d::Vector h, v, uxx;

  u.normalize();

  uxx = cross(u, x) * (float)sin(ang);

  h = u * (dot(x, u));

  v = (x - h) * (float)cos(ang);

  return (h + v) + uxx;
}

// Our world is Z up.
const carve::geom3d::Vector WORLD_RIGHT = carve::geom::VECTOR(1, 0, 0);
const carve::geom3d::Vector WORLD_UP = carve::geom::VECTOR(0, 0, 1);
const carve::geom3d::Vector WORLD_IN = carve::geom::VECTOR(0, 1, 0);

void getCameraVectors(float rot, float elev, carve::geom3d::Vector& right,
                      carve::geom3d::Vector& up, carve::geom3d::Vector& in) {
  right = WORLD_RIGHT;
  up = WORLD_UP;
  in = WORLD_IN;

  right = rotateWithVector(right, WORLD_RIGHT, carve::math::radians(elev));
  up = rotateWithVector(up, WORLD_RIGHT, carve::math::radians(elev));
  in = rotateWithVector(in, WORLD_RIGHT, carve::math::radians(elev));
  right = rotateWithVector(right, WORLD_UP, carve::math::radians(rot));
  up = rotateWithVector(up, WORLD_UP, carve::math::radians(rot));
  in = rotateWithVector(in, WORLD_UP, carve::math::radians(rot));
}

GLvoid Scene::_drag(int x, int y) {
  int dx = x - lastx;
  int dy = y - lasty;

  if (buttons == 0x04) {
    CAM_DIST *= 1.0 + 0.01 * (y - lasty);
    CAM_DIST_REAL *= 1.0 + 0.01 * (y - lasty);
    updateDisplay();
  } else if (buttons == 0x01) {
    CAM_ELEVATION += 0.5 * (y - lasty);
    CAM_ROT -= 0.5 * (x - lastx);
    updateDisplay();
  } else if (buttons == 0x02) {
    carve::geom3d::Vector right, up, in;
    getCameraVectors(CAM_ROT, CAM_ELEVATION, right, up, in);

    right.scaleBy(2 * dx * CAM_DIST / WIDTH);
    up.scaleBy(2 * dy * CAM_DIST / HEIGHT);
    CAM_LOOK += right;
    CAM_LOOK += up;
    CAM_LOOK_REAL += right;
    CAM_LOOK_REAL += up;
    updateDisplay();
  }

  lastx = x;
  lasty = y;
}

GLvoid Scene::_click(int button, int state, int x, int y) {
  unsigned mask = 1 << button;
  if (state) {
    buttons &= ~mask;
  } else {
    buttons |= mask;
  }

  lastx = x;
  lasty = y;
  click(button, state, x, y);
}

GLvoid Scene::_key(unsigned char k, int x, int y) {
  double rate = 1.0;
  if (isupper(k)) {
    rate = 0.1;
    k = tolower(k);
  }

  switch (k) {
    case 'q':
      exit(0);
      break;
    case 'g':
      disp_grid = !disp_grid;
      break;
    case 'h':
      disp_axes = !disp_axes;
      break;
    case 'w':
      if (CAM_ELEVATION > -85.0) {
        CAM_ELEVATION -= 5.0 * rate;
      }
      break;
    case 's':
      if (CAM_ELEVATION < 85.0) {
        CAM_ELEVATION += 5.0 * rate;
      }
      break;
    case 'a':
      CAM_ROT += 5.0 * rate;
      break;
    case 'd':
      CAM_ROT -= 5.0 * rate;
      break;
    case 'r':
      CAM_DIST += 2.0 * rate;
      break;
    case 'f':
      CAM_DIST -= 2.0 * rate;
      break;
    case 'i':
      CAM_LOOK.x -= cos(carve::math::radians(CAM_ROT)) * 2.0 * rate;
      CAM_LOOK.z -= sin(carve::math::radians(CAM_ROT)) * 2.0 * rate;
      break;
    case 'j':
      CAM_LOOK.x -= sin(carve::math::radians(CAM_ROT)) * 2.0 * rate;
      CAM_LOOK.z += cos(carve::math::radians(CAM_ROT)) * 2.0 * rate;
      break;
    case 'k':
      CAM_LOOK.x += cos(carve::math::radians(CAM_ROT)) * 2.0 * rate;
      CAM_LOOK.z += sin(carve::math::radians(CAM_ROT)) * 2.0 * rate;
      break;
    case 'l':
      CAM_LOOK.x += sin(carve::math::radians(CAM_ROT)) * 2.0 * rate;
      CAM_LOOK.z -= cos(carve::math::radians(CAM_ROT)) * 2.0 * rate;
      break;
    case 'z':
      near_plane *= 1.1;
      break;
    case 'x':
      near_plane /= 1.1;
      break;
    case 'c':
      far_plane *= 1.1;
      break;
    case 'v':
      far_plane /= 1.1;
      break;
    default: {
      if (!key(k, x, y)) {
        goto skip_redisplay;
      }
      break;
    }
  }
  updateDisplay();
skip_redisplay:;
}

GLvoid Scene::_draw() {
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  double w = WIDTH;
  double h = HEIGHT;

  if (h == 0) {
    h = 1;
  }
  glViewport(0, 0, WIDTH, HEIGHT);
  if (w > h) {
    double r = w / h;
    glFrustum(-0.2 * r, 0.2 * r, -0.2, 0.2, near_plane, far_plane);
  } else {
    double r = h / w;
    glFrustum(-0.2, 0.2, -0.2 * r, 0.2 * r, near_plane, far_plane);
  }

  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();

  CAM_LOOK_REAL = (CAM_LOOK_REAL * 4 + CAM_LOOK) / 5;
  CAM_DIST_REAL = (CAM_DIST_REAL * 4 + CAM_DIST) / 5;

  // if they are far apart post another redisplay
  if ((CAM_LOOK_REAL - CAM_LOOK).length() > 0.001) {
    glutPostRedisplay();
  } else {
    CAM_LOOK_REAL = CAM_LOOK;
  }
  if (fabs(CAM_DIST_REAL - CAM_DIST) > 0.001) {
    glutPostRedisplay();
  } else {
    CAM_DIST_REAL = CAM_DIST;
  }

  carve::geom3d::Vector right, up, in;
  getCameraVectors(CAM_ROT, CAM_ELEVATION, right, up, in);
  in = CAM_DIST_REAL * in;
  gluLookAt(CAM_LOOK_REAL.x + in.x, CAM_LOOK_REAL.y + in.y,
            CAM_LOOK_REAL.z + in.z, CAM_LOOK_REAL.x, CAM_LOOK_REAL.y,
            CAM_LOOK_REAL.z, up.x, up.y, up.z);

  glGetDoublev(GL_MODELVIEW_MATRIX, g_modelview.v);
  glGetDoublev(GL_PROJECTION_MATRIX, g_projection.v);

  glShadeModel(GL_SMOOTH);
  glClearDepth(1.0);
  glClearColor(0.05f, 0.075f, 0.2f, 1.0f);
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

  {
    const float amb[] = {0.05f, 0.05f, 0.05f, 1.0f};
    const float diff[] = {1.0f, 0.8f, 0.5f, 1.0f};
    const float spec[] = {0.4f, 0.4f, 0.4f, 1.0f};
    const float pos[] = {-40.0f, 40.0f, 80.0f, 0.0f};

    glLightfv(GL_LIGHT0, GL_AMBIENT, amb);
    glLightfv(GL_LIGHT0, GL_DIFFUSE, diff);
    glLightfv(GL_LIGHT0, GL_SPECULAR, spec);
    glLightfv(GL_LIGHT0, GL_POSITION, pos);
  }

  {
    const float amb[] = {0.05f, 0.05f, 0.05f, 1.0f};
    const float diff[] = {0.8f, 0.8f, 1.0f, 1.0f};
    const float spec[] = {0.4f, 0.4f, 0.4f, 1.0f};
    const float pos[] = {+50.0f, -10.0f, 10.0f, 0.0f};

    glLightfv(GL_LIGHT1, GL_AMBIENT, amb);
    glLightfv(GL_LIGHT1, GL_DIFFUSE, diff);
    glLightfv(GL_LIGHT1, GL_SPECULAR, spec);
    glLightfv(GL_LIGHT1, GL_POSITION, pos);
  }

  glCullFace(GL_BACK);

  glEnable(GL_DEPTH_TEST);
  glEnable(GL_LIGHT0);
  glEnable(GL_LIGHT1);
  glEnable(GL_LIGHTING);
  glEnable(GL_NORMALIZE);
  glEnable(GL_COLOR_MATERIAL);

  glMatrixMode(GL_MODELVIEW);
  glPushMatrix();

  glEnable(GL_BLEND);
  glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

  glDisable(GL_LIGHTING);
  glDisable(GL_DEPTH);

  if (disp_grid) {
    glBegin(GL_LINE_LOOP);
    glColor4f(1, 1, 1, 0.5);
    glVertex3f(-30, -30, 0);
    glVertex3f(+30, -30, 0);
    glVertex3f(+30, +30, 0);
    glVertex3f(-30, +30, 0);
    glEnd();

    glBegin(GL_LINES);
    glColor4f(1.0f, 1.0f, 1.0f, 0.3f);
    for (int i = 0; i < 30; i++) {
      glVertex3f(i * 2.0f - 30.0f, -30.0f, 0.0f);
      glVertex3f(i * 2.0f - 30.0f, +30.0f, 0.0f);
      glVertex3f(-30.0f, i * 2.0f - 30.0f, 0.0f);
      glVertex3f(+30.0f, i * 2.0f - 30.0f, 0.0f);
    }
    glEnd();
  }

  glEnable(GL_DEPTH_TEST);
  glEnable(GL_LIGHTING);
  glEnable(GL_COLOR_MATERIAL);
  glDepthFunc(GL_LESS);
  glDepthMask(GL_TRUE);

  draw();

  glDisable(GL_COLOR_MATERIAL);
  glDisable(GL_DEPTH_TEST);
  glDisable(GL_LIGHTING);

  if (disp_axes) {
    glBegin(GL_LINES);
    glColor4f(1, 0, 0, 1);
    glVertex3f(0, 0, 0);
    glVertex3f(10, 0, 0);
    glColor4f(0, 1, 0, 1);
    glVertex3f(0, 0, 0);
    glVertex3f(0, 10, 0);
    glColor4f(0, 0, 1, 1);
    glVertex3f(0, 0, 0);
    glVertex3f(0, 0, 10);
    glEnd();
  }

  glPopMatrix();

  glutSwapBuffers();
}

bool Scene::key(unsigned char k, int x, int y) {
  return false;
}

GLvoid Scene::draw() {}

void Scene::click(int button, int state, int x, int y) {}

GLvoid Scene::_resize(int w, int h) {
  int tx, ty, tw, th;
  GLUI_Master.get_viewport_area(&tx, &ty, &tw, &th);
  WIDTH = tw;
  HEIGHT = th;
}

void control_cb(int control) {
  glutPostRedisplay();
  glutPostWindowRedisplay(g_mainWindow);
}

#define WIREFRAME_ENABLED_ID 200

int wireframe = 0;

static std::map<OptionGroup*, GLUI_Rollout*> groupToRollouts;
static std::map<Option*, GLUI_Checkbox*> optionToCheckboxes;

OptionGroup* Scene::createOptionGroup(const char* caption) {
  // make sure our UI has been initialised.
  init();

  // Create a rollout GUI item.
  OptionGroup* group = new OptionGroup();
  GLUI_Rollout* rollout = new GLUI_Rollout(g_rightPanel, caption);
  groupToRollouts[group] = rollout;
  return group;
}

Option* OptionGroup::createOption(const char* caption, bool initialValue) {
  GLUI_Rollout* rollout = groupToRollouts[this];
  if (rollout == nullptr) {
    return nullptr;
  }

  GLUI_Checkbox* cb =
      new GLUI_Checkbox(rollout, caption, nullptr, 1, control_cb);
  cb->set_int_val(initialValue);

  Option* option = new Option();

  optionToCheckboxes[option] = cb;

  return option;
}

bool Option::isChecked() {
  GLUI_Checkbox* cb = optionToCheckboxes[this];
  if (cb != nullptr) {
    return cb->get_int_val() != 0;
  } else {
    return false;
  }
}

void Option::setChecked(bool value) {
  GLUI_Checkbox* cb = optionToCheckboxes[this];
  if (cb != nullptr) {
    return cb->set_int_val(value);
  }
}

void Scene::init() {
  if (!initialised) {
    initialised = true;

    GLUI_Master.set_glutDisplayFunc(s_draw);
    GLUI_Master.set_glutReshapeFunc(s_resize);
    GLUI_Master.set_glutKeyboardFunc(s_key);
    GLUI_Master.set_glutSpecialFunc(nullptr);
    GLUI_Master.set_glutMouseFunc(s_click);
    GLUI_Master.set_glutMotionFunc(s_drag);

    g_rightPanel =
        GLUI_Master.create_glui_subwindow(g_mainWindow, GLUI_SUBWINDOW_RIGHT);

    this->_init();
  }
}

carve::geom3d::Ray Scene::getRay(int x, int y) {
  carve::geom3d::Vector from, to;

  GLint view[4];
  glGetIntegerv(GL_VIEWPORT, view);
  gluUnProject(x, view[3] - y, 0, g_modelview.v, g_projection.v, view, &from.x,
               &from.y, &from.z);
  gluUnProject(x, view[3] - y, 50, g_modelview.v, g_projection.v, view, &to.x,
               &to.y, &to.z);

  return carve::geom3d::Ray((to - from).normalized(), from);
}

void Scene::zoomTo(carve::geom3d::Vector pos, double dist) {
  CAM_LOOK = pos;
  CAM_DIST = dist;
  updateDisplay();
}
void Scene::_init() {}

GLvoid Scene::s_draw() {
  g_scene->_draw();
}
GLvoid Scene::s_resize(int w, int h) {
  g_scene->_resize(w, h);
}
GLvoid Scene::s_drag(int x, int y) {
  g_scene->_drag(x, y);
}
GLvoid Scene::s_click(int button, int state, int x, int y) {
  g_scene->_click(button, state, x, y);
}
GLvoid Scene::s_key(unsigned char k, int x, int y) {
  g_scene->_key(k, x, y);
}

Scene::Scene(int argc, char** argv) {
  CAM_ROT = 0.0;
  CAM_ELEVATION = 45.0;
  CAM_DIST = 70.0;
  CAM_DIST_REAL = 10000;
  CAM_LOOK = CAM_LOOK_REAL = carve::geom::vector<3>::ZERO();

  WIDTH = 1024;
  HEIGHT = 768;

  disp_grid = true;
  disp_axes = true;

  glutInit(&argc, argv);
  glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH);

  glutInitWindowSize(WIDTH, HEIGHT);
  g_mainWindow = glutCreateWindow("Main");

  initialised = false;

  g_scene = this;
}

Scene::~Scene() {
  g_scene = nullptr;
}

void Scene::run() {
  init();
  glutMainLoop();
}
