/***

  Olive - Non-Linear Video Editor
  Copyright (C) 2021 Olive Team

  This program is free software: you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation, either version 3 of the License, or
  (at your option) any later version.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program.  If not, see <http://www.gnu.org/licenses/>.

***/

#include "keyframeviewbase.h"

#include <QMouseEvent>
#include <QToolTip>
#include <QVBoxLayout>

#include "dialog/keyframeproperties/keyframeproperties.h"
#include "keyframeviewundo.h"
#include "node/node.h"
#include "widget/menu/menu.h"
#include "widget/menu/menushared.h"
#include "widget/nodeparamview/nodeparamviewundo.h"

namespace olive {

KeyframeViewBase::KeyframeViewBase(QWidget *parent) :
  TimeBasedView(parent),
  dragging_bezier_point_(nullptr),
  currently_autoselecting_(false),
  dragging_(false)
{
  SetDefaultDragMode(RubberBandDrag);
  setContextMenuPolicy(Qt::CustomContextMenu);

  connect(this, &KeyframeViewBase::customContextMenuRequested, this, &KeyframeViewBase::ShowContextMenu);
  connect(scene(), &QGraphicsScene::selectionChanged, this, &KeyframeViewBase::AutoSelectKeyTimeNeighbors);
}

void KeyframeViewBase::Clear()
{
  QMap<NodeKeyframe*, KeyframeViewItem*>::iterator iterator;

  for (iterator=item_map_.begin();iterator!=item_map_.end();iterator++) {
    delete iterator.value();
  }

  item_map_.clear();
}

void KeyframeViewBase::DeleteSelected()
{
  MultiUndoCommand* command = new MultiUndoCommand();

  QMap<NodeKeyframe*, KeyframeViewItem*>::const_iterator i;

  for (i=item_map_.constBegin(); i!=item_map_.constEnd(); i++) {
    if (i.value()->isSelected()) {
      command->add_child(new NodeParamRemoveKeyframeCommand(i.key()));
    }
  }

  Core::instance()->undo_stack()->pushIfHasChildren(command);
}

void KeyframeViewBase::AddKeyframesOfNode(Node *n)
{
  foreach (const QString& i, n->inputs()) {
    AddKeyframesOfInput(n, i);
  }
}

void KeyframeViewBase::AddKeyframesOfInput(Node* n, const QString& input)
{
  if (!n->IsInputKeyframable(input)) {
    return;
  }

  int arr_sz = n->InputArraySize(input);
  for (int i=-1; i<arr_sz; i++) {
    AddKeyframesOfElement(NodeInput(n, input, i));
  }
}

void KeyframeViewBase::AddKeyframesOfElement(const NodeInput& input)
{
  const QVector<NodeKeyframeTrack>& tracks = input.node()->GetKeyframeTracks(input);

  for (int i=0; i<tracks.size(); i++) {
    AddKeyframesOfTrack(NodeKeyframeTrackReference(input, i));
  }
}

void KeyframeViewBase::AddKeyframesOfTrack(const NodeKeyframeTrackReference& ref)
{
  const QVector<NodeKeyframeTrack>& tracks = ref.input().node()->GetKeyframeTracks(ref.input());
  const NodeKeyframeTrack& t = tracks.at(ref.track());

  foreach (NodeKeyframe* key, t) {
    AddKeyframe(key);
  }
}

void KeyframeViewBase::RemoveKeyframesOfNode(Node *n)
{
  foreach (const QString& i, n->inputs()) {
    RemoveKeyframesOfInput(n, i);
  }
}

void KeyframeViewBase::RemoveKeyframesOfInput(Node* n, const QString& input)
{
  if (!n->IsInputKeyframable(input)) {
    return;
  }

  int arr_sz = n->InputArraySize(input);
  for (int i=-1; i<arr_sz; i++) {
    RemoveKeyframesOfElement(NodeInput(n, input, i));
  }
}

void KeyframeViewBase::RemoveKeyframesOfElement(const NodeInput& input)
{
  const QVector<NodeKeyframeTrack>& tracks = input.node()->GetKeyframeTracks(input);

  for (int i=0; i<tracks.size(); i++) {
    RemoveKeyframesOfTrack(NodeKeyframeTrackReference(input, i));
  }
}

void KeyframeViewBase::RemoveKeyframesOfTrack(const NodeKeyframeTrackReference& ref)
{
  const QVector<NodeKeyframeTrack>& tracks = ref.input().node()->GetKeyframeTracks(ref.input());
  const NodeKeyframeTrack& t = tracks.at(ref.track());

  foreach (NodeKeyframe* key, t) {
    RemoveKeyframe(key);
  }
}

void KeyframeViewBase::SelectAll()
{
  for (auto it=item_map_.cbegin(); it!=item_map_.cend(); it++) {
    it.value()->setSelected(true);
  }
}

void KeyframeViewBase::DeselectAll()
{
  for (auto it=item_map_.cbegin(); it!=item_map_.cend(); it++) {
    it.value()->setSelected(false);
  }
}

void KeyframeViewBase::RemoveKeyframe(NodeKeyframe* key)
{
  KeyframeAboutToBeRemoved(key);

  delete item_map_.take(key);
}

KeyframeViewItem *KeyframeViewBase::AddKeyframe(NodeKeyframe* key)
{
  KeyframeViewItem* item = item_map_.value(key);

  if (!item) {
    item = new KeyframeViewItem(key);
    item->SetTimeTarget(GetTimeTarget());
    item->SetScale(GetScale());
    item_map_.insert(key, item);
    scene()->addItem(item);
  }

  return item;
}

void KeyframeViewBase::mousePressEvent(QMouseEvent *event)
{
  QGraphicsItem* item_under_cursor = itemAt(event->pos());

  if (HandPress(event) || (!item_under_cursor && PlayheadPress(event))) {
    return;
  }

  active_tool_ = Core::instance()->tool();

  if (event->button() == Qt::LeftButton) {
    QGraphicsView::mousePressEvent(event);

    if (active_tool_ == Tool::kPointer) {
      if (item_under_cursor) {

        dragging_ = true;
        drag_start_ = mapToScene(event->pos());

        // Determine what type of item is under the cursor
        dragging_bezier_point_ = dynamic_cast<BezierControlPointItem*>(item_under_cursor);

        if (dragging_bezier_point_) {

          dragging_bezier_point_start_ = dragging_bezier_point_->GetCorrespondingKeyframeHandle();
          dragging_bezier_point_opposing_start_ = dragging_bezier_point_->key()->bezier_control(NodeKeyframe::get_opposing_bezier_type(dragging_bezier_point_->mode()));

        } else {

          QList<QGraphicsItem*> selected_items = scene()->selectedItems();

          selected_keys_.resize(selected_items.size());

          initial_drag_item_ = static_cast<KeyframeViewItem*>(item_under_cursor);

          for (int i=0;i<selected_items.size();i++) {
            KeyframeViewItem* key = static_cast<KeyframeViewItem*>(selected_items.at(i));

            selected_keys_.replace(i, {key,
                                       key->x(),
                                       GetAdjustedTime(key->key()->parent(), GetTimeTarget(), key->key()->time(), false),
                                       key->key()->value().toDouble()});
          }
        }
      }
    }
  }
}

void KeyframeViewBase::mouseMoveEvent(QMouseEvent *event)
{
  if (HandMove(event) || PlayheadMove(event)) {
    return;
  }

  if (event->buttons() & Qt::LeftButton) {
    QGraphicsView::mouseMoveEvent(event);

    if (dragging_) {
      // Calculate cursor difference and scale it
      QPointF mouse_diff_scaled = GetScaledCursorPos(mapToScene(event->pos()) - drag_start_);

      if (event->modifiers() & Qt::ShiftModifier) {
        // If holding shift, only move one axis
        mouse_diff_scaled.setY(0);
      }

      if (dragging_bezier_point_) {

        // Flip the mouse Y because bezier control points are drawn bottom to top, not top to bottom
        mouse_diff_scaled.setY(-mouse_diff_scaled.y());

        QPointF new_bezier_pos = GenerateBezierControlPosition(dragging_bezier_point_->mode(),
                                                               dragging_bezier_point_start_,
                                                               mouse_diff_scaled);

        // If the user is NOT holding control, we set the other handle to the exact negative of this handle
        QPointF new_opposing_pos;
        NodeKeyframe::BezierType opposing_type = NodeKeyframe::get_opposing_bezier_type(dragging_bezier_point_->mode());


        if (!(event->modifiers() & Qt::ControlModifier)) {
          new_opposing_pos = GenerateBezierControlPosition(opposing_type,
                                                           dragging_bezier_point_opposing_start_,
                                                           -mouse_diff_scaled);
        } else {
          new_opposing_pos = dragging_bezier_point_opposing_start_;
        }

        dragging_bezier_point_->key()->set_bezier_control(dragging_bezier_point_->mode(),
                                                          new_bezier_pos);

        dragging_bezier_point_->key()->set_bezier_control(opposing_type,
                                                          new_opposing_pos);

        // Bezier control points are parented to keyframe items making their positions relative
        // to those items. We need to map them to the scene coordinates for this to work properly.
        QPointF bezier_pos = dragging_bezier_point_->pos() + dragging_bezier_point_->parentItem()->pos();
        emit Dragged(qRound(bezier_pos.x()), qRound(bezier_pos.y()));

      } else if (!selected_keys_.isEmpty()) {

        // Validate movement - ensure no keyframe goes above its max point or below its min point
        FloatSlider::DisplayType display_type = FloatSlider::kNormal;

        if (IsYAxisEnabled()) {
          foreach (const KeyframeItemAndTime& keypair, selected_keys_) {
            Node* node = keypair.key->key()->parent();
            const QString& input = keypair.key->key()->input();
            double new_val = keypair.value - mouse_diff_scaled.y();
            double limited = new_val;

            if (node->HasInputProperty(input, QStringLiteral("min"))) {
              limited = qMax(limited, node->GetInputProperty(input, QStringLiteral("min")).toDouble());
            }

            if (node->HasInputProperty(input, QStringLiteral("max"))) {
              limited = qMin(limited, node->GetInputProperty(input, QStringLiteral("max")).toDouble());
            }

            if (limited != new_val) {
              mouse_diff_scaled.setY(keypair.value - limited);
            }
          }

          Node* initial_drag_input = initial_drag_item_->key()->parent();
          const QString& initial_drag_input_id = initial_drag_item_->key()->input();
          if (initial_drag_input->HasInputProperty(initial_drag_input_id, QStringLiteral("view"))) {
            display_type = static_cast<FloatSlider::DisplayType>(initial_drag_input->GetInputProperty(initial_drag_input_id, QStringLiteral("view")).toInt());
          }
        }

        foreach (const KeyframeItemAndTime& keypair, selected_keys_) {
          rational node_time = GetAdjustedTime(GetTimeTarget(),
                                               keypair.key->key()->parent(),
                                               CalculateNewTimeFromScreen(keypair.time, mouse_diff_scaled.x()),
                                               true);

          keypair.key->key()->set_time(node_time);

          if (IsYAxisEnabled()) {
            keypair.key->key()->set_value(keypair.value - mouse_diff_scaled.y());
          }
        }

        // Show information about this keyframe
        QString tip = Timecode::time_to_timecode(initial_drag_item_->key()->time(), timebase(),
                                                 Core::instance()->GetTimecodeDisplay(), false);

        if (IsYAxisEnabled()) {
          bool ok;
          double num_value = initial_drag_item_->key()->value().toDouble(&ok);

          if (ok) {
            tip.append('\n');
            tip.append(FloatSlider::ValueToString(num_value, display_type, 2, true));
          }

          // Force viewport to update since Qt might try to optimize it out if the keyframe is
          // offscreen
          viewport()->update();
        }

        QToolTip::hideText();
        QToolTip::showText(QCursor::pos(), tip);

        emit Dragged(qRound(initial_drag_item_->x()), qRound(initial_drag_item_->y()));

      }
    }
  }
}

void KeyframeViewBase::mouseReleaseEvent(QMouseEvent *event)
{
  if (HandRelease(event) || PlayheadRelease(event)) {
    return;
  }

  if (event->button() == Qt::LeftButton) {
    QGraphicsView::mouseReleaseEvent(event);

    if (dragging_) {
      if (dragging_bezier_point_) {
        MultiUndoCommand* command = new MultiUndoCommand();

        // Create undo command with the current bezier point and the old one
        command->add_child(new KeyframeSetBezierControlPoint(dragging_bezier_point_->key(),
                                                             dragging_bezier_point_->mode(),
                                                             dragging_bezier_point_->key()->bezier_control(dragging_bezier_point_->mode()),
                                                             dragging_bezier_point_start_));

        if (!(event->modifiers() & Qt::ControlModifier)) {
          auto opposing_type = NodeKeyframe::get_opposing_bezier_type(dragging_bezier_point_->mode());

          command->add_child(new KeyframeSetBezierControlPoint(dragging_bezier_point_->key(),
                                                               opposing_type,
                                                               dragging_bezier_point_->key()->bezier_control(opposing_type),
                                                               dragging_bezier_point_opposing_start_));
        }

        dragging_bezier_point_ = nullptr;

        Core::instance()->undo_stack()->push(command);
      } else if (!selected_keys_.isEmpty()) {
        MultiUndoCommand* command = new MultiUndoCommand();

        foreach (const KeyframeItemAndTime& keypair, selected_keys_) {
          NodeKeyframe* item = keypair.key->key();

          // Commit movement
          command->add_child(new NodeParamSetKeyframeTimeCommand(item,
                                                                 item->time(),
                                                                 keypair.time));

          // Commit value if we're setting a value
          if (IsYAxisEnabled()) {
            command->add_child(new NodeParamSetKeyframeValueCommand(item,
                                                                    item->value(),
                                                                    keypair.value));
          }
        }

        Core::instance()->undo_stack()->push(command);
      }

      selected_keys_.clear();

      dragging_ = false;

      QToolTip::hideText();
    }
  }
}

void KeyframeViewBase::ScaleChangedEvent(const double &scale)
{
  TimeBasedView::ScaleChangedEvent(scale);

  for (auto iterator=item_map_.begin();iterator!=item_map_.end();iterator++) {
    iterator.value()->SetScale(scale);
  }
}

const QMap<NodeKeyframe *, KeyframeViewItem *> &KeyframeViewBase::item_map() const
{
  return item_map_;
}

void KeyframeViewBase::KeyframeAboutToBeRemoved(NodeKeyframe *)
{
}

void KeyframeViewBase::TimeTargetChangedEvent(Node *target)
{
  QMap<NodeKeyframe*, KeyframeViewItem*>::const_iterator i;

  for (i=item_map_.begin();i!=item_map_.end();i++) {
    i.value()->SetTimeTarget(target);
  }
}

void KeyframeViewBase::ContextMenuEvent(Menu& m)
{
  Q_UNUSED(m)
}

rational KeyframeViewBase::CalculateNewTimeFromScreen(const rational &old_time, double cursor_diff)
{
  return rational::fromDouble(old_time.toDouble() + cursor_diff);
}

QPointF KeyframeViewBase::GenerateBezierControlPosition(const NodeKeyframe::BezierType mode, const QPointF &start_point, const QPointF &scaled_cursor_diff)
{
  QPointF new_bezier_pos = start_point;

  new_bezier_pos += scaled_cursor_diff;

  // LIMIT bezier handles from overlapping each other
  if (mode == NodeKeyframe::kInHandle) {
    if (new_bezier_pos.x() > 0) {
      new_bezier_pos.setX(0);
    }
  } else {
    if (new_bezier_pos.x() < 0) {
      new_bezier_pos.setX(0);
    }
  }

  return new_bezier_pos;
}

QPointF KeyframeViewBase::GetScaledCursorPos(const QPointF &cursor_pos)
{
  return QPointF(cursor_pos.x() / GetScale(),
                 cursor_pos.y() / GetYScale());
}

void KeyframeViewBase::ShowContextMenu()
{
  Menu m;

  MenuShared::instance()->AddItemsForEditMenu(&m, false);

  QAction* linear_key_action = nullptr;
  QAction* bezier_key_action = nullptr;
  QAction* hold_key_action = nullptr;

  QList<QGraphicsItem*> items = scene()->selectedItems();
  if (!items.isEmpty()) {
    bool all_keys_are_same_type = true;
    NodeKeyframe::Type type = static_cast<KeyframeViewItem*>(items.first())->key()->type();

    for (int i=1;i<items.size();i++) {
      KeyframeViewItem* key_item = static_cast<KeyframeViewItem*>(items.at(i));
      KeyframeViewItem* prev_item = static_cast<KeyframeViewItem*>(items.at(i-1));

      if (key_item->key()->type() != prev_item->key()->type()) {
        all_keys_are_same_type = false;
        break;
      }
    }

    m.addSeparator();

    linear_key_action = m.addAction(tr("Linear"));
    bezier_key_action = m.addAction(tr("Bezier"));
    hold_key_action = m.addAction(tr("Hold"));

    if (all_keys_are_same_type) {
      switch (type) {
      case NodeKeyframe::kLinear:
        linear_key_action->setChecked(true);
        break;
      case NodeKeyframe::kBezier:
        bezier_key_action->setChecked(true);
        break;
      case NodeKeyframe::kHold:
        hold_key_action->setChecked(true);
        break;
      }
    }
  }

  m.addSeparator();

  AddSetScrollZoomsByDefaultActionToMenu(&m);

  m.addSeparator();

  ContextMenuEvent(m);

  if (!items.isEmpty()) {
    m.addSeparator();

    QAction* properties_action = m.addAction(tr("P&roperties"));
    connect(properties_action, &QAction::triggered, this, &KeyframeViewBase::ShowKeyframePropertiesDialog);
  }

  QAction* selected = m.exec(QCursor::pos());

  // Process keyframe type changes
  if (!items.isEmpty()) {
    if (selected == linear_key_action
        || selected == bezier_key_action
        || selected == hold_key_action) {
      NodeKeyframe::Type new_type;

      if (selected == hold_key_action) {
        new_type = NodeKeyframe::kHold;
      } else if (selected == bezier_key_action) {
        new_type = NodeKeyframe::kBezier;
      } else {
        new_type = NodeKeyframe::kLinear;
      }

      MultiUndoCommand* command = new MultiUndoCommand();
      foreach (QGraphicsItem* item, items) {
        command->add_child(new KeyframeSetTypeCommand(static_cast<KeyframeViewItem*>(item)->key(),
                                                      new_type));
      }
      Core::instance()->undo_stack()->pushIfHasChildren(command);
    }
  }
}

void KeyframeViewBase::ShowKeyframePropertiesDialog()
{
  QList<QGraphicsItem*> items = scene()->selectedItems();
  QVector<NodeKeyframe*> keys;

  foreach (QGraphicsItem* item, items) {
    keys.append(static_cast<KeyframeViewItem*>(item)->key());
  }

  if (!keys.isEmpty()) {
    KeyframePropertiesDialog kd(keys, timebase(), this);
    kd.exec();
  }
}

void KeyframeViewBase::AutoSelectKeyTimeNeighbors()
{
  if (currently_autoselecting_ || IsYAxisEnabled()) {
    return;
  }

  // Prevents infinite loop
  currently_autoselecting_ = true;

  QList<QGraphicsItem*> selected_items = scene()->selectedItems();

  foreach (QGraphicsItem* g, selected_items) {
    KeyframeViewItem* key_item = static_cast<KeyframeViewItem*>(g);

    rational key_time = key_item->key()->time();

    QVector<NodeKeyframe*> keys = key_item->key()->parent()->GetKeyframesAtTime(key_item->key()->input(), key_time, key_item->key()->element());

    foreach (NodeKeyframe* k, keys) {
      if (k == key_item->key()) {
        continue;
      }

      // Ensure this key is not already selected
      KeyframeViewItem* item = item_map_.value(k);

      if (item) {
        item->setSelected(true);
      }
    }
  }

  currently_autoselecting_ = false;
}

}
