

#include "keyframemover.h"

// Tnz6 includes
#include "tapp.h"
#include "xsheetviewer.h"

// TnzQt includes
#include "historytypes.h"

// TnzLib includes
#include "toonz/txsheethandle.h"
#include "toonz/tscenehandle.h"
#include "toonz/txsheet.h"
#include "toonz/preferences.h"

// TnzCore includes
#include "tundo.h"

// Qt includes
#include <QPainter>

//=============================================================================
// KeyframeMover
//-----------------------------------------------------------------------------

KeyframeMover::KeyframeMover()
    : m_qualifiers(eMoveKeyframes), m_lastKeyframeData(0) {}

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

KeyframeMover::~KeyframeMover() {
  delete m_lastKeyframeData;
  m_lastKeyframeData = 0;
}

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

TXsheet *KeyframeMover::getXsheet() const {
  return TApp::instance()->getCurrentXsheet()->getXsheet();
}

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

void KeyframeMover::setKeyframes() {
  TXsheet *xsh = getXsheet();
  std::set<KeyframePosition>::iterator posIt;
  for (auto const &key : m_lastKeyframes) {
    int c = key.second;
    TStageObjectId objId =
        c >= 0 ? TStageObjectId::ColumnId(c)
               : TStageObjectId::CameraId(xsh->getCameraColumnIndex());
    TStageObject *stObj = xsh->getStageObject(objId);
    TStageObject::KeyframeMap keyframes;
    stObj->getKeyframes(keyframes);
    for (auto const &frame : keyframes) {
      stObj->removeKeyframeWithoutUndo(frame.first);
    }
  }
  m_lastKeyframeData->getKeyframes(m_lastKeyframes, xsh);
}

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

void KeyframeMover::getKeyframes() {
  TXsheet *xsh = getXsheet();
  for (auto const &pos : m_startSelectedKeyframes) {
    int c = pos.second;
    TStageObjectId objId =
        c >= 0 ? TStageObjectId::ColumnId(c)
               : TStageObjectId::CameraId(xsh->getCameraColumnIndex());
    TStageObject *stObj = xsh->getStageObject(objId);
    assert(stObj->isKeyframe(pos.first));
    TStageObject::KeyframeMap keyframes;
    stObj->getKeyframes(keyframes);
    for (auto const &frame : keyframes) {
      m_lastKeyframes.insert(KeyframePosition(frame.first, c));
    }
  }

  if (m_lastKeyframeData) {
    delete m_lastKeyframeData;
    m_lastKeyframeData = 0;
  }
  m_lastKeyframeData = new TKeyframeData();
  m_lastKeyframeData->setKeyframes(m_lastKeyframes, xsh);
}

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

void KeyframeMover::start(TKeyframeSelection *selection, int qualifiers) {
  selection->unselectLockedColumn();
  m_qualifiers                                     = qualifiers;
  std::set<TKeyframeSelection::Position> positions = selection->getSelection();
  m_startSelectedKeyframes                         = positions;
  getKeyframes();
}

//-----------------------------------------------------------------------------
// If can't move keyframe return false; otherwise move keyframe
bool KeyframeMover::moveKeyframes(
    int dr, std::set<TKeyframeSelection::Position> &newPositions,
    TKeyframeSelection *selection) {
  // return if there is no movement
  if (dr == 0) return false;
  TXsheet *xsh = getXsheet();
  std::set<TKeyframeSelection::Position> positions;
  if (selection)
    positions = selection->getSelection();
  else
    positions = m_startSelectedKeyframes;
  if (positions.empty()) return false;
  std::set<TKeyframeSelection::Position>::iterator posIt;

  if (m_qualifiers & eMoveKeyframes) {
    for (posIt = positions.begin(); posIt != positions.end(); ++posIt) {
      int c = posIt->second;
      int r = posIt->first;
      TStageObjectId objId =
          c >= 0 ? TStageObjectId::ColumnId(c)
                 : TStageObjectId::CameraId(xsh->getCameraColumnIndex());
      TStageObject *stObj = xsh->getStageObject(objId);
      if (r + dr < 0) {
        dr = -r;
        // dragging backward stops when the selection reaches the first frame
        if (dr == 0) return false;
      }
      if (stObj->isKeyframe(r + dr)) {
        if (m_qualifiers & eCopyKeyframes) return false;
        // occupying key may be dragged one which is to be moved out
        else if (positions.count(TKeyframeSelection::Position(r + dr, c)) == 0)
          return false;
      }
    }

    bool firstTime = false;

    // move keys from the end of the selection on dragging forward
    if (dr > 0) {
      for (std::set<TKeyframeSelection::Position>::reverse_iterator revIt =
               positions.rbegin();
           revIt != positions.rend(); ++revIt) {
        int c = revIt->second;
        int r = revIt->first;
        TStageObjectId objId =
            c >= 0 ? TStageObjectId::ColumnId(c)
                   : TStageObjectId::CameraId(xsh->getCameraColumnIndex());
        TStageObject *stObj = xsh->getStageObject(objId);
        if (m_qualifiers & eCopyKeyframes) {
          firstTime = true;
          stObj->setKeyframeWithoutUndo(r + dr, stObj->getKeyframe(r));
        } else
          stObj->moveKeyframe(r + dr, r);
        newPositions.insert(TKeyframeSelection::Position(r + dr, c));
      }
    } else {  // ... and vice versa
      for (posIt = positions.begin(); posIt != positions.end(); ++posIt) {
        int c = posIt->second;
        int r = posIt->first;
        TStageObjectId objId =
            c >= 0 ? TStageObjectId::ColumnId(c)
                   : TStageObjectId::CameraId(xsh->getCameraColumnIndex());
        TStageObject *stObj = xsh->getStageObject(objId);
        if (m_qualifiers & eCopyKeyframes) {
          firstTime = true;
          stObj->setKeyframeWithoutUndo(r + dr, stObj->getKeyframe(r));
        } else
          stObj->moveKeyframe(r + dr, r);
        newPositions.insert(TKeyframeSelection::Position(r + dr, c));
      }
    }
    if (firstTime) {
      m_qualifiers = 0;
      m_qualifiers = eMoveKeyframes;
    }
    return true;
  }

  setKeyframes();
  bool notChange = false;
  for (posIt = m_startSelectedKeyframes.begin();
       posIt != m_startSelectedKeyframes.end(); ++posIt) {
    int c = posIt->second;
    int r = posIt->first;
    TStageObjectId objId =
        c >= 0 ? TStageObjectId::ColumnId(c)
               : TStageObjectId::CameraId(xsh->getCameraColumnIndex());
    TStageObject *stObj    = xsh->getStageObject(objId);
    if (r + dr < 0) dr     = -r;
    if (dr == 0) notChange = true;
    newPositions.insert(TKeyframeSelection::Position(r, c));
  }
  if (notChange) return true;

  newPositions.clear();
  for (posIt = m_startSelectedKeyframes.begin();
       posIt != m_startSelectedKeyframes.end(); ++posIt) {
    int c = posIt->second;
    int r = posIt->first;
    TStageObjectId objId =
        c >= 0 ? TStageObjectId::ColumnId(c)
               : TStageObjectId::CameraId(xsh->getCameraColumnIndex());
    TStageObject *stObj = xsh->getStageObject(objId);

    if (m_qualifiers & eOverwriteKeyframes) {
      stObj->removeKeyframeWithoutUndo(r + dr);
      if (m_qualifiers & eCopyKeyframes)
        stObj->setKeyframeWithoutUndo(r + dr, stObj->getKeyframe(r));
      else
        stObj->moveKeyframe(r + dr, r);
      newPositions.insert(TKeyframeSelection::Position(r + dr, c));
    } else if (m_qualifiers & eInsertKeyframes) {
      int s                           = r;
      TStageObject::Keyframe keyframe = stObj->getKeyframe(r);
      if (!(m_qualifiers & eCopyKeyframes)) stObj->removeKeyframeWithoutUndo(r);
      std::set<int> keyframeToShift;
      while (stObj->isKeyframe(s + dr) && s + dr != r) {
        keyframeToShift.insert(s + dr);
        s++;
      }
      stObj->moveKeyframes(keyframeToShift, 1);
      stObj->setKeyframeWithoutUndo(r + dr, keyframe);
      newPositions.insert(TKeyframeSelection::Position(r + dr, c));
    }
  }

  return true;
}

//=============================================================================
namespace {
//-----------------------------------------------------------------------------

//=============================================================================
// UndoMoveKeyFrame
//-----------------------------------------------------------------------------

class UndoMoveKeyFrame final : public TUndo {
  int m_dr;
  KeyframeMover *m_mover;

public:
  UndoMoveKeyFrame(int dr, KeyframeMover *mover) : m_dr(dr), m_mover(mover) {}

  void undo() const override {
    m_mover->undoMoveKeyframe();
    TApp::instance()->getCurrentXsheet()->notifyXsheetChanged();
  }

  void redo() const override {
    std::set<TKeyframeSelection::Position> newPositions;
    m_mover->moveKeyframes(m_dr, newPositions);
    TApp::instance()->getCurrentXsheet()->notifyXsheetChanged();
  }

  int getSize() const override { return sizeof(*this); }

  QString getHistoryString() override { return QObject::tr("Move Keyframe"); }

  int getHistoryType() override { return HistoryType::Xsheet; }
};

//-----------------------------------------------------------------------------
}  // namespace
//-----------------------------------------------------------------------------

//=============================================================================
// KeyframeMoverTool
//-----------------------------------------------------------------------------

KeyframeMoverTool::KeyframeMoverTool(XsheetViewer *viewer, bool justMovement)
    : XsheetGUI::DragTool(viewer)
    , m_startSelection()
    , m_offset(0)
    , m_firstRow(0)
    , m_selecting(false)
    , m_startPos()
    , m_curPos()
    , m_firstKeyframeMovement(false)
    , m_justMovement(justMovement) {
  m_mover    = new KeyframeMover();
  m_firstCol = Preferences::instance()->isXsheetCameraColumnVisible() ? -1 : 0;
}

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

TKeyframeSelection *KeyframeMoverTool::getSelection() {
  return getViewer()->getKeyframeSelection();
}

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

void KeyframeMoverTool::ctrlSelect(int row, int col) {
  TKeyframeSelection *selection = getSelection();
  bool isSelected               = selection->isSelected(row, col);
  if (isSelected)
    selection->unselect(row, col);
  else
    selection->select(row, col);
}

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

void KeyframeMoverTool::shiftSelect(int row, int col) {
  TXsheet *xsh = getViewer()->getXsheet();
  int r0 = 0, c0 = 0, r1 = -1, c1 = -1;
  int c = 0;
  if (Preferences::instance()->isXsheetCameraColumnVisible()) {
    c0--;
    c1--;
    c--;
  }
  for (; c < xsh->getColumnCount(); c++) {
    TStageObject *obj = xsh->getStageObject(getViewer()->getObjectId(c));
    int ra, rb;
    obj->getKeyframeRange(ra, rb);
    for (int r = ra; r <= rb; r++) {
      if (getSelection()->isSelected(r, c)) {
        if (r0 > r1)
          r0 = r1 = r;
        else if (r < r0)
          r0 = r;
        else if (r > r1)
          r1 = r;
        if (c0 > c1)
          c0 = c1 = c;
        else if (c < c0)
          c0 = c;
        else if (c > c1)
          c1 = c;
      }
    }
  }
  if (row >= r0 && col >= c0) {
    r1 = row;
    c1 = col;
  } else if (row <= r1 && col <= c1) {
    r0 = row;
    c0 = col;
  } else if (col <= c0) {
    c0 = col;
    r1 = row;
  } else {
    c1 = col;
    r0 = row;
  }

  for (int c = c0; c <= c1; c++) {
    TStageObject *obj = xsh->getStageObject(getViewer()->getObjectId(c));
    for (int r = r0; r <= r1; r++)
      if (obj->isKeyframe(r)) getSelection()->select(r, c);
  }
}

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

void KeyframeMoverTool::rectSelect(int row, int col) {
  TKeyframeSelection *selection = getSelection();
  selection->selectNone();
  TXsheet *xsh = getViewer()->getXsheet();
  int r0 = row, c0 = col, r1 = m_firstRow, c1 = m_firstCol;
  if (r0 > r1) std::swap(r0, r1);
  if (c0 > c1) std::swap(c0, c1);
  for (int c = c0; c <= c1; c++) {
    TStageObject *obj = xsh->getStageObject(getViewer()->getObjectId(c));
    for (int r = r0; r <= r1; r++) {
      if (obj->isKeyframe(r)) selection->select(r, c);
    }
  }
  m_startSelection = *getSelection();
  refreshCellsArea();
}

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

bool KeyframeMoverTool::canMove(const TPoint &pos) {
  const Orientation *o = getViewer()->orientation();
  TXsheet *xsh         = getViewer()->getXsheet();

  TPoint usePos = pos;
  if (!o->isVerticalTimeline()) {
    usePos.x = pos.y;
    usePos.y = pos.x;
  }

  if (usePos.x < 0) return false;

  int col      = usePos.x;
  int startCol = getViewer()->xyToPosition(m_startPos).layer();
  if (col != startCol) return false;

  return true;
}

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

void KeyframeMoverTool::onCellChange(int row, int col) {
  int lastRow  = getSelection()->getFirstRow() + m_offset;
  int firstRow = m_startSelection.getFirstRow() + m_offset;
  int dr       = row - lastRow;
  int dfr      = row - firstRow;
  int d = (m_mover->getQualifiers() & KeyframeMover::eMoveKeyframes) ? dr : dfr;
  std::set<TKeyframeSelection::Position> newPositions;
  bool ret = m_mover->moveKeyframes(d, newPositions, getSelection());
  if (!ret) return;
  getSelection()->m_positions.clear();
  getSelection()->m_positions.swap(newPositions);
}

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

void KeyframeMoverTool::onClick(const QMouseEvent *event) {
  m_firstKeyframeMovement   = true;
  m_selecting               = false;
  TXsheet *xsheet           = getViewer()->getXsheet();
  CellPosition cellPosition = getViewer()->xyToPosition(event->pos());
  int row                   = cellPosition.frame();
  int col                   = cellPosition.layer();
  m_firstRow                = row;
  m_firstCol                = col;
  bool isSelected           = getSelection()->isSelected(row, col);
  if (!m_justMovement) {
    if (event->modifiers() & Qt::ControlModifier)
      ctrlSelect(row, col);
    else if (event->modifiers() & Qt::ShiftModifier)
      shiftSelect(row, col);
    else if (!isSelected) {
      getSelection()->selectNone();
      getSelection()->select(row, col);
      m_selecting = true;
    }
    getSelection()->makeCurrent();
  }
  if (!getSelection()->isEmpty())
    m_offset       = row - getSelection()->getFirstRow();
  m_startSelection = *getSelection();
  getViewer()->update();
  m_startPos = TPointD(event->pos().x(), event->pos().y());
  m_curPos   = m_startPos;
}

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

void KeyframeMoverTool::onDrag(const QMouseEvent *e) {
  int x                     = e->pos().x();
  int y                     = e->pos().y();
  m_curPos                  = TPointD(x, y);
  CellPosition cellPosition = getViewer()->xyToPosition(e->pos());
  int row                   = cellPosition.frame();
  int col                   = cellPosition.layer();
  if (m_selecting)
    rectSelect(row, col);
  else {
    if (m_firstKeyframeMovement) {
      int qualifiers = 0;
      if (e->modifiers() & Qt::ControlModifier)
        qualifiers |= KeyframeMover::eCopyKeyframes;
      if (e->modifiers() & Qt::ShiftModifier)
        qualifiers |= KeyframeMover::eInsertKeyframes;
      else if (e->modifiers() & Qt::AltModifier)
        qualifiers |= KeyframeMover::eOverwriteKeyframes;
      else
        qualifiers |= KeyframeMover::eMoveKeyframes;
      m_mover->start(getSelection(), qualifiers);
    }
    m_firstKeyframeMovement = false;
    onCellChange(row, col);
    refreshCellsArea();
  }
}

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

void KeyframeMoverTool::onRelease(const CellPosition &pos) {
  if (m_selecting) {
    m_selecting = false;
    getViewer()->updateCells();
    return;
  }
  m_offset = 0;
  int dr   = getSelection()->getFirstRow() - m_startSelection.getFirstRow();
  if (dr) {
    TUndoManager::manager()->add(new UndoMoveKeyFrame(dr, m_mover));
    TApp::instance()->getCurrentScene()->setDirtyFlag(true);
    TApp::instance()->getCurrentXsheet()->notifyXsheetChanged();
  }
}

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

void KeyframeMoverTool::drawCellsArea(QPainter &p) {
  if (!m_selecting) return;
  QPen pen(Qt::DashLine);
  pen.setColor(Qt::black);
  p.setPen(pen);
  double endRectPosX = m_curPos.x - m_startPos.x;
  double endRectPosY = m_curPos.y - m_startPos.y;
  p.drawRect(m_startPos.x, m_startPos.y, endRectPosX, endRectPosY);
}
