/***

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

#include <QInputDialog>
#include <QMessageBox>
#include <QMimeData>
#include <QMouseEvent>
#include <QPushButton>
#include <QScrollBar>
#include <QToolTip>

#include "nodeviewundo.h"
#include "node/audio/volume/volume.h"
#include "node/distort/transform/transformdistortnode.h"
#include "node/factory.h"
#include "node/group/group.h"
#include "node/project/serializer/serializer.h"
#include "node/traverser.h"
#include "ui/icons/icons.h"
#include "widget/menu/menushared.h"
#include "widget/timebased/timebasedview.h"

#define super HandMovableView

namespace olive {

const double NodeView::kMinimumScale = 0.1;
const int NodeView::kMaximumContexts = 10;

NodeView::NodeView(QWidget *parent) :
  HandMovableView(parent),
  drop_edge_(nullptr),
  create_edge_(nullptr),
  create_edge_output_item_(nullptr),
  create_edge_input_item_(nullptr),
  overlay_view_(nullptr),
  scale_(1.0),
  dont_emit_selection_signals_(false)
{
  setScene(&scene_);
  SetDefaultDragMode(RubberBandDrag);
  setContextMenuPolicy(Qt::CustomContextMenu);
  setMouseTracking(true);
  setRenderHint(QPainter::Antialiasing);
  setViewportUpdateMode(FullViewportUpdate);

  connect(this, &NodeView::customContextMenuRequested, this, &NodeView::ShowContextMenu);

  ConnectSelectionChangedSignal();

  SetFlowDirection(NodeViewCommon::kLeftToRight);

  UpdateSceneBoundingRect();
  connect(&scene_, &QGraphicsScene::changed, this, &NodeView::UpdateSceneBoundingRect);

  minimap_ = new NodeViewMiniMap(&scene_, this);
  minimap_->show();
  connect(minimap_, &NodeViewMiniMap::Resized, this, &NodeView::RepositionMiniMap);
  connect(minimap_, &NodeViewMiniMap::MoveToScenePoint, this, &NodeView::MoveToScenePoint);
  connect(horizontalScrollBar(), &QScrollBar::valueChanged, this, &NodeView::UpdateViewportOnMiniMap);
  connect(verticalScrollBar(), &QScrollBar::valueChanged, this, &NodeView::UpdateViewportOnMiniMap);

  viewport()->installEventFilter(this);
}

NodeView::~NodeView()
{
  // Unset the current graph
  ClearGraph();
}

void NodeView::SetContexts(const QVector<Node*> &nodes)
{
  if (overlay_view_) {
    CloseOverlay();
  }

  // Remove contexts that are no longer in the list
  foreach (Node *n, contexts_) {
    if (!nodes.contains(n)) {
      RemoveContext(n);
    }
  }

  // Add contexts that are now in the list
  foreach (Node *n, nodes) {
    if (scene_.context_map().size() >= kMaximumContexts) {
      break;
    }

    if (!contexts_.contains(n)) {
      AddContext(n);
    }
  }

  contexts_ = nodes;

  CenterOnItemsBoundingRect();
}

void NodeView::CloseContextsBelongingToProject(Project *project)
{
  QVector<Node*> new_contexts = contexts_;

  for (auto it = new_contexts.begin(); it != new_contexts.end(); ) {
    if ((*it)->project() == project) {
      it = new_contexts.erase(it);
    } else {
      it++;
    }
  }

  SetContexts(new_contexts);
}

void NodeView::ClearGraph()
{
  SetContexts(QVector<Node*>());
}

void NodeView::DeleteSelected()
{
  NodeViewDeleteCommand* command = new NodeViewDeleteCommand();

  foreach (NodeViewContext *ctx, scene_.context_map()) {
    ctx->DeleteSelected(command);
  }

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

void NodeView::SelectAll()
{
  // Optimization: rather than respond to every single item being selected, ignore the signal and
  //               then handle them all at the end.
  DisconnectSelectionChangedSignal();

  scene_.SelectAll();

  ConnectSelectionChangedSignal();

  UpdateSelectionCache();
}

void NodeView::DeselectAll()
{
  if (selected_nodes_.isEmpty()) {
    return;
  }

  // Optimization: rather than respond to every single item being selected, ignore the signal and
  //               then handle them all at the end.
  DisconnectSelectionChangedSignal();

  scene_.DeselectAll();

  ConnectSelectionChangedSignal();

  // Just emit all the nodes that are currently selected as no longer selected
  emit NodesDeselected(selected_nodes_);
  selected_nodes_.clear();
  emit NodeSelectionChanged(selected_nodes_);
  emit NodeSelectionChangedWithContexts(QVector<Node::ContextPair>());
}

void NodeView::Select(const QVector<Node::ContextPair> &nodes, bool center_view_on_item)
{
  // Optimization: rather than respond to every single item being selected, ignore the signal and
  //               then handle them all at the end.
  DisconnectSelectionChangedSignal();

  QVector<Node*> deselections = selected_nodes_;
  QVector<Node*> new_selections;

  scene_.DeselectAll();

  foreach (const Node::ContextPair &p, nodes) {
    NodeViewContext *ctx = scene_.context_map().value(p.context);
    if (ctx) {
      NodeViewItem *item = ctx->GetItemFromMap(p.node);
      if (item) {
        item->setSelected(true);
      }
    }
  }

  // Center on something
  if (center_view_on_item && !nodes.isEmpty()) {
    QMetaObject::invokeMethod(this, "CenterOnNode", Qt::QueuedConnection, OLIVE_NS_ARG(Node*, nodes.first().node));
  }

  ConnectSelectionChangedSignal();

  // Don't signal when this function was likely triggered from another widget's signal anyway
  dont_emit_selection_signals_ = true;
  UpdateSelectionCache();
  dont_emit_selection_signals_ = false;
}

void NodeView::CopySelected(bool cut)
{
  if (selected_nodes_.isEmpty()) {
    return;
  }

  QString copy_str;
  QXmlStreamWriter writer(&copy_str);

  ProjectSerializer::SaveData sdata(selected_nodes_.first()->project());
  sdata.SetOnlySerializeNodesAndResolveGroups(selected_nodes_);

  ProjectSerializer::SerializedProperties properties;

  for (Node *n : selected_nodes_) {
    NodeViewItem *item = GetAssumedItemForSelectedNode(n);

    if (item) {
      Node::Position pos = item->GetNodePositionData();

      properties[n][QStringLiteral("x")] = QString::number(pos.position.x());
      properties[n][QStringLiteral("y")] = QString::number(pos.position.y());
      properties[n][QStringLiteral("expanded")] = QString::number(pos.expanded);
    }
  }

  sdata.SetProperties(properties);

  ProjectSerializer::Save(&writer, sdata, QStringLiteral("nodes"));

  Core::CopyStringToClipboard(copy_str);

  if (cut) {
    DeleteSelected();
  }
}

void NodeView::Paste()
{
  if (contexts_.isEmpty()) {
    return;
  }

  ProjectSerializer::Result res = ProjectSerializer::Paste(QStringLiteral("nodes"));
  if (res.GetLoadedNodes().isEmpty()) {
    return;
  }

  Node::PositionMap map;

  for (auto it=res.GetLoadData().properties.cbegin(); it!=res.GetLoadData().properties.cend(); it++) {
    Node::Position pos;

    const QMap<QString, QString> &node_props = it.value();
    pos.position.setX(node_props.value(QStringLiteral("x")).toDouble());
    pos.position.setY(node_props.value(QStringLiteral("y")).toDouble());
    pos.expanded = node_props.value(QStringLiteral("expanded")).toDouble();

    qDebug() << it.key() << pos.position;

    map.insert(it.key(), pos);
  }

  PostPaste(res.GetLoadedNodes(), map);
}

void NodeView::Duplicate()
{
  if (!selected_nodes_.isEmpty()) {
    QVector<Node*> selected = selected_nodes_;
    QVector<Node*> new_nodes;
    Node::PositionMap map;

    new_nodes.resize(selected.size());

    // Create copies of each selected node, checking for groups and adding children if necessary
    for (int i=0; i<selected.size(); i++) {
      new_nodes[i] = selected.at(i)->copy();

      if (NodeGroup *g = dynamic_cast<NodeGroup*>(selected.at(i))) {
        for (auto it=g->GetContextPositions().cbegin(); it!=g->GetContextPositions().cend(); it++) {
          if (!selected.contains(it.key())) {
            // This should automatically recurse if this is a group inside a group
            selected.append(it.key());
          }
        }
        new_nodes.resize(selected.size());
      }
    }

    // Get positions in contexts, add input passthroughs, and copy input values/keyframes
    for (int i=0; i<new_nodes.size(); i++) {
      Node *og = selected.at(i);
      Node *copy = new_nodes.at(i);

      Node::Position pos;
      if (GetAssumedPositionForSelectedNode(og, &pos)) {
        map.insert(copy, pos);
      }

      for (auto it=og->GetContextPositions().cbegin(); it!=og->GetContextPositions().cend(); it++) {
        Node *child_og = it.key();
        int child_index = selected.indexOf(child_og);

        if (child_index != -1) {
          Node *child_copy = new_nodes.at(child_index);

          copy->SetNodePositionInContext(child_copy, it.value());
        }
      }

      if (NodeGroup *src_group = dynamic_cast<NodeGroup*>(og)) {
        NodeGroup *dst_group = static_cast<NodeGroup*>(copy);

        for (auto it=src_group->GetInputPassthroughs().cbegin(); it!=src_group->GetInputPassthroughs().cend(); it++) {
          NodeInput input = it->second;
          input.set_node(new_nodes.at(selected.indexOf(input.node())));
          dst_group->AddInputPassthrough(input, it->first);
        }

        dst_group->SetOutputPassthrough(new_nodes.at(selected.indexOf(src_group->GetOutputPassthrough())));
      }

      Node::CopyInputs(selected.at(i), new_nodes.at(i), false);
    }

    // Copy connections
    Node::CopyDependencyGraph(selected, new_nodes, nullptr);

    // Set root level context positions and attach to
    PostPaste(new_nodes, map);
  }
}

void NodeView::SetColorLabel(int index)
{
  MultiUndoCommand *command = new MultiUndoCommand();

  for (Node* node : qAsConst(selected_nodes_)) {
    command->add_child(new NodeOverrideColorCommand(node, index));
  }

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

void NodeView::ZoomIn()
{
  ZoomFromKeyboard(1.25);
}

void NodeView::ZoomOut()
{
  ZoomFromKeyboard(0.8);
}

void NodeView::keyPressEvent(QKeyEvent *event)
{
  switch (event->key()) {
  case Qt::Key_Left:
  case Qt::Key_Right:
  case Qt::Key_Up:
  case Qt::Key_Down:
  {
    MultiUndoCommand *pos_command = new MultiUndoCommand();
    for (Node *n : qAsConst(selected_nodes_)) {
      for (Node *context : qAsConst(contexts_)) {
        if (context->ContextContainsNode(n)) {
          Node::Position old_pos = context->GetNodePositionInContext(n);

          // Determine one pixel in scene units
          double movement_amt = 1.0 / scale_;

          // Translate to 2D movement
          QPointF node_movement;
          switch (event->key()) {
          case Qt::Key_Left:
            node_movement.setX(-movement_amt);
            break;
          case Qt::Key_Right:
            node_movement.setX(movement_amt);
            break;
          case Qt::Key_Up:
            node_movement.setY(-movement_amt);
            break;
          case Qt::Key_Down:
            node_movement.setY(movement_amt);
            break;
          }

          // Translate from screen units into node units
          node_movement = NodeViewItem::ScreenToNodePoint(node_movement, scene_.GetFlowDirection());

          // Move command
          pos_command->add_child(new NodeSetPositionCommand(n, context, old_pos + node_movement));
        }
      }
    }
    Core::instance()->undo_stack()->pushIfHasChildren(pos_command);
    break;
  }
  case Qt::Key_Escape:
    if (!attached_items_.isEmpty()) {
      DetachItemsFromCursor();
      break;
    }

    emit EscPressed();

    /* fall through */
  default:
    super::keyPressEvent(event);
    break;
  }
}

void NodeView::mousePressEvent(QMouseEvent *event)
{
  // Handle mouse press event
  if (HandPress(event)) return;

  // Get the item that the user clicked on, if any
  QGraphicsItem* item = itemAt(event->pos());

  if (event->button() == Qt::LeftButton) {
    // Sane defaults
    create_edge_already_exists_ = false;
    create_edge_from_output_ = true;
    create_edge_input_.Reset();

    if (event->modifiers() & Qt::ControlModifier) {
      NodeViewItem *mouse_item = dynamic_cast<NodeViewItem*>(item);

      if (mouse_item) {
        if (mouse_item->IsOutputItem()) {
          create_edge_output_item_ = mouse_item;
        } else {
          create_edge_input_item_ = mouse_item;
          create_edge_input_ = mouse_item->GetInput();
          create_edge_from_output_ = false;
        }

        // Highlight start item for better user experience
        mouse_item->SetHighlighted(true);
      }
    }

    if (!create_edge_output_item_ && !create_edge_input_item_) {
      // Determine if user clicked on a connector
      if (NodeViewItemConnector *connector = dynamic_cast<NodeViewItemConnector *>(item)) {
        NodeViewItem *attached = static_cast<NodeViewItem*>(connector->parentItem());

        if (connector->IsOutput()) {
          create_edge_output_item_ = attached;
        } else {
          create_edge_input_item_ = attached;

          if (!create_edge_input_item_->edges().isEmpty()) {
            // Drag existing edge instead
            create_edge_ = create_edge_input_item_->edges().first();
            create_edge_input_item_ = nullptr;
            create_edge_output_item_ = create_edge_->from_item();
            create_edge_already_exists_ = true;
          } else {
            create_edge_from_output_ = false;
            create_edge_input_ = create_edge_input_item_->GetInput();
          }
        }
      }
    }

    if ((create_edge_output_item_ || create_edge_input_item_) && !create_edge_already_exists_) {
      // Create a new edge from this output
      create_edge_ = new NodeViewEdge();
      create_edge_->SetCurved(scene_.GetEdgesAreCurved());

      // Add edge to scene
      scene_.addItem(create_edge_);

      // Position edge to mouse cursor
      PositionNewEdge(event->pos());
      return;
    }
  }

  // Handle selections with the right mouse button
  if (event->button() == Qt::RightButton) {
    if (!item || !item->isSelected()) {
      // Qt doesn't do this by default for some reason
      if (!(event->modifiers() & Qt::ShiftModifier)) {
        scene_.clearSelection();
      }

      // If there's an item here, select it
      if (item) {
        item->setSelected(true);
      }
    }
  }

  if (attached_items_.isEmpty()) {
    // Default QGraphicsView functionality (selecting, dragging, etc.)
    super::mousePressEvent(event);
  }

  // For any selected item, store its position in case the user is dragging it somewhere else
  auto selected_items = scene_.GetSelectedItems();
  foreach (NodeViewItem *i, selected_items) {
    // Ignore items attached to the cursor
    if (!IsItemAttachedToCursor(i)) {
      dragging_items_.insert(i, i->GetNodePosition());
    }
  }
}

void NodeView::mouseMoveEvent(QMouseEvent *event)
{
  if (HandMove(event)) return;

  if (create_edge_) {
    PositionNewEdge(event->pos());
    return;
  }

  if (attached_items_.isEmpty()) {
    super::mouseMoveEvent(event);
  }

  // See if there are any items attached
  if (!attached_items_.isEmpty()) {
    ProcessMovingAttachedNodes(event->pos());
  }
}

void NodeView::mouseReleaseEvent(QMouseEvent *event)
{
  if (HandRelease(event)) return;

  if (create_edge_) {
    EndEdgeDrag();
  }

  MultiUndoCommand* command = new MultiUndoCommand();

  Node *select_context = nullptr;
  QVector<Node*> select_nodes;

  bool had_attached_items = !attached_items_.isEmpty();

  if (!attached_items_.isEmpty()) {
    select_context = GetContextAtMousePos(event->pos());

    if (select_context) {
      select_nodes = ProcessDroppingAttachedNodes(command, select_context, event->pos());
    } else {
      QToolTip::showText(QCursor::pos(), tr("Nodes must be placed inside a context."));
    }
  }

  for (auto it=dragging_items_.cbegin(); it!=dragging_items_.cend(); it++) {
    NodeViewItem *i = it.key();
    QPointF current_pos = i->GetNodePosition();
    if (it.value() != current_pos) {
      command->add_child(new NodeSetPositionCommand(i->GetNode(), i->GetContext(), current_pos));
    }
  }
  dragging_items_.clear();

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

  if (!had_attached_items) {
    super::mouseReleaseEvent(event);
  }

  if (select_context) {
    DeselectAll();
    scene_.context_map().value(select_context)->Select(select_nodes);
  }
}

void NodeView::mouseDoubleClickEvent(QMouseEvent *event)
{
  super::mouseDoubleClickEvent(event);

  if (!(event->modifiers() & Qt::ControlModifier)) {
    NodeViewItem *item_at_cursor = dynamic_cast<NodeViewItem*>(itemAt(event->pos()));
    if (item_at_cursor) {
      item_at_cursor->ToggleExpanded();
    }
  }
}

void NodeView::dragEnterEvent(QDragEnterEvent *event)
{
  if (contexts_.empty()) {
    event->ignore();
    return;
  }

  QStringList mime_fmts = event->mimeData()->formats();

  if (mime_fmts.contains(Project::kItemMimeType)) {
    QByteArray model_data = event->mimeData()->data(Project::kItemMimeType);
    QDataStream stream(&model_data, QIODevice::ReadOnly);

    // Variables to deserialize into
    quintptr item_ptr;
    QVector<Track::Reference> enabled_streams;
    QVector<AttachedItem> new_attached;

    int y = 0;

    while (!stream.atEnd()) {
      stream >> enabled_streams >> item_ptr;

      // Get Item object
      Node* item = reinterpret_cast<Node*>(item_ptr);

      if (ViewerOutput* f = dynamic_cast<ViewerOutput*>(item)) {
        NodeViewItem *new_item;

        new_item = new NodeViewItem(f, nullptr);
        new_item->SetFlowDirection(scene_.GetFlowDirection());
        new_item->SetNodePosition(QPointF(0, y));
        y++;
        scene_.addItem(new_item);

        new_attached.append({new_item, f, new_item->pos()});
      }
    }

    if (new_attached.empty()) {
      event->ignore();
    } else {
      SetAttachedItems(new_attached);

      event->accept();
    }
  }
}

void NodeView::dragMoveEvent(QDragMoveEvent *event)
{
  if (attached_items_.empty()) {
    event->ignore();
  } else {
    ProcessMovingAttachedNodes(event->pos());

    if (GetContextAtMousePos(event->pos())) {
      event->accept();
    } else {
      event->ignore();
    }
  }
}

void NodeView::dropEvent(QDropEvent *event)
{
  if (Node *drop_ctx = GetContextAtMousePos(event->pos())) {
    MultiUndoCommand *command = new MultiUndoCommand();
    QVector<Node*> select_nodes = ProcessDroppingAttachedNodes(command, drop_ctx, event->pos());
    Core::instance()->undo_stack()->pushIfHasChildren(command);

    DeselectAll();
    scene_.context_map().value(drop_ctx)->Select(select_nodes);

    event->accept();
  } else {
    DetachItemsFromCursor(false);
    event->ignore();
  }
}

void NodeView::dragLeaveEvent(QDragLeaveEvent *event)
{
  if (attached_items_.empty()) {
    event->ignore();
  } else {
    DetachItemsFromCursor(false);

    event->accept();
  }
}

void NodeView::resizeEvent(QResizeEvent *event)
{
  super::resizeEvent(event);

  RepositionMiniMap();

  if (overlay_view_) {
    ResizeOverlay();
  }
}

void NodeView::UpdateSelectionCache()
{
  QVector<NodeViewItem*> current_selection = scene_.GetSelectedItems();

  QVector<Node*> selected;
  QVector<Node*> deselected;

  QVector<Node::ContextPair> sel_with_ctx(current_selection.size());

  // Determine which nodes are newly selected
  for (int j=0; j<current_selection.size(); j++) {
    NodeViewItem *i = current_selection.at(j);
    Node *n = i->GetNode();
    if (!selected_nodes_.contains(n)) {
      selected.append(n);
      selected_nodes_.append(n);
    }

    sel_with_ctx[j] = {n, i->GetContext()};
  }

  // Determine which nodes are newly deselected
  if (current_selection.isEmpty()) {
    // All nodes that were selected have been deselected, so we'll just set them all to `deselected`
    deselected = selected_nodes_;
    selected_nodes_.clear();
  } else {
    foreach (Node* n, selected_nodes_) {
      bool still_selected = false;

      foreach (NodeViewItem *i, current_selection) {
        if (i->GetNode() == n) {
          still_selected = true;
          break;
        }
      }

      if (!still_selected) {
        deselected.append(n);
        selected_nodes_.removeOne(n);
      }
    }
  }

  if (!deselected.isEmpty()) {
    emit NodesDeselected(deselected);
  }

  if (!selected.isEmpty()) {
    emit NodesSelected(selected);
  }

  if (!dont_emit_selection_signals_) {
    emit NodeSelectionChanged(selected_nodes_);
    emit NodeSelectionChangedWithContexts(sel_with_ctx);
  }
}

void NodeView::ShowContextMenu(const QPoint &pos)
{
  if (contexts_.isEmpty()) {
    return;
  }

  Menu m;

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

  m.addSeparator();

  QVector<NodeViewItem*> selected = scene_.GetSelectedItems();

  if (itemAt(pos) && !selected.isEmpty()) {

    // Grouping
    if (selected.size() == 1 && dynamic_cast<NodeGroup*>(selected.first()->GetNode())) {
      QAction *ungroup_action = m.addAction(tr("Ungroup"));
      connect(ungroup_action, &QAction::triggered, this, &NodeView::UngroupNodes);
    } else {
      QAction *group_action = m.addAction(tr("Group"));
      connect(group_action, &QAction::triggered, this, &NodeView::GroupNodes);
    }

    // Color menu
    MenuShared::instance()->AddColorCodingMenu(&m);

    // Show in Viewer option for nodes based on Viewer
    if (ViewerOutput* viewer = dynamic_cast<ViewerOutput*>(selected.first()->GetNode())) {
      Q_UNUSED(viewer)
      m.addSeparator();
      QAction* open_in_viewer_action = m.addAction(tr("Open in Viewer"));
      connect(open_in_viewer_action, &QAction::triggered, this, &NodeView::OpenSelectedNodeInViewer);
    }

    m.addSeparator();

    // Properties
    QAction *properties_action = m.addAction(tr("P&roperties"));
    connect(properties_action, &QAction::triggered, this, &NodeView::ShowNodeProperties);

  } else {

    QAction* curved_action = m.addAction(tr("Smooth Edges"));
    curved_action->setCheckable(true);
    curved_action->setChecked(scene_.GetEdgesAreCurved());
    connect(curved_action, &QAction::triggered, &scene_, &NodeViewScene::SetEdgesAreCurved);

    m.addSeparator();

    Menu* direction_menu = new Menu(tr("Direction"), &m);
    m.addMenu(direction_menu);

    direction_menu->AddActionWithData(tr("Top to Bottom"),
                                      NodeViewCommon::kTopToBottom,
                                      scene_.GetFlowDirection());

    direction_menu->AddActionWithData(tr("Bottom to Top"),
                                      NodeViewCommon::kBottomToTop,
                                      scene_.GetFlowDirection());

    direction_menu->AddActionWithData(tr("Left to Right"),
                                      NodeViewCommon::kLeftToRight,
                                      scene_.GetFlowDirection());

    direction_menu->AddActionWithData(tr("Right to Left"),
                                      NodeViewCommon::kRightToLeft,
                                      scene_.GetFlowDirection());

    connect(direction_menu, &Menu::triggered, this, &NodeView::ContextMenuSetDirection);

    m.addSeparator();

    Menu* add_menu = CreateAddMenu(&m);
    m.addMenu(add_menu);

  }

  m.exec(mapToGlobal(pos));
}

void NodeView::CreateNodeSlot(QAction *action)
{
  Node* new_node = NodeFactory::CreateFromMenuAction(action);

  if (new_node) {
    NodeViewItem *new_item = new NodeViewItem(new_node, nullptr);
    new_item->SetFlowDirection(scene_.GetFlowDirection());
    scene_.addItem(new_item);

    QVector<AttachedItem> new_attached;

    new_attached.append({new_item, new_node, QPointF(0, 0)});

    if (NodeGroup *new_group = dynamic_cast<NodeGroup*>(new_node)) {
      for (auto it=new_group->GetContextPositions().cbegin(); it!=new_group->GetContextPositions().cend(); it++) {
        new_attached.append({nullptr, it.key(), QPointF(0, 0)});
      }
    }

    SetAttachedItems(new_attached);
  }
}

void NodeView::ContextMenuSetDirection(QAction *action)
{
  SetFlowDirection(static_cast<NodeViewCommon::FlowDirection>(action->data().toInt()));
}

void NodeView::OpenSelectedNodeInViewer()
{
  // Find first viewer in list of selected nodes and open it
  foreach (Node *n, selected_nodes_) {
    if (ViewerOutput* viewer = dynamic_cast<ViewerOutput*>(n)) {
      Core::instance()->OpenNodeInViewer(viewer);
      break;
    }
  }
}

void NodeView::UpdateSceneBoundingRect()
{
  // Get current items bounding rect
  QRectF r = scene_.itemsBoundingRect();

  // Adjust so that it fills the view
  r.adjust(-width(), -height(), width(), height());

  // Set it
  scene_.setSceneRect(r);
}

void NodeView::CenterOnItemsBoundingRect()
{
  centerOn(scene_.itemsBoundingRect().center());
}

void NodeView::CenterOnNode(Node *n)
{
  foreach (NodeViewContext *ctx, scene_.context_map()) {
    if (NodeViewItem* item = ctx->GetItemFromMap(n)) {
      centerOn(item);
      break;
    }
  }
}

void NodeView::RepositionMiniMap()
{
  if (minimap_->isVisible()) {
    int margin = fontMetrics().height();

    int w = width() - minimap_->width() - margin;
    int h = height() - minimap_->height() - margin;

    if (verticalScrollBar()->isVisible()) {
      w -= verticalScrollBar()->width();
    }

    if (horizontalScrollBar()->isVisible()) {
      h -= horizontalScrollBar()->height();
    }

    minimap_->move(w, h);

    UpdateViewportOnMiniMap();
  }
}

void NodeView::UpdateViewportOnMiniMap()
{
  if (minimap_->isVisible()) {
    minimap_->SetViewportRect(mapToScene(viewport()->rect()));
  }
}

void NodeView::MoveToScenePoint(const QPointF &pos)
{
  centerOn(pos);
}

void NodeView::NodeRemovedFromGraph()
{
  Node *context = static_cast<Node*>(sender());

  RemoveContext(context);

  contexts_.removeOne(context);
}

void NodeView::DetachItemsFromCursor(bool delete_nodes_too)
{
  foreach (const AttachedItem &ai, attached_items_) {
    delete ai.item;

    if (delete_nodes_too) {
      qDebug() << "deleting" << ai.node;
      delete ai.node;
    }
  }

  attached_items_.clear();
}

void NodeView::SetFlowDirection(NodeViewCommon::FlowDirection dir)
{
  scene_.SetFlowDirection(dir);
}

void NodeView::MoveAttachedNodesToCursor(const QPoint& p)
{
  QPointF item_pos = mapToScene(p);

  for (const AttachedItem& i : qAsConst(attached_items_)) {
    if (i.item) {
      i.item->setPos(item_pos + i.original_pos);
    }
  }
}

void NodeView::ProcessMovingAttachedNodes(const QPoint &pos)
{
  // Move those items to the cursor
  MoveAttachedNodesToCursor(pos);

  // See if the user clicked on an edge (only when dropping single nodes)
  if (attached_items_.size() == 1) {
    Node* attached_node = attached_items_.first().item->GetNode();

    QRect edge_detect_rect(pos, pos);

    int edge_detect_radius = fontMetrics().height();
    edge_detect_rect.adjust(-edge_detect_radius, -edge_detect_radius, edge_detect_radius, edge_detect_radius);

    QList<QGraphicsItem*> items = this->items(edge_detect_rect);

    NodeViewEdge* new_drop_edge = nullptr;

    // See if there is an edge here
    for (QGraphicsItem* item : qAsConst(items)) {
      new_drop_edge = dynamic_cast<NodeViewEdge*>(item);

      if (new_drop_edge) {
        drop_input_.Reset();

        NodeValue::Type drop_edge_data_type = new_drop_edge->input().GetDataType();

        // Determine best input to connect to our new node
        if (attached_node->GetEffectInput().IsValid()) {
          // If node specifies an effect input, use that immediately
          drop_input_ = attached_node->GetEffectInput();
        } else {
          // Otherwise, we may have to iterate to find a valid one
          for (const QString& input : attached_node->inputs()) {
            if (input == Node::kEnabledInput) {
              // Ignore enabled input
              continue;
            }

            NodeInput i(attached_node, input);

            if (attached_node->IsInputConnectable(input)) {
              if (attached_node->GetInputDataType(input) == drop_edge_data_type) {
                // Found exactly the type we're looking for, set and break this loop
                drop_input_ = i;
                break;
              } else if (!drop_input_.IsValid()) {
                // Default to first connectable input
                drop_input_ = i;
              }
            }
          }
        }

        if (new_drop_edge->input().node()->OutputsTo(attached_node, true)) {
          drop_input_.Reset();
        }

        if (drop_input_.IsValid()) {
          break;
        } else {
          new_drop_edge = nullptr;
        }
      }
    }

    if (drop_edge_ != new_drop_edge) {
      if (drop_edge_) {
        drop_edge_->SetHighlighted(false);
      }

      drop_edge_ = new_drop_edge;

      if (drop_edge_) {
        drop_edge_->SetHighlighted(true);
      }
    }
  }
}

QVector<Node*> NodeView::ProcessDroppingAttachedNodes(MultiUndoCommand *command, Node *select_context, const QPoint &pos)
{
  QVector<Node*> select_nodes;

  // Make a copy
  QVector<AttachedItem> attached = attached_items_;

  for (int i=0; i<attached.size(); i++) {
    const AttachedItem &ai = attached.at(i);

    if (select_context->OutputsTo(ai.node, true)) {
      attached.removeAt(i);
    } else if (select_context->ContextContainsNode(ai.node)) {
      select_nodes.append(ai.node);
      attached.removeAt(i);
    }
  }

  {
    MultiUndoCommand *add_command = new MultiUndoCommand();

    foreach (const AttachedItem &ai, attached) {
      // Add node to the same graph that the context is in
      if (ai.node->parent() != select_context->parent()) {
        add_command->add_child(new NodeAddCommand(select_context->parent(), ai.node));
      }

      // Add node to the context
      if (ai.item) {
        select_nodes.append(ai.node);
        add_command->add_child(new NodeSetPositionCommand(ai.node, select_context, scene_.context_map().value(select_context)->MapScenePosToNodePosInContext(ai.item->pos())));
      }
    }

    if (add_command->child_count()) {
      add_command->redo_now();
      command->add_child(add_command);
    } else {
      delete add_command;
    }
  }

  {
    // Dropped attached item onto an edge, connect it between them
    MultiUndoCommand *drop_edge_command = new MultiUndoCommand();
    if (attached.size() == 1) {
      Node* dropping_node = nullptr;

      foreach (const AttachedItem &ai, attached) {
        if (ai.item && !select_context->OutputsTo(ai.node, true)) {
          dropping_node = ai.node;
          break;
        }
      }

      if (dropping_node && drop_edge_) {
        // Remove old edge
        drop_edge_command->add_child(new NodeEdgeRemoveCommand(drop_edge_->output(), drop_edge_->input()));

        // Place new edges
        drop_edge_command->add_child(new NodeEdgeAddCommand(drop_edge_->output(), drop_input_));
        drop_edge_command->add_child(new NodeEdgeAddCommand(dropping_node, drop_edge_->input()));
      }

      drop_edge_ = nullptr;
    }
    if (drop_edge_command->child_count()) {
      drop_edge_command->redo_now();
      command->add_child(drop_edge_command);
    } else {
      delete drop_edge_command;
    }
  }

  DetachItemsFromCursor(false);

  return select_nodes;
}

Node *NodeView::GetContextAtMousePos(const QPoint &p)
{
  QList<QGraphicsItem*> items_at_cursor = this->items(p);
  foreach (QGraphicsItem *i, items_at_cursor) {
    if (NodeViewContext *context_item = dynamic_cast<NodeViewContext*>(i)) {
      return context_item->GetContext();
    }
  }

  return nullptr;
}

void NodeView::ConnectSelectionChangedSignal()
{
  connect(&scene_, &QGraphicsScene::selectionChanged, this, &NodeView::UpdateSelectionCache);
}

void NodeView::DisconnectSelectionChangedSignal()
{
  disconnect(&scene_, &QGraphicsScene::selectionChanged, this, &NodeView::UpdateSelectionCache);
}

void NodeView::ZoomIntoCursorPosition(QWheelEvent *event, double multiplier, const QPointF& cursor_pos)
{
  Q_UNUSED(event)

  double test_scale = scale_ * multiplier;

  if (test_scale > kMinimumScale) {
    int anchor_x = qRound(double(cursor_pos.x() + horizontalScrollBar()->value()) / scale_ * test_scale - cursor_pos.x());
    int anchor_y = qRound(double(cursor_pos.y() + verticalScrollBar()->value()) / scale_ * test_scale - cursor_pos.y());

    scale(multiplier, multiplier);

    this->horizontalScrollBar()->setValue(anchor_x);
    this->verticalScrollBar()->setValue(anchor_y);

    scale_ = test_scale;
  }
}

bool NodeView::event(QEvent *event)
{
  if (event->type() == QEvent::ShortcutOverride) {
    QKeyEvent *se = static_cast<QKeyEvent *>(event);
    if (se->key() == Qt::Key_Left
        || se->key() == Qt::Key_Right
        || se->key() == Qt::Key_Up
        || se->key() == Qt::Key_Down) {
      se->accept();
      return true;
    }
  }

  return super::event(event);
}

bool NodeView::eventFilter(QObject *object, QEvent *event)
{
  return super::eventFilter(object, event);
}

void NodeView::changeEvent(QEvent *e)
{
  // Add translation code

  super::changeEvent(e);
}

void NodeView::ZoomFromKeyboard(double multiplier)
{
  QPoint cursor_pos = mapFromGlobal(QCursor::pos());

  // If the cursor is not currently within the widget, zoom into the center
  if (!rect().contains(cursor_pos)) {
    cursor_pos = QPoint(width()/2, height()/2);
  }

  ZoomIntoCursorPosition(nullptr, multiplier, cursor_pos);
}

void NodeView::ClearCreateEdgeInputIfNecessary()
{
  if (create_edge_from_output_ && create_edge_input_.IsValid()) {
    create_edge_input_.Reset();
  }
}

QPointF NodeView::GetEstimatedPositionForContext(NodeViewItem *item, Node *context) const
{
  return item->GetNodePosition() - context_offsets_.value(context);
}

NodeViewItem *NodeView::GetAssumedItemForSelectedNode(Node *node)
{
  // Try to find corresponding selected item
  foreach (NodeViewContext *ctx, scene_.context_map()) {
    NodeViewItem *item = ctx->GetItemFromMap(node);
    if (item && item->GetNode() == node && item->isSelected()) {
      // Good enough
      return item;
    }
  }

  return nullptr;
}

bool NodeView::GetAssumedPositionForSelectedNode(Node *node, Node::Position *pos)
{
  if (NodeViewItem *item = GetAssumedItemForSelectedNode(node)) {
    *pos = item->GetNodePositionData();
    return true;
  } else {
    return false;
  }
}

Menu *NodeView::CreateAddMenu(Menu *parent)
{
  Menu* add_menu = NodeFactory::CreateMenu(parent);
  add_menu->setTitle(tr("Add"));
  connect(add_menu, &Menu::triggered, this, &NodeView::CreateNodeSlot);
  return add_menu;
}

void NodeView::PositionNewEdge(const QPoint &pos)
{
  // Determine scene coordinate
  QPointF scene_pt = mapToScene(pos);

  // Find if the cursor is currently inside an item
  NodeViewItem* item_at_cursor = dynamic_cast<NodeViewItem*>(itemAt(pos));

  NodeViewItem *source_item = create_edge_from_output_ ? create_edge_output_item_ : create_edge_input_item_;
  NodeViewItem *&opposing_item = create_edge_from_output_ ? create_edge_input_item_ : create_edge_output_item_;

  // Filter out connecting to self
  if (item_at_cursor && item_at_cursor->GetNode() == source_item->GetNode()) {
    item_at_cursor = nullptr;
  }

  // Collapse any items that the cursor is no longer inside
  int i=create_edge_expanded_items_.size() - 1;
  for ( ; i>=0; i--) {
    NodeViewItem* nvi = create_edge_expanded_items_.at(i);
    QPointF local_pt = nvi->mapFromScene(scene_pt);

    if (nvi->scene() == &scene_ && (nvi->contains(local_pt) || (!nvi->IsOutputItem() && nvi->parentItem()->contains(nvi->parentItem()->mapFromScene(scene_pt)) && local_pt.y() > nvi->rect().bottom()))) {
      break;
    } else {
      // Collapsing an item will destroy its children, so if the cursor item happens to be a child
      // of the item we're about to collapse, set it to null
      if (item_at_cursor && item_at_cursor->parentItem() == nvi) {
        item_at_cursor = nullptr;
      }

      if (opposing_item && opposing_item->parentItem() == nvi) {
        opposing_item = nullptr;
        ClearCreateEdgeInputIfNecessary();
      }

      CollapseItem(nvi);
    }
  }
  create_edge_expanded_items_.resize(i + 1);

  // Expand item if possible
  if (item_at_cursor
      && item_at_cursor->CanBeExpanded()
      && !item_at_cursor->IsExpanded()
      && create_edge_from_output_) {
    ExpandItem(item_at_cursor);
    create_edge_expanded_items_.append(item_at_cursor);
  }

  // Filter out connecting to a node that connects to us or an item of the same type
  if (item_at_cursor
      && ((create_edge_from_output_ && item_at_cursor->GetNode()->OutputsTo(source_item->GetNode(), true))
          || (!create_edge_from_output_ && item_at_cursor->GetNode()->InputsFrom(source_item->GetNode(), true))
          || (create_edge_from_output_ == item_at_cursor->IsOutputItem()))) {
    item_at_cursor = nullptr;
  }

  // Filter out "output node" of the context, we assume users won't want to fetch the output of this
  if (item_at_cursor && !create_edge_from_output_ && item_at_cursor->IsLabelledAsOutputOfContext()) {
    item_at_cursor = nullptr;
  }

  // If the item has changed
  if (item_at_cursor != opposing_item) {
    // If we had a destination active, disconnect from it since the item has changed
    if (opposing_item) {
      opposing_item->SetHighlighted(false);
      opposing_item = nullptr;
    }

    // Clear cached input
    ClearCreateEdgeInputIfNecessary();

    // If this is an input and we're
    opposing_item = item_at_cursor;

    if (opposing_item) {
      opposing_item->SetHighlighted(true);
      if (!opposing_item->IsOutputItem()) {
        create_edge_input_ = opposing_item->GetInput();
      }
    }
  }

  QPointF output_point = create_edge_output_item_ ? create_edge_output_item_->GetOutputPoint() : scene_pt;
  QPointF input_point = create_edge_input_.IsValid() ? create_edge_input_item_->GetInputPoint() : scene_pt;

  create_edge_->SetPoints(output_point, input_point);
  create_edge_->SetConnected(create_edge_output_item_ && create_edge_input_.IsValid());
}

void NodeView::GroupNodes()
{
  // Get items
  QVector<NodeViewItem*> items = scene_.GetSelectedItems();
  if (items.isEmpty()) {
    return;
  }

  // Get node context
  Node *context = items.first()->GetContext();
  QPointF avg_pos = items.first()->GetNodePosition();
  for (int i=1; i<items.size(); i++) {
    if (items.at(i)->GetContext() != context) {
      QMessageBox::critical(this, tr("Failed to group nodes"), tr("Nodes can only be grouped if they're in the same context."));
      return;
    }

    avg_pos += items.at(i)->GetNodePosition();
  }
  avg_pos /= items.size();

  // Create group
  NodeGroup *group = new NodeGroup();

  // Add group to graph and context
  MultiUndoCommand *command = new MultiUndoCommand();

  // Add nodes to group
  Node *output_passthrough = nullptr;
  QVector<Node*> nodes_to_group = selected_nodes_;
  DeselectAll();
  foreach (Node *n, nodes_to_group) {
    command->add_child(new NodeRemovePositionFromContextCommand(n, context));
    command->add_child(new NodeSetPositionCommand(n, group, context->GetNodePositionDataInContext(n)));

    for (auto it=n->inputs().cbegin(); it!=n->inputs().cend(); it++) {
      NodeInput input(n, *it, -1);

      if (!input.IsConnected() || !nodes_to_group.contains(input.GetConnectedOutput())) {
        command->add_child(new NodeGroupAddInputPassthrough(group, input));
      }
    }

    if (!output_passthrough) {
      // Default to the first node we find that doesn't output to a node inside the group
      output_passthrough = nodes_to_group.first();
      foreach (Node *potential_in, nodes_to_group) {
        if (potential_in != n && !n->OutputsTo(potential_in, false)) {
          output_passthrough = n;
          break;
        }
      }
    }
  }

  // Set output passthrough
  command->add_child(new NodeGroupSetOutputPassthrough(group, output_passthrough));

  // Add group to graph
  command->add_child(new NodeAddCommand(context->parent(), group));
  command->add_child(new NodeSetPositionCommand(group, context, avg_pos));

  // Do command
  Core::instance()->LabelNodes({group}, command);

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

void NodeView::UngroupNodes()
{
  NodeViewItem *group_item = nullptr;
  QVector<NodeViewItem*> items = scene_.GetSelectedItems();
  if (items.isEmpty()) {
    return;
  }

  NodeGroup *group = nullptr;
  foreach (NodeViewItem *i, items) {
    if ((group = dynamic_cast<NodeGroup*>(i->GetNode()))) {
      group_item = i;
      break;
    }
  }

  if (!group_item) {
    return;
  }

  MultiUndoCommand *command = new MultiUndoCommand();

  Node *context = group_item->GetContext();

  command->add_child(new NodeRemovePositionFromContextCommand(group, context));
  command->add_child(new NodeRemoveAndDisconnectCommand(group));

  for (auto it=group->GetContextPositions().cbegin(); it!=group->GetContextPositions().cend(); it++) {
    command->add_child(new NodeRemovePositionFromContextCommand(it.key(), group));
    command->add_child(new NodeSetPositionCommand(it.key(), context, group->GetNodePositionDataInContext(it.key())));
  }

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

void NodeView::ShowNodeProperties()
{
  Node *first_node = selected_nodes_.first();

  if (NodeGroup *group = dynamic_cast<NodeGroup*>(first_node)) {
    if (!overlay_view_) {
      overlay_view_ = new NodeView(this);
      overlay_view_->show();

      QPushButton *overlay_close_btn = new QPushButton(overlay_view_);
      overlay_close_btn->setIcon(icon::Error);
      int offset = overlay_close_btn->sizeHint().width()/2;
      overlay_close_btn->move(offset, offset);
      overlay_close_btn->show();

      connect(overlay_view_, &NodeView::NodesSelected, this, &NodeView::NodesSelected);
      connect(overlay_view_, &NodeView::NodesDeselected, this, &NodeView::NodesDeselected);
      connect(overlay_view_, &NodeView::NodeGroupOpened, this, &NodeView::NodeGroupOpened);
      connect(overlay_view_, &NodeView::NodeGroupClosed, this, &NodeView::NodeGroupClosed);
      connect(overlay_view_, &NodeView::EscPressed, this, &NodeView::CloseOverlay);
      connect(overlay_close_btn, &QPushButton::clicked, this, &NodeView::CloseOverlay);

      const QColor &bgcol = overlay_view_->palette().base().color();
      overlay_view_->setStyleSheet(QStringLiteral("QGraphicsView { background: rgba(%1, %2, %3, 0.8); }").arg(QString::number(bgcol.red()), QString::number(bgcol.green()), QString::number(bgcol.blue())));

      overlay_close_btn->setStyleSheet(QStringLiteral("background: transparent; border: none;"));
    }
    overlay_view_->SetContexts({group});
    ResizeOverlay();
    QMetaObject::invokeMethod(overlay_view_, &NodeView::CenterOnItemsBoundingRect, Qt::QueuedConnection);
    overlay_view_->setFocus();

    emit NodesDeselected(selected_nodes_);
    emit NodeSelectionChanged(QVector<Node*>());
    emit NodeSelectionChangedWithContexts(QVector<Node::ContextPair>());
    overlay_view_->SelectAll();

    emit NodeGroupOpened(group);
  } else {
    LabelSelectedNodes();
  }
}

void NodeView::LabelSelectedNodes()
{
  Core::instance()->LabelNodes(selected_nodes_);
}

void NodeView::ItemAboutToBeDeleted(NodeViewItem *item)
{
  dragging_items_.remove(item);

  if (create_edge_) {
    // Item should be removed from scene, but not yet deleted, allowing a safe PositionNewEdge call
    // to disconnect
    PositionNewEdge(mapFromGlobal(QCursor::pos()));

    QGraphicsItem *test = item;
    do {
      if (test == item) {
        break;
      }

      test = test->parentItem();
    } while (test);

    if (test == item) {
      // Cancel edge function
      EndEdgeDrag(true);
    }
  }
}

void NodeView::CloseOverlay()
{
  if (overlay_view_->overlay_view_) {
    overlay_view_->CloseOverlay();
  }

  overlay_view_->deleteLater();
  overlay_view_ = nullptr;
  emit NodeGroupClosed();
}

void NodeView::AddContext(Node *n)
{
  NodeViewContext *ctx = scene_.AddContext(n);

  connect(ctx, &NodeViewContext::ItemAboutToBeDeleted, this, &NodeView::ItemAboutToBeDeleted);

  connect(n, &Node::RemovedFromGraph, this, &NodeView::NodeRemovedFromGraph);
}

void NodeView::RemoveContext(Node *n)
{
  scene_.RemoveContext(n);
  disconnect(n, &Node::RemovedFromGraph, this, &NodeView::NodeRemovedFromGraph);
}

bool NodeView::IsItemAttachedToCursor(NodeViewItem *item) const
{
  foreach (const AttachedItem &ai, attached_items_) {
    if (ai.item == item) {
      return true;
    }
  }

  return false;
}

void NodeView::ExpandItem(NodeViewItem *item)
{
  item->SetExpanded(true);
  item->setZValue(100);
}

void NodeView::CollapseItem(NodeViewItem *item)
{
  item->SetExpanded(false);
  item->setZValue(0);
}

void NodeView::EndEdgeDrag(bool cancel)
{
  // Check if the edge was reconnected to the same place as before
  MultiUndoCommand* command = new MultiUndoCommand();

  bool reconnected_to_itself = false;

  if (create_edge_already_exists_) {
    if (!cancel) {
      if (create_edge_output_item_ == create_edge_->from_item() && create_edge_->input() == create_edge_input_) {
        reconnected_to_itself = true;
      } else {
        // We are moving (or removing) an existing edge
        command->add_child(new NodeEdgeRemoveCommand(create_edge_->output(), create_edge_->input()));
      }
    }
  } else {
    // We're creating a new edge, which means this UI object is only temporary
    delete create_edge_;
  }

  create_edge_ = nullptr;

  // Clear highlight if we set one
  if (create_edge_output_item_) {
    create_edge_output_item_->SetHighlighted(false);
  }
  if (create_edge_input_item_) {
    create_edge_input_item_->SetHighlighted(false);
  }

  NodeInput &creating_input = create_edge_input_;
  if (create_edge_output_item_ && create_edge_input_item_ && !cancel) {
    if (creating_input.IsValid()) {
      // Make connection
      if (!reconnected_to_itself) {
        Node *creating_output = create_edge_output_item_->GetNode();

        while (NodeGroup *output_group = dynamic_cast<NodeGroup*>(creating_output)) {
          creating_output = output_group->GetOutputPassthrough();
        }

        while (NodeGroup *input_group = dynamic_cast<NodeGroup*>(creating_input.node())) {
          creating_input = input_group->GetInputFromID(creating_input.input());
        }

        if (creating_input.IsConnected()) {
          Node::OutputConnection existing_edge_to_remove = {creating_input.GetConnectedOutput(), creating_input};

          Node *already_connected_output = creating_input.GetConnectedOutput();
          NodeViewContext *ctx = GetContextItemFromNodeItem(create_edge_input_item_);
          if (ctx && !ctx->GetItemFromMap(already_connected_output)) {
            if (QMessageBox::warning(this, QString(), tr("Input \"%1\" is currently connected to node \"%2\", which is not visible in this context. "
                                                         "By connecting this, that connection will be removed. Do you wish to continue?").arg(creating_input.name(), already_connected_output->GetLabelAndName()),
                                     QMessageBox::Yes | QMessageBox::No) == QMessageBox::No) {
              cancel = true;
            }
          }

          if (!cancel) {
            command->add_child(new NodeEdgeRemoveCommand(existing_edge_to_remove.first, existing_edge_to_remove.second));
          }
        }

        if (!cancel) {
          command->add_child(new NodeEdgeAddCommand(creating_output, creating_input));

          // If the output is not in the input's context, add it now. We check the item rather than
          // the node itself, because sometimes a node may not be in the context but another node
          // representing it will be (e.g. groups)
          if (!scene_.context_map().value(create_edge_input_item_->GetContext())->GetItemFromMap(creating_output)) {
            command->add_child(new NodeSetPositionCommand(creating_output, create_edge_input_item_->GetContext(), scene_.context_map().value(create_edge_input_item_->GetContext())->MapScenePosToNodePosInContext(create_edge_output_item_->scenePos())));
          }
        }
      }

    }
  }

  creating_input.Reset();
  create_edge_output_item_ = nullptr;
  create_edge_input_item_ = nullptr;

  // Collapse any items we expanded
  for (auto it=create_edge_expanded_items_.crbegin(); it!=create_edge_expanded_items_.crend(); it++) {
    CollapseItem(*it);
  }
  create_edge_expanded_items_.clear();

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

void NodeView::PostPaste(const QVector<Node *> &new_nodes, const Node::PositionMap &map)
{
  QVector<AttachedItem> new_attached;

  NodeViewItem *first_item = nullptr;

  for (int i=0; i<new_nodes.size(); i++) {
    Node *node = new_nodes.at(i);

    // Determine if item had a position, if not don't create an item for it
    NodeViewItem *new_item;

    if (map.contains(node)) {
      new_item = new NodeViewItem(node, nullptr);
      new_item->SetFlowDirection(scene_.GetFlowDirection());
      new_item->SetNodePosition(map.value(node));
      scene_.addItem(new_item);

      if (!first_item) {
        first_item = new_item;
      }
    } else {
      new_item = nullptr;
    }

    new_attached.append({new_item, node, QPointF(0, 0)});
  }

  // Correct positions
  if (first_item) {
    for (int i=0; i<new_attached.size(); i++) {
      AttachedItem &ai = new_attached[i];

      if (ai.item) {
        ai.original_pos = ai.item->pos() - first_item->pos();
      }
    }
  }

  SetAttachedItems(new_attached);
}

void NodeView::ResizeOverlay()
{
  overlay_view_->resize(this->size());
}

NodeViewContext *NodeView::GetContextItemFromNodeItem(NodeViewItem *item)
{
  QGraphicsItem *i = item;
  while ((i = i->parentItem())) {
    if (NodeViewContext *nvc = dynamic_cast<NodeViewContext*>(i)) {
      return nvc;
    }
  }
  return nullptr;
}

void NodeView::SetAttachedItems(const QVector<AttachedItem> &items)
{
  // Detach anything currently attached
  DetachItemsFromCursor();

  attached_items_ = items;

  // Move to cursor
  MoveAttachedNodesToCursor(mapFromGlobal(QCursor::pos()));
}

}
