/***

  Olive - Non-Linear Video Editor
  Copyright (C) 2019 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 "nodeviewitem.h"

#include <QDebug>
#include <QGraphicsScene>
#include <QGraphicsSceneMouseEvent>
#include <QPainter>
#include <QStyleOptionGraphicsItem>

#include "common/flipmodifiers.h"
#include "common/qtutils.h"
#include "config/config.h"
#include "core.h"
#include "nodeview.h"
#include "nodeviewscene.h"
#include "nodeviewundo.h"
#include "ui/icons/icons.h"
#include "window/mainwindow/mainwindow.h"

OLIVE_NAMESPACE_ENTER

NodeViewItem::NodeViewItem(QGraphicsItem *parent) :
  QGraphicsRectItem(parent),
  node_(nullptr),
  dragging_edge_(nullptr),
  cached_drop_item_(nullptr),
  cached_drop_item_expanded_(false),
  expanded_(false),
  hide_titlebar_(false),
  standard_click_(false),
  highlighted_index_(-1),
  node_edge_change_command_(nullptr),
  flow_dir_(NodeViewCommon::kLeftToRight)
{
  // Set flags for this widget
  setFlag(QGraphicsItem::ItemIsMovable);
  setFlag(QGraphicsItem::ItemIsSelectable);
  setFlag(QGraphicsItem::ItemSendsGeometryChanges);

  //
  // We use font metrics to set all the UI measurements for DPI-awareness
  //

  // Set border width
  node_border_width_ = DefaultItemBorder();

  int widget_width = DefaultItemWidth();
  int widget_height = DefaultItemHeight();

  title_bar_rect_ = QRectF(-widget_width/2, -widget_height/2, widget_width, widget_height);
  setRect(title_bar_rect_);
}

QPointF NodeViewItem::GetNodePosition() const
{
  QPointF node_pos;

  qreal adjusted_x = pos().x() / DefaultItemHorizontalPadding();
  qreal adjusted_y = pos().y() / DefaultItemVerticalPadding();

  switch (flow_dir_) {
  case NodeViewCommon::kLeftToRight:
    node_pos.setX(adjusted_x);
    node_pos.setY(adjusted_y);
    break;
  case NodeViewCommon::kRightToLeft:
    node_pos.setX(-adjusted_x);
    node_pos.setY(adjusted_y);
    break;
  case NodeViewCommon::kTopToBottom:
    node_pos.setX(adjusted_y);
    node_pos.setY(adjusted_x);
    break;
  case NodeViewCommon::kBottomToTop:
    node_pos.setX(-adjusted_y);
    node_pos.setY(adjusted_x);
    break;
  }

  return node_pos;
}

void NodeViewItem::SetNodePosition(const QPointF &pos)
{
  switch (flow_dir_) {
  case NodeViewCommon::kLeftToRight:
    setPos(pos.x() * DefaultItemHorizontalPadding(),
           pos.y() * DefaultItemVerticalPadding());
    break;
  case NodeViewCommon::kRightToLeft:
    setPos(-pos.x() * DefaultItemHorizontalPadding(),
           pos.y() * DefaultItemVerticalPadding());
    break;
  case NodeViewCommon::kTopToBottom:
    setPos(pos.y() * DefaultItemHorizontalPadding(),
           pos.x() * DefaultItemVerticalPadding());
    break;
  case NodeViewCommon::kBottomToTop:
    setPos(pos.y() * DefaultItemHorizontalPadding(),
           -pos.x() * DefaultItemVerticalPadding());
    break;
  }
}

int NodeViewItem::DefaultTextPadding()
{
  return QFontMetrics(QFont()).height() / 4;
}

int NodeViewItem::DefaultItemHeight()
{
  return QFontMetrics(QFont()).height() + DefaultTextPadding() * 2;
}

int NodeViewItem::DefaultItemWidth()
{
  return QFontMetricsWidth(QFontMetrics(QFont()), "HHHHHHHHHHHH");;
}

int NodeViewItem::DefaultItemBorder()
{
  return QFontMetrics(QFont()).height() / 12;
}

qreal NodeViewItem::DefaultItemHorizontalPadding() const
{
  if (NodeViewCommon::GetFlowOrientation(flow_dir_) == Qt::Horizontal) {
    return DefaultItemWidth() * 1.5;
  } else {
    return DefaultItemWidth() * 1.25;
  }
}

qreal NodeViewItem::DefaultItemVerticalPadding() const
{
  if (NodeViewCommon::GetFlowOrientation(flow_dir_) == Qt::Horizontal) {
    return DefaultItemHeight() * 1.5;
  } else {
    return DefaultItemHeight() * 2.0;
  }
}

void NodeViewItem::SetNode(Node *n)
{
  node_ = n;

  node_inputs_.clear();

  if (node_) {
    node_->Retranslate();

    foreach (NodeParam* p, node_->parameters()) {
      if (p->type() == NodeParam::kInput) {
        NodeInput* input = static_cast<NodeInput*>(p);

        if (input->is_connectable()) {
          node_inputs_.append(input);
        }
      }
    }

    SetNodePosition(node_->GetPosition());
  }

  update();
}

Node *NodeViewItem::GetNode() const
{
  return node_;
}

bool NodeViewItem::IsExpanded() const
{
  return expanded_;
}

void NodeViewItem::SetExpanded(bool e, bool hide_titlebar)
{
  if (node_inputs_.isEmpty()
      || (expanded_ == e && hide_titlebar_ == hide_titlebar)) {
    return;
  }

  expanded_ = e;
  hide_titlebar_ = hide_titlebar;

  if (expanded_ && !node_inputs_.isEmpty()) {
    // Create new rect
    QRectF new_rect = title_bar_rect_;

    if (hide_titlebar_) {
      new_rect.setHeight(new_rect.height() * node_inputs_.size());
    } else {
      new_rect.setHeight(new_rect.height() * (node_inputs_.size() + 1));
    }

    setRect(new_rect);
  } else {
    setRect(title_bar_rect_);
  }

  update();
}

void NodeViewItem::ToggleExpanded()
{
  SetExpanded(!IsExpanded());
}

void NodeViewItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *)
{
  // HACK for getting the main QWidget palette color (the `widget`'s palette uses the NodeView color instead which we
  // don't want here)
  QPalette app_pal = Core::instance()->main_window()->palette();

  // Draw background rect if expanded
  if (IsExpanded()) {
    painter->setPen(Qt::NoPen);
    painter->setBrush(app_pal.color(QPalette::Window));

    painter->drawRect(rect());

    painter->setPen(app_pal.color(QPalette::Text));

    for (int i=0;i<node_inputs_.size();i++) {
      QRectF input_rect = GetInputRect(i);

      if (highlighted_index_ == i) {
        QColor highlight_col = app_pal.color(QPalette::Text);
        highlight_col.setAlpha(64);
        painter->fillRect(input_rect, highlight_col);
      }

      painter->drawText(input_rect, Qt::AlignCenter, node_inputs_.at(i)->name());
    }
  }

  // Draw the titlebar
  if (!hide_titlebar_ && node_) {

    Color node_color = Config::Current()[QStringLiteral("NodeCatColor%1")
        .arg(node_->Category().first())].value<Color>();

    painter->setPen(Qt::black);
    painter->setBrush(node_color.toQColor());

    painter->drawRect(title_bar_rect_);

    painter->setPen(app_pal.color(QPalette::Text));

    QString node_label;

    if (node_->GetLabel().isEmpty()) {
      node_label = node_->ShortName();
    } else {
      node_label = node_->GetLabel();
    }

    QFont f;
    QFontMetrics fm(f);

    // Draw right or down arrow based on expanded state
    int icon_size = fm.height() / 2;
    int icon_padding = title_bar_rect_.height() / 2 - icon_size / 2;
    int icon_full_size = icon_size + icon_padding * 2;
    const QIcon& expand_icon = IsExpanded() ? icon::TriDown : icon::TriRight;
    expand_icon.paint(painter, QRect(title_bar_rect_.x() + icon_padding,
                                     title_bar_rect_.y() + icon_padding,
                                     icon_size,
                                     icon_size));

    // Calculate how much space we have for text
    int item_width = title_bar_rect_.width();
    int max_text_width = item_width - DefaultTextPadding() * 2 - icon_full_size;
    int label_width = QFontMetricsWidth(fm, node_label);

    // Concatenate text if necessary (adds a "..." to the end and removes characters until the
    // string fits in the bounds)
    if (label_width > max_text_width) {
      QString concatenated;

      do {
        node_label.chop(1);
        concatenated = QCoreApplication::translate("NodeViewItem", "%1...").arg(node_label);
      } while ((label_width = QFontMetricsWidth(fm, concatenated)) > max_text_width);

      node_label = concatenated;
    }

    // Determine the text color (automatically calculate from node background color)
    if (node_color.GetRoughLuminance() > 0.66) {
      painter->setPen(Qt::black);
    } else {
      painter->setPen(Qt::white);
    }

    // Determine X position (favors horizontal centering unless it'll overrun the arrow)
    QRectF text_rect = title_bar_rect_;
    Qt::Alignment text_align = Qt::AlignCenter;
    int likely_x = item_width / 2 - label_width / 2;
    if (likely_x < icon_full_size) {
      text_rect.adjust(icon_full_size, 0, 0, 0);
      text_align = Qt::AlignLeft | Qt::AlignVCenter;
    }

    // Draw the text in a rect (the rect is sized around text already in the constructor)
    painter->drawText(text_rect,
                      text_align,
                      node_label);
  }

  // Draw final border
  QPen border_pen;
  border_pen.setWidth(node_border_width_);

  if (option->state & QStyle::State_Selected) {
    border_pen.setColor(app_pal.color(QPalette::Highlight));
  } else {
    border_pen.setColor(Qt::black);
  }

  painter->setPen(border_pen);
  painter->setBrush(Qt::NoBrush);

  painter->drawRect(rect());
}

void NodeViewItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
  // We override standard mouse behavior in some cases. In these cases, we don't want the standard "move" and "release"
  // events to trigger if we haven't already triggered the "press" event. We use this variable to determine whether
  // base class behavior is valid here.
  standard_click_ = false;

  // If CTRL is held, we start a new edge
  if (node_ && event->modifiers() & Qt::ControlModifier) {

    NodeParam* param = node_->output();

    // Create draggable object
    dragging_edge_ = new NodeViewEdge();
    dragging_edge_->SetFlowDirection(flow_dir_);

    // Set up a QUndoCommand to make this action undoable
    node_edge_change_command_ = new QUndoCommand();

    if (param->type() == NodeParam::kOutput || param->edges().isEmpty()) {
      // For an output param (or an input param with no connections), we default to creating a new edge
      drag_source_ = this;
      drag_src_param_ = param;

      // Set the starting position to the current param's connector
      dragging_edge_start_ = GetParamPoint(param, QPointF());

    } else if (param->type() == NodeParam::kInput) {
      // For an input param, we default to moving an existing edge
      // (here we use the last one, which will usually also be the first)
      NodeEdgePtr edge = param->edges().last();

      // The starting position will be the OPPOSING parameter's rectangle

      // Get the opposing parameter
      drag_src_param_ = edge->output();

      // Get its Node UI object
      drag_source_ = static_cast<NodeViewScene*>(scene())->NodeToUIObject(drag_src_param_->parentNode());

      // Get the opposing parameter's rect center using the line's current coordinates
      // (we use the current coordinates because a complex formula is used for the line's coords if
      // the opposing node is collapsed, therefore it's easier to just retrieve it from line itself)
      NodeViewEdge* existing_edge_ui = static_cast<NodeViewScene*>(scene())->EdgeToUIObject(edge);
      QPainterPath existing_edge_line = existing_edge_ui->path();
      QPointF edge_start = existing_edge_line.pointAtPercent(0);
      QPointF edge_end = existing_edge_line.pointAtPercent(1);
      if (existing_edge_ui->contains(edge_start)) {
        dragging_edge_start_ = edge_start;
      } else {
        dragging_edge_start_ = edge_end;
      }


      // Remove old edge
      NodeEdgeRemoveCommand* remove_command = new NodeEdgeRemoveCommand(edge->output(),
                                                                        edge->input(),
                                                                        node_edge_change_command_);
      remove_command->redo();

    }

    // Add it to the scene
    scene()->addItem(dragging_edge_);

    // Trigger initial line setting
    mouseMoveEvent(event);

    return;

  } else {

    // We aren't using any override behaviors, switch back to standard click behavior
    standard_click_ = true;

    event->setModifiers(FlipControlAndShiftModifiers(event->modifiers()));

    QGraphicsRectItem::mousePressEvent(event);

  }
}

void NodeViewItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
{
  // Check if an edge drag was initiated
  if (dragging_edge_ != nullptr) {
    QPointF end_point = event->scenePos();
    drag_dest_param_ = nullptr;

    if (!cached_drop_item_
        || !cached_drop_item_->contains(cached_drop_item_->mapFromScene(event->scenePos()))) {

      // Cursor has left last entered item, we need to find a new one
      if (cached_drop_item_) {

        // If we expanded an item below but are no longer dragging over it, re-collapse it
        if (cached_drop_item_expanded_) {
          cached_drop_item_->SetExpanded(false);
        }

        cached_drop_item_->SetHighlightedIndex(-1);

        cached_drop_item_->setZValue(0);
        cached_drop_item_ = nullptr;
      }

      NodeViewItem* cursor_item  = dynamic_cast<NodeViewItem*>(scene()->itemAt(event->scenePos(), sceneTransform()));

      if (cursor_item
          && cursor_item != drag_source_
          && !cursor_item->node_inputs_.isEmpty()) {
        cached_drop_item_ = cursor_item;

        // If the item we're dragging over is collapsed, expand it
        cached_drop_item_expanded_ = !cached_drop_item_->IsExpanded();

        if (cached_drop_item_expanded_) {
          cached_drop_item_->SetExpanded(true, true);
        }

        cached_drop_item_->setZValue(1);
      } else {
        cached_drop_item_ = nullptr;
      }

    }

    if (cached_drop_item_) {

      int highlight_their_index = -1;

      // See if the mouse is currently inside a connector rect
      for (int i=0;i<cached_drop_item_->node_inputs_.size();i++) {

        // Get the parameter we're dragging into
        NodeParam* comp_param = cached_drop_item_->node_inputs_.at(i);

        // See if cursor is inside its UI
        if (cached_drop_item_->GetInputRect(i).contains(cached_drop_item_->mapFromScene(event->scenePos()))) { // See if we're dragging inside the hitbox

          // Attempt to prevent circular dependency - ensure the receiving node doesn't output to the outputting node
          if (!cached_drop_item_->GetNode()->OutputsTo(node_, true)) {
            drag_dest_param_ = comp_param;
            highlight_their_index = i;

            QPointF end_point_local = cached_drop_item_->GetInputPoint(i, pos());
            end_point = cached_drop_item_->mapToScene(end_point_local);
          }

          break;
        }
      }

      cached_drop_item_->SetHighlightedIndex(highlight_their_index);
    }

    dragging_edge_->SetConnected(drag_dest_param_);

    dragging_edge_->SetPoints(dragging_edge_start_,
                              end_point,
                              cached_drop_item_ ? cached_drop_item_->IsExpanded() : false);

    return;
  }

  if (standard_click_) {
    event->setModifiers(FlipControlAndShiftModifiers(event->modifiers()));

    QGraphicsRectItem::mouseMoveEvent(event);
  }
}

void NodeViewItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
{
  // Check if an edge drag was initiated
  if (dragging_edge_ != nullptr) {

    // Remove the drag object
    scene()->removeItem(dragging_edge_);

    // If we expanded an item in the drag, re-collapse it now
    if (cached_drop_item_) {
      if (cached_drop_item_expanded_) {
        cached_drop_item_->SetExpanded(false);
      }

      cached_drop_item_->SetHighlightedIndex(-1);
      cached_drop_item_->setZValue(0);

      cached_drop_item_ = nullptr;
    }

    if (drag_dest_param_ != nullptr) {
      // We dragged to somewhere, so we'll make a new connection

      NodeEdgePtr new_edge;

      NodeOutput* output;
      NodeInput* input;

      // Connecting will automatically add an edge UI object through the signal/slot system
      if (drag_dest_param_->type() == NodeParam::kOutput) {
        output = static_cast<NodeOutput*>(drag_dest_param_);
        input = static_cast<NodeInput*>(drag_src_param_);
      } else {
        output = static_cast<NodeOutput*>(drag_src_param_);
        input = static_cast<NodeInput*>(drag_dest_param_);
      }

      // Use a command to make Node connecting undoable
      new NodeEdgeAddCommand(output,
                             input,
                             node_edge_change_command_);
    }

    dragging_edge_ = nullptr;

    Core::instance()->undo_stack()->pushIfHasChildren(node_edge_change_command_);
    node_edge_change_command_ = nullptr;
    return;
  }

  if (standard_click_) {
    event->setModifiers(FlipControlAndShiftModifiers(event->modifiers()));

    QGraphicsRectItem::mouseReleaseEvent(event);
  }
}

void NodeViewItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
{
  QGraphicsRectItem::mouseDoubleClickEvent(event);

  if (!(event->modifiers() & Qt::ControlModifier)) {
    SetExpanded(!IsExpanded());
  }
}

QVariant NodeViewItem::itemChange(QGraphicsItem::GraphicsItemChange change, const QVariant &value)
{
  if (change == ItemPositionHasChanged && node_) {
    node_->blockSignals(true);
    node_->SetPosition(GetNodePosition());
    node_->blockSignals(false);
  }

  return QGraphicsItem::itemChange(change, value);
}

void NodeViewItem::SetHighlightedIndex(int index)
{
  if (highlighted_index_ == index) {
    return;
  }

  highlighted_index_ = index;

  update();
}

QRectF NodeViewItem::GetInputRect(int index) const
{
  QRectF r = title_bar_rect_;

  if (!hide_titlebar_) {
    index++;
  }

  if (IsExpanded()) {
    r.translate(0, r.height() * index);
  }

  return r;
}

QPointF NodeViewItem::GetParamPoint(NodeParam *param, const QPointF& source_pos) const
{
  if (param->type() == NodeParam::kOutput) {

    switch (flow_dir_) {
    case NodeViewCommon::kLeftToRight:
    default:
      return pos() + QPointF(rect().right(), rect().center().y());
    case NodeViewCommon::kRightToLeft:
      return pos() + QPointF(rect().left(), rect().center().y());
    case NodeViewCommon::kTopToBottom:
      return pos() + QPointF(rect().center().x(), rect().bottom());
    case NodeViewCommon::kBottomToTop:
      return pos() + QPointF(rect().center().x(), rect().top());
    }

  } else {
    NodeInput* input = static_cast<NodeInput*>(param);

    // Resolve NodeInputArray elements
    while (input->parentNode() != input->parent()) {
      input = static_cast<NodeInput*>(input->parent());
    }

    return pos() + GetInputPoint(node_inputs_.indexOf(input), source_pos);
  }
}

void NodeViewItem::SetFlowDirection(NodeViewCommon::FlowDirection dir)
{
  flow_dir_ = dir;
}

QPointF NodeViewItem::GetInputPoint(int index, const QPointF& source_pos) const
{
  QRectF input_rect = GetInputRect(index);

  Qt::Orientation flow_orientation = NodeViewCommon::GetFlowOrientation(flow_dir_);

  if (flow_orientation == Qt::Horizontal || IsExpanded()) {
    if (flow_dir_ == NodeViewCommon::kLeftToRight
        || (flow_orientation == Qt::Vertical && source_pos.x() < pos().x())) {
      return QPointF(input_rect.left(), input_rect.center().y());
    } else {
      return QPointF(input_rect.right(), input_rect.center().y());
    }
  } else {
    if (flow_dir_ == NodeViewCommon::kTopToBottom) {
      return QPointF(input_rect.center().x(), input_rect.top());
    } else {
      return QPointF(input_rect.center().x(), input_rect.bottom());
    }
  }
}

OLIVE_NAMESPACE_EXIT
