

// Toonz app
#include "tapp.h"

// Toonz stage structures
#include "toonz/tscenehandle.h"
#include "toonz/toonzscene.h"
#include "toonz/txsheethandle.h"
#include "toonz/txsheet.h"
#include "toonz/tcamera.h"
#include "toonz/tframehandle.h"

// Scene viewer
#include "sceneviewer.h"

// Qt event-handling includes
#include <QMouseEvent>

#include "subcameramanager.h"

//********************************************************************************
//    Local namespace stuff
//********************************************************************************

namespace {
inline bool bitwiseContains(UCHAR flag, UCHAR state) {
  return flag == (flag | state);
}

inline bool bitwiseExclude(UCHAR flag, UCHAR state) {
  return bitwiseContains(~state, flag);
}

inline bool areNear(double v0, double v1, double thres = 20.0) {
  return std::abs(v0 - v1) < thres;
}

}  // namespace

//********************************************************************************
//    Classes implementation
//********************************************************************************

//================================
//    PreviewSubCameraManager
//--------------------------------

PreviewSubCameraManager::PreviewSubCameraManager()
    : m_mousePressed(false), m_dragType(NODRAG), m_clickAndDrag(false) {}

//----------------------------------------------------------------------

PreviewSubCameraManager::~PreviewSubCameraManager() {}

//----------------------------------------------------------------------

PreviewSubCameraManager *PreviewSubCameraManager::instance() {
  static PreviewSubCameraManager theInstance;
  return &theInstance;
}

//-----------------------------------------------------------------------------

TRect PreviewSubCameraManager::getEditingCameraInterestRect() const {
  if (m_mousePressed)
    return m_editingInterestRect;
  else {
    // Return the actual current camera's interest rect
    TCamera *currCamera =
        TApp::instance()->getCurrentScene()->getScene()->getCurrentCamera();
    return currCamera->getInterestRect();
  }
}

//-----------------------------------------------------------------------------

TRectD PreviewSubCameraManager::getEditingCameraInterestStageRect() const {
  TCamera *currCamera =
      TApp::instance()->getCurrentScene()->getScene()->getCurrentCamera();

  if (m_mousePressed)
    // Build the stage rect associated with m_editingInterestRect
    return currCamera->getCameraToStageRef() *
           TRectD(m_editingInterestRect.x0, m_editingInterestRect.y0,
                  m_editingInterestRect.x1 + 1, m_editingInterestRect.y1 + 1);
  else
    // Return the actual current camera's stage interest rect
    return currCamera->getInterestStageRect();
}

//-----------------------------------------------------------------------------

TPointD PreviewSubCameraManager::winToCamera(SceneViewer *viewer,
                                             const QPointF &pos) const {
  TPointD worldPos(viewer->winToWorld(pos));

  TApp *app = TApp::instance();
  TAffine stageToWorldRef(app->getCurrentXsheet()->getXsheet()->getCameraAff(
      app->getCurrentFrame()->getFrame()));

  TCamera *currCamera = app->getCurrentScene()->getScene()->getCurrentCamera();
  return currCamera->getStageToCameraRef() * stageToWorldRef.inv() * worldPos;
}

//-----------------------------------------------------------------------------

TPointD PreviewSubCameraManager::cameraToWin(SceneViewer *viewer,
                                             const TPointD &cameraPos) const {
  TApp *app = TApp::instance();
  TAffine stageToWorldRef(app->getCurrentXsheet()->getXsheet()->getCameraAff(
      app->getCurrentFrame()->getFrame()));
  TCamera *currCamera = app->getCurrentScene()->getScene()->getCurrentCamera();

  TPointD worldPos(stageToWorldRef * currCamera->getCameraToStageRef() *
                   cameraPos);
  return viewer->worldToPos(worldPos);
}

//----------------------------------------------------------------------

bool PreviewSubCameraManager::mousePressEvent(SceneViewer *viewer,
                                              const TMouseEvent &event) {
  if (viewer->is3DView()) return true;

  m_mousePressed  = true;
  m_mousePressPos = event.mousePos() * viewer->getDevPixRatio();
  m_dragType      = getSubCameraDragEnum(viewer, m_mousePressPos);

  if (bitwiseExclude(m_dragType, OUTER))
    m_cameraMousePressPos = winToCamera(viewer, m_mousePressPos);

  return false;
}

//----------------------------------------------------------------------

bool PreviewSubCameraManager::mouseMoveEvent(SceneViewer *viewer,
                                             const TMouseEvent &event) {
  if (viewer->is3DView()) return true;
  QPointF curPos(event.mousePos() * viewer->getDevPixRatio());
  if (event.buttons() == Qt::LeftButton) {
    if (!bitwiseContains(m_dragType, INNER)) {
      if (std::abs(curPos.x() - m_mousePressPos.x()) > 10 ||
          std::abs(curPos.y() - m_mousePressPos.y()) > 10)
        m_clickAndDrag = true;
    }

    if (m_clickAndDrag == true) {
      // Write the temporary preview subcamera to current camera
      TPointD worldMousePressPos(viewer->winToWorld(m_mousePressPos));
      TPointD worldCurPos(viewer->winToWorld(curPos));

      TApp *app = TApp::instance();

      TCamera *camera = app->getCurrentScene()->getScene()->getCurrentCamera();
      TRectD cameraStageRect(camera->getCameraToStageRef() *
                             convert(TRect(camera->getRes())));

      // Snap to the current camera frame
      // horizontal
      if (worldCurPos.x < worldMousePressPos.x &&
          areNear(worldCurPos.x, cameraStageRect.x0))
        worldCurPos.x = cameraStageRect.x0;
      else if (worldCurPos.x > worldMousePressPos.x &&
               areNear(worldCurPos.x, cameraStageRect.x1))
        worldCurPos.x = cameraStageRect.x1;
      // vertical
      if (worldCurPos.y < worldMousePressPos.y &&
          areNear(worldCurPos.y, cameraStageRect.y0))
        worldCurPos.y = cameraStageRect.y0;
      else if (worldCurPos.y > worldMousePressPos.y &&
               areNear(worldCurPos.y, cameraStageRect.y1))
        worldCurPos.y = cameraStageRect.y1;

      TAffine cameraAffInv(
          app->getCurrentXsheet()
              ->getXsheet()
              ->getCameraAff(app->getCurrentFrame()->getFrame())
              .inv());

      worldMousePressPos = cameraAffInv * worldMousePressPos;
      worldCurPos        = cameraAffInv * worldCurPos;

      TRectD worldPreviewSubCameraRect(
          std::min(worldMousePressPos.x, worldCurPos.x),
          std::min(worldMousePressPos.y, worldCurPos.y),
          std::max(worldMousePressPos.x, worldCurPos.x),
          std::max(worldMousePressPos.y, worldCurPos.y));

      // camera->setInterestStageRect(worldPreviewSubCameraRect);

      TRectD previewSubCameraD(camera->getStageToCameraRef() *
                               worldPreviewSubCameraRect);
      m_editingInterestRect =
          TRect(previewSubCameraD.x0, previewSubCameraD.y0,
                previewSubCameraD.x1 - 1, previewSubCameraD.y1 - 1);
      // m_editingInterestRect =
      //   TRect(previewSubCameraD.x0, previewSubCameraD.y0,
      //     previewSubCameraD.x1 - 1, previewSubCameraD.y1 - 1) *
      //   TRect(camera->getRes());

      viewer->update();
    } else {
      TPoint dragDistance = getSubCameraDragDistance(viewer, curPos);

      // Adjust the camera subrect
      TCamera *camera =
          TApp::instance()->getCurrentScene()->getScene()->getCurrentCamera();
      TRect subRect(camera->getInterestRect());

      if (bitwiseExclude(m_dragType, OUTER))
        subRect += dragDistance;
      else {
        if (bitwiseContains(m_dragType, DRAG_LEFT))
          subRect.x0 = subRect.x0 + dragDistance.x;
        else if (bitwiseContains(m_dragType, DRAG_RIGHT))
          subRect.x1 = subRect.x1 + dragDistance.x;
        if (bitwiseContains(m_dragType, DRAG_BOTTOM))
          subRect.y0 = subRect.y0 + dragDistance.y;
        else if (bitwiseContains(m_dragType, DRAG_TOP))
          subRect.y1 = subRect.y1 + dragDistance.y;
      }

      m_editingInterestRect = subRect;
      // m_editingInterestRect = subRect * TRect(camera->getRes());

      viewer->update();
    }
  } else {
    UCHAR dragEnum = getSubCameraDragEnum(viewer, curPos);

    if (dragEnum == NODRAG)
      viewer->setCursor(Qt::ArrowCursor);
    else if (bitwiseExclude(dragEnum, OUTER))
      viewer->setCursor(Qt::SizeAllCursor);
    else
      switch (dragEnum) {
      case DRAG_LEFT:
      case DRAG_RIGHT:
        viewer->setCursor(Qt::SizeHorCursor);
        break;
      case DRAG_TOP:
      case DRAG_BOTTOM:
        viewer->setCursor(Qt::SizeVerCursor);
        break;
      case DRAG_LEFT | DRAG_TOP:
      case DRAG_RIGHT | DRAG_BOTTOM:
        viewer->setCursor(Qt::SizeFDiagCursor);
        break;
      case DRAG_LEFT | DRAG_BOTTOM:
      case DRAG_RIGHT | DRAG_TOP:
        viewer->setCursor(Qt::SizeBDiagCursor);
        break;
      default:
        viewer->setCursor(Qt::ArrowCursor);
        break;
      }
  }

  // In case, perform the pan
  return event.buttons() == Qt::MiddleButton;
}

//----------------------------------------------------------------------

bool PreviewSubCameraManager::mouseReleaseEvent(SceneViewer *viewer) {
  if (viewer->is3DView()) return true;

  m_mousePressed = false;
  m_dragType     = NODRAG;
  m_clickAndDrag = false;

  TCamera *camera =
      TApp::instance()->getCurrentScene()->getScene()->getCurrentCamera();
  camera->setInterestRect(m_editingInterestRect);

  // Request a previewer update. Observe that whereas this previewer may not be
  // in preview mode,
  // another visible one may.
  Previewer::instance(true)->updateView();

  // Refresh viewer
  viewer->update();

  return false;
}

//-----------------------------------------------------------------------------

//! Builds the drag enum and camera distance for subcamera refinement drags.
UCHAR PreviewSubCameraManager::getSubCameraDragEnum(SceneViewer *viewer,
                                                    const QPointF &mousePos) {
  TCamera *camera =
      TApp::instance()->getCurrentScene()->getScene()->getCurrentCamera();
  TRect subCamera = camera->getInterestRect();

  if (subCamera.getLx() <= 0 || subCamera.getLy() <= 0) return NODRAG;

  TPointD cameraPosL(winToCamera(viewer, mousePos - QPointF(10, 0)));
  TPointD cameraPosR(winToCamera(viewer, mousePos + QPointF(10, 0)));
  TPointD cameraPosT(winToCamera(viewer, mousePos - QPointF(0, 10)));
  TPointD cameraPosB(winToCamera(viewer, mousePos + QPointF(0, 10)));

  TRectD cameraPosBox(
      std::min({cameraPosL.x, cameraPosR.x, cameraPosT.x, cameraPosB.x}),
      std::min({cameraPosL.y, cameraPosR.y, cameraPosT.y, cameraPosB.y}),
      std::max({cameraPosL.x, cameraPosR.x, cameraPosT.x, cameraPosB.x}),
      std::max({cameraPosL.y, cameraPosR.y, cameraPosT.y, cameraPosB.y}));

  TRectD subCameraD(subCamera.x0, subCamera.y0, subCamera.x1 + 1,
                    subCamera.y1 + 1);

  // Now, find out the drag enums, in case the mouse pos is near a sensible part
  // of the rect
  UCHAR dragType = NODRAG;

  if (cameraPosBox.y0 < subCameraD.y1) dragType |= INNER_TOP;
  if (cameraPosBox.y1 > subCameraD.y0) dragType |= INNER_BOTTOM;
  if (cameraPosBox.x0 < subCameraD.x1) dragType |= INNER_RIGHT;
  if (cameraPosBox.x1 > subCameraD.x0) dragType |= INNER_LEFT;

  if (cameraPosBox.y1 > subCameraD.y1) dragType |= OUTER_TOP;
  if (cameraPosBox.y0 < subCameraD.y0) dragType |= OUTER_BOTTOM;
  if (cameraPosBox.x1 > subCameraD.x1) dragType |= OUTER_RIGHT;
  if (cameraPosBox.x0 < subCameraD.x0) dragType |= OUTER_LEFT;

  return dragType;
}

//-----------------------------------------------------------------------------

TPoint PreviewSubCameraManager::getSubCameraDragDistance(SceneViewer *viewer,
                                                         QPointF &mousePos) {
  // Build the camera drag distance
  if (m_clickAndDrag) return TPoint();

  // Snap to the current camera frame
  TCamera *camera =
      TApp::instance()->getCurrentScene()->getScene()->getCurrentCamera();
  if (!bitwiseExclude(m_dragType, OUTER)) {
    TPointD btmLft(cameraToWin(viewer, TPointD(0, 0)));
    TPointD tpRght(
        cameraToWin(viewer, TPointD(camera->getRes().lx, camera->getRes().ly)));
    if (bitwiseContains(m_dragType, DRAG_LEFT) &&
        areNear(mousePos.x(), btmLft.x))
      mousePos.setX(btmLft.x);
    else if (bitwiseContains(m_dragType, DRAG_RIGHT) &&
             areNear(mousePos.x(), tpRght.x))
      mousePos.setX(tpRght.x);
    if (bitwiseContains(m_dragType, DRAG_BOTTOM) &&
        areNear(mousePos.y(), (double)viewer->height() - btmLft.y))
      mousePos.setY((double)viewer->height() - btmLft.y);
    else if (bitwiseContains(m_dragType, DRAG_TOP) &&
             areNear(mousePos.y(), (double)viewer->height() - tpRght.y))
      mousePos.setY((double)viewer->height() - tpRght.y);
  }

  TPointD cameraMousePos(winToCamera(viewer, mousePos));

  if (bitwiseExclude(m_dragType, OUTER)) {
    TPointD resultD(cameraMousePos - m_cameraMousePressPos);
    return TPoint(resultD.x, resultD.y);
  }

  TRect subCamera = camera->getInterestRect();
  TRectD subCameraD(subCamera.x0, subCamera.y0, subCamera.x1 + 1,
                    subCamera.y1 + 1);

  TPoint result;
  if (bitwiseContains(m_dragType, DRAG_LEFT))
    result.x = cameraMousePos.x - subCameraD.x0;
  else if (bitwiseContains(m_dragType, DRAG_RIGHT))
    result.x = cameraMousePos.x - subCameraD.x1;
  if (bitwiseContains(m_dragType, DRAG_BOTTOM))
    result.y = cameraMousePos.y - subCameraD.y0;
  else if (bitwiseContains(m_dragType, DRAG_TOP))
    result.y = cameraMousePos.y - subCameraD.y1;

  return result;
}

//-----------------------------------------------------------------------------
/*! Delete sub camera frame. Executed from context menu of the viewer.
 */
void PreviewSubCameraManager::deleteSubCamera(SceneViewer *viewer) {
  TCamera *camera =
      TApp::instance()->getCurrentScene()->getScene()->getCurrentCamera();
  camera->setInterestRect(TRect());

  Previewer::instance(true)->updateView();

  // Refresh viewer
  viewer->update();
}
