/*
    This file is part of Rocs.
    Copyright 2008-2011  Tomaz Canabrava <tomaz.canabrava@gmail.com>
    Copyright 2008       Ugo Sangiori <ugorox@gmail.com>
    Copyright 2010-2011  Wagner Reck <wagner.reck@gmail.com>
    Copyright 2011-2012  Andreas Cord-Landwehr <cola@uni-paderborn.de>

    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 program.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "Document.h"
#include "DocumentManager.h"
#include "DataStructure.h"
#include "Data.h"
#include "Pointer.h"
#include "PointerType.h"
#include "DataType.h"
#include "DataStructureBackendManager.h"
#include "DataStructurePluginInterface.h"
#include "QtScriptBackend.h"

#include <boost/shared_ptr.hpp>
#include <boost/concept_check.hpp>

#include <KStandardDirs>
#include <KSaveFile>
#include <KDebug>

#include <QString>
#include <QByteArray>
#include <QSvgRenderer>
#include <QMap>

class DocumentPrivate
{
public:
    DocumentPrivate() {}

    enum borderSize { SceneBorder = 50 };

    QString _lastSavedDocumentPath;
    QString _name;
    qreal _left;
    qreal _right;
    qreal _top;
    qreal _bottom;
    qreal _minWidth;
    qreal _minHeight;
    bool _modified;
    bool _saved;
    DataStructurePtr _activeDataStructure;
    QPointer<DataStructurePluginInterface> _backend;
    QtScriptBackend* _engineBackend;
    QList< DataStructurePtr > _dataStructures;

    // data and pointer types
    QMap<int, DataTypePtr> _dataTypes;           // list of data types
    QMap<int, PointerTypePtr> _pointerTypes;        // list of pointer types
    DataTypePtr _groupType;

    QString _iconPackage;   // available icons for data types
};



Document::Document(const QString& name, qreal left, qreal right, qreal top, qreal bottom, QObject *parent)
    : QObject(parent), d(new DocumentPrivate())
{
    d->_name = name;
    d->_left = left;
    d->_right = right;
    d->_top = top;
    d->_bottom = bottom;
    d->_minWidth = 0;
    d->_minHeight = 0;
    d->_saved = false;
    d->_engineBackend = new QtScriptBackend(this);
    d->_backend = DataStructureBackendManager::self()->activeBackend();
    d->_modified = false;

    d->_iconPackage = KGlobal::dirs()->locate("appdata", "iconpacks/default.svg");
    if (!DocumentManager::self().sharedRenderer(d->_iconPackage)) {
         DocumentManager::self().registerSharedRenderer(d->_iconPackage);
    }

    // default types
    d->_dataTypes.insert(0, DataType::create(this, 0));
    d->_pointerTypes.insert(0, PointerType::create(this, 0));

    kDebug() << "Construct Graph Document of type : " << d->_backend->name();
}

void Document::setModified(const bool mod)
{
    d->_modified = mod;
}

void Document::changeBackend()
{
    cleanUpBeforeConvert();
    d->_backend = DataStructureBackendManager::self()->activeBackend();

    // create list of existing data structures, then convert them one by one
    QList<DataStructurePtr> dataStructures = QList<DataStructurePtr>(d->_dataStructures);
    d->_dataStructures.clear();

    for (int i = 0; i < dataStructures.count(); ++i) {
        DataStructurePtr newDataStructure = addDataStructure(DataStructureBackendManager::self()->createDataStructure(dataStructures.at(i), this));
        // remove origin data structure
        dataStructures[i]->remove();
    }
}

Document::~Document()
{
    clear();
    delete d->_engineBackend;
}

int Document::registerDataType(const QString& name, int identifier)
{
    // create new identifier if identifier is already in use or 0
    if (identifier==0 || d->_dataTypes.contains(identifier)) {
        QList<int> usedIdentifier = d->_dataTypes.keys();
        qSort(usedIdentifier);
        identifier = usedIdentifier.last() + 1;
    }

    DataTypePtr dataType = DataType::create(this, identifier);
    dataType->setName(name);

    d->_dataTypes.insert(identifier, dataType);

    emit(dataTypeCreated(identifier));
    return identifier;
}

int Document::registerPointerType(const QString& name, int identifier)
{
    // create new identifier if identifier is already in use or 0
    if (identifier==0 || d->_pointerTypes.contains(identifier)) {
        QList<int> usedIdentifier = d->_pointerTypes.keys();
        qSort(usedIdentifier);
        identifier = usedIdentifier.last() + 1;
    }

    PointerTypePtr pointerType = PointerType::create(this, identifier);
    pointerType->setName(name);

    d->_pointerTypes.insert(identifier, pointerType);

    emit(pointerTypeCreated(identifier));
    return identifier;
}

QList<int> Document::dataTypeList() const
{
    return d->_dataTypes.keys();
}

QList<int> Document::pointerTypeList() const
{
    return d->_pointerTypes.keys();
}

QString Document::iconPackage() const
{
    return d->_iconPackage;
}

bool Document::removeDataType(int identifier)
{
    Q_ASSERT(d->_dataTypes.contains(identifier));

    emit(dataTypeRemoved(identifier));
    if (d->_dataTypes.contains(identifier)) {
        d->_dataTypes[identifier]->remove();
    }
    return d->_dataTypes.remove(identifier) > 0;
}

bool Document::removePointerType(int identifier)
{
    Q_ASSERT(d->_pointerTypes.contains(identifier));

    emit(pointerTypeRemoved(identifier));
    if (d->_pointerTypes.contains(identifier)) {
        d->_pointerTypes[identifier]->remove();
    }
    return d->_pointerTypes.remove(identifier) > 0;
}

DataTypePtr Document::dataType(int identifier) const
{
    Q_ASSERT(d->_dataTypes.contains(identifier));

    if (!d->_dataTypes.contains(identifier)) {
        return DataTypePtr();
    }
    return d->_dataTypes[identifier];
}

PointerTypePtr Document::pointerType(int identifier) const
{
    Q_ASSERT(d->_pointerTypes.contains(identifier));

    if (!d->_pointerTypes.contains(identifier)) {
        return PointerTypePtr();
    }
    return d->_pointerTypes[identifier];
}

int Document::groupType()
{
    if (!d->_groupType) {
        int typeId = registerDataType("Subdatastructure");
        d->_groupType = dataType(typeId);
        return typeId;
    }
    return d->_groupType->identifier();
}

QList< DataStructurePtr >& Document::dataStructures() const
{
    return d->_dataStructures;
}

bool Document::isValidIdentifier(const QString& identifier)
{
    QRegExp validator("(^([a-z]|[A-Z])+([0-9]|[a-z]|[A-Z]|_)*$)");
    return (validator.indexIn(identifier) != -1);
}


QtScriptBackend * Document::engineBackend() const
{
    return d->_engineBackend;
}

// Sets the current file name of the DataStructure Collection
void Document::setName(const QString& name)
{
    d->_name = name;
    d->_modified = true;
    emit nameChanged(name);
}

// Gets the name of the DataStructure
QString Document::name() const
{
    return d->_name;
}

// gets the wheight of the drawable area
qreal Document::height() const
{
    return d->_bottom - d->_top;
}

// sets the width of the drawable area
qreal Document::width() const
{
    return d->_right - d->_left;
}

qreal Document::left() const
{
    return d->_left;
}

qreal Document::right() const
{
    return d->_right;
}

qreal Document::top() const
{
    return d->_top;
}

qreal Document::bottom() const
{
    return d->_bottom;
}

void Document::setLeft(qreal leftValue)
{
    d->_left = leftValue;
    //d->_modified = true;
}

void Document::setRight(qreal rightValue)
{
    d->_right = rightValue;
    //d->_modified = true;
}

void Document::setTop(qreal topValue)
{
    d->_top = topValue;
    //d->_modified = true;
}

void Document::setBottom(qreal bottomValue)
{
    d->_bottom = bottomValue;
    //d->_modified = true;
}

QRectF Document::size()
{
    return QRectF(d->_left, d->_top, d->_right - d->_left, d->_bottom - d->_top);
}

bool Document::isPointAtDocument(qreal x, qreal y)  const
{
    if (x < d->_left)      return false;
    if (x > d->_right)     return false;
    if (y < d->_top)       return false;
    if (y > d->_bottom)    return false;

    return true;
}

void Document::changeMinimalSize(qreal width, qreal height)
{
    if (width >= 0) d->_minWidth = width;
    if (height >= 0) d->_minHeight = height;

    if (width > d->_right - d->_left) {
        d->_left -= (d->_right - d->_left - width) / 2;
        d->_right += (d->_right - d->_left - width) / 2;
        emit resized();
    } else {
        resizeDocumentBorder(BorderLeft);
    }

    if (height > d->_bottom - d->_top) {
        d->_top -= (d->_bottom - d->_top - height) / 2;
        d->_bottom += (d->_bottom - d->_top - height) / 2;
        emit resized();
    } else {
        resizeDocumentBorder(BorderTop);
    }
}

void Document::resizeDocumentIncrease()
{
    int elements = dataStructures().size();
    for (int i = 0; i < elements; i++) {
        bool resizeDocument = false;
        foreach(DataPtr n,  dataStructures().at(i)->dataList()) {
            if (n->x() < d->_left + DocumentPrivate::SceneBorder) {
                setLeft(d->_left - DocumentPrivate::SceneBorder);
                resizeDocument = true;
            }
            if (n->x() > d->_right - DocumentPrivate::SceneBorder) {
                setRight(d->_right + DocumentPrivate::SceneBorder);
                resizeDocument = true;
            }
            if (n->y() < d->_top + DocumentPrivate::SceneBorder) {
                setTop(d->_top - DocumentPrivate::SceneBorder);
                resizeDocument = true;
            }
            if (n->y() > d->_bottom - DocumentPrivate::SceneBorder) {
                setBottom(d->_bottom + DocumentPrivate::SceneBorder);
                resizeDocument = true;
            }
        }
        if (resizeDocument) {
            emit resized();
        }
    }
}

void Document::resizeDocumentBorder(Document::Border orientation)
{
    bool empty = true;
    int elements = dataStructures().size();

    // scans doubled border of specified size: if empty or not
    for (int i = 0; i < elements; i++) {
        foreach(DataPtr n,  dataStructures().at(i)->dataList()) {
            switch (orientation) {
            case BorderLeft: {
                if (n != 0 && n->x() < d->_left + DocumentPrivate::SceneBorder * 2) empty = false;
                break;
            }
            case BorderRight: {
                if (n != 0 && n->x() > d->_right - DocumentPrivate::SceneBorder * 2) empty = false;
                break;
            }
            case BorderTop: {
                if (n != 0 && n->y() < d->_top + DocumentPrivate::SceneBorder * 2) empty = false;
                break;
            }
            case BorderBottom: {
                if (n != 0 && n->y() > d->_bottom - DocumentPrivate::SceneBorder * 2) empty = false;
                break;
            }
            }
        }
    }

    //TODO connect to graphvisualeditor-size
    if (empty == true) {
        switch (orientation) {
        case BorderLeft: {
            if (d->_right - d->_left < DocumentPrivate::SceneBorder * 4)
                return;
            setLeft(d->_left + DocumentPrivate::SceneBorder);
            emit resized();
            break;
        }
        case BorderRight: {
            if (d->_right - d->_left < DocumentPrivate::SceneBorder * 4)
                return;
            setRight(d->_right - DocumentPrivate::SceneBorder);
            emit resized();
            break;
        }
        case BorderTop: {
            if (d->_bottom - d->_top < DocumentPrivate::SceneBorder * 4)
                return;
            setTop(d->_top + DocumentPrivate::SceneBorder);
            emit resized();
            break;
        }
        case BorderBottom: {
            if (d->_bottom - d->_top < DocumentPrivate::SceneBorder * 4)
                return;
            setBottom(d->_bottom - DocumentPrivate::SceneBorder);
            emit resized();
            break;
        }
        }
    }
}

bool Document::isPointAtDocument(const QPointF& point)  const
{
    return isPointAtDocument(point.x(), point.y());
}

bool Document::isModified() const
{
    return d->_modified;
}

void Document::cleanUpBeforeConvert()
{
    foreach(DataStructurePtr ds, d->_dataStructures) {
        ds->cleanUpBeforeConvert();
    }
}

void Document::setActiveDataStructure(int index)
{
    if (index >= 0 && index < d->_dataStructures.length()) {
        d->_activeDataStructure = d->_dataStructures.at(index);
        emit activeDataStructureChanged(d->_activeDataStructure);
        d->_modified = true;
    }
}

void Document::setActiveDataStructure(DataStructurePtr g)
{
    if (d->_dataStructures.indexOf(g) != -1) {
        d->_activeDataStructure = g;
        emit activeDataStructureChanged(g);
        d->_modified = true;
    }
}

DataStructurePtr Document::addDataStructure(const QString& name)
{
    DataStructurePtr dataStructure = DataStructureBackendManager::self()->createDataStructure(this,
                         d->_backend->internalName());
    dataStructure->setName(name);
    return addDataStructure(dataStructure);
}

DataStructurePtr Document::addDataStructure(DataStructurePtr dataStructure)
{
    // find unused name
    QList<QString> usedNames;
    foreach(DataStructurePtr ds, d->_dataStructures) {
        usedNames.append(ds->name());
    }
    // Change unique data structure name if the currently set one is not ok.
    // For at least one i in the following range, the name is not used, yet.
    QString uniqueName = dataStructure->name();
    if (uniqueName.isEmpty() || usedNames.contains(uniqueName)) {
        for (int i = 0; i < dataStructures().length() + 1; ++i) {
            uniqueName = QString("%1%2").arg(d->_backend->internalName()).arg(i);
            if (!usedNames.contains(uniqueName)) {
                break;
            }
        }
    }

    dataStructure->setName(uniqueName);
    d->_dataStructures.append(dataStructure);
    d->_activeDataStructure = dataStructure;
    d->_modified = true;
    connect(dataStructure.get(), SIGNAL(changed()), this, SLOT(setModified()));
    emit dataStructureCreated(dataStructure);
    emit dataStructureListChanged();
    return dataStructure;
}

void Document::save()
{
    Q_ASSERT(!fileUrl().isEmpty());
    DocumentManager::self().saveDocumentAs(this, KUrl::fromLocalFile(fileUrl()));
}

void Document::saveAs(const QString& fileUrl)
{
    DocumentManager::self().saveDocumentAs(this, KUrl::fromLocalFile(fileUrl));
}

QString Document::fileUrl() const
{
    return d->_lastSavedDocumentPath;
}

void Document::setFileUrl(const KUrl& fileUrl)
{
    d->_lastSavedDocumentPath = fileUrl.toLocalFile();
}

void Document::remove(DataStructurePtr dataStructure)
{
    d->_dataStructures.removeOne(dataStructure);
    if (d->_dataStructures.count() == 0) {
        addDataStructure();
    }
    setActiveDataStructure(d->_dataStructures.count()-1);
    d->_modified = true;
    emit dataStructureListChanged();
}

void Document::clear()
{
    // remove types
    for (QMap<int,DataTypePtr>::const_iterator iter= d->_dataTypes.begin(); iter != d->_dataTypes.end(); ++iter) {
        emit(dataTypeRemoved(iter.key()));
        d->_dataTypes[iter.key()]->remove();
    }
    d->_dataTypes.clear();

    for (QMap<int,PointerTypePtr>::const_iterator iter= d->_pointerTypes.begin(); iter != d->_pointerTypes.end(); ++iter) {
        emit(pointerTypeRemoved(iter.key()));
        d->_pointerTypes[iter.key()]->remove();
    }
    d->_pointerTypes.clear();

    // remove data structures
    foreach (const DataStructurePtr &dataStructure, d->_dataStructures) {
        dataStructure->remove();
    }
    d->_activeDataStructure.reset();
    d->_dataStructures.clear();
}

DataStructurePtr Document::activeDataStructure() const
{
    return d->_activeDataStructure;
}

DataStructurePluginInterface * Document::backend() const
{
    return d->_backend;
}

void Document::setBackend(const QString &pluginIdentifier)
{
    DataStructurePluginInterface * plugin = DataStructureBackendManager::self()->backend(pluginIdentifier);
    if (plugin) {
        d->_backend = plugin;
    }
}
