/*
 * sxerecord.cpp - A class for storing the record of an individual node
 * Copyright (C) 2007  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 "sxerecord.h"


static bool referencedEditLessThan(const SxeEdit* e1, const SxeEdit* e2) { return *e1 < *e2; }


//----------------------------------------------------------------------------
// SxeRecord
//----------------------------------------------------------------------------

SxeRecord::SxeRecord(QString rid)
    : rid_(rid)
    , version_(0)
    , primaryWeight_(0.)
    , lastPrimaryWeight_(0.)
{
};

SxeRecord::~SxeRecord()
{
    qDebug("destruct SxeRecord");
    while(!edits_.isEmpty()) {
        delete edits_.takeFirst();
    }
};

QDomNode SxeRecord::node() const
{
    return node_;
}

void SxeRecord::apply(QDomDocument &doc, SxeEdit* edit) {
    if(edit->rid() == rid()) {
        if(edit->type() == SxeEdit::New)
            applySxeNewEdit(doc, dynamic_cast<SxeNewEdit*>(edit));
        else if (edit->type() == SxeEdit::Remove)
            applySxeRemoveEdit(dynamic_cast<SxeRemoveEdit*>(edit));
        else if (edit->type() == SxeEdit::Record)
            applySxeRecordEdit(dynamic_cast<SxeRecordEdit*>(edit));
    } else {
        qDebug("Tried to apply an edit meant for %s to %s.", qPrintable(edit->rid()), qPrintable(rid()));
    }
}

QList<const SxeEdit*> SxeRecord::edits() const {
    QList<const SxeEdit*> edits;
        foreach(SxeEdit* e, edits_) {
            edits.append(e);
        }
    return edits;
};

bool SxeRecord::applySxeNewEdit(QDomDocument &doc, SxeNewEdit* edit) {
    if(!(edits_.size() == 0 && node_.isNull())) {
        qDebug("Someone's not behaving! Tried to apply a SxeNewEdit to an existing node.");
        emit nodeRemovalRequired(node_);
        return false;
    }

    // create the new node
    if(edit->nodeType() == QDomNode::ElementNode) {

        if(edit->nameSpace().isEmpty())
            node_ = doc.createElement(edit->name());
        else
            node_ = doc.createElementNS(edit->nameSpace(), edit->name());

    } else if(edit->nodeType() == QDomNode::AttributeNode) {

        if(edit->nameSpace().isEmpty())
            node_ = doc.createAttribute(edit->name());
        else
            node_ = doc.createAttributeNS(edit->nameSpace(), edit->name());

        node_.toAttr().setValue(edit->chdata());

    } else if(edit->nodeType() == QDomNode::TextNode) {

        node_ = doc.createTextNode(edit->chdata());

    } else if(edit->nodeType() == QDomNode::CommentNode) {

        node_ = doc.createComment(edit->chdata());

    } else if(edit->nodeType() == QDomNode::ProcessingInstructionNode) {

        node_ = doc.createProcessingInstruction(edit->processingInstructionTarget(), edit->processingInstructionData());

    } else
        return false;

    edits_ += edit;
    revertToZero();

    emit nodeToBeAdded(node_, edit->remote(), edit->rid());

    lastParent_ = parent();
    lastPrimaryWeight_ = primaryWeight();

    return true;
}

void SxeRecord::revertToZero() {
#ifndef NDEBUG
    if (edits_[0]->type() != SxeEdit::New)
        qDebug("First edit is of type %d!", edits_[0]->type());
#endif

    const SxeNewEdit* edit = dynamic_cast<const SxeNewEdit*>(edits_[0]);

    parent_ = edit->parent();
    primaryWeight_ = edit->primaryWeight();
    identifier_ = edit->name();
    data_ = edit->chdata();
}

bool SxeRecord::applySxeRemoveEdit(SxeRemoveEdit* edit) {
    if(!node_.isNull()) {
        emit nodeToBeRemoved(node_, edit->remote());

        if(node_.isAttr()) {
            QDomNode parent = node_.parentNode();
            while(!parent.isElement() && !parent.isNull())
                parent = parent.parentNode();

            if(!parent.isNull()) {
                parent.toElement().removeAttributeNode(node_.toAttr());
                emit nodeRemoved(node_, edit->remote());
            } // else, the attr hadn't been added to the doc yet.
        } else {
            node_.parentNode().removeChild(node_);
            emit nodeRemoved(node_, edit->remote());
        }

        edits_ += edit;

        // delete the record
        deleteLater();
    }

    return true;
}

bool SxeRecord::applySxeRecordEdit(SxeRecordEdit* edit) {
    if(!node_.isNull() && edits_.size() > 0) {

        if(edit->version() == version() + 1 && !edits_.last()->overridenBy(*edit)) {

            // process the "in order" edit
            edits_ += edit;
            processInOrderRecordEdit(edit);

        } else {

            edits_ += edit;
            reorderEdits();

        }

        updateNode(edit->remote());

        return true;

    }
    return false;
}

void SxeRecord::processInOrderRecordEdit(const SxeRecordEdit* edit) {

    foreach(SxeRecordEdit::Key key, edit->keys()) {

        if(key == SxeRecordEdit::Parent && edit->value(key) != parent()) {

            parent_ = edit->value(key);

        } else if(key == SxeRecordEdit::PrimaryWeight && edit->value(key).toDouble() != primaryWeight()) {

            primaryWeight_ = edit->value(key).toDouble();

        } else if(key == SxeRecordEdit::Name && edit->value(key) != name()) {

            identifier_ = edit->value(key);

        } else if(key == SxeRecordEdit::ProcessingInstructionTarget && edit->value(key) != processingInstructionTarget()) {

            identifier_ = edit->value(key);

        } else if(key == SxeRecordEdit::Chdata || key == SxeRecordEdit::ProcessingInstructionData) {

            // Check for partial replacements
            QList<SxeRecordEdit::Key> keys = edit->keys();
            if(keys.contains(SxeRecordEdit::ReplaceFrom)
                && keys.contains(SxeRecordEdit::ReplaceN)) {

                // 'replacefrom' & 'replacen' exist, do they contain integers?
                bool ok1, ok2;
                int from = edit->value(SxeRecordEdit::ReplaceFrom).toInt(&ok1);
                int n = edit->value(SxeRecordEdit::ReplaceN).toInt(&ok2);

                if(ok1 && ok2 && from >= 0 && n >= 0 && from + n <= data_.length()) {

                    // Do partial replace if the range makes sense
                    data_.replace(from, n, edit->value(key));

                } else {
                    if(!ok1)
                        qDebug("Could not convert 'replacefrom' = '%s' to int.", qPrintable(edit->value(SxeRecordEdit::ReplaceFrom)));
                    if(!ok2)
                        qDebug("Could not convert 'replacen' = '%s' to int.", qPrintable(edit->value(SxeRecordEdit::ReplaceN)));
                    if(from < 0)
                        qDebug("'replacefrom' = '%s' is negative.", qPrintable(edit->value(SxeRecordEdit::ReplaceFrom)));
                    if(n < 0)
                        qDebug("'replacen' = '%s' is negative.", qPrintable(edit->value(SxeRecordEdit::ReplaceN)));
                    if(from + n > data_.length())
                        qDebug("from (%d) + n (%d) > data_.length() (%d).", from, n, data_.length());
                }

            } else {

                data_ = edit->value(key);

            }

        }
    }

}

void SxeRecord::reorderEdits() {
    qSort(edits_.begin(), edits_.end(), referencedEditLessThan);

    revertToZero();

    // Apply all the valid record edits
    for(int i = 1; i < edits_.size(); i++) {

        if (edits_[i]->type() == SxeEdit::Record) {
            SxeRecordEdit* edit = dynamic_cast<SxeRecordEdit*>(edits_[i]);

            // Check that the version matches and that the next edit doesn't override it.
            if (i == edit->version() && (i+1 == edits_.size() || !edit->overridenBy(*edits_[i+1])))
                processInOrderRecordEdit(edit);
            else if (edit->version() <= i)
                edit->nullify();  // There's no way the edit could be applied anymore

        } else {
            qDebug("Edit of type %d at %d!", edits_[i]->type(), i);
        }

    }

}

void SxeRecord::updateNode(bool remote) {
    if(parent_ != lastParent_ || primaryWeight_ != lastPrimaryWeight_)
        emit nodeToBeMoved(node_, remote);

    if(identifier_ != lastIdentifier_) {

        if ((node_.isElement() || node_.isAttr())
            && node_.nodeName() != name()) {

            // emit nameToBeChanged(node_, remote);
            // TODO: update the name somehow
            // emit nameChanged(node_, remote);

        } else if(node_.isProcessingInstruction()) {

            // emit processingInstructionTargetToBeChanged(node_, remote);
            // TODO: figure out a way to change the target
            //      will probably need to recreate the pi
            // emit processingInstructionTargetChanged(node_, remote);
        }
    }

    if(data_ != lastData_) {

        // qDebug() << QString("Setting '%1' to \"%2\"").arg(node_.nodeName()).arg(data_).toLatin1();

        if((node_.isText() || node_.isAttr() || node_.isComment())) {

            emit chdataToBeChanged(node_, remote);
            node_.setNodeValue(data_);
            emit chdataChanged(node_, remote);

        } else if(node_.isProcessingInstruction()) {

            emit processingInstructionDataToBeChanged(node_, remote);
            node_.toProcessingInstruction().setData(data_);
            emit processingInstructionDataChanged(node_, remote);

        }

    }

}

QString SxeRecord::rid() const {
    return rid_;
};

QString SxeRecord::parent() const {
    return parent_;
}

double SxeRecord::primaryWeight() const {
    return primaryWeight_;
}

int SxeRecord::version() const {
    return edits_.size() - 1;
}

QString SxeRecord::name() const {
    return identifier_;
}

QString SxeRecord::nameSpace() const {
    if(node_.isNull())
        return QString();
    else
        return node_.namespaceURI();
}

QString SxeRecord::chdata() const {
    return data_;
}

QString SxeRecord::processingInstructionTarget() const {
    return identifier_;
}

QString SxeRecord::processingInstructionData() const {
    return data_;
}

bool SxeRecord::hasSmallerSecondaryWeight(const SxeRecord &other) const {
    // compare the "secondary weight" (the id's)
    QString selfid = rid();
    QString otherid = other.rid();
    for(int i = 0; i < selfid.length() && i < otherid.length(); i++) {
        if(selfid[i].unicode() < otherid[i].unicode())
            return true;
        else if(selfid[i].unicode() > otherid[i].unicode())
            return false;
    }
    // if no difference was found. one must be shorter
    return (selfid.length() < otherid.length());
}

bool SxeRecord::operator==(const SxeRecord &other) const {
    return rid() == other.rid();
}

bool SxeRecord::operator<(const SxeRecord &other) const {
    if(other == *this)
        return false;

    if(primaryWeight() < other.primaryWeight())
        return true;
    else if(primaryWeight() == other.primaryWeight()) {
        return hasSmallerSecondaryWeight(other);
    } else
        return false;
}

bool SxeRecord::operator>(const SxeRecord &other) const {
    return !(*this == other || *this < other);
}
