// Copyright (c) 2023, ETH Zurich and UNC Chapel Hill.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
//     * Redistributions of source code must retain the above copyright
//       notice, this list of conditions and the following disclaimer.
//
//     * Redistributions in binary form must reproduce the above copyright
//       notice, this list of conditions and the following disclaimer in the
//       documentation and/or other materials provided with the distribution.
//
//     * Neither the name of ETH Zurich and UNC Chapel Hill nor the names of
//       its contributors may be used to endorse or promote products derived
//       from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.
//
// Author: Johannes L. Schoenberger (jsch-at-demuc-dot-de)

#include "ui/model_viewer_widget.h"

#include "ui/main_window.h"

#define SELECTION_BUFFER_IMAGE_IDX 0
#define SELECTION_BUFFER_POINT_IDX 1

const Eigen::Vector4f kSelectedPointColor(0.0f, 1.0f, 0.0f, 1.0f);

const Eigen::Vector4f kSelectedImagePlaneColor(1.0f, 0.0f, 1.0f, 0.6f);
const Eigen::Vector4f kSelectedImageFrameColor(0.8f, 0.0f, 0.8f, 1.0f);

const Eigen::Vector4f kMovieGrabberImagePlaneColor(0.0f, 1.0f, 1.0f, 0.6f);
const Eigen::Vector4f kMovieGrabberImageFrameColor(0.0f, 0.8f, 0.8f, 1.0f);

const Eigen::Vector4f kGridColor(0.2f, 0.2f, 0.2f, 0.6f);
const Eigen::Vector4f kXAxisColor(0.9f, 0.0f, 0.0f, 0.5f);
const Eigen::Vector4f kYAxisColor(0.0f, 0.9f, 0.0f, 0.5f);
const Eigen::Vector4f kZAxisColor(0.0f, 0.0f, 0.9f, 0.5f);

namespace colmap {
namespace {

// Generate unique index from RGB color in the range [0, 256^3].
inline size_t RGBToIndex(const uint8_t r, const uint8_t g, const uint8_t b) {
  return static_cast<size_t>(r) + static_cast<size_t>(g) * 256 +
         static_cast<size_t>(b) * 65536;
}

// Derive color from unique index, generated by `RGBToIndex`.
inline Eigen::Vector4f IndexToRGB(const size_t index) {
  Eigen::Vector4f color;
  color(0) = ((index & 0x000000FF) >> 0) / 255.0f;
  color(1) = ((index & 0x0000FF00) >> 8) / 255.0f;
  color(2) = ((index & 0x00FF0000) >> 16) / 255.0f;
  color(3) = 1.0f;
  return color;
}

void BuildImageModel(const Image& image, const Camera& camera,
                     const float image_size, const Eigen::Vector4f& plane_color,
                     const Eigen::Vector4f& frame_color,
                     std::vector<TrianglePainter::Data>* triangle_data,
                     std::vector<LinePainter::Data>* line_data) {
  // Generate camera dimensions in OpenGL (world) coordinate space.
  const float kBaseCameraWidth = 1024.0f;
  const float image_width = image_size * camera.Width() / kBaseCameraWidth;
  const float image_height = image_width * static_cast<float>(camera.Height()) /
                             static_cast<float>(camera.Width());
  const float image_extent = std::max(image_width, image_height);
  const float camera_extent = std::max(camera.Width(), camera.Height());
  const float camera_extent_world =
      static_cast<float>(camera.ImageToWorldThreshold(camera_extent));
  const float focal_length = 2.0f * image_extent / camera_extent_world;

  const Eigen::Matrix<float, 3, 4> inv_proj_matrix =
      image.InverseProjectionMatrix().cast<float>();

  // Projection center, top-left, top-right, bottom-right, bottom-left corners.

  const Eigen::Vector3f pc = inv_proj_matrix.rightCols<1>();
  const Eigen::Vector3f tl =
      inv_proj_matrix *
      Eigen::Vector4f(-image_width, image_height, focal_length, 1);
  const Eigen::Vector3f tr =
      inv_proj_matrix *
      Eigen::Vector4f(image_width, image_height, focal_length, 1);
  const Eigen::Vector3f br =
      inv_proj_matrix *
      Eigen::Vector4f(image_width, -image_height, focal_length, 1);
  const Eigen::Vector3f bl =
      inv_proj_matrix *
      Eigen::Vector4f(-image_width, -image_height, focal_length, 1);

  // Image plane as two triangles.
  if (triangle_data != nullptr) {
    triangle_data->emplace_back(
        PointPainter::Data(tl(0), tl(1), tl(2), plane_color(0), plane_color(1),
                           plane_color(2), plane_color(3)),
        PointPainter::Data(tr(0), tr(1), tr(2), plane_color(0), plane_color(1),
                           plane_color(2), plane_color(3)),
        PointPainter::Data(bl(0), bl(1), bl(2), plane_color(0), plane_color(1),
                           plane_color(2), plane_color(3)));

    triangle_data->emplace_back(
        PointPainter::Data(bl(0), bl(1), bl(2), plane_color(0), plane_color(1),
                           plane_color(2), plane_color(3)),
        PointPainter::Data(tr(0), tr(1), tr(2), plane_color(0), plane_color(1),
                           plane_color(2), plane_color(3)),
        PointPainter::Data(br(0), br(1), br(2), plane_color(0), plane_color(1),
                           plane_color(2), plane_color(3)));
  }

  if (line_data != nullptr) {
    // Frame around image plane and connecting lines to projection center.

    line_data->emplace_back(
        PointPainter::Data(pc(0), pc(1), pc(2), frame_color(0), frame_color(1),
                           frame_color(2), frame_color(3)),
        PointPainter::Data(tl(0), tl(1), tl(2), frame_color(0), frame_color(1),
                           frame_color(2), frame_color(3)));

    line_data->emplace_back(
        PointPainter::Data(pc(0), pc(1), pc(2), frame_color(0), frame_color(1),
                           frame_color(2), frame_color(3)),
        PointPainter::Data(tr(0), tr(1), tr(2), frame_color(0), frame_color(1),
                           frame_color(2), frame_color(3)));

    line_data->emplace_back(
        PointPainter::Data(pc(0), pc(1), pc(2), frame_color(0), frame_color(1),
                           frame_color(2), frame_color(3)),
        PointPainter::Data(br(0), br(1), br(2), frame_color(0), frame_color(1),
                           frame_color(2), frame_color(3)));

    line_data->emplace_back(
        PointPainter::Data(pc(0), pc(1), pc(2), frame_color(0), frame_color(1),
                           frame_color(2), frame_color(3)),
        PointPainter::Data(bl(0), bl(1), bl(2), frame_color(0), frame_color(1),
                           frame_color(2), frame_color(3)));

    line_data->emplace_back(
        PointPainter::Data(tl(0), tl(1), tl(2), frame_color(0), frame_color(1),
                           frame_color(2), frame_color(3)),
        PointPainter::Data(tr(0), tr(1), tr(2), frame_color(0), frame_color(1),
                           frame_color(2), frame_color(3)));

    line_data->emplace_back(
        PointPainter::Data(tr(0), tr(1), tr(2), frame_color(0), frame_color(1),
                           frame_color(2), frame_color(3)),
        PointPainter::Data(br(0), br(1), br(2), frame_color(0), frame_color(1),
                           frame_color(2), frame_color(3)));

    line_data->emplace_back(
        PointPainter::Data(br(0), br(1), br(2), frame_color(0), frame_color(1),
                           frame_color(2), frame_color(3)),
        PointPainter::Data(bl(0), bl(1), bl(2), frame_color(0), frame_color(1),
                           frame_color(2), frame_color(3)));

    line_data->emplace_back(
        PointPainter::Data(bl(0), bl(1), bl(2), frame_color(0), frame_color(1),
                           frame_color(2), frame_color(3)),
        PointPainter::Data(tl(0), tl(1), tl(2), frame_color(0), frame_color(1),
                           frame_color(2), frame_color(3)));
  }
}

}  // namespace

ModelViewerWidget::ModelViewerWidget(QWidget* parent, OptionManager* options)
    : QOpenGLWidget(parent),
      options_(options),
      point_viewer_widget_(new PointViewerWidget(parent, this, options)),
      image_viewer_widget_(
          new DatabaseImageViewerWidget(parent, this, options)),
      movie_grabber_widget_(new MovieGrabberWidget(parent, this)),
      mouse_is_pressed_(false),
      focus_distance_(kInitFocusDistance),
      selected_image_id_(kInvalidImageId),
      selected_point3D_id_(kInvalidPoint3DId),
      coordinate_grid_enabled_(true),
      near_plane_(kInitNearPlane) {
  background_color_[0] = 1.0f;
  background_color_[1] = 1.0f;
  background_color_[2] = 1.0f;

  QSurfaceFormat format;
  format.setDepthBufferSize(24);
  format.setMajorVersion(3);
  format.setMinorVersion(2);
  format.setSamples(4);
  format.setProfile(QSurfaceFormat::CoreProfile);
#ifdef DEBUG
  format.setOption(QSurfaceFormat::DebugContext);
#endif
  setFormat(format);
  QSurfaceFormat::setDefaultFormat(format);

  SetPointColormap(new PointColormapPhotometric());
  SetImageColormap(new ImageColormapUniform());

  image_size_ = static_cast<float>(devicePixelRatio() * image_size_);
  point_size_ = static_cast<float>(devicePixelRatio() * point_size_);
}

void ModelViewerWidget::initializeGL() {
  initializeOpenGLFunctions();
  glEnable(GL_DEPTH_TEST);
  glEnable(GL_BLEND);
  glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
  glEnable(GL_VERTEX_PROGRAM_POINT_SIZE);
  SetupPainters();
  SetupView();
}

void ModelViewerWidget::paintGL() {
  glClearColor(background_color_[0], background_color_[1], background_color_[2],
               1.0f);
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

  const QMatrix4x4 pmv_matrix = projection_matrix_ * model_view_matrix_;

  // Model view matrix for center of view
  QMatrix4x4 model_view_center_matrix = model_view_matrix_;
  const Eigen::Vector4f rot_center =
      QMatrixToEigen(model_view_matrix_).inverse() *
      Eigen::Vector4f(0, 0, -focus_distance_, 1);
  model_view_center_matrix.translate(rot_center(0), rot_center(1),
                                     rot_center(2));

  // Coordinate system
  if (coordinate_grid_enabled_) {
    const QMatrix4x4 pmvc_matrix =
        projection_matrix_ * model_view_center_matrix;
    coordinate_axes_painter_.Render(pmv_matrix, width(), height(), 2);
    coordinate_grid_painter_.Render(pmvc_matrix, width(), height(), 1);
  }

  // Points
  point_painter_.Render(pmv_matrix, point_size_);
  point_connection_painter_.Render(pmv_matrix, width(), height(), 1);

  // Images
  image_line_painter_.Render(pmv_matrix, width(), height(), 1);
  image_triangle_painter_.Render(pmv_matrix);
  image_connection_painter_.Render(pmv_matrix, width(), height(), 1);

  // Movie grabber cameras
  movie_grabber_path_painter_.Render(pmv_matrix, width(), height(), 1.5);
  movie_grabber_line_painter_.Render(pmv_matrix, width(), height(), 1);
  movie_grabber_triangle_painter_.Render(pmv_matrix);
}

void ModelViewerWidget::resizeGL(int width, int height) {
  glViewport(0, 0, width, height);
  ComposeProjectionMatrix();
  UploadCoordinateGridData();
}

void ModelViewerWidget::ReloadReconstruction() {
  if (reconstruction == nullptr) {
    return;
  }

  cameras = reconstruction->Cameras();
  points3D = reconstruction->Points3D();
  reg_image_ids = reconstruction->RegImageIds();

  images.clear();
  for (const image_t image_id : reg_image_ids) {
    images[image_id] = reconstruction->Image(image_id);
  }

  statusbar_status_label->setText(QString().asprintf(
      "%d Images - %d Points", static_cast<int>(reg_image_ids.size()),
      static_cast<int>(points3D.size())));

  Upload();
}

void ModelViewerWidget::ClearReconstruction() {
  cameras.clear();
  images.clear();
  points3D.clear();
  reg_image_ids.clear();
  reconstruction = nullptr;
  Upload();
}

int ModelViewerWidget::GetProjectionType() const {
  return options_->render->projection_type;
}

void ModelViewerWidget::SetPointColormap(PointColormapBase* colormap) {
  point_colormap_.reset(colormap);
}

void ModelViewerWidget::SetImageColormap(ImageColormapBase* colormap) {
  image_colormap_.reset(colormap);
}

void ModelViewerWidget::UpdateMovieGrabber() {
  UploadMovieGrabberData();
  update();
}

void ModelViewerWidget::EnableCoordinateGrid() {
  coordinate_grid_enabled_ = true;
  update();
}

void ModelViewerWidget::DisableCoordinateGrid() {
  coordinate_grid_enabled_ = false;
  update();
}

void ModelViewerWidget::ChangeFocusDistance(const float delta) {
  if (delta == 0.0f) {
    return;
  }
  const float prev_focus_distance = focus_distance_;
  float diff = delta * ZoomScale() * kFocusSpeed;
  focus_distance_ -= diff;
  if (focus_distance_ < kMinFocusDistance) {
    focus_distance_ = kMinFocusDistance;
    diff = prev_focus_distance - focus_distance_;
  } else if (focus_distance_ > kMaxFocusDistance) {
    focus_distance_ = kMaxFocusDistance;
    diff = prev_focus_distance - focus_distance_;
  }
  const Eigen::Matrix4f vm_mat = QMatrixToEigen(model_view_matrix_).inverse();
  const Eigen::Vector3f tvec(0, 0, diff);
  const Eigen::Vector3f tvec_rot = vm_mat.block<3, 3>(0, 0) * tvec;
  model_view_matrix_.translate(tvec_rot(0), tvec_rot(1), tvec_rot(2));
  ComposeProjectionMatrix();
  UploadCoordinateGridData();
  update();
}

void ModelViewerWidget::ChangeNearPlane(const float delta) {
  if (delta == 0.0f) {
    return;
  }
  near_plane_ *= (1.0f + delta / 100.0f * kNearPlaneScaleSpeed);
  near_plane_ = std::max(kMinNearPlane, std::min(kMaxNearPlane, near_plane_));
  ComposeProjectionMatrix();
  UploadCoordinateGridData();
  update();
}

void ModelViewerWidget::ChangePointSize(const float delta) {
  if (delta == 0.0f) {
    return;
  }
  point_size_ *= (1.0f + delta / 100.0f * kPointScaleSpeed);
  point_size_ = std::max(kMinPointSize, std::min(kMaxPointSize, point_size_));
  update();
}

void ModelViewerWidget::RotateView(const float x, const float y,
                                   const float prev_x, const float prev_y) {
  if (x - prev_x == 0 && y - prev_y == 0) {
    return;
  }

  // Rotation according to the Arcball method "ARCBALL: A User Interface for
  // Specifying Three-Dimensional Orientation Using a Mouse", Ken Shoemake,
  // University of Pennsylvania, 1992.

  // Determine Arcball vector on unit sphere.
  const Eigen::Vector3f u = PositionToArcballVector(x, y);
  const Eigen::Vector3f v = PositionToArcballVector(prev_x, prev_y);

  // Angle between vectors.
  const float angle = 2.0f * std::acos(std::min(1.0f, u.dot(v)));

  const float kMinAngle = 1e-3f;
  if (angle > kMinAngle) {
    const Eigen::Matrix4f vm_mat = QMatrixToEigen(model_view_matrix_).inverse();

    // Rotation axis.
    Eigen::Vector3f axis = vm_mat.block<3, 3>(0, 0) * v.cross(u);
    axis = axis.normalized();
    // Center of rotation is current focus.
    const Eigen::Vector4f rot_center =
        vm_mat * Eigen::Vector4f(0, 0, -focus_distance_, 1);
    // First shift to rotation center, then rotate and shift back.
    model_view_matrix_.translate(rot_center(0), rot_center(1), rot_center(2));
    model_view_matrix_.rotate(RadToDeg(angle), axis(0), axis(1), axis(2));
    model_view_matrix_.translate(-rot_center(0), -rot_center(1),
                                 -rot_center(2));
    update();
  }
}

void ModelViewerWidget::TranslateView(const float x, const float y,
                                      const float prev_x, const float prev_y) {
  if (x - prev_x == 0 && y - prev_y == 0) {
    return;
  }

  Eigen::Vector3f tvec(x - prev_x, prev_y - y, 0.0f);

  if (options_->render->projection_type ==
      RenderOptions::ProjectionType::PERSPECTIVE) {
    tvec *= ZoomScale();
  } else if (options_->render->projection_type ==
             RenderOptions::ProjectionType::ORTHOGRAPHIC) {
    tvec *= 2.0f * OrthographicWindowExtent() / height();
  }

  const Eigen::Matrix4f vm_mat = QMatrixToEigen(model_view_matrix_).inverse();

  const Eigen::Vector3f tvec_rot = vm_mat.block<3, 3>(0, 0) * tvec;
  model_view_matrix_.translate(tvec_rot(0), tvec_rot(1), tvec_rot(2));

  update();
}

void ModelViewerWidget::ChangeCameraSize(const float delta) {
  if (delta == 0.0f) {
    return;
  }
  image_size_ *= (1.0f + delta / 100.0f * kImageScaleSpeed);
  image_size_ = std::max(kMinImageSize, std::min(kMaxImageSize, image_size_));
  UploadImageData();
  UploadMovieGrabberData();
  update();
}

void ModelViewerWidget::ResetView() {
  SetupView();
  Upload();
}

QMatrix4x4 ModelViewerWidget::ModelViewMatrix() const {
  return model_view_matrix_;
}

void ModelViewerWidget::SetModelViewMatrix(const QMatrix4x4& matrix) {
  model_view_matrix_ = matrix;
  update();
}

void ModelViewerWidget::SelectObject(const int x, const int y) {
  makeCurrent();

  // Ensure that anti-aliasing does not change the colors of objects.
  glDisable(GL_MULTISAMPLE);

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

  // Upload data in selection mode (one color per object).
  UploadImageData(true);
  UploadPointData(true);

  // Render in selection mode, with larger points to improve selection accuracy.
  const QMatrix4x4 pmv_matrix = projection_matrix_ * model_view_matrix_;
  image_triangle_painter_.Render(pmv_matrix);
  point_painter_.Render(pmv_matrix, 2 * point_size_);

  const int scaled_x = devicePixelRatio() * x;
  const int scaled_y = devicePixelRatio() * (height() - y - 1);

  QOpenGLFramebufferObjectFormat fbo_format;
  fbo_format.setSamples(0);
  QOpenGLFramebufferObject fbo(1, 1, fbo_format);

  glBindFramebuffer(GL_READ_FRAMEBUFFER, defaultFramebufferObject());
  glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo.handle());
  glBlitFramebuffer(scaled_x, scaled_y, scaled_x + 1, scaled_y + 1, 0, 0, 1, 1,
                    GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT, GL_NEAREST);

  fbo.bind();
  std::array<uint8_t, 3> color;
  glReadPixels(0, 0, 1, 1, GL_RGB, GL_UNSIGNED_BYTE, color.data());
  fbo.release();

  const size_t index = RGBToIndex(color[0], color[1], color[2]);

  if (index < selection_buffer_.size()) {
    const char buffer_type = selection_buffer_[index].second;
    if (buffer_type == SELECTION_BUFFER_IMAGE_IDX) {
      selected_image_id_ = static_cast<image_t>(selection_buffer_[index].first);
      selected_point3D_id_ = kInvalidPoint3DId;
      ShowImageInfo(selected_image_id_);
    } else if (buffer_type == SELECTION_BUFFER_POINT_IDX) {
      selected_image_id_ = kInvalidImageId;
      selected_point3D_id_ = selection_buffer_[index].first;
      ShowPointInfo(selection_buffer_[index].first);
    } else {
      selected_image_id_ = kInvalidImageId;
      selected_point3D_id_ = kInvalidPoint3DId;
      image_viewer_widget_->hide();
    }
  } else {
    selected_image_id_ = kInvalidImageId;
    selected_point3D_id_ = kInvalidPoint3DId;
    image_viewer_widget_->hide();
  }

  // Re-enable, since temporarily disabled above.
  glEnable(GL_MULTISAMPLE);

  selection_buffer_.clear();

  UploadPointData();
  UploadImageData();
  UploadPointConnectionData();
  UploadImageConnectionData();

  update();
}

void ModelViewerWidget::SelectMoviewGrabberView(const size_t view_idx) {
  selected_movie_grabber_view_ = view_idx;
  UploadMovieGrabberData();
  update();
}

QImage ModelViewerWidget::GrabImage() {
  makeCurrent();

  DisableCoordinateGrid();

  paintGL();

  const int scaled_width = static_cast<int>(devicePixelRatio() * width());
  const int scaled_height = static_cast<int>(devicePixelRatio() * height());

  QOpenGLFramebufferObjectFormat fbo_format;
  fbo_format.setSamples(0);
  QOpenGLFramebufferObject fbo(scaled_width, scaled_height, fbo_format);

  glBindFramebuffer(GL_READ_FRAMEBUFFER, defaultFramebufferObject());
  glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo.handle());
  glBlitFramebuffer(0, 0, scaled_width, scaled_height, 0, 0, scaled_width,
                    scaled_height, GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT,
                    GL_NEAREST);

  fbo.bind();
  QImage image(scaled_width, scaled_height, QImage::Format_RGB888);
  glReadPixels(0, 0, scaled_width, scaled_height, GL_RGB, GL_UNSIGNED_BYTE,
               image.bits());
  fbo.release();

  EnableCoordinateGrid();

  return image.mirrored();
}

void ModelViewerWidget::GrabMovie() { movie_grabber_widget_->show(); }

void ModelViewerWidget::ShowPointInfo(const point3D_t point3D_id) {
  point_viewer_widget_->Show(point3D_id);
}

void ModelViewerWidget::ShowImageInfo(const image_t image_id) {
  image_viewer_widget_->ShowImageWithId(image_id);
}

float ModelViewerWidget::PointSize() const { return point_size_; }

float ModelViewerWidget::ImageSize() const { return image_size_; }

void ModelViewerWidget::SetPointSize(const float point_size) {
  point_size_ = point_size;
}

void ModelViewerWidget::SetImageSize(const float image_size) {
  image_size_ = image_size;
  UploadImageData();
}

void ModelViewerWidget::SetBackgroundColor(const float r, const float g,
                                           const float b) {
  background_color_[0] = r;
  background_color_[1] = g;
  background_color_[2] = b;
  update();
}

void ModelViewerWidget::mousePressEvent(QMouseEvent* event) {
  if (mouse_press_timer_.isActive()) {  // Select objects (2. click)
    mouse_is_pressed_ = false;
    mouse_press_timer_.stop();
    selection_buffer_.clear();
    SelectObject(event->pos().x(), event->pos().y());
  } else {  // Set timer to remember 1. click
    mouse_press_timer_.setSingleShot(true);
    mouse_press_timer_.start(kDoubleClickInterval);
    mouse_is_pressed_ = true;
    prev_mouse_pos_ = event->pos();
  }
  event->accept();
}

void ModelViewerWidget::mouseReleaseEvent(QMouseEvent* event) {
  mouse_is_pressed_ = false;
  event->accept();
}

void ModelViewerWidget::mouseMoveEvent(QMouseEvent* event) {
  if (mouse_is_pressed_) {
    if (event->buttons() & Qt::RightButton ||
        (event->buttons() & Qt::LeftButton &&
         event->modifiers() & Qt::ControlModifier)) {
      TranslateView(event->pos().x(), event->pos().y(), prev_mouse_pos_.x(),
                    prev_mouse_pos_.y());
    } else if (event->buttons() & Qt::LeftButton) {
      RotateView(event->pos().x(), event->pos().y(), prev_mouse_pos_.x(),
                 prev_mouse_pos_.y());
    }
  }
  prev_mouse_pos_ = event->pos();
  event->accept();
}

void ModelViewerWidget::wheelEvent(QWheelEvent* event) {
  // We don't mind whether horizontal or vertical scroll.
  const float delta = event->angleDelta().x() + event->angleDelta().y();
  if (event->modifiers().testFlag(Qt::ControlModifier)) {
    ChangePointSize(delta);
  } else if (event->modifiers().testFlag(Qt::AltModifier)) {
    ChangeCameraSize(delta);
  } else if (event->modifiers().testFlag(Qt::ShiftModifier)) {
    ChangeNearPlane(delta);
  } else {
    ChangeFocusDistance(delta);
  }
  event->accept();
}

void ModelViewerWidget::SetupPainters() {
  makeCurrent();

  coordinate_axes_painter_.Setup();
  coordinate_grid_painter_.Setup();

  point_painter_.Setup();
  point_connection_painter_.Setup();

  image_line_painter_.Setup();
  image_triangle_painter_.Setup();
  image_connection_painter_.Setup();

  movie_grabber_path_painter_.Setup();
  movie_grabber_line_painter_.Setup();
  movie_grabber_triangle_painter_.Setup();
}

void ModelViewerWidget::SetupView() {
  point_size_ = kInitPointSize;
  image_size_ = kInitImageSize;
  focus_distance_ = kInitFocusDistance;
  model_view_matrix_.setToIdentity();
  model_view_matrix_.translate(0, 0, -focus_distance_);
  model_view_matrix_.rotate(225, 1, 0, 0);
  model_view_matrix_.rotate(-45, 0, 1, 0);
}

void ModelViewerWidget::Upload() {
  point_colormap_->Prepare(cameras, images, points3D, reg_image_ids);
  image_colormap_->Prepare(cameras, images, points3D, reg_image_ids);

  ComposeProjectionMatrix();

  UploadPointData();
  UploadImageData();
  UploadMovieGrabberData();
  UploadPointConnectionData();
  UploadImageConnectionData();

  update();
}

void ModelViewerWidget::UploadCoordinateGridData() {
  makeCurrent();

  const float scale = ZoomScale();

  // View center grid.
  std::vector<LinePainter::Data> grid_data(3);

  grid_data[0].point1 =
      PointPainter::Data(-20 * scale, 0, 0, kGridColor(0), kGridColor(1),
                         kGridColor(2), kGridColor(3));
  grid_data[0].point2 =
      PointPainter::Data(20 * scale, 0, 0, kGridColor(0), kGridColor(1),
                         kGridColor(2), kGridColor(3));

  grid_data[1].point1 =
      PointPainter::Data(0, -20 * scale, 0, kGridColor(0), kGridColor(1),
                         kGridColor(2), kGridColor(3));
  grid_data[1].point2 =
      PointPainter::Data(0, 20 * scale, 0, kGridColor(0), kGridColor(1),
                         kGridColor(2), kGridColor(3));

  grid_data[2].point1 =
      PointPainter::Data(0, 0, -20 * scale, kGridColor(0), kGridColor(1),
                         kGridColor(2), kGridColor(3));
  grid_data[2].point2 =
      PointPainter::Data(0, 0, 20 * scale, kGridColor(0), kGridColor(1),
                         kGridColor(2), kGridColor(3));

  coordinate_grid_painter_.Upload(grid_data);

  // Coordinate axes.
  std::vector<LinePainter::Data> axes_data(3);

  axes_data[0].point1 = PointPainter::Data(
      0, 0, 0, kXAxisColor(0), kXAxisColor(1), kXAxisColor(2), kXAxisColor(3));
  axes_data[0].point2 =
      PointPainter::Data(50 * scale, 0, 0, kXAxisColor(0), kXAxisColor(1),
                         kXAxisColor(2), kXAxisColor(3));

  axes_data[1].point1 = PointPainter::Data(
      0, 0, 0, kYAxisColor(0), kYAxisColor(1), kYAxisColor(2), kYAxisColor(3));
  axes_data[1].point2 =
      PointPainter::Data(0, 50 * scale, 0, kYAxisColor(0), kYAxisColor(1),
                         kYAxisColor(2), kYAxisColor(3));

  axes_data[2].point1 = PointPainter::Data(
      0, 0, 0, kZAxisColor(0), kZAxisColor(1), kZAxisColor(2), kZAxisColor(3));
  axes_data[2].point2 =
      PointPainter::Data(0, 0, 50 * scale, kZAxisColor(0), kZAxisColor(1),
                         kZAxisColor(2), kZAxisColor(3));

  coordinate_axes_painter_.Upload(axes_data);
}

void ModelViewerWidget::UploadPointData(const bool selection_mode) {
  makeCurrent();

  std::vector<PointPainter::Data> data;

  // Assume we want to display the majority of points
  data.reserve(points3D.size());

  const size_t min_track_len =
      static_cast<size_t>(options_->render->min_track_len);

  if (selected_image_id_ == kInvalidImageId &&
      images.count(selected_image_id_) == 0) {
    for (const auto& point3D : points3D) {
      if (point3D.second.Error() <= options_->render->max_error &&
          point3D.second.Track().Length() >= min_track_len) {
        PointPainter::Data painter_point;

        painter_point.x = static_cast<float>(point3D.second.XYZ(0));
        painter_point.y = static_cast<float>(point3D.second.XYZ(1));
        painter_point.z = static_cast<float>(point3D.second.XYZ(2));

        Eigen::Vector4f color;
        if (selection_mode) {
          const size_t index = selection_buffer_.size();
          selection_buffer_.push_back(
              std::make_pair(point3D.first, SELECTION_BUFFER_POINT_IDX));
          color = IndexToRGB(index);

        } else if (point3D.first == selected_point3D_id_) {
          color = kSelectedPointColor;
        } else {
          color = point_colormap_->ComputeColor(point3D.first, point3D.second);
        }

        painter_point.r = color(0);
        painter_point.g = color(1);
        painter_point.b = color(2);
        painter_point.a = color(3);

        data.push_back(painter_point);
      }
    }
  } else {  // Image selected
    const auto& selected_image = images[selected_image_id_];
    for (const auto& point3D : points3D) {
      if (point3D.second.Error() <= options_->render->max_error &&
          point3D.second.Track().Length() >= min_track_len) {
        PointPainter::Data painter_point;

        painter_point.x = static_cast<float>(point3D.second.XYZ(0));
        painter_point.y = static_cast<float>(point3D.second.XYZ(1));
        painter_point.z = static_cast<float>(point3D.second.XYZ(2));

        Eigen::Vector4f color;
        if (selection_mode) {
          const size_t index = selection_buffer_.size();
          selection_buffer_.push_back(
              std::make_pair(point3D.first, SELECTION_BUFFER_POINT_IDX));
          color = IndexToRGB(index);
        } else if (selected_image.HasPoint3D(point3D.first)) {
          color = kSelectedImagePlaneColor;
        } else if (point3D.first == selected_point3D_id_) {
          color = kSelectedPointColor;
        } else {
          color = point_colormap_->ComputeColor(point3D.first, point3D.second);
        }

        painter_point.r = color(0);
        painter_point.g = color(1);
        painter_point.b = color(2);
        painter_point.a = color(3);

        data.push_back(painter_point);
      }
    }
  }

  point_painter_.Upload(data);
}

void ModelViewerWidget::UploadPointConnectionData() {
  makeCurrent();

  std::vector<LinePainter::Data> line_data;

  if (selected_point3D_id_ == kInvalidPoint3DId) {
    // No point selected, so upload empty data
    point_connection_painter_.Upload(line_data);
    return;
  }

  const auto& point3D = points3D[selected_point3D_id_];

  // 3D point position.
  LinePainter::Data line;
  line.point1 = PointPainter::Data(
      static_cast<float>(point3D.XYZ(0)), static_cast<float>(point3D.XYZ(1)),
      static_cast<float>(point3D.XYZ(2)), kSelectedPointColor(0),
      kSelectedPointColor(1), kSelectedPointColor(2), 0.8f);

  // All images in which 3D point is observed.
  for (const auto& track_el : point3D.Track().Elements()) {
    const Image& conn_image = images[track_el.image_id];
    const Eigen::Vector3f conn_proj_center =
        conn_image.ProjectionCenter().cast<float>();
    line.point2 = PointPainter::Data(
        conn_proj_center(0), conn_proj_center(1), conn_proj_center(2),
        kSelectedPointColor(0), kSelectedPointColor(1), kSelectedPointColor(2),
        0.8f);
    line_data.push_back(line);
  }

  point_connection_painter_.Upload(line_data);
}

void ModelViewerWidget::UploadImageData(const bool selection_mode) {
  makeCurrent();

  std::vector<LinePainter::Data> line_data;
  line_data.reserve(8 * reg_image_ids.size());

  std::vector<TrianglePainter::Data> triangle_data;
  triangle_data.reserve(2 * reg_image_ids.size());

  for (const image_t image_id : reg_image_ids) {
    const Image& image = images[image_id];
    const Camera& camera = cameras[image.CameraId()];

    Eigen::Vector4f plane_color;
    Eigen::Vector4f frame_color;
    if (selection_mode) {
      const size_t index = selection_buffer_.size();
      selection_buffer_.push_back(
          std::make_pair(image_id, SELECTION_BUFFER_IMAGE_IDX));
      plane_color = frame_color = IndexToRGB(index);
    } else {
      if (image_id == selected_image_id_) {
        plane_color = kSelectedImagePlaneColor;
        frame_color = kSelectedImageFrameColor;
      } else {
        image_colormap_->ComputeColor(image, &plane_color, &frame_color);
      }
    }

    // Lines are not colored with the indexed color in selection mode, so do not
    // show them, so they do not block the selection process
    BuildImageModel(image, camera, image_size_, plane_color, frame_color,
                    &triangle_data, selection_mode ? nullptr : &line_data);
  }

  image_line_painter_.Upload(line_data);
  image_triangle_painter_.Upload(triangle_data);
}

void ModelViewerWidget::UploadImageConnectionData() {
  makeCurrent();

  std::vector<LinePainter::Data> line_data;
  std::vector<image_t> image_ids;

  if (selected_image_id_ != kInvalidImageId) {
    // Show connections to selected images
    image_ids.push_back(selected_image_id_);
  } else if (options_->render->image_connections) {
    // Show all connections
    image_ids = reg_image_ids;
  } else {  // Disabled, so upload empty data
    image_connection_painter_.Upload(line_data);
    return;
  }

  for (const image_t image_id : image_ids) {
    const Image& image = images.at(image_id);

    const Eigen::Vector3f proj_center = image.ProjectionCenter().cast<float>();

    // Collect all connected images
    std::unordered_set<image_t> conn_image_ids;

    for (const Point2D& point2D : image.Points2D()) {
      if (point2D.HasPoint3D()) {
        const Point3D& point3D = points3D[point2D.Point3DId()];
        for (const auto& track_elem : point3D.Track().Elements()) {
          conn_image_ids.insert(track_elem.image_id);
        }
      }
    }

    // Selected image in the center.
    LinePainter::Data line;
    line.point1 = PointPainter::Data(
        proj_center(0), proj_center(1), proj_center(2),
        kSelectedImageFrameColor(0), kSelectedImageFrameColor(1),
        kSelectedImageFrameColor(2), 0.8f);

    // All connected images to the selected image.
    for (const image_t conn_image_id : conn_image_ids) {
      const Image& conn_image = images[conn_image_id];
      const Eigen::Vector3f conn_proj_center =
          conn_image.ProjectionCenter().cast<float>();
      line.point2 = PointPainter::Data(
          conn_proj_center(0), conn_proj_center(1), conn_proj_center(2),
          kSelectedImageFrameColor(0), kSelectedImageFrameColor(1),
          kSelectedImageFrameColor(2), 0.8f);
      line_data.push_back(line);
    }
  }

  image_connection_painter_.Upload(line_data);
}

void ModelViewerWidget::UploadMovieGrabberData() {
  makeCurrent();

  std::vector<LinePainter::Data> path_data;
  path_data.reserve(movie_grabber_widget_->views.size());

  std::vector<LinePainter::Data> line_data;
  line_data.reserve(4 * movie_grabber_widget_->views.size());

  std::vector<TrianglePainter::Data> triangle_data;
  triangle_data.reserve(2 * movie_grabber_widget_->views.size());

  if (movie_grabber_widget_->views.size() > 0) {
    const Image& image0 = movie_grabber_widget_->views[0];
    Eigen::Vector3f prev_proj_center = image0.ProjectionCenter().cast<float>();

    for (size_t i = 1; i < movie_grabber_widget_->views.size(); ++i) {
      const Image& image = movie_grabber_widget_->views[i];
      const Eigen::Vector3f curr_proj_center =
          image.ProjectionCenter().cast<float>();
      LinePainter::Data path;
      path.point1 = PointPainter::Data(
          prev_proj_center(0), prev_proj_center(1), prev_proj_center(2),
          kSelectedImagePlaneColor(0), kSelectedImagePlaneColor(1),
          kSelectedImagePlaneColor(2), kSelectedImagePlaneColor(3));
      path.point2 = PointPainter::Data(
          curr_proj_center(0), curr_proj_center(1), curr_proj_center(2),
          kSelectedImagePlaneColor(0), kSelectedImagePlaneColor(1),
          kSelectedImagePlaneColor(2), kSelectedImagePlaneColor(3));
      path_data.push_back(path);
      prev_proj_center = curr_proj_center;
    }

    // Setup dummy camera with same settings as current OpenGL viewpoint.
    const float kDefaultImageWdith = 2048.0f;
    const float kDefaultImageHeight = 1536.0f;
    const float focal_length =
        -2.0f * std::tan(DegToRad(kFieldOfView) / 2.0f) * kDefaultImageWdith;
    Camera camera;
    camera.InitializeWithId(SimplePinholeCameraModel::model_id, focal_length,
                            kDefaultImageWdith, kDefaultImageHeight);

    // Build all camera models
    for (size_t i = 0; i < movie_grabber_widget_->views.size(); ++i) {
      const Image& image = movie_grabber_widget_->views[i];
      Eigen::Vector4f plane_color;
      Eigen::Vector4f frame_color;
      if (i == selected_movie_grabber_view_) {
        plane_color = kSelectedImagePlaneColor;
        frame_color = kSelectedImageFrameColor;
      } else {
        plane_color = kMovieGrabberImagePlaneColor;
        frame_color = kMovieGrabberImageFrameColor;
      }

      BuildImageModel(image, camera, image_size_, plane_color, frame_color,
                      &triangle_data, &line_data);
    }
  }

  movie_grabber_path_painter_.Upload(path_data);
  movie_grabber_line_painter_.Upload(line_data);
  movie_grabber_triangle_painter_.Upload(triangle_data);
}

void ModelViewerWidget::ComposeProjectionMatrix() {
  projection_matrix_.setToIdentity();
  if (options_->render->projection_type ==
      RenderOptions::ProjectionType::PERSPECTIVE) {
    projection_matrix_.perspective(kFieldOfView, AspectRatio(), near_plane_,
                                   kFarPlane);
  } else if (options_->render->projection_type ==
             RenderOptions::ProjectionType::ORTHOGRAPHIC) {
    const float extent = OrthographicWindowExtent();
    projection_matrix_.ortho(-AspectRatio() * extent, AspectRatio() * extent,
                             -extent, extent, near_plane_, kFarPlane);
  }
}

float ModelViewerWidget::ZoomScale() const {
  // "Constant" scale factor w.r.t. zoom-level.
  return 2.0f * std::tan(static_cast<float>(DegToRad(kFieldOfView)) / 2.0f) *
         std::abs(focus_distance_) / height();
}

float ModelViewerWidget::AspectRatio() const {
  return static_cast<float>(width()) / static_cast<float>(height());
}

float ModelViewerWidget::OrthographicWindowExtent() const {
  return std::tan(DegToRad(kFieldOfView) / 2.0f) * focus_distance_;
}

Eigen::Vector3f ModelViewerWidget::PositionToArcballVector(
    const float x, const float y) const {
  Eigen::Vector3f vec(2.0f * x / width() - 1, 1 - 2.0f * y / height(), 0.0f);
  const float norm2 = vec.squaredNorm();
  if (norm2 <= 1.0f) {
    vec.z() = std::sqrt(1.0f - norm2);
  } else {
    vec = vec.normalized();
  }
  return vec;
}

}  // namespace colmap
