#include "story_graph_model.h"
#include "media_node_model.h"

#include <QtNodes/ConnectionIdUtils>
#include <QJsonArray>
#include <iterator>
#include <QDir>

extern "C" int miniaudio_play(const char* filename);

StoryGraphModel::StoryGraphModel(StoryProject &project)
    : m_project(project)
{
    m_audioThread = std::thread( std::bind(&StoryGraphModel::AudioThread, this) );
//    connect(m_player, &QMediaPlayer::playbackStateChanged, this, [&](QMediaPlayer::PlaybackState newState) {
//        if (newState == QMediaPlayer::PlaybackState::StoppedState) {
//            m_player->stop();
//            emit sigAudioStopped();
//        }
//    });
}

StoryGraphModel::~StoryGraphModel()
{
    // Quit audio thread
    m_audioQueue.push({"quit", ""});
    if (m_audioThread.joinable())
    {
        m_audioThread.join();
    }
}

void StoryGraphModel::AudioThread()
{
    for (;;)
    {
        auto cmd = m_audioQueue.front();

        if (cmd.order == "play") {
            miniaudio_play(cmd.filename.c_str());
            QMetaObject::invokeMethod(this, "sigAudioStopped", Qt::QueuedConnection);
            m_audioQueue.pop();
        } else {
            return;
        }
    }
}

void StoryGraphModel::PlaySoundFile(const QString &fileName)
{
    m_audioQueue.push({"play", fileName.toStdString()});
}


std::unordered_set<NodeId> StoryGraphModel::allNodeIds() const
{
    return _nodeIds;
}

std::unordered_set<ConnectionId> StoryGraphModel::allConnectionIds(NodeId const nodeId) const
{
    std::unordered_set<ConnectionId> result;

    std::copy_if(_connectivity.begin(),
                 _connectivity.end(),
                 std::inserter(result, std::end(result)),
                 [&nodeId](ConnectionId const &cid) {
                     return cid.inNodeId == nodeId || cid.outNodeId == nodeId;
                 });

    return result;
}

std::unordered_set<ConnectionId> StoryGraphModel::connections(NodeId nodeId,
                                                                PortType portType,
                                                                PortIndex portIndex) const
{
    std::unordered_set<ConnectionId> result;

    std::copy_if(_connectivity.begin(),
                 _connectivity.end(),
                 std::inserter(result, std::end(result)),
                 [&portType, &portIndex, &nodeId](ConnectionId const &cid) {
                     return (getNodeId(portType, cid) == nodeId
                             && getPortIndex(portType, cid) == portIndex);
                 });

    return result;
}

bool StoryGraphModel::connectionExists(ConnectionId const connectionId) const
{
    return (_connectivity.find(connectionId) != _connectivity.end());
}

NodeId StoryGraphModel::addNode(QString const nodeType)
{
    NodeId id = InvalidNodeId;
    std::cout << "-------------------> add node: " << nodeType.toStdString() << std::endl;

    try {

        NodeId newId = newNodeId();

        // Create new node.
        _nodeIds.insert(newId);

        _models[newId] = createNode(nodeType.toStdString());
        _models[newId]->setNodeId(newId);

        Q_EMIT nodeCreated(newId);

        id = newId;

    } catch(std::invalid_argument e) {
        std::cout << e.what() << std::endl;
    }

    return id;
}


NodeId StoryGraphModel::addNode(std::shared_ptr<StoryNodeBase> model)
{
    NodeId newId = newNodeId();

    // Create new node.
    _nodeIds.insert(newId);

    _models[newId] = model;

    Q_EMIT nodeCreated(newId);

    return newId;
}



bool StoryGraphModel::connectionPossible(ConnectionId const connectionId) const
{
    return !connectionExists(connectionId);
}

void StoryGraphModel::addConnection(ConnectionId const connectionId)
{
    _connectivity.insert(connectionId);

    Q_EMIT connectionCreated(connectionId);
}

bool StoryGraphModel::nodeExists(NodeId const nodeId) const
{
    return (_nodeIds.find(nodeId) != _nodeIds.end());
}


QVariant StoryGraphModel::nodeData(NodeId nodeId, NodeRole role) const
{
    QVariant result;

    auto it = _models.find(nodeId);
    if (it == _models.end())
        return result;

    auto &model = it->second;

    switch (role) {
    case NodeRole::Type:
        result = QString("Default Node Type");
        break;

    case NodeRole::Position:
        result = model->geometryData().pos;
        break;

    case NodeRole::Size:
        result = model->geometryData().size;
        break;

    case NodeRole::CaptionVisible:
        result = true;
        break;

    case NodeRole::Caption:
        result = model->caption();
        break;

    case NodeRole::Style: {
        result = model->nodeStyle().toJson().toVariantMap();
    } break;

    case NodeRole::InternalData:
        break;

    case NodeRole::InPortCount:
        result = model->nPorts(PortType::In);
        break;

    case NodeRole::OutPortCount:
        result = model->nPorts(PortType::Out);
        break;

    case NodeRole::Widget: {
        auto w = model->embeddedWidget();
        result = QVariant::fromValue(w);
        break;
    }
    case NodeRole::Id:
        result = model->getNodeId();
        break;
    }

    return result;
}

void StoryGraphModel::SetInternalData(NodeId nodeId, nlohmann::json &j)
{
    auto it = _models.find(nodeId);
    if (it == _models.end())
        return;

    auto &model = it->second;

    model->setInternalData(j);
}

bool StoryGraphModel::setNodeData(NodeId nodeId, NodeRole role, QVariant value)
{
    bool result = false;

    auto it = _models.find(nodeId);
    if (it == _models.end())
        return result;

    auto &model = it->second;

    switch (role) {
    case NodeRole::Id:
        break;
    case NodeRole::Type:
        break;
    case NodeRole::Position: {        
        model->geometryData().pos = value.value<QPointF>();

        Q_EMIT nodePositionUpdated(nodeId);

        result = true;
    } break;

    case NodeRole::Size: {
        model->geometryData().size = value.value<QSize>();
        result = true;
    } break;

    case NodeRole::CaptionVisible:
        break;

    case NodeRole::Caption:
        break;

    case NodeRole::Style:
        break;

    case NodeRole::InternalData:
        break;

    case NodeRole::InPortCount:
        break;

    case NodeRole::OutPortCount:
        break;

    case NodeRole::Widget:
        break;
    }

    return result;
}

QVariant StoryGraphModel::portData(NodeId nodeId,
                                     PortType portType,
                                     PortIndex portIndex,
                                     PortRole role) const
{
    switch (role) {
    case PortRole::Data:
        return QVariant();
        break;

    case PortRole::DataType:
        return QVariant();
        break;

    case PortRole::ConnectionPolicyRole:
        return QVariant::fromValue(ConnectionPolicy::One);
        break;

    case PortRole::CaptionVisible:
        return true;
        break;

    case PortRole::Caption:
        if (portType == PortType::In)
            return QString::fromUtf8("Port In");
        else
            return QString::fromUtf8("Port Out");

        break;
    }

    return QVariant();
}

bool StoryGraphModel::setPortData(
    NodeId nodeId, PortType portType, PortIndex portIndex, QVariant const &value, PortRole role)
{
    Q_UNUSED(nodeId);
    Q_UNUSED(portType);
    Q_UNUSED(portIndex);
    Q_UNUSED(value);
    Q_UNUSED(role);

    return false;
}

bool StoryGraphModel::deleteConnection(ConnectionId const connectionId)
{
    bool disconnected = false;

    auto it = _connectivity.find(connectionId);

    if (it != _connectivity.end()) {
        disconnected = true;

        _connectivity.erase(it);
    };

    if (disconnected)
        Q_EMIT connectionDeleted(connectionId);

    return disconnected;
}

bool StoryGraphModel::deleteNode(NodeId const nodeId)
{
    // Delete connections to this node first.
    auto connectionIds = allConnectionIds(nodeId);
    for (auto &cId : connectionIds) {
        deleteConnection(cId);
    }

    _nodeIds.erase(nodeId);
    Q_EMIT nodeDeleted(nodeId);

    return true;
}

namespace QtNodes {
    void to_json(nlohmann::json& j, const ConnectionId& p) {
        j = nlohmann::json{
            {"outNodeId", static_cast<qint64>(p.outNodeId)},
            {"outPortIndex", static_cast<qint64>(p.outPortIndex)},
            {"inNodeId", static_cast<qint64>(p.inNodeId)},
            {"inPortIndex", static_cast<qint64>(p.inPortIndex)},
        };
    }

    void from_json(const nlohmann::json& j, ConnectionId& p) {
        j.at("outNodeId").get_to(p.outNodeId);
        j.at("outPortIndex").get_to(p.outPortIndex);
        j.at("inNodeId").get_to(p.inNodeId);
        j.at("inPortIndex").get_to(p.inPortIndex);
    }
} // namespace QtNodes


nlohmann::json StoryGraphModel::Save() const
{
    nlohmann::json j;
    nlohmann::json nodesJsonArray;
    for (auto const nodeId : allNodeIds()) {
        nodesJsonArray.push_back(SaveNode(nodeId));
    }
    j["nodes"] = nodesJsonArray;

    nlohmann::json connJsonArray;
    for (auto const &cid : _connectivity) {
        nlohmann::json o = cid;
        connJsonArray.push_back(o);
    }
    j["connections"] = connJsonArray;

    return j;
}

void StoryGraphModel::Load(const nlohmann::json &j)
{
    nlohmann::json nodesJsonArray = j["nodes"];

    for (auto& element : nodesJsonArray) {
        LoadNode(element);
    }

    std::cout << j.dump(4) << std::endl;

    nlohmann::json connectionJsonArray = j["connections"];

    for (auto& connection : connectionJsonArray) {
        ConnectionId connId = connection.get<QtNodes::ConnectionId>();
        // Restore the connection
        addConnection(connId);
    }
}

nlohmann::json StoryGraphModel::SaveNode(NodeId const nodeId) const
{
    nlohmann::json nodeJson;
    nodeJson["id"] = static_cast<qint64>(nodeId);

    auto it = _models.find(nodeId);
    if (it == _models.end())
        return nodeJson;

    auto &model = it->second;

    nodeJson["internal-data"] = model->ToJson();

    {
        QPointF const pos = nodeData(nodeId, NodeRole::Position).value<QPointF>();

        nlohmann::json posJson;
        posJson["x"] = pos.x();
        posJson["y"] = pos.y();
        nodeJson["position"] = posJson;

        nodeJson["inPortCount"] = nodeData(nodeId, NodeRole::InPortCount).value<int>();
        nodeJson["outPortCount"] = nodeData(nodeId, NodeRole::OutPortCount).value<int>();
    }

    return nodeJson;
}


void StoryGraphModel::LoadNode(const nlohmann::json &nodeJson)
{
    // Possibility of the id clash when reading it from json and not generating a
    // new value.
    // 1. When restoring a scene from a file.
    // Conflict is not possible because the scene must be cleared by the time of
    // loading.
    // 2. When undoing the deletion command.  Conflict is not possible
    // because all the new ids were created past the removed nodes.
    NodeId restoredNodeId = nodeJson["id"].get<int>();

    _nextNodeId = std::max(_nextNodeId, restoredNodeId + 1);

    nlohmann::json internalDataJson = nodeJson["internal-data"];

    std::string delegateModelName = internalDataJson["model-name"].get<std::string>();

//    std::unique_ptr<NodeDelegateModel> model = _registry->create(delegateModelName);

    auto model = createNode(delegateModelName);

    if (model) {
//        connect(model.get(),
//                &NodeDelegateModel::dataUpdated,
//                [restoredNodeId, this](PortIndex const portIndex) {
//                    onOutPortDataUpdated(restoredNodeId, portIndex);
//                });


        _models[restoredNodeId] = model;
        model->setNodeId(restoredNodeId);
        _nodeIds.insert(restoredNodeId);

        Q_EMIT nodeCreated(restoredNodeId);

        nlohmann::json posJson = nodeJson["position"];
        QPointF const pos(posJson["x"].get<double>(), posJson["y"].get<double>());

        setNodeData(restoredNodeId, NodeRole::Position, pos);
        model->SetOutPortCount(nodeJson["outPortCount"].get<int>());

        _models[restoredNodeId]->FromJson(internalDataJson);
    } else {
        throw std::logic_error(std::string("No registered model with name ") + delegateModelName);
    }
}

NodeId StoryGraphModel::FindFirstNode() const
{
    NodeId id{InvalidNodeId};

    // First node is the one without connection on its input port

    for (auto const nodeId : allNodeIds())
    {
        bool foundConnection{false};
        for (auto& c : _connectivity)
        {
            if (c.inNodeId == nodeId)
            {
                foundConnection = true;
            }
        }

        if (!foundConnection)
        {
            id = nodeId;
            qDebug() << "First node is: " << id;
            break;
        }
    }

    return id;
}

std::string StoryGraphModel::Build()
{
    std::stringstream chip32;

    NodeId firstNode = FindFirstNode();

    chip32 << "\tjump    " << GetNodeEntryLabel(firstNode) << "\r\n";

    // First generate all constants
    for (auto const nodeId : allNodeIds())
    {
        auto it = _models.find(nodeId);
        if (it != _models.end())
        {
            chip32 << it->second->GenerateConstants() << "\n";
        }
    }

    nlohmann::json nodesJsonArray;
    for (auto const nodeId : allNodeIds())
    {
        auto it = _models.find(nodeId);
        if (it != _models.end())
        {
            chip32 << it->second->Build() << "\n";
        }
    }

    return chip32.str();
}

std::string StoryGraphModel::GetNodeEntryLabel(NodeId nodeId) const
{
    std::string label;
    auto it = _models.find(nodeId);
    if (it != _models.end())
    {
        label = it->second->EntryLabel();
    }
    return label;
}

void StoryGraphModel::addPort(NodeId nodeId, PortType portType, PortIndex portIndex)
{
    // STAGE 1.
    // Compute new addresses for the existing connections that are shifted and
    // placed after the new ones
    PortIndex first = portIndex;
    PortIndex last = first;
    portsAboutToBeInserted(nodeId, portType, first, last);

    // STAGE 3. Re-create previouly existed and now shifted connections
    portsInserted();   
}

void StoryGraphModel::removePort(NodeId nodeId, PortType portType, PortIndex portIndex)
{
    // STAGE 1.
    // Compute new addresses for the existing connections that are shifted upwards
    // instead of the deleted ports.
    PortIndex first = portIndex;
    PortIndex last = first;
    portsAboutToBeDeleted(nodeId, portType, first, last);

    portsDeleted();
}

QString StoryGraphModel::GetImagesDir() const
{
    return QString(m_project.GetWorkingDir().c_str()) + QDir::separator() + "images";
}

QString StoryGraphModel::BuildFullImagePath(const QString &fileName) const
{
    return GetImagesDir() + QDir::separator() + fileName;
}

QString StoryGraphModel::GetSoundsDir() const
{
    return QString(m_project.GetWorkingDir().c_str()) + QDir::separator() + "sounds";
}

QString StoryGraphModel::BuildFullSoundPath(const QString &fileName) const
{
    return GetSoundsDir() + QDir::separator() + fileName;
}

void StoryGraphModel::Clear()
{
    _nodeIds.clear();
    _connectivity.clear();
    _models.clear();
    emit modelReset();
}
