#include "FlowScene.hpp"

#include <stdexcept>
#include <utility>

#include <QtWidgets/QGraphicsSceneMoveEvent>
#include <QtWidgets/QFileDialog>
#include <QtCore/QByteArray>
#include <QtCore/QBuffer>
#include <QtCore/QDataStream>
#include <QtCore/QFile>

#include <QtCore/QJsonObject>
#include <QtCore/QJsonArray>
#include <QtCore/QtGlobal>
#include <QtCore/QDebug>
#include <QFileInfo>
#include <QString>
#include <QDir>

#include "Node.hpp"
#include "NodeGraphicsObject.hpp"

#include "NodeGraphicsObject.hpp"
#include "ConnectionGraphicsObject.hpp"

#include "Connection.hpp"

#include "FlowView.hpp"
#include "DataModelRegistry.hpp"

using QtNodes::FlowScene;
using QtNodes::Node;
using QtNodes::NodeGraphicsObject;
using QtNodes::Connection;
using QtNodes::DataModelRegistry;
using QtNodes::NodeDataModel;
using QtNodes::PortType;
using QtNodes::PortIndex;
using QtNodes::TypeConverter;


FlowScene::
FlowScene(std::shared_ptr<DataModelRegistry> registry,
          QObject * parent)
  : QGraphicsScene(parent)
  , _registry(std::move(registry))
{
  setItemIndexMethod(QGraphicsScene::NoIndex);

  // This connection should come first
  connect(this, &FlowScene::connectionCreated, this, &FlowScene::setupConnectionSignals);
  connect(this, &FlowScene::connectionCreated, this, &FlowScene::sendConnectionCreatedToNodes);
  connect(this, &FlowScene::connectionDeleted, this, &FlowScene::sendConnectionDeletedToNodes);
}

FlowScene::
FlowScene(QObject * parent)
  : FlowScene(std::make_shared<DataModelRegistry>(),
              parent)
{}


FlowScene::
~FlowScene()
{
  clearScene();
}


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

std::shared_ptr<Connection>
FlowScene::
createConnection(PortType connectedPort,
                 Node& node,
                 PortIndex portIndex)
{
  auto connection = std::make_shared<Connection>(connectedPort, node, portIndex);

  auto cgo = detail::make_unique<ConnectionGraphicsObject>(*this, *connection);

  // after this function connection points are set to node port
  connection->setGraphicsObject(std::move(cgo));

  _connections[connection->id()] = connection;

  // Note: this connection isn't truly created yet. It's only partially created.
  // Thus, don't send the connectionCreated(...) signal.

  connect(connection.get(),
          &Connection::connectionCompleted,
          this,
          [this](Connection const& c) {
            connectionCreated(c);
          });

  _changed = true;
  Q_EMIT changed();
  return connection;
}


std::shared_ptr<Connection>
FlowScene::
createConnection(Node& nodeIn,
                 PortIndex portIndexIn,
                 Node& nodeOut,
                 PortIndex portIndexOut,
                 TypeConverter const &converter)
{
  auto connection =
    std::make_shared<Connection>(nodeIn,
                                 portIndexIn,
                                 nodeOut,
                                 portIndexOut,
                                 converter);

  auto cgo = detail::make_unique<ConnectionGraphicsObject>(*this, *connection);

  nodeIn.nodeState().setConnection(PortType::In, portIndexIn, *connection);
  nodeOut.nodeState().setConnection(PortType::Out, portIndexOut, *connection);

  // after this function connection points are set to node port
  connection->setGraphicsObject(std::move(cgo));

  // trigger data propagation
  nodeOut.onDataUpdated(portIndexOut, true);

  _connections[connection->id()] = connection;

  connectionCreated(*connection);

  connect(connection.get(),
          &Connection::connectionCompleted,
          this,
          [this](Connection const& c) {
              connectionCreated(c);
          });

  _changed = true;
  Q_EMIT changed();
  return connection;
}


std::shared_ptr<Connection>
FlowScene::
restoreConnection(QJsonObject const &connectionJson)
{
  QUuid nodeInId  = QUuid(connectionJson["in_id"].toString());
  QUuid nodeOutId = QUuid(connectionJson["out_id"].toString());

  PortIndex portIndexIn  = connectionJson["in_index"].toInt();
  PortIndex portIndexOut = connectionJson["out_index"].toInt();

  auto nodeIn  = _nodes[nodeInId].get();
  auto nodeOut = _nodes[nodeOutId].get();

  auto getConverter = [&]()
  {
    QJsonValue converterVal = connectionJson["converter"];

    if (!converterVal.isUndefined())
    {
      QJsonObject converterJson = converterVal.toObject();

      NodeDataType inType { converterJson["in"].toObject()["id"].toString(),
                            converterJson["in"].toObject()["name"].toString() };

      NodeDataType outType { converterJson["out"].toObject()["id"].toString(),
                             converterJson["out"].toObject()["name"].toString() };

      auto converter  =
        registry().getTypeConverter(outType, inType);

      if (converter)
        return converter;
    }

    return TypeConverter{};
  };

  std::shared_ptr<Connection> connection =
    createConnection(*nodeIn, portIndexIn,
                     *nodeOut, portIndexOut,
                     getConverter());

  // Note: the connectionCreated(...) signal has already been sent
  // by createConnection(...)

  _changed = true;
  Q_EMIT changed();
  return connection;
}


void
FlowScene::
deleteConnection(Connection& connection)
{
  auto it = _connections.find(connection.id());
  if (it != _connections.end()) {
    connection.removeFromNodes();
    _connections.erase(it);
    _changed = true;
  Q_EMIT changed();
  }
}


Node&
FlowScene::
createNode(std::unique_ptr<NodeDataModel> && dataModel)
{
  auto node = detail::make_unique<Node>(std::move(dataModel));

  connect(node.get(), &Node::requestConnectionRemove, this, &FlowScene::deleteConnection);

  auto ngo  = detail::make_unique<NodeGraphicsObject>(*this, *node);

  node->setGraphicsObject(std::move(ngo));

  auto nodePtr = node.get();
  _nodes[node->id()] = std::move(node);

  nodeCreated(*nodePtr);
  return *nodePtr;
  _changed = true;
  Q_EMIT changed();
}


Node&
FlowScene::
restoreNode(QJsonObject const& nodeJson)
{
  QString modelName = nodeJson["model"].toObject()["name"].toString();

  auto dataModel = registry().create(modelName);

  if (!dataModel)
    throw std::logic_error(std::string("No registered model with name ") +
                           modelName.toLocal8Bit().data());

  auto node = detail::make_unique<Node>(std::move(dataModel));

  connect(node.get(), &Node::requestConnectionRemove, this, &FlowScene::deleteConnection);

  auto ngo  = detail::make_unique<NodeGraphicsObject>(*this, *node);
  node->setGraphicsObject(std::move(ngo));

  node->restore(nodeJson);

  auto nodePtr = node.get();
  _nodes[node->id()] = std::move(node);

  nodePlaced(*nodePtr);
  nodeCreated(*nodePtr);
  return *nodePtr;
  _changed = true;
  Q_EMIT changed();
}


void
FlowScene::
removeNode(Node& node)
{
  // call signal
  nodeDeleted(node);

  if (_last_selected == &node)
  {
    Q_EMIT nodeSelected(nullptr);
    _last_selected = nullptr;
  }

  for(auto portType: {PortType::In,PortType::Out})
  {
    auto nodeState = node.nodeState();
    auto const & nodeEntries = nodeState.getEntries(portType);

    for (auto &connections : nodeEntries)
    {
      for (auto const &pair : connections)
        deleteConnection(*pair.second);
    }
  }

  _nodes.erase(node.id());
  _changed = true;
  Q_EMIT changed();
}


DataModelRegistry&
FlowScene::
registry() const
{
  return *_registry;
}


void
FlowScene::
setRegistry(std::shared_ptr<DataModelRegistry> registry)
{
  _registry = std::move(registry);
}


void
FlowScene::
iterateOverNodes(std::function<void(Node*)> const & visitor)
{
  for (const auto& _node : _nodes)
  {
    visitor(_node.second.get());
  }
}


void
FlowScene::
iterateOverNodeData(std::function<void(NodeDataModel*)> const & visitor)
{
  for (const auto& _node : _nodes)
  {
    visitor(_node.second->nodeDataModel());
  }
}


void
FlowScene::
iterateOverNodeDataDependentOrder(std::function<void(NodeDataModel*)> const & visitor)
{
  std::set<QUuid> visitedNodesSet;

  //A leaf node is a node with no input ports, or all possible input ports empty
  auto isNodeLeaf =
    [](Node const &node, NodeDataModel const &model)
    {
      for (unsigned int i = 0; i < model.nPorts(PortType::In); ++i)
      {
        auto& connections = node.nodeState().connectionsRef(PortType::In, i);
        if (!connections.empty())
        {
          return false;
        }
      }

      return true;
    };

  //Iterate over "leaf" nodes
  for (auto const &_node : _nodes)
  {
    auto const &node = _node.second;
    auto model       = node->nodeDataModel();

    if (isNodeLeaf(*node, *model))
    {
      visitor(model);
      visitedNodesSet.insert(node->id());
    }
  }

  auto areNodeInputsVisitedBefore =
    [&](Node const &node, NodeDataModel const &model)
    {
      for (size_t i = 0; i < model.nPorts(PortType::In); ++i)
      {
        auto& connections = node.nodeState().connectionsRef(PortType::In, i);

        for (auto& conn : connections)
        {
          if (visitedNodesSet.find(conn.second->getNode(PortType::Out)->id()) == visitedNodesSet.end())
          {
            return false;
          }
        }
      }

      return true;
    };

  //Iterate over dependent nodes
  while (_nodes.size() != visitedNodesSet.size())
  {
    for (auto const &_node : _nodes)
    {
      auto const &node = _node.second;
      if (visitedNodesSet.find(node->id()) != visitedNodesSet.end())
        continue;

      auto model = node->nodeDataModel();

      if (areNodeInputsVisitedBefore(*node, *model))
      {
        visitor(model);
        visitedNodesSet.insert(node->id());
      }
    }
  }
}


QPointF
FlowScene::
getNodePosition(const Node& node) const
{
  return node.nodeGraphicsObject().pos();
}


void
FlowScene::
setNodePosition(Node& node, const QPointF& pos) const
{
  node.nodeGraphicsObject().setPos(pos);
  node.nodeGraphicsObject().moveConnections();
}


QSizeF
FlowScene::
getNodeSize(const Node& node) const
{
  return QSizeF(node.nodeGeometry().width(), node.nodeGeometry().height());
}


std::unordered_map<QUuid, std::unique_ptr<Node> > const &
FlowScene::
nodes() const
{
  return _nodes;
}


std::unordered_map<QUuid, std::shared_ptr<Connection> > const &
FlowScene::
connections() const
{
  return _connections;
}


std::vector<Node*>
FlowScene::
allNodes() const
{
  std::vector<Node*> nodes;

  std::transform(_nodes.begin(),
                 _nodes.end(),
                 std::back_inserter(nodes),
                 [](std::pair<QUuid const, std::unique_ptr<Node>> const & p) { return p.second.get(); });

  return nodes;
}


std::vector<Node*>
FlowScene::
selectedNodes() const
{
  QList<QGraphicsItem*> graphicsItems = selectedItems();

  std::vector<Node*> ret;
  ret.reserve(graphicsItems.size());

  for (QGraphicsItem* item : graphicsItems)
  {
    auto ngo = qgraphicsitem_cast<NodeGraphicsObject*>(item);

    if (ngo != nullptr)
    {
      ret.push_back(&ngo->node());
    }
  }

  return ret;
}


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

void
FlowScene::
clearScene()
{
  // Manual node cleanup. Simply clearing the holding datastructures doesn't work, the code crashes when
  // there are both nodes and connections in the scene. (The data propagation internal logic tries to propagate
  // data through already freed connections.)
  while (_connections.size() > 0)
  {
    deleteConnection( *_connections.begin()->second );
  }

  while (_nodes.size() > 0)
  {
    removeNode( *_nodes.begin()->second );
  }
  
  _changed = true;
  Q_EMIT changed();
}


bool
FlowScene::
save()
{
  bool use_relative = false;
  QString fileName;
  if (!_relative_path.length())
  {
    fileName =  QFileDialog::getSaveFileName(nullptr,
                                             tr("Save Noggit Script file"),
                                             "./scripts/",
                                             tr("Noggit Script file (*.ns)"));
  }
  else
  {
    use_relative = true;
    fileName = QDir("./scripts/").absoluteFilePath(_relative_path);
  }

  if (!fileName.isEmpty())
  {
    if (!fileName.endsWith("ns", Qt::CaseInsensitive))
      fileName += ".ns";

    QFile file(fileName);
    if (file.open(QIODevice::WriteOnly))
    {
      file.write(saveToMemory());
    }
    auto file_info = QFileInfo(file.fileName());
    _name = file_info.fileName();

    if (!use_relative)
      _relative_path = QDir("./scripts/").relativeFilePath(file_info.absoluteFilePath());

    _changed = false;

    return true;
  }

  return false;
}


bool
FlowScene::
load()
{
  QString fileName =
    QFileDialog::getOpenFileName(nullptr,
                                 tr("Open Noggit Script file"),
                                 "./scripts/",
                                 tr("Noggit Script files (*.ns)"));

  if (!QFileInfo::exists(fileName))
    return false;

  return load(fileName);
}

bool FlowScene::load(QString const& filepath)
{
  if (!QFileInfo::exists(filepath))
    return false;

  QFile file(filepath);

  if (file.open(QIODevice::ReadOnly)){
    clearScene();
    loadFromMemory(file.readAll());

    auto file_info = QFileInfo(file.fileName());
    _name = file_info.fileName();
    _relative_path = QDir("./scripts/").relativeFilePath(file_info.absoluteFilePath());
    _changed = false;
    return true;
  }

  return false;
}

QJsonDocument FlowScene::saveToJson(QJsonDocument::JsonFormat format) const
{
  QJsonObject sceneJson;

  QJsonArray nodesJsonArray;

  for (auto const & pair : _nodes)
  {
    auto const &node = pair.second;

    nodesJsonArray.append(node->save());
  }

  sceneJson["nodes"] = nodesJsonArray;

  QJsonArray connectionJsonArray;
  for (auto const & pair : _connections)
  {
    auto const &connection = pair.second;

    QJsonObject connectionJson = connection->save();

    if (!connectionJson.isEmpty())
      connectionJsonArray.append(connectionJson);
  }

  sceneJson["connections"] = connectionJsonArray;

  QJsonDocument document(sceneJson);

  return std::move(document);
}


QByteArray
FlowScene::
saveToMemory(QJsonDocument::JsonFormat format) const
{
  QJsonDocument document = saveToJson(format);
  return document.toJson(format);
}

void
FlowScene::
loadFromMemory(const QByteArray& data)
{
  const QJsonObject jsonDocument = QJsonDocument::fromJson(data).object();
  loadFromMemory(jsonDocument);
}

void FlowScene::loadFromMemory(const QJsonObject& data)
{
   QJsonArray nodesJsonArray = data["nodes"].toArray();

   std::vector<Node*> nodes_restored;
   nodes_restored.reserve(nodesJsonArray.count());

   for (QJsonValueRef node : nodesJsonArray)
   {
     nodes_restored.push_back(&restoreNode(node.toObject()));
   }

   QJsonArray connectionJsonArray = data["connections"].toArray();

   for (QJsonValueRef connection : connectionJsonArray)
   {
      restoreConnection(connection.toObject());
   }

   for (int i = 0; i < nodesJsonArray.count(); ++i)
   {
     nodes_restored[i]->nodeDataModel()->restorePostConnection(nodesJsonArray[i].toObject()["model"].toObject());
   }

}

QByteArray
FlowScene::
copyNodes(const std::vector<QtNodes::Node*>& nodes) const
{
   QJsonObject sceneJson;
   QSet<QUuid> nodeIds;
   QJsonArray nodesJsonArray;

   // center of gravity
   std::vector<int> cogX;
   std::vector<int> cogY;

   for (auto const & n : nodes)
   {
      auto nodeJson = n->save();
      nodesJsonArray.append(nodeJson);
      nodeIds.insert(n->id());

      auto nodePosition = nodeJson = nodeJson["position"].toObject();
      cogX.emplace_back(nodePosition["x"].toDouble());
      cogY.emplace_back(nodePosition["y"].toDouble());
   }

   sceneJson["nodes"] = nodesJsonArray;

   QJsonArray connectionJsonArray;
   for (auto const & pair : connections())
   {
      auto const &connection = pair.second;

      auto inNodeId = connection->getNode(PortType::In)->id();
      auto outNodeId = connection->getNode(PortType::Out)->id();
      if (nodeIds.contains(inNodeId) && nodeIds.contains(outNodeId))
      {
         QJsonObject connectionJson = connection->save();
         if (!connectionJson.isEmpty())
            connectionJsonArray.append(connectionJson);
      }
   }

   sceneJson["connections"] = connectionJsonArray;

   // save center of gravity too
   if(cogX.size() > 0){
      QJsonObject jsonCog;
      jsonCog["x"] = std::accumulate(cogX.begin(), cogX.end(), 0) / static_cast<double>(cogX.size());
      jsonCog["y"] = std::accumulate(cogY.begin(), cogY.end(), 0) / static_cast<double>(cogY.size());
      sceneJson["cog"] = jsonCog;
   }

   QJsonDocument document(sceneJson);

   return document.toJson();
}

void FlowScene::pasteNodes(const QByteArray& data, const QPointF& pointOffset)
{
   QMap<QUuid,QUuid> newIdsMap;
   QJsonObject jsonDocument = QJsonDocument::fromJson(data).object();

   // if exists a center of gravity, take it
   auto jsonCog = jsonDocument["cog"].toObject();
   double offsetX = pointOffset.x() - jsonCog["x"].toDouble();
   double offsetY = pointOffset.y() - jsonCog["y"].toDouble();

   QJsonArray nodesJsonArray = jsonDocument["nodes"].toArray();
   for (auto node : nodesJsonArray)
   {
      auto nodeJson = node.toObject();

      QString oldId = nodeJson["id"].toString();
      if (!newIdsMap.contains(oldId))
         newIdsMap[oldId] = QUuid::createUuid();

      nodeJson["id"] = newIdsMap[oldId].toString();

      // update position of the copy of the nodes
      auto nodePosition = nodeJson["position"].toObject();
      nodePosition["x"] = nodePosition["x"].toDouble() + offsetX;
      nodePosition["y"] = nodePosition["y"].toDouble() + offsetY;
      nodeJson["position"] = nodePosition;

      node = nodeJson;
   }
   jsonDocument["nodes"] = nodesJsonArray;

   QJsonArray connectionJsonArray = jsonDocument["connections"].toArray();
   for (QJsonValueRef connection : connectionJsonArray)
   {
      auto connectionJson = connection.toObject();

      QString oldId = connectionJson["in_id"].toString();
      if (newIdsMap.contains(oldId))
         connectionJson["in_id"] = newIdsMap[oldId].toString();

      oldId = connectionJson["out_id"].toString();
      if (newIdsMap.contains(oldId))
         connectionJson["out_id"] = newIdsMap[oldId].toString();

      connection = connectionJson;
   }
   jsonDocument["connections"] = connectionJsonArray;

   try {
      loadFromMemory(QJsonDocument(jsonDocument).toJson());
   } catch (const std::logic_error& e) {
      // prevent pasting if receving scene has not registered the source node
      // it can be happens copying and pasting between different scenes

      qDebug() << "cannot copy selected nodes";
   }
 }


void
FlowScene::
setupConnectionSignals(Connection const& c)
{
  connect(&c,
          &Connection::connectionMadeIncomplete,
          this,
          &FlowScene::connectionDeleted,
          Qt::UniqueConnection);
}


void
FlowScene::
sendConnectionCreatedToNodes(Connection const& c)
{
  Node* from = c.getNode(PortType::Out);
  Node* to   = c.getNode(PortType::In);

  Q_ASSERT(from != nullptr);
  Q_ASSERT(to != nullptr);

  from->nodeDataModel()->outputConnectionCreated(c);
  to->nodeDataModel()->inputConnectionCreated(c);
}


void
FlowScene::
sendConnectionDeletedToNodes(Connection const& c)
{
  Node* from = c.getNode(PortType::Out);
  Node* to   = c.getNode(PortType::In);

  Q_ASSERT(from != nullptr);
  Q_ASSERT(to != nullptr);

  from->nodeDataModel()->outputConnectionDeleted(c);
  to->nodeDataModel()->inputConnectionDeleted(c);
}


//------------------------------------------------------------------------------
namespace QtNodes
{

Node*
locateNodeAt(QPointF scenePoint, FlowScene &scene,
             QTransform const & viewTransform)
{
  // items under cursor
  QList<QGraphicsItem*> items =
    scene.items(scenePoint,
                Qt::IntersectsItemShape,
                Qt::DescendingOrder,
                viewTransform);

  //// items convertable to NodeGraphicsObject
  std::vector<QGraphicsItem*> filteredItems;

  std::copy_if(items.begin(),
               items.end(),
               std::back_inserter(filteredItems),
               [] (QGraphicsItem * item)
    {
      return (dynamic_cast<NodeGraphicsObject*>(item) != nullptr);
    });

  Node* resultNode = nullptr;

  if (!filteredItems.empty())
  {
    QGraphicsItem* graphicsItem = filteredItems.front();
    auto ngo = dynamic_cast<NodeGraphicsObject*>(graphicsItem);

    resultNode = &ngo->node();
  }

  return resultNode;
}
}
