// Copyright (c) 2012-2017 VideoStitch SAS
// Copyright (c) 2018 stitchEm

#include "videowidget.hpp"
#include "yprsignalcaps.hpp"
#include "geometry.hpp"

VideoWidget::VideoWidget(QWidget* parent)
    : QOpenGLWidget(parent),
      yprsignal(nullptr),
      yaw(0.0),
      pitch(0.0),
      roll(0.0),
      button(Qt::MouseButton::NoButton),
      proj(VideoStitch::equirectangular),
      HFOV(360.0),
      zoom(MIN_ZOOM),
      editOrientation(false),
      enableZoom(false),
      width_(0),
      height_(0),
      gridSizeX(1),
      gridSizeY(1) {}

VideoWidget::~VideoWidget() {}

void VideoWidget::initializeGL() {
  initializeOpenGLFunctions();
  const QColor clearColor(Qt::black);
  glClearColor(clearColor.redF(), clearColor.greenF(), clearColor.blueF(), clearColor.alphaF());
  glDisable(GL_DEPTH_TEST);
  glDisable(GL_LIGHTING);
  glShadeModel(GL_FLAT);
  glMatrixMode(GL_MODELVIEW);

  glGenTextures(1, &Texture::get().id);
}

void VideoWidget::paintGL() {
  std::lock_guard<std::mutex> textureLock(*(Texture::get().lock));
  Texture::Type textureType = Texture::get().getType();
  switch (textureType) {
    case Texture::Type::PANORAMIC:
      paintPano();
      break;
    case Texture::Type::CUBEMAP:
    case Texture::Type::EQUIANGULAR_CUBEMAP:
      paintCubemap();
      break;
  }

  paintGrid();
}

void VideoWidget::paintGrid() {
  if (editOrientation) {
    glLineWidth(2);
    glColor3f(gridColor.redF(), gridColor.greenF(), gridColor.blueF());
    glBegin(GL_LINES);

    // Horizontals
    float step_y = height() / gridSizeY;
    for (int i = 1; i != gridSizeY; ++i) {
      glVertex2f(-width() / 2, -height() / 2 + step_y * i);
      glVertex2f(width() / 2, -height() / 2 + step_y * i);
    }

    // Verticals
    float step_x = width() / gridSizeX;
    for (int i = 1; i != gridSizeX; ++i) {
      glVertex2f(-width() / 2 + step_x * i, -height() / 2);
      glVertex2f(-width() / 2 + step_x * i, height() / 2);
    }

    // Middle
    glColor3f(gridColorHighlight.redF(), gridColorHighlight.greenF(), gridColorHighlight.blueF());
    glVertex2f(-width() / 2, -height() / 2 + step_y * gridSizeY / 2);
    glVertex2f(width() / 2, -height() / 2 + step_y * gridSizeY / 2);
    glVertex2f(-width() / 2 + step_x * gridSizeX / 2, -height() / 2);
    glVertex2f(-width() / 2 + step_x * gridSizeX / 2, height() / 2);

    glEnd();
    glColor3f(1, 1, 1);
    glLineWidth(1);
  }
}

void VideoWidget::paintPano() {
  Texture::get().latePanoramaDef();

  glEnable(GL_TEXTURE_2D);

  glClear(GL_COLOR_BUFFER_BIT);
  glBindTexture(GL_TEXTURE_2D, Texture::get().id);

  width_ = Texture::get().getWidth();
  height_ = Texture::get().getHeight();

  if (Texture::get().getWidth() != 0) {
    if (Texture::get().pixelBuffer != 0) {
      glBindBuffer(GL_PIXEL_UNPACK_BUFFER, Texture::get().pixelBuffer);
      glBindTexture(GL_TEXTURE_2D, Texture::get().id);
      glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, Texture::get().getWidth(), Texture::get().getHeight(), GL_RGBA,
                      GL_UNSIGNED_BYTE, nullptr);
      glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
    }

    glLoadIdentity();
    glTranslatef(width() / 2., height() / 2., 0.);
    // normalize pan
    QPointF maxPan((width() * (zoom - 100.) / 100.) / 2., (height() * (zoom - 100.) / 100.) / 2.);
    float x = 0., y = 0.;
    if (pan.x() >= 0) {
      x = std::min(pan.x(), maxPan.x());
    } else {
      x = std::max(pan.x(), -maxPan.x());
    }
    if (pan.y() >= 0) {
      y = std::min(pan.y(), maxPan.y());
    } else {
      y = std::max(pan.y(), -maxPan.y());
    }
    pan = QPointF(x, y);
    glTranslatef(pan.x(), pan.y(), 0.);
    // zoom in
    glScalef(zoom / 100., zoom / 100., 1.);

    glBegin(GL_QUADS);
    if (getWidgetAspectRatio() > getTextureAspectRatio()) {
      const float margin = (width() - Texture::get().getWidth() * height() / Texture::get().getHeight()) / 2;
      glTexCoord2f(0, 0);
      glVertex2f(margin - width() / 2., -height() / 2.);
      glTexCoord2f(0, 1);
      glVertex2f(margin - width() / 2., height() / 2.);
      glTexCoord2f(1, 1);
      glVertex2f(width() / 2. - margin, height() / 2);
      glTexCoord2f(1, 0);
      glVertex2f(width() / 2. - margin, -height() / 2.);
    } else {
      const float margin = (height() - Texture::get().getHeight() * width() / Texture::get().getWidth()) / 2;
      glTexCoord2f(0, 0);
      glVertex2f(-width() / 2, margin - height() / 2.);
      glTexCoord2d(0, 1);
      glVertex2f(-width() / 2, height() / 2. - margin);
      glTexCoord2d(1, 1);
      glVertex2f(width() / 2., height() / 2. - margin);
      glTexCoord2d(1, 0);
      glVertex2f(width() / 2., margin - height() / 2.);
    }
    glEnd();

    glBindTexture(GL_TEXTURE_2D, 0);
  }

  glDisable(GL_TEXTURE_2D);
}

void VideoWidget::paintCubemap() {
  Texture::get().lateCubemapDef();

  glEnable(GL_TEXTURE_CUBE_MAP);

  glClear(GL_COLOR_BUFFER_BIT);
  glBindTexture(GL_TEXTURE_CUBE_MAP, Texture::get().id);

  width_ = Texture::get().getWidth();
  height_ = Texture::get().getHeight();

  if (Texture::get().getWidth() != 0) {
    for (int i = 0; i < 6; ++i) {
      if (Texture::get().pbo[i] != 0) {
        glBindBuffer(GL_PIXEL_UNPACK_BUFFER, Texture::get().pbo[i]);
        glTexSubImage2D(cube[i], 0, 0, 0, Texture::get().getLength(), Texture::get().getLength(), GL_RGBA,
                        GL_UNSIGNED_BYTE, nullptr);
      }
    }
    glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);

    // paintDice();
    paintCompact();

    glBindTexture(GL_TEXTURE_CUBE_MAP, 0);
  }

  glDisable(GL_TEXTURE_CUBE_MAP);
}

void VideoWidget::paintDice() {
  glBegin(GL_QUADS);
  float hMargin = 0, vMargin = 0;
  float width;
  if (getWidgetAspectRatio() > 4. / 3.) {
    hMargin = (this->width() - 4 * Texture::get().getLength() * height() / (3 * Texture::get().getLength())) / 2;
    width = height() / 3;
  } else {
    vMargin = (height() - 3 * Texture::get().getLength() * this->width() / (4 * Texture::get().getLength())) / 2;
    width = this->width() / 4.;
  }

  // +x
  glTexCoord3f(1, -1., -1.);
  glVertex2f(hMargin + 2 * width, vMargin + width);
  glTexCoord3f(1., 1., -1.);
  glVertex2f(hMargin + 2 * width, vMargin + 2 * width);
  glTexCoord3f(1., 1., 1.);
  glVertex2f(hMargin + 3 * width, vMargin + 2 * width);
  glTexCoord3f(1., -1., 1.);
  glVertex2f(hMargin + 3 * width, vMargin + width);
  // -x
  glTexCoord3f(-1., -1., 1.);
  glVertex2f(hMargin, vMargin + width);
  glTexCoord3f(-1., 1., 1.);
  glVertex2f(hMargin, vMargin + 2 * width);
  glTexCoord3f(-1., 1., -1.);
  glVertex2f(hMargin + width, vMargin + 2 * width);
  glTexCoord3f(-1., -1., -1.);
  glVertex2f(hMargin + width, vMargin + width);
  // +y
  glTexCoord3f(-1., 1., -1.);
  glVertex2f(hMargin + width, vMargin + 2 * width);
  glTexCoord3f(-1., 1., 1.);
  glVertex2f(hMargin + width, vMargin + 3 * width);
  glTexCoord3f(1., 1., 1.);
  glVertex2f(hMargin + 2 * width, vMargin + 3 * width);
  glTexCoord3f(1., 1., -1.);
  glVertex2f(hMargin + 2 * width, vMargin + 2 * width);
  // -y
  glTexCoord3f(-1., -1., 1.);
  glVertex2f(hMargin + width, vMargin);
  glTexCoord3f(-1., -1., -1.);
  glVertex2f(hMargin + width, vMargin + width);
  glTexCoord3f(1., -1., -1.);
  glVertex2f(hMargin + 2 * width, vMargin + width);
  glTexCoord3f(1., -1., 1.);
  glVertex2f(hMargin + 2 * width, vMargin);
  // +z
  glTexCoord3f(-1., -1., -1.);
  glVertex2f(hMargin + width, vMargin + width);
  glTexCoord3f(-1., 1., -1.);
  glVertex2f(hMargin + width, vMargin + 2 * width);
  glTexCoord3f(1., 1., -1.);
  glVertex2f(hMargin + 2 * width, vMargin + 2 * width);
  glTexCoord3f(1., -1., -1.);
  glVertex2f(hMargin + 2 * width, vMargin + width);
  // -z
  glTexCoord3f(1., -1., 1.);
  glVertex2f(hMargin + 3 * width, vMargin + width);
  glTexCoord3f(1., 1., 1.);
  glVertex2f(hMargin + 3 * width, vMargin + 2 * width);
  glTexCoord3f(-1., 1., 1.);
  glVertex2f(hMargin + 4 * width, vMargin + 2 * width);
  glTexCoord3f(-1., -1., 1.);
  glVertex2f(hMargin + 4 * width, vMargin + width);

  glEnd();
}

void VideoWidget::paintCompact() {
  glBegin(GL_QUADS);
  float hMargin = 0, vMargin = 0;
  float width;
  if (getWidgetAspectRatio() > 3. / 2.) {
    hMargin = (this->width() - 3 * Texture::get().getLength() * height() / (2 * Texture::get().getLength())) / 2;
    width = height() / 2;
  } else {
    vMargin = (height() - 2 * Texture::get().getLength() * this->width() / (3 * Texture::get().getLength())) / 2;
    width = this->width() / 3.;
  }

  // +x
  glTexCoord3f(-1., -1., 1.);
  glVertex2f(hMargin, vMargin);
  glTexCoord3f(-1., 1., 1.);
  glVertex2f(hMargin, vMargin + width);
  glTexCoord3f(-1., 1., -1.);
  glVertex2f(hMargin + width, vMargin + width);
  glTexCoord3f(-1, -1., -1.);
  glVertex2f(hMargin + width, vMargin);
  // -x
  glTexCoord3f(1., -1., -1.);
  glVertex2f(hMargin + 2 * width, vMargin);
  glTexCoord3f(1., 1., -1.);
  glVertex2f(hMargin + 2 * width, vMargin + width);
  glTexCoord3f(1., 1., 1.);
  glVertex2f(hMargin + 3 * width, vMargin + width);
  glTexCoord3f(1., -1., 1.);
  glVertex2f(hMargin + 3 * width, vMargin);
  // -y
  glTexCoord3f(1., 1., -1.);
  glVertex2f(hMargin, vMargin + width);
  glTexCoord3f(-1., 1., -1.);
  glVertex2f(hMargin, vMargin + 2 * width);
  glTexCoord3f(-1., 1., 1.);
  glVertex2f(hMargin + width, vMargin + 2 * width);
  glTexCoord3f(1., 1., 1.);
  glVertex2f(hMargin + width, vMargin + width);
  // +y
  glTexCoord3f(1., -1., 1.);
  glVertex2f(hMargin + 2 * width, vMargin + width);
  glTexCoord3f(-1., -1., 1.);
  glVertex2f(hMargin + 2 * width, vMargin + 2 * width);
  glTexCoord3f(-1., -1., -1.);
  glVertex2f(hMargin + 3 * width, vMargin + 2 * width);
  glTexCoord3f(1., -1., -1.);
  glVertex2f(hMargin + 3 * width, vMargin + width);
  // +z
  glTexCoord3f(1., 1., 1.);
  glVertex2f(hMargin + width, vMargin + width);
  glTexCoord3f(1., -1., 1.);
  glVertex2f(hMargin + 2 * width, vMargin + width);
  glTexCoord3f(-1., -1., 1.);
  glVertex2f(hMargin + 2 * width, vMargin + 2 * width);
  glTexCoord3f(-1., 1., 1.);
  glVertex2f(hMargin + width, vMargin + 2 * width);
  // -z
  glTexCoord3f(-1., -1., -1.);
  glVertex2f(hMargin + width, vMargin);
  glTexCoord3f(-1., 1., -1.);
  glVertex2f(hMargin + width, vMargin + width);
  glTexCoord3f(1., 1., -1.);
  glVertex2f(hMargin + 2 * width, vMargin + width);
  glTexCoord3f(1., -1., -1.);
  glVertex2f(hMargin + 2 * width, vMargin);

  glEnd();
}

void VideoWidget::resizeGL(int w, int h) {
  glViewport(0, 0, w, h);
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  glOrtho(0, w, h, 0, 0, 1);
  glMatrixMode(GL_MODELVIEW);
}

void VideoWidget::mousePressEvent(QMouseEvent* e) {
  if (editOrientation) {
    prevPos = e->pos();
    std::lock_guard<std::mutex> textureLock(*(Texture::get().lock));
    point2pano(prevPos);
    button = e->button();
  } else {
    prevPos = e->pos();
  }
  if (!((e->buttons() & Qt::RightButton) && (e->buttons() & Qt::LeftButton))) {
    yprsignal = YPRSignalCaps::create();
  }
}

void VideoWidget::mouseMoveEvent(QMouseEvent* event) {
  if (editOrientation) {
    // accessing texture width and height
    std::lock_guard<std::mutex> textureLock(*(Texture::get().lock));
    curPos = event->pos();
    point2pano(curPos);
    QVector3D v1, v2;
    switch (proj) {
      case VideoStitch::equirectangular:
        equirectangular2sphere(curPos, v2, sphereRadius());
        equirectangular2sphere(prevPos, v1, sphereRadius());
        break;
      case VideoStitch::fullframe_fisheye:
      case VideoStitch::circular_fisheye:
        spherical2sphere(curPos, v2, sphereRadius());
        spherical2sphere(prevPos, v1, sphereRadius());
        break;
      case VideoStitch::rectilinear:
        rectilinear2sphere(curPos, v2, sphereRadius());
        rectilinear2sphere(prevPos, v1, sphereRadius());
        break;
      case VideoStitch::stereographic:
        stereographic2sphere(curPos, v2, sphereRadius());
        stereographic2sphere(prevPos, v1, sphereRadius());
        break;
      case VideoStitch::equiangular_cubemap:
      case VideoStitch::cubemap:
      case VideoStitch::interactive:
      case VideoStitch::unknownProjection:
        break;
    }
    qreal y, p, r;
    sphere2orientation(v2, v1, y, p, r);
    roll = radToDeg(r);
    if (button == Qt::RightButton) {
      yaw = 0.0;
      pitch = 0.0;
    } else {
      yaw = radToDeg(y);
      pitch = radToDeg(p);
    }
    prevPos = curPos;
  } else {
    pan += event->pos() - prevPos;
    prevPos = event->pos();
    update();
  }
  if (editOrientation && yprsignal) {
    emit rotatePanorama(yprsignal->add(yaw, pitch, roll));
  }
}

void VideoWidget::mouseReleaseEvent(QMouseEvent*) {
  if (yprsignal) {
    yprsignal->terminate();
  }
  yprsignal = nullptr;
  if (editOrientation) {
    emit applyOrientation();
  }
}

void VideoWidget::wheelEvent(QWheelEvent* event) {
  if (enableZoom) {
    if (event->delta() > 0 && zoom < MAX_ZOOM) {
      zoom += WHEEL_STEP;
    } else if (event->delta() < 0 && zoom > MIN_ZOOM) {
      zoom -= WHEEL_STEP;
    }
    update();
  } else {
    event->ignore();
  }
}

void VideoWidget::keyPressEvent(QKeyEvent* event) { event->ignore(); }

void VideoWidget::setEditOrientationActivated(bool active) {
  editOrientation = active;
  update();
}

void VideoWidget::setZoomActivated(bool active) { enableZoom = active; }

void VideoWidget::setProjection(VideoStitch::Projection p, double hfov) {
  proj = p;
  HFOV = hfov;
}

void VideoWidget::point2pano(QPointF& p) {
  // coords inside the quad
  QPoint topLeftCorner(0, 0);
  int w = width();
  int h = height();
  if (getWidgetAspectRatio() > getTextureAspectRatio()) {
    int margin = (width() - Texture::get().getWidth() * h / Texture::get().getHeight()) / 2;
    margin *= 100.0f / zoom;
    topLeftCorner.rx() = margin;
    w -= 2 * margin;
  } else {
    int margin = (height() - Texture::get().getHeight() * w / Texture::get().getWidth()) / 2;
    margin *= 100.0f / zoom;
    topLeftCorner.ry() = margin;
    h -= 2 * margin;
  }
  p -= topLeftCorner;
  // coords inside the panorama
  p.rx() *= Texture::get().getWidth() / (float)w;
  p.ry() *= Texture::get().getHeight() / (float)h;
  p.rx() -= Texture::get().getWidth() / 2;
  p.ry() -= Texture::get().getHeight() / 2;
}

float VideoWidget::getTextureAspectRatio() const {
  return (Texture::get().getWidth() / (float)Texture::get().getHeight());
}

float VideoWidget::getWidgetAspectRatio() const { return width() / (float)height(); }

float VideoWidget::sphereRadius() {
  switch (proj) {
    case VideoStitch::equirectangular:
    case VideoStitch::fullframe_fisheye:
    case VideoStitch::circular_fisheye:
      return Texture::get().getWidth() / degToRad((qreal)HFOV);
    case VideoStitch::rectilinear:
      return Texture::get().getWidth() / (2.0f * qTan(degToRad((qreal)HFOV) / 2.0f));
    case VideoStitch::stereographic:
      return Texture::get().getWidth() / (4.0f * qTan(degToRad((qreal)HFOV) / 4.0f));
    default:
      Q_ASSERT(false);
      break;
  }

  return -1;
}