/***

  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/qtversionabstraction.h"
#include "core.h"
#include "nodeview.h"
#include "nodeviewundo.h"
#include "ui/icons/icons.h"
#include "window/mainwindow/mainwindow.h"

NodeViewItem::NodeViewItem(QGraphicsItem *parent) :
  QGraphicsRectItem(parent),
  node_(nullptr),
  font_metrics(font),
  dragging_edge_(nullptr),
  drag_expanded_item_(nullptr),
  expanded_(false),
  standard_click_(false),
  node_edge_change_command_(nullptr)
{
  // 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
  //

  // Not particularly great way of using text scaling to set the width (DPI-awareness, etc.)
  int widget_width = QFontMetricsWidth(font_metrics, "HHHHHHHHHHHHHHHH");

  // Set border width
  node_border_width_ = font_metrics.height() / 12;

  // Set default node connector size
  node_connector_size_ = font_metrics.height() / 3;

  // Set text and icon padding
  node_text_padding_ = font_metrics.height() / 6;

  // FIXME: Revise icon sizing algorithm (share with NodeParamViewItem)
  node_icon_padding_ = node_text_padding_ * 3;

  // Use the current default font height to size this widget
  // Set default "collapsed" size
  int widget_height = font_metrics.height() + node_text_padding_ * 2;

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

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

  setPos(node_->GetPosition());

  update();
}

Node *NodeViewItem::node()
{
  return node_;
}

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

void NodeViewItem::SetExpanded(bool e)
{
  if (expanded_ == e) {
    return;
  }

  expanded_ = e;

  QRectF new_rect;

  if (expanded_) {
    QRectF full_size_rect = title_bar_rect_;

    // If a node is connected, use its parameter count to set the height
    if (node_ != nullptr) {
      full_size_rect.adjust(0, 0, 0, node_text_padding_*2 + font_metrics.height() * node_->parameters().size());
    }

    // Store content_rect (the rect without the titlebar)
    content_rect_ = full_size_rect.adjusted(0, title_bar_rect_.height(), 0, 0);

    new_rect = full_size_rect;
  } else {
    new_rect = title_bar_rect_;
  }

  update();

  setRect(new_rect);
}

QRectF NodeViewItem::GetParameterConnectorRect(int index)
{
  if (node_ == nullptr) {
    return QRectF();
  }

  NodeParam* param = node_->parameters().at(index);

  QRectF connector_rect(rect().x(),
                        content_rect_.y() + node_text_padding_ + font_metrics.height() / 2 - node_connector_size_ / 2,
                        node_connector_size_,
                        node_connector_size_);

  if (index > 0) {
    connector_rect.translate(0, font_metrics.height() * index);
  }

  if (param->type() == NodeParam::kOutput) {
    connector_rect.translate(rect().width() - node_connector_size_, 0);
  }

  return connector_rect;
}

QPointF NodeViewItem::GetParameterTextPoint(int index)
{
  if (node_ == nullptr) {
    return QPointF();
  }

  NodeParam* param = node_->parameters().at(index);

  if (param->type() == NodeParam::kOutput) {
    return content_rect_.topRight() + QPointF(-(node_connector_size_ + node_text_padding_),
                                              node_text_padding_ + font_metrics.ascent() + font_metrics.height()*index);
  } else {
    return content_rect_.topLeft() + QPointF(node_connector_size_ + node_text_padding_,
                                             node_text_padding_ + font_metrics.ascent() + font_metrics.height()*index);
  }
}

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();

  // Set up border, which will change color if selected
  QPen border_pen(css_proxy_.BorderColor(), node_border_width_);

  QPen text_pen(app_pal.color(QPalette::Text));

  QBrush connector_brush(app_pal.color(QPalette::Text));

  painter->setPen(border_pen);

  if (expanded_ && node_ != nullptr) {

    // Use main widget color for node contents
    painter->setBrush(app_pal.window());

    // Draw background rect
    painter->drawRect(rect());

    // Set pen to draw text
    painter->setPen(text_pen);

    // Draw text and a connector rectangle for each parameter

    // Store the text points which will steadily increase sa we loop

    // Loop through all the parameters
    for (int i=0;i<node_->parameters().size();i++) {
      NodeParam* param = node_->parameters().at(i);

      // If parameter is not connectable, don't draw its slot
      if (param->IsConnectable()) {
        // Draw connector square
        painter->fillRect(GetParameterConnectorRect(i), connector_brush);
      }

      // Draw text
      QPointF text_pt = GetParameterTextPoint(i);

      if (param->type() == NodeParam::kOutput) {
        text_pt -= QPointF(QFontMetricsWidth(font_metrics, param->name()), 0);
      }

      painter->drawText(text_pt, param->name());
    }

    painter->setPen(border_pen);
  }

  // Draw rect
  painter->setBrush(css_proxy_.TitleBarColor());
  painter->drawRect(title_bar_rect_);

  // If selected, draw selection outline
  if (option->state & QStyle::State_Selected) {
    QPen pen = painter->pen();
    pen.setColor(app_pal.color(QPalette::Highlight));
    painter->setPen(pen);

    painter->setBrush(Qt::transparent);

    painter->drawRect(rect());
  }

  // Draw text
  if (node_ != nullptr) {
    painter->setPen(text_pen);

    // Draw the expand icon
    expand_hitbox_ = title_bar_rect_.adjusted(node_icon_padding_,
                                              node_icon_padding_,
                                              -node_icon_padding_,
                                              -node_icon_padding_);

    // Make the icon rect a square
    expand_hitbox_.setWidth(expand_hitbox_.height());

    // Draw the icon
    if (IsExpanded()) {
      icon::TriDown.paint(painter, expand_hitbox_.toRect(), Qt::AlignLeft | Qt::AlignVCenter);
    } else {
      icon::TriRight.paint(painter, expand_hitbox_.toRect(), Qt::AlignLeft | Qt::AlignVCenter);
    }

    // Draw the text in a rect (the rect is sized around text already in the constructor)
    QRectF text_rect = title_bar_rect_.adjusted(node_icon_padding_ + expand_hitbox_.width() + node_text_padding_,
                                                node_text_padding_,
                                                -node_text_padding_,
                                                -node_text_padding_);
    painter->drawText(text_rect, static_cast<int>(Qt::AlignVCenter | Qt::AlignLeft), node_->Name());
  }
}

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;

  // Don't initiate a drag if we clicked the expand hitbox
  if (expand_hitbox_.contains(event->pos())) {
    return;
  }

  // See if the mouse click was on a parameter connector
  if (IsExpanded() // This is only possible if the node is expanded
      && node_ != nullptr) { // We can only loop through a node's parameters if a valid node is attached
    for (int i=0;i<node_->parameters().size();i++) {

      if (GetParameterConnectorRect(i).contains(event->pos())) { // See if the cursor is in the rect

        NodeParam* param = node_->parameters().at(i);

        // Create draggable object
        dragging_edge_ = new NodeViewEdge();

        // 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_ = mapToScene(GetParameterConnectorRect(i).center());

        } 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_ = NodeView::NodeToUIObject(scene(), 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 = NodeView::EdgeToUIObject(scene(), edge);
          QLineF existing_edge_line = existing_edge_ui->line();
          if (existing_edge_ui->contains(existing_edge_line.p1())) {
            dragging_edge_start_ = existing_edge_line.p1();
          } else {
            dragging_edge_start_ = existing_edge_line.p2();
          }


          // 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;
      }

    }
  }

  // We aren't using any override behaviors, switch back to standard click behavior
  standard_click_ = true;
  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;

    // See if the mouse is currently inside a node
    NodeViewItem* drop_item = dynamic_cast<NodeViewItem*>(scene()->itemAt(event->scenePos(), sceneTransform()));

    // If we expanded an item below but are no longer dragging over it, re-collapse it
    if (drag_expanded_item_ != nullptr && drop_item != drag_expanded_item_) {
      drag_expanded_item_->SetExpanded(false);
      drag_expanded_item_ = nullptr;
    }

    if (drop_item != nullptr && drop_item != drag_source_) {

      // If the item we're dragging over is collapsed, expand it
      if (!drop_item->IsExpanded()) {
        drag_expanded_item_ = drop_item;
        drop_item->SetExpanded(true);
      }

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

        // Make a larger "hitbox" rect to make it easier to drag into
        QRectF param_hitbox = drop_item->GetParameterConnectorRect(i).adjusted(-node_connector_size_,
                                                                               -node_connector_size_,
                                                                               node_connector_size_,
                                                                               node_connector_size_);

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

        if (comp_param->IsConnectable() && param_hitbox.contains(drop_item->mapFromScene(event->scenePos()))) { // See if we're dragging inside the hitbox

          // Prevent circular dependency - check if the Node we'll be outputting to already outputs to this Node
          Node* outputting_node;
          Node* receiving_node;

          // Determine which Node will be "submitting output" and which node will be "receiving input"
          if (drag_src_param_->type() == NodeParam::kInput) {
            receiving_node = drag_src_param_->parentNode();
            outputting_node = drop_item->node();
          } else {
            receiving_node = drop_item->node();
            outputting_node = drag_src_param_->parentNode();
          }

          // Ensure the receiving node doesn't output to the outputting node
          if (!receiving_node->OutputsTo(outputting_node)) {
            drag_dest_param_ = comp_param;
            end_point = drop_item->mapToScene(drop_item->GetParameterConnectorRect(i).center());
          }

          break;
        }
      }
    }

    dragging_edge_->SetConnected(drag_dest_param_ != nullptr);

    dragging_edge_->setLine(QLineF(dragging_edge_start_, end_point));

    return;
  }

  if (standard_click_) {
    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 (drag_expanded_item_ != nullptr) {
      drag_expanded_item_->SetExpanded(false);
      drag_expanded_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;
  }

  // Check if we clicked the Expand/Collapse icon
  if (expand_hitbox_.contains(event->pos())) {
    SetExpanded(!IsExpanded());
  }

  if (standard_click_) {
    QGraphicsRectItem::mouseReleaseEvent(event);
  }
}

QVariant NodeViewItem::itemChange(QGraphicsItem::GraphicsItemChange change, const QVariant &value)
{
  if (change == ItemPositionHasChanged && node_) {
    node_->SetPosition(value.toPointF());
  }

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