/*
 * sxesession.cpp - Sxe Session
 * Copyright (C) 2006  Joonas Govenius
 *
 * 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 2
 * 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 library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 *
 */

#include "sxesession.h"
#include "sxemanager.h"

#include "QTimer"
#include "QUuid"

// The maxlength of a chdata that gets put in one edit
enum {MAXCHDATA = 1024};

using namespace XMPP;

//----------------------------------------------------------------------------
// SxeSession
//----------------------------------------------------------------------------

SxeSession::SxeSession(SxeManager *manager, const Jid &target, const QString &session, const Jid &ownJid,
                        bool groupChat, bool serverSupport, const QList<QString> &features)
    : QObject(manager)
    , session_(session)
    , target_(target)
    , ownJid_(ownJid)
    , groupChat_(groupChat)
    , serverSupport_(serverSupport)
    , queueing_(false)
    , importing_(false)
    , features_(features)
    , uuidMaxPostfix_(0)

{
    setUUIDPrefix();
}

SxeSession::~SxeSession() {
    qDebug("destruct SxeSession");
    qDeleteAll(recordByNodeId_);
    recordByNodeId_.clear();
    emit sessionEnded(this);
}

void SxeSession::initializeDocument(const QDomDocument &doc) {
    bool origImporting = importing_;

    importing_ = true;

    // reset the document
    doc_ = QDomDocument();
    foreach(SxeRecord* meta, recordByNodeId_.values())
        meta->deleteLater();
    // recordByNode_.clear();
    recordByNodeId_.clear();
    queuedIncomingEdits_.clear();
    queuedOutgoingEdits_.clear();


    // import prolog
    doc_.setContent(parseProlog(doc));

    // import other nodes
    // create all nodes recursively from root
    QDomNodeList children = doc.childNodes();
    for(int i = 0; i < children.size(); i++) {
        // skip the XML declaration <?xml ...?> because it isn't a processing instruction
        if(!(children.at(i).isProcessingInstruction() && children.at(i).toProcessingInstruction().target().toLower() == "xml"))
            generateNewNode(children.at(i), QString(), i);
    }

    importing_ = origImporting;
}

void SxeSession::processIncomingSxeElement(const QDomElement &sxe, const QString &id) {
    if(id.isEmpty() && !importing_) {
        qDebug("Trying to process an SXE element without an associated id!");
        return;
    }

    if(processSxe(sxe, id))
        emit documentUpdated(true);
}

bool SxeSession::processSxe(const QDomElement &sxe, const QString &id) {
    // Don't accept duplicates
    if(!id.isEmpty() && usedSxeIds_.contains(id)) {
        qDebug() << QString("Tried to process a duplicate %1 (received: %2).").arg(sxe.attribute("id")).arg(usedSxeIds_.size()).toLatin1();
        return false;
    }

    if(!id.isEmpty())
        usedSxeIds_ += id;

    // store incoming edits when queueing
    if(queueing_) {
        // Make sure the element is not already in the queue.
        foreach(IncomingEdit i, queuedIncomingEdits_)
            if(i.xml == sxe)  return false;

        IncomingEdit incoming;
        incoming.id = id;
        incoming.xml = sxe.cloneNode(true).toElement();

        queuedIncomingEdits_.append(incoming);
        return false;
    }

    // create an SxeEdit for each child of the <sxe/>
    QList<SxeEdit*> edits;
    for(QDomNode n = sxe.firstChild(); !n.isNull(); n = n.nextSibling()) {
        if(n.nodeName() == "new")
            edits.append(new SxeNewEdit(n.toElement()));
        else if(n.nodeName() == "set")
            edits.append(new SxeRecordEdit(n.toElement()));
        else if(n.nodeName() == "remove")
            edits.append(new SxeRemoveEdit(n.toElement()));
    }

    if (edits.size() == 0)  return false;

    // process all the edits
    foreach(SxeEdit* e, edits) {
        SxeRecord* meta;
        if(e->type() == SxeEdit::New)
            meta = createRecord(e->rid());
        else
            meta = record(e->rid());

        if(meta)
            meta->apply(doc_, e);
    }

    return true;
}

const QDomDocument& SxeSession::document() const {
    return doc_;
}

bool SxeSession::groupChat() const {
    return groupChat_;
}

bool SxeSession::serverSupport() const {
    return serverSupport_;
}

const Jid SxeSession::target() const {
    return target_;
}

const QString SxeSession::session() const {
    return session_;
}

const Jid SxeSession::ownJid() const {
    return ownJid_;
}

const QList<QString> SxeSession::features() const {
    return features_;
}

QList<const SxeEdit*> SxeSession::startQueueing() {
    // do nothing if already queueing
    if(queueing_)
        return QList<const SxeEdit*>();

    queueing_ = true;

    // Return all the effective Edits to the session so far (snapshot)
    // make sure that they are added in the right order (parents first)
    QString rootid;
    QList<const SxeEdit*> nonDocElementEdits;
    QMultiHash<QString, QString> ridByParent;

    // first collect all nodes into a hash by their parent
    foreach(SxeRecord* m, recordByNodeId_.values()) {
        if(!m->parent().isEmpty()) {
            ridByParent.insert(m->parent(), m->rid());
        } else if(!m->node().isElement()) {
            nonDocElementEdits += m->edits();
        } else
            rootid = m->rid();
    }

    // starting from the root, add all edits to a list recursively
    QList<const SxeEdit*> edits;

    if(!rootid.isNull())
        arrangeEdits(ridByParent, edits, rootid);

    return nonDocElementEdits + edits;
}

void SxeSession::arrangeEdits(QHash<QString, QString> &ridByParent, QList<const SxeEdit*> &output, const QString &iterator) {
    // add the edits to this node
    if(recordByNodeId_.contains(iterator))
        output += recordByNodeId_.value(iterator)->edits();

    // process all the children
    QString child;
    while(!(child = ridByParent.take(iterator)).isNull()) {
        arrangeEdits(ridByParent, output, child);
    }
}

void SxeSession::stopQueueing() {
    // do nothing if not queueing
    if(!queueing_)
        return;

    queueing_ = false;

    // Process queued elements
    flush();

    if(!queuedIncomingEdits_.isEmpty()) {
        while(!queuedIncomingEdits_.isEmpty()) {
            IncomingEdit queued = queuedIncomingEdits_.takeFirst();
            processSxe(queued.xml, queued.id);
        }

        emit documentUpdated(true);
    }
}

void SxeSession::startImporting(const QDomDocument &doc) {
    importing_ = true;

    // reset the document
    initializeDocument(doc);

    // start queueing outgoing edits
    startQueueing();
}

void SxeSession::stopImporting() {
    stopQueueing();

    importing_ = false;
}

void SxeSession::endSession() {
    deleteLater();
}

const QDomNode SxeSession::insertNodeBefore(const QDomNode &node, const QDomNode &parent, const QDomNode &referenceNode) {
    if(referenceNode.isNull() || referenceNode.previousSibling().isNull()) {
        // insert as the first node
        SxeRecord* firstMeta = record(parent.firstChild());
        double primaryWeight;
        if(firstMeta)
            primaryWeight = firstMeta->primaryWeight() - 1;
        else
            primaryWeight = 0;

        // find out the rid of the parent node
        QString parentId;
        SxeRecord* parentMeta = record(parent);
        if(parentMeta)
            parentId = parentMeta->rid();
        else {
            qDebug("Trying to insert a node to parent without an id");
            return QDomNode();
        }

        return insertNode(node, parentId, primaryWeight);
    } else
        return insertNodeAfter(node, parent, referenceNode.previousSibling());
}

const QDomNode SxeSession::insertNodeAfter(const QDomNode &node, const QDomNode &parent, const QDomNode &referenceNode) {
    if(node.isNull())
        return QDomNode();

    // process each child of a document fragment separately
    if(node.isDocumentFragment()) {
        // insert the first node relative to the specified referenceNode
        QDomNode reference = referenceNode;
        QDomNodeList children = node.childNodes();
        for(int i = 0; i < children.size(); i++) {
            QDomNode newNode = children.at(i);
            insertNodeAfter(newNode, parent, reference);
            // and the rest relative to the previous sibling
            reference = newNode;
        }

        return QDomNode();
    }

    // find out the rid of the parent node
    QString parentId;
    SxeRecord* parentMeta = record(parent);
    if(parentMeta)
        parentId = parentMeta->rid();
    else {
        qDebug("Trying to insert a node to parent without an id");
        return QDomNode();
    }

    // find out the appropriate weight for the node
    double primaryWeight;
    SxeRecord* referenceMeta = record(referenceNode);
    SxeRecord* nextReferenceMeta = record(referenceNode.nextSibling());
    if(parent.childNodes().count() == 0)
        primaryWeight = 0;
    else if(referenceMeta && nextReferenceMeta && referenceNode.parentNode() == parent) {
        // get the average of the weights of the reference and it's next sibling
        primaryWeight = (referenceMeta->primaryWeight() + nextReferenceMeta->primaryWeight()) / 2;
    } else {
        // insert as the last node
        referenceMeta = record(parent.lastChild());
        if(referenceMeta)
            primaryWeight = referenceMeta->primaryWeight() + 1;
        else
            primaryWeight = 0;
    }

    return insertNode(node, parentId, primaryWeight);
}

const QDomNode SxeSession::insertNode(const QDomNode &node, const QString &parentId, double primaryWeight) {
    QDomNode newNode;

    SxeRecord* meta = record(node);
    if(meta) {
        // move an existing node

        // figure out what's to be changed
        QHash<SxeRecordEdit::Key, QString> changes;
        if(meta->parent() != parentId)
            changes.insert(SxeRecordEdit::Parent, parentId);
        if(meta->primaryWeight() != primaryWeight)
            changes.insert(SxeRecordEdit::PrimaryWeight, QString::number(primaryWeight));

        if(changes.size() > 0) {
            // create the edit
            SxeRecordEdit* edit = new SxeRecordEdit(meta->rid(), meta->version() + 1, changes);

            // apply it
            meta->apply(doc_, edit);

            // send the edit to others
            queueOutgoingEdit(edit);

            emit documentUpdated(false);
        }
        return node;

    } else {

        QList<SxeEdit*> edits;
        // create a new node
        QDomNode result = generateNewNode(node, parentId, primaryWeight);

        emit documentUpdated(false);
        return result;
    }
}

void SxeSession::removeNode(const QDomNode &node) {
    if(node.isNull())
        return;

    // create SxeRemoveEdits for all child nodes
    generateRemoves(node);
    flush();

    emit documentUpdated(false);
}

void SxeSession::setAttribute(const QDomNode &node, const QString &attribute, const QString &value, int from, int n) {
    if(!node.isElement() || attribute.isEmpty())
        return;

    if(value.isNull()) {
        if(from < 0) {
            // Interpret passing QString() as value as wishing to remove the attribute
            if(node.toElement().hasAttribute(attribute))
                removeNode(node.toElement().attributeNode(attribute));
        }

        return;
    }

    if(node.toElement().hasAttribute(attribute)) {
        setNodeValue(node.attributes().namedItem(attribute), value, from, n);
    } else {
        if(from >= 0) {
            qDebug("from > 0 although attribute doesn't exist yet.");
            return;
        }

        QDomAttr domattr = document_.createAttribute(attribute);
        domattr.setValue(value);
        insertNodeAfter(domattr, node, QDomNode());
    }
}

void SxeSession::setNodeValue(const QDomNode &node, const QString &value, int from, int n) {
    SxeRecord* meta = record(node);

    if(!meta) {
        qDebug() << "Trying to set value of " << node.nodeName() << " (a non-existent node) to \"" << value << "\"";
        return;
    }

    if(!(node.isAttr() || node.isText())) {
        qDebug() << "Trying to set value of a non-attr/text node " << node.nodeName();
        return;
    }

    // Check whether anythings changing:
    QString newValue;
    if(from >= 0 && n >= 0) {
        if((from + n) > node.nodeValue().length()) {
            qDebug() << QString("from (%1) + n (%2) > (length of existing node value) (%3).").arg(from).arg(n).arg(node.nodeValue().length());
            return;
        }
        newValue = node.nodeValue().replace(from, n, value);
    } else
        newValue = value;

    if(newValue == node.nodeValue())
        return;

    // Create the appropriate RecordEdit
    QHash<SxeRecordEdit::Key, QString> changes;
    changes.insert(SxeRecordEdit::Chdata, value);
    if(from >= 0 && n >= 0) {
        changes.insert(SxeRecordEdit::ReplaceFrom, QString("%1").arg(from));
        changes.insert(SxeRecordEdit::ReplaceN, QString("%1").arg(n));
    }

    // create the edit
    SxeRecordEdit* edit = new SxeRecordEdit(meta->rid(), meta->version() + 1, changes);

    // apply it
    meta->apply(doc_, edit);

    // send the edit to others
    queueOutgoingEdit(edit);

    emit documentUpdated(false);
}

void SxeSession::flush() {
    if(queuedOutgoingEdits_.isEmpty())
        return;

    // create the sxe element
    QDomDocument *doc = ((SxeManager*)parent())->client()->doc();
    QDomElement sxe = doc->createElementNS(SXENS, "sxe");
    sxe.setAttribute("session", session_);

    // append all queued edits
    while(!queuedOutgoingEdits_.isEmpty()) {
        sxe.appendChild(queuedOutgoingEdits_.takeFirst());
    }

    // pass the bundle to SxeManager
    emit newSxeElement(sxe, target(), groupChat_);
}

QDomNode SxeSession::generateNewNode(const QDomNode node, const QString &parent, double primaryWeight) {
    if(!record(node)) {
        // generate the appropriate edit(s) for the node
        QString rid = generateUUIDForSession();
        // create the SxeRecord
        SxeRecord* meta = createRecord(rid);
        if(!meta)
            return QDomNode();

        if((node.isAttr() || node.isText()) && node.nodeValue().length() > MAXCHDATA) {
            // Generate a "stub" of the new node
            QDomNode clone = node.cloneNode();
            QString full = clone.nodeValue();
            clone.setNodeValue("");
            QDomNode newNode = generateNewNode(clone, parent, primaryWeight);
            flush();

            // append the value
            for(int i = 0; i < full.length(); i += MAXCHDATA) {
                setNodeValue(newNode, full.mid(i, MAXCHDATA), i, 0);
                flush();
            }
        } else {
            SxeEdit* edit = new SxeNewEdit(rid, node, parent, primaryWeight, false);

            meta->apply(doc_, edit);
            queueOutgoingEdit(edit);
        }

        // process all the attributes and child nodes recursively
        if(node.isElement()) {
            // attributes
            QDomNamedNodeMap attributes = node.attributes();
            for(int i = 0; i < attributes.count(); i++)
                generateNewNode(attributes.item(i), rid, i);

            // child nodes
            int i = 0;
            for(QDomNode n = node.firstChild(); !n.isNull(); n = n.nextSibling())
                generateNewNode(n, rid, i++);
        }

        return meta->node();
    }

    return QDomNode();
}

void SxeSession::generateRemoves(const QDomNode &node) {
    SxeRecord* meta = record(node);
    if(meta) {
        // process all the attributes and child nodes recursively
        if(node.isElement()) {
            // attributes
            QDomNamedNodeMap attributes = node.attributes();
            for(int i = 0; i < attributes.count(); i++)
                generateRemoves(attributes.item(i));

            // child nodes
            QDomNodeList children = node.childNodes();
            for(int i = 0; i < children.count(); i++)
                generateRemoves(children.at(i));
        }

        // generate the appropriate edit for the node
        SxeRemoveEdit* edit = new SxeRemoveEdit(meta->rid());
        queueOutgoingEdit(edit);
        meta->apply(doc_, edit);
    }
}

void SxeSession::reposition(const QDomNode &node, bool remote) {
    Q_UNUSED(remote);
    SxeRecord* meta = record(node);

    if(!meta) {
        qDebug("Trying to reposition a node without record.");
        return;
    }

    // inserting nodes to the document node is a special case
    if(meta->parent().isEmpty()) {
        if(node.isElement() && !(doc_.documentElement().isNull() || doc_.documentElement() == node)) {
            qDebug("Trying to add a root node when one already exists.");
            removeNode(node);
            flush();
            return;
        }

        doc_.appendChild(node);

        return;
    }

    // find the parent node
    SxeRecord* parentMeta = record(meta->parent());
    QDomNode parentNode;
    if(!parentMeta
        || (parentNode = parentMeta->node()).isNull()) {
        qDebug("non-existent parent. Deleting node.");
        removeNode(node);
        flush();
        return;
    }

    // simply insert if an attribute
    if(node.isAttr()) {
        QDomElement parentElement = parentNode.toElement();
        if(parentElement.isNull()) {
            qDebug("Trying to insert an attribute to a non-element.");
            return;
        }

        // unless an attribute with the same name already exists
        if(parentElement.hasAttribute(node.nodeName())
            && node != parentElement.attributeNode(node.nodeName())) {

            // qDebug() << QString("Removing an attribute node '%1' because one already exists").arg(node.nodeName());

            // delete the node with smaller secondary weight
            if(removeSmaller(meta, record(parentElement.attributeNode(node.nodeName()))))
                return;

        }

        parentElement.setAttributeNode(node.toAttr());

        return;
    }

    // get the list of siblings
    QDomNodeList children = parentNode.childNodes();

    // default to appending
    QDomNode before;
    bool insertLast = true;
    if(children.length() > 0) {
        // find the child with the smallest weight greater than the weight of the node itself
        // if any, insert the node before that node
        for(int i=0; i < children.length(); i++) {
            if(children.item(i) != node) {
                SxeRecord* siblingMeta = record(children.item(i));
                if(siblingMeta && *meta < *siblingMeta) {
                    // qDebug() << QString("%1 (pw: %2) is less than %3 (pw: %4)").arg(meta->name()).arg(meta->primaryWeight()).arg(siblingMeta->name()).arg(siblingMeta->primaryWeight()).toLatin1();
                    before = children.item(i);
                    insertLast = false;
                    break;
                }
            }
        }
    }

    if(insertLast) {
        // qDebug() << QString("Repositioning '%1' (pw: %2) as last.").arg(node.nodeName()).arg(meta->primaryWeight()).toLatin1();
        parentNode.appendChild(node);
    } else {
        // qDebug() << QString("Repositioning '%1' (pw: %2) before '%3' (pw: %4).").arg(node.nodeName()).arg(meta->primaryWeight()).arg(before.nodeName()).arg(record(before)->primaryWeight()).toLatin1();
        parentNode.insertBefore(node, before);
    }
}

// void SxeSession::addToLookup(const QDomNode &node, bool, const QString &rid) {
    // recordByNode_[node]
    //     = recordByNodeId_[rid];
// }

void SxeSession::handleNodeToBeAdded(const QDomNode &node, bool remote) {
    emit nodeToBeAdded(node, remote);
    reposition(node, remote);
    emit nodeAdded(node, remote);
}

void SxeSession::handleNodeToBeMoved(const QDomNode &node, bool remote) {
    emit nodeToBeMoved(node, remote);
    reposition(node, remote);
    emit nodeMoved(node, remote);
}

void SxeSession::handleNodeToBeRemoved(const QDomNode &node, bool remote) {
    emit nodeToBeRemoved(node, remote);
    removeRecord(node);
}


void SxeSession::removeRecord(const QDomNode &node) {
    QMutableHashIterator<QString, SxeRecord*> i(recordByNodeId_);

    while(i.hasNext()) {
        if(node == i.next().value()->node()) {
            i.remove();
            return;
        }
    }
}

bool SxeSession::removeSmaller(SxeRecord* meta1, SxeRecord* meta2) {
    if(!meta1)
        return true;
    if(!meta2)
        return false;

    if(meta1->hasSmallerSecondaryWeight(*meta2)) {
        removeNode(meta1->node());
        flush();
        return true;
    } else {
        removeNode(meta2->node());
        flush();
        return false;
    }
}

void SxeSession::addUsedSxeId(QString id) {
    usedSxeIds_ += id;
}

QList<QString> SxeSession::usedSxeIds() {
    return usedSxeIds_;
}

void SxeSession::queueOutgoingEdit(SxeEdit* edit) {
    if(!importing_) {
        QDomElement el = edit->xml(doc_);
        queuedOutgoingEdits_.append(((SxeManager*)parent())->client()->doc()->importNode(el, true));
    }
}

SxeRecord* SxeSession::createRecord(const QString &id) {
    if(recordByNodeId_.contains(id)) {
        qDebug() << QString("record by id '%1' already exists.").arg(id).toLatin1();
        return NULL;
    }

    SxeRecord* m = new SxeRecord(id);
    recordByNodeId_[id] = m;

    // once the node is actually created, add it to the lookup table
    // connect(m, SIGNAL(nodeAdded(QDomNode, bool, QString)), SLOT(addToLookup(const QDomNode &, bool, const QString &)));

    // remove the node in case of a conflicting edit
    connect(m, SIGNAL(nodeRemovalRequired(QDomNode)), SLOT(removeNode(QDomNode)));

    // reposition and emit public signals as needed when record is changed
    connect(m, SIGNAL(nodeToBeAdded(QDomNode, bool, QString)), SLOT(handleNodeToBeAdded(const QDomNode &, bool)));
    connect(m, SIGNAL(nodeToBeMoved(QDomNode, bool)), SLOT(handleNodeToBeMoved(const QDomNode &, bool)));
    connect(m, SIGNAL(nodeToBeRemoved(QDomNode, bool)), SLOT(handleNodeToBeRemoved(const QDomNode &, bool)));
    connect(m, SIGNAL(chdataToBeChanged(QDomNode, bool)), SIGNAL(chdataToBeChanged(const QDomNode &, bool)));
    connect(m, SIGNAL(chdataChanged(QDomNode, bool)), SIGNAL(chdataChanged(const QDomNode &, bool)));
    // connect(m, SIGNAL(nameChanged(QDomNode, bool)), SIGNAL(nameChanged(const QDomNode &, bool)));

    return m;
}

SxeRecord* SxeSession::record(const QString &id) {
    return recordByNodeId_.value(id);
}

SxeRecord* SxeSession::record(const QDomNode &node) const {
    if(node.isNull())
        return NULL;

    // go through all the SxeRecord's
    foreach(SxeRecord* meta, recordByNodeId_.values()) {
        // qDebug() << QString("id: %1").arg(meta->rid()).toLatin1();
        if(node == meta->node())
            return meta;
    }

    return NULL;
}

void SxeSession::setUUIDPrefix(const QString uuidPrefix) {
    if(!uuidPrefix.isNull())
        uuidPrefix_ = uuidPrefix;
    else
        uuidPrefix_ = generateUUID();
}

QString SxeSession::generateUUIDForSession() {
    return QString("%1.%2").arg(uuidPrefix_).arg(++uuidMaxPostfix_, 0, 36); // 36 is the max allowed base
}

QString SxeSession::generateUUID() {
    QString fullstring = QUuid::createUuid().toString();
    // return the string between "{" and "}"
    int start = fullstring.indexOf("{") + 1;
    return fullstring.mid(start, fullstring.lastIndexOf("}") - start);
}

QString SxeSession::parseProlog(const QDomDocument &doc) {
    QString prolog;
    QTextStream stream(&prolog);

    // check for the XML declaration
    if(doc.childNodes().at(0).isProcessingInstruction()
          && doc.childNodes().at(0).toProcessingInstruction().target().toLower() == "xml")
        doc.childNodes().at(0).save(stream, 1);

    if(!doc.doctype().isNull())
        doc.doctype().save(stream, 1);

    return prolog;
}
