/***************************************************************************
 *   Copyright (C) 2008 by S. MANKOWSKI / G. DE BURE support@mankowski.fr  *
 *                                                                         *
 *   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/>  *
 ***************************************************************************/
/** @file
 * This file implements classes SKGDocument.
 *
 * @author Stephane MANKOWSKI / Guillaume DE BURE
 */
#include "skgdocument.h"

#include <KLocale>
#include <KIcon>
#include <KUrl>
#include <KColorScheme>
#include <KGlobal>

#include <QSqlDatabase>
#include <QSqlQuery>
#include <QSqlError>
#include <QSqlDriver>
#include <QHash>
#include <QRegExp>
#include <QUuid>
#include <QVariant>
#include <QFile>
#include <QDir>
#include <QProcess>
#include <QDBusConnection>
#include <QApplication>
#include <qjson/serializer.h>

#include <sqlite3.h>
#include <cmath>

#include "skgtraces.h"
#include "skgerror.h"
#include "skgservices.h"
#include "skgpropertyobject.h"
#include "skgtransactionmng.h"
#include "skgreport.h"

Q_DECLARE_METATYPE(sqlite3*)  // NOLINT(readability/casting)

#ifdef Q_OS_WIN
#define isnan(a) _isnan(a)
#define isinf(a) !_finite(a)
#else
using std::isnan;
using std::isinf;
#endif

SKGError SKGDocument::m_lastCallbackError;
int SKGDocument::m_databaseUniqueIdentifier = 0;

static void periodFunction(sqlite3_context* context, int /*argc*/, sqlite3_value** argv)
{
    int len1 = sqlite3_value_bytes16(argv[ 0 ]);
    const void* data1 = sqlite3_value_text16(argv[ 0 ]);
    int len2 = sqlite3_value_bytes16(argv[ 1 ]);
    const void* data2 = sqlite3_value_text16(argv[ 1 ]);

    if (Q_LIKELY(data1 && data2)) {
        QDate date = SKGServices::stringToTime(QString(reinterpret_cast<const QChar*>(data1), len1 / sizeof(QChar))).date();
        QString format = QString::fromRawData(reinterpret_cast<const QChar*>(data2), len2 / sizeof(QChar)).toUpper();
        QString period = SKGServices::dateToPeriod(date, format);
        QByteArray output = period.toUtf8();
        sqlite3_result_text(context, output.constData(), output.size() , SQLITE_TRANSIENT);
    }
}

static void dateFunction(sqlite3_context* context, int /*argc*/, sqlite3_value** argv)
{
    int len1 = sqlite3_value_bytes16(argv[ 0 ]);
    const void* data1 = sqlite3_value_text16(argv[ 0 ]);
    int len2 = sqlite3_value_bytes16(argv[ 1 ]);
    const void* data2 = sqlite3_value_text16(argv[ 1 ]);

    if (Q_LIKELY(data1 && data2)) {
        QString string(reinterpret_cast<const QChar*>(data1), len1 / sizeof(QChar));
        QString format = QString::fromRawData(reinterpret_cast<const QChar*>(data2), len2 / sizeof(QChar));

        QString date = SKGServices::dateToSqlString(string, format).trimmed();
        if (date.isEmpty()) {
            date = QDate::currentDate().toString("yyyy-MM-dd");
        }

        QByteArray output = date.toUtf8();
        sqlite3_result_text(context, output.constData(), output.size() , SQLITE_TRANSIENT);
    }
}

static void wordFunction(sqlite3_context* context, int /*argc*/, sqlite3_value** argv)
{
    int len1 = sqlite3_value_bytes16(argv[ 0 ]);
    const void* data1 = sqlite3_value_text16(argv[ 0 ]);
    int len2 = sqlite3_value_bytes16(argv[ 1 ]);
    const void* data2 = sqlite3_value_text16(argv[ 1 ]);

    if (Q_LIKELY(data1 && data2)) {
        QString string1(reinterpret_cast<const QChar*>(data1), len1 / sizeof(QChar));
        QStringList list = string1.split(' ');

        int pos = SKGServices::stringToInt(QString::fromRawData(reinterpret_cast<const QChar*>(data2), len2 / sizeof(QChar)));
        if (pos == 0) {
            pos = 1;
        } else if (pos > list.count()) {
            pos = list.count();
        } else if (pos < -list.count()) {
            pos = 1;
        } else if (pos < 0) {
            pos = list.count() + pos + 1;
        }

        QByteArray output = list[pos - 1].toUtf8();

        sqlite3_result_text(context, output.constData(), output.size() , SQLITE_TRANSIENT);
    }
}

static void wildcardFunction(sqlite3_context* context, int /*argc*/, sqlite3_value** argv)
{
    int len1 = sqlite3_value_bytes16(argv[ 0 ]);
    const void* data1 = sqlite3_value_text16(argv[ 0 ]);
    int len2 = sqlite3_value_bytes16(argv[ 1 ]);
    const void* data2 = sqlite3_value_text16(argv[ 1 ]);

    if (Q_LIKELY(data1 && data2)) {
        QString string1(reinterpret_cast<const QChar*>(data1), len1 / sizeof(QChar));
        QString string2 = QString::fromRawData(reinterpret_cast<const QChar*>(data2), len2 / sizeof(QChar));

        QRegExp pattern(string1, Qt::CaseInsensitive, QRegExp::Wildcard);
        sqlite3_result_int(context, pattern.exactMatch(string2) ? 1 : 0);
    }
}

static void regexpFunction(sqlite3_context* context, int /*argc*/, sqlite3_value** argv)
{
    int len1 = sqlite3_value_bytes16(argv[ 0 ]);
    const void* data1 = sqlite3_value_text16(argv[ 0 ]);
    int len2 = sqlite3_value_bytes16(argv[ 1 ]);
    const void* data2 = sqlite3_value_text16(argv[ 1 ]);

    if (Q_LIKELY(data1 && data2)) {
        QString string1(reinterpret_cast<const QChar*>(data1), len1 / sizeof(QChar));
        QString string2 = QString::fromRawData(reinterpret_cast<const QChar*>(data2), len2 / sizeof(QChar));

        QRegExp pattern(string1, Qt::CaseInsensitive, QRegExp::RegExp2);
        sqlite3_result_int(context, pattern.exactMatch(string2) ? 1 : 0);
    }
}

static void upperFunction(sqlite3_context* context, int /*argc*/, sqlite3_value** argv)
{
    int len1 = sqlite3_value_bytes16(argv[ 0 ]);
    const void* data1 = sqlite3_value_text16(argv[ 0 ]);

    if (Q_LIKELY(data1)) {
        // do not use fromRawData for pattern string because it may be cached internally by the regexp engine
        QByteArray output = QString::fromRawData(reinterpret_cast<const QChar*>(data1), len1 / sizeof(QChar)).toUpper().toUtf8();
        sqlite3_result_text(context, output.constData(), output.size() , SQLITE_TRANSIENT);
    }
}

static void lowerFunction(sqlite3_context* context, int /*argc*/, sqlite3_value** argv)
{
    int len1 = sqlite3_value_bytes16(argv[ 0 ]);
    const void* data1 = sqlite3_value_text16(argv[ 0 ]);

    if (Q_LIKELY(data1)) {
        // do not use fromRawData for pattern string because it may be cached internally by the regexp engine
        QByteArray output = QString::fromRawData(reinterpret_cast<const QChar*>(data1), len1 / sizeof(QChar)).toLower().toUtf8();
        sqlite3_result_text(context, output.constData(), output.size() , SQLITE_TRANSIENT);
    }
}

static void capitalizeFunction(sqlite3_context* context, int /*argc*/, sqlite3_value** argv)
{
    int len1 = sqlite3_value_bytes16(argv[ 0 ]);
    const void* data1 = sqlite3_value_text16(argv[ 0 ]);

    if (Q_LIKELY(data1)) {
        // do not use fromRawData for pattern string because it may be cached internally by the regexp engine
        QString str = QString::fromRawData(reinterpret_cast<const QChar*>(data1), len1 / sizeof(QChar));
        QByteArray output = (str.left(1).toUpper() + str.mid(1).toLower()).toUtf8();
        sqlite3_result_text(context, output.constData(), output.size() , SQLITE_TRANSIENT);
    }
}

SKGDocument::SKGDocument()
    : QObject(), m_lastSavedTransaction(0), m_progressFunction(NULL), m_progressData(NULL),
      m_currentFileName(""), m_currentDatabase(NULL),  m_inundoRedoTransaction(0), m_currentTransaction(0),
      m_inProgress(false), m_directAccessDb(false), m_modeReadOnly(false)
{
    SKGTRACEINFUNC(10);

    // DBUS registration
    QDBusConnection dbus = QDBusConnection::sessionBus();
    dbus.registerObject("/skg/skgdocument", this, QDBusConnection::ExportAllContents);
    dbus.registerService("org.skg");

    // Initialisation of undoable tables
    SKGListNotUndoable.push_back("T.doctransaction");
    SKGListNotUndoable.push_back("T.doctransactionitem");
    SKGListNotUndoable.push_back("T.doctransactionmsg");

    // Database unique identifier
    ++m_databaseUniqueIdentifier;
    m_databaseIdentifier = "SKGDATABASE_" % SKGServices::intToString(m_databaseUniqueIdentifier);

    // Initialisation of backup file parameters
    setBackupParameters("", ".old");

    // 320157 vvvv
    // Disable OS lock
    sqlite3_vfs* vfs = sqlite3_vfs_find("unix-none");
    if (Q_LIKELY(vfs)) {
        sqlite3_vfs_register(vfs, 1);
    } else {
        SKGTRACE << "WARNING: Impossible to use the 'unix-none' vfs mode of sqlite3. Use:'" << sqlite3_vfs_find(NULL)->zName << "'" << endl;
    }
    // 320157 ^^^^
}

SKGDocument::~SKGDocument()
{
    SKGTRACEINFUNC(10);
    close();
    m_progressFunction = NULL;
    m_progressData = NULL;
}

QString SKGDocument::getUniqueIdentifier() const
{
    return m_uniqueIdentifier;
}

QString SKGDocument::getDatabaseIdentifier() const
{
    return m_databaseIdentifier;
}

SKGError SKGDocument::setProgressCallback(int (*iProgressFunction)(int, qint64, const QString&, void*), void* iData)
{
    m_progressFunction = iProgressFunction;
    m_progressData = iData;
    return SKGError();
}

SKGError SKGDocument::stepForward(int iPosition, const QString& iText)
{
    SKGError err;

    // Increase the step for the last transaction
    if (Q_LIKELY(getDepthTransaction())) {
        m_posStepForTransaction.pop_back();
        m_posStepForTransaction.push_back(iPosition);
    }

    // Check if a callback function exists
    if (Q_LIKELY(m_progressFunction)) {
        // YES ==> compute
        double min = 0;
        double max = 100;

        bool emitevent = true;
        SKGIntListIterator nbIt = m_nbStepForTransaction.begin();
        SKGIntListIterator posIt = m_posStepForTransaction.begin();
        for (; emitevent && nbIt != m_nbStepForTransaction.end(); ++nbIt) {
            int p = *posIt;
            int n = *nbIt;
            if (Q_UNLIKELY(p < 0 || p > n)) {
                p = n;
            }

            if (Q_LIKELY(n != 0)) {
                double pmin = min;
                double pmax = max;
                min = pmin + (pmax - pmin) * (static_cast<double>(p) / static_cast<double>(n));
                max = pmin + (pmax - pmin) * (static_cast<double>(p + 1) / static_cast<double>(n));
                if (Q_UNLIKELY(max > 100)) {
                    max = 100;
                }
            } else {
                emitevent = false;
            }

            ++posIt;
        }

        int posPercent = rint(min);

        // Call the call back
        if (emitevent) {
            m_inProgress = true;
            QString text;
            qint64 time = QDateTime::currentMSecsSinceEpoch() - m_timeBeginTransaction;
            if (Q_UNLIKELY(time >  3000)) {
                text = iText;
                if (text.isEmpty()) {
                    text = m_nameForTransaction.at(m_nameForTransaction.count() - 1);
                }
            }
            if (Q_LIKELY(m_progressFunction(posPercent, time, text, m_progressData) != 0)) {
                err.setReturnCode(ERR_ABORT).setMessage(i18nc("User interrupted something that Skrooge was performing", "The current operation has been interrupted"));

                // Remove all untransactionnal messaged
                m_unTransactionnalMessages.clear();
                m_unTransactionnalMessagesTypes.clear();
            }
            m_inProgress = false;
        }
    }
    return err;
}

SKGError SKGDocument::beginTransaction(const QString& iName, int iNbStep, const QDateTime& iDate, bool iRefreshViews)
{
    SKGError err;
    SKGTRACEINFUNCRC(5, err);
    SKGTRACEL(10) << "Input parameter [name]=[" << iName << "]  [nb step]=[" << iNbStep << "]  [refresh]=[" << (iRefreshViews ? "Y" : "N") << ']' << endl;
    bool overrideCursor = false;
    if (m_nbStepForTransaction.size() == 0) {
        // Open SQLtransaction
        err = executeSqliteOrder("BEGIN;");
        IFOK(err) {
            overrideCursor = true;

            // Create undo redo transaction
            err = executeSqliteOrder(QString("insert into doctransaction (d_date, t_name, i_parent") %
                                     (!iRefreshViews ? ", t_refreshviews" : "") %
                                     ") values "
                                     "('" % SKGServices::timeToString(iDate) %
                                     "','" % SKGServices::stringToSqlString(iName) %
                                     "', " % SKGServices::intToString(getTransactionToProcess(SKGDocument::UNDO)) %
                                     (!iRefreshViews ? ",'N'" : "") %
                                     ");");
            addValueInCache("SKG_REFRESH_VIEW", (iRefreshViews ? "Y" : "N"));
            m_currentTransaction = getTransactionToProcess(SKGDocument::UNDO);
            m_timeBeginTransaction = QDateTime::currentMSecsSinceEpoch();
        }
    } else {
        // A transaction already exists
        // Check if the child transaction is a opened in the progress callback
        if (m_inProgress) {
            err.setReturnCode(ERR_FAIL).setMessage(i18nc("Something went wrong with SQL transactions", "A transaction cannot be started during execution of another one"));
        }
    }
    IFOK(err) {
        m_nbStepForTransaction.push_back(iNbStep);
        m_posStepForTransaction.push_back(iNbStep);
        QString n = iName;
        n = n.remove("#INTERNAL#");
        if (n.isEmpty() && m_nameForTransaction.count()) {
            n = m_nameForTransaction.at(m_nameForTransaction.count() - 1);
        }
        m_nameForTransaction.push_back(n);

        if (iNbStep) {
            err = stepForward(0);
        }
    } else {
        executeSqliteOrder("ROLLBACK;");
    }

    if (Q_LIKELY(overrideCursor && !err && qApp->type() != QApplication::Tty)) {
        QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
    }

    return err;
}

SKGError SKGDocument::checkExistingTransaction() const
{
    SKGError err;
    if (m_nbStepForTransaction.isEmpty()) {
        err.setReturnCode(ERR_ABORT).setMessage(i18nc("Something went wrong with SQL transactions", "A transaction must be opened to do this action"));
    }
    return err;
}

SKGError SKGDocument::endTransaction(bool succeedded)
{
    SKGError err;
    SKGTRACEINFUNCRC(5, err);
    if (Q_UNLIKELY(m_nbStepForTransaction.size() == 0)) {
        err.setReturnCode(ERR_ABORT).setMessage(i18nc("Something went wrong with SQL transactions", "Closing transaction failed because too many transactions ended"));
    } else {
        stepForward(m_nbStepForTransaction.at(m_nbStepForTransaction.count() - 1));  // =100%
        if (Q_LIKELY(m_nbStepForTransaction.size())) {  // This test is needed. It's a security in some cases.
            m_nbStepForTransaction.pop_back();
            m_posStepForTransaction.pop_back();
            m_nameForTransaction.pop_back();
        }
        QString currentTransactionString = SKGServices::intToString(getCurrentTransaction());

        if (m_nbStepForTransaction.size() == 0) {
            QStringList listModifiedTables;
            if (succeedded) {
                // Link items on current transaction
                IFOK(err) {
                    err = executeSqliteOrder("UPDATE doctransactionitem set rd_doctransaction_id=" % currentTransactionString % " WHERE rd_doctransaction_id=0;");
                }

                // Optimization of the current transaction
                IFOK(err) {
                    SKGStringListList listTmp;
                    err = executeSelectSqliteOrder("SELECT count(1) FROM doctransactionitem where rd_doctransaction_id=" % currentTransactionString, listTmp);
                    IFOK(err) {
                        int nbItem = SKGServices::stringToInt(listTmp.at(1).at(0));
                        if (nbItem == 0) {
                            // Optimization is needed
                            // Get non hidden messages
                            QStringList messages;
                            QList<MessageType>  messagesTypes;
                            getMessages(getCurrentTransaction(), messages, messagesTypes, false);

                            // Delete current transaction
                            err = executeSqliteOrder("DELETE FROM doctransaction WHERE id=" % currentTransactionString);

                            int nb = messages.count();
                            for (int i = 0; i < nb; ++i) {
                                m_unTransactionnalMessages.push_back(messages.at(i));
                                m_unTransactionnalMessagesTypes.push_back(messagesTypes.at(i));
                            }
                        }
                    }
                }

                // Optimization 2: remove duplicate orders
                IFOK(err) {
                    QString wc = "DELETE FROM doctransactionitem WHERE id IN "
                                 "(SELECT a.id FROM doctransactionitem a INDEXED BY idx_doctransactionitem_optimization, doctransactionitem b INDEXED BY idx_doctransactionitem_optimization "
                                 "WHERE a.rd_doctransaction_id=" % currentTransactionString % " AND b.rd_doctransaction_id=" % currentTransactionString %
                                 " AND a.i_object_id=b.i_object_id AND a.t_object_table=b.t_object_table AND b.t_action=a.t_action AND b.t_sqlorder=a.t_sqlorder AND a.id>b.id );";
                    err = executeSqliteOrder(wc);
                }

                // Get current transaction information to be able to emit envent in case of SKG_UNDO_MAX_DEPTH=0
                IFOK(err) {
                    err = this->getDistinctValues("doctransactionitem",
                                                  "t_object_table",
                                                  "rd_doctransaction_id=" % currentTransactionString,
                                                  listModifiedTables);
                }

                // Remove oldest transaction
                IFOK(err) {
                    QString maxdepthstring = getParameter("SKG_UNDO_MAX_DEPTH");
                    if (maxdepthstring.isEmpty()) {
                        maxdepthstring = "-1";
                    }
                    int maxdepth = SKGServices::stringToInt(maxdepthstring);
                    if (maxdepth >= 0) {
                        err = executeSqliteOrder("delete from doctransaction where id in (select id from doctransaction limit max(0,((select count(1) from doctransaction)-(" % maxdepthstring % "))))");
                    }
                }

                // Remove SKGDocument::REDO transactions if we are not in a undo / redo transaction
                if (!m_inundoRedoTransaction) {
                    int i = 0;
                    while ((i = getTransactionToProcess(SKGDocument::REDO)) && !err) {
                        err = executeSqliteOrder("delete from doctransaction where id=" % SKGServices::intToString(i));
                    }
                }

                // Commit the transaction
                IFOK(err) {
                    err = executeSqliteOrder("COMMIT;");
                }
            }

            if (!succeedded || err) {
                // Rollback the transaction
                SKGError err2 = executeSqliteOrder("ROLLBACK;");
                // delete the transaction
                IFOKDO(err2, executeSqliteOrder("delete from doctransaction where id=" % currentTransactionString));

                if (err2) {
                    err.addError(err2.getReturnCode(), err2.getMessage());
                }
            } else {
                // For better performance, events are submitted only for the first recursive undo
                if (Q_UNLIKELY(m_inundoRedoTransaction <= 1)) {
                    // Is it a light transaction?
                    bool lightTransaction = (getCachedValue("SKG_REFRESH_VIEW") != "Y");

                    // Emit modification events
                    QStringList tablesRefreshed;
                    foreach(const QString & table, listModifiedTables) {
                        Q_EMIT tableModified(table, getCurrentTransaction(), lightTransaction);
                        tablesRefreshed.push_back(table);
                    }
                    Q_EMIT tableModified("doctransaction", getCurrentTransaction(), lightTransaction);
                    Q_EMIT tableModified("doctransactionitem", getCurrentTransaction(), lightTransaction);

                    // WARNING: list is modified during treatement
                    for (int i = 0; !err && i < listModifiedTables.count(); ++i) {
                        QString table = listModifiedTables.at(i);
                        QStringList toAdd = getImpactedViews(table);
                        int nbToAdd = toAdd.count();
                        for (int j = 0; !err &&  j < nbToAdd; ++j) {
                            QString toAddTable = toAdd.at(j);
                            if (!listModifiedTables.contains(toAddTable)) {
                                // Compute materialized view of modified table
                                if (!lightTransaction) {
                                    err = computeMaterializedViews(toAddTable);
                                }
                                listModifiedTables.push_back(toAddTable);
                            }
                        }
                    }

                    // Emit events
                    for (int i = tablesRefreshed.count(); i < listModifiedTables.count(); ++i) {
                        Q_EMIT tableModified(listModifiedTables.at(i), 0, lightTransaction);
                    }

                    Q_EMIT transactionSuccessfullyEnded(getCurrentTransaction());
                }
            }

            // Remove temporary transaction if needed
            IFOKDO(err, executeSqliteOrder("delete from doctransaction where t_name LIKE '#INTERNAL#%';"))

            m_currentTransaction = 0;

            // clean cache
            m_cache.clear();

            if (Q_LIKELY(qApp->type() != QApplication::Tty)) {
                QApplication::restoreOverrideCursor();
            }
        }
    }
    return err;
}

SKGError SKGDocument::removeAllTransactions()
{
    SKGError err;
    SKGTRACEINFUNCRC(10, err);
    // Check if a transaction is still opened
    err = checkExistingTransaction();
    IFOK(err) err.setReturnCode(ERR_ABORT).setMessage(i18nc("Something went wrong with SQL transactions", "Remove of transactions is forbidden inside a transaction"));
    else {
        err = SKGDocument::beginTransaction("#INTERNAL#");
        IFOKDO(err, executeSqliteOrder("delete from doctransaction"))
        SKGENDTRANSACTION(this,  err);

        // Force the save
        m_lastSavedTransaction = -1;
    }
    return err;
}

SKGError SKGDocument::computeMaterializedViews(const QString& iTable)
{
    SKGError err;
    SKGTRACEINFUNCRC(5, err);

    // Compute additional where clause
    QStringList tables;
    if (m_MaterializedViews.contains(iTable)) {
        tables = m_MaterializedViews[iTable];
    } else {
        QString wc;
        if (!iTable.isEmpty()) {
            QString t = iTable;
            if (t.startsWith(QLatin1String("v_"))) {
                t.replace(0, 2, "vm_");
            }
            wc = " AND name='" % t % '\'';
        }

        // Get list of materialized table
        err = getDistinctValues("sqlite_master", "name", "type='table' AND name LIKE 'vm_%' " % wc, tables);
        m_MaterializedViews[iTable] = tables;
    }

    // Refresh tables
    int nb = tables.count();
    for (int i = 0; !err && i < nb; ++i) {
        QString table = tables.at(i);
        QString view = table;
        view.replace(0, 3, "v_");

        // Remove previous table
        {
            SKGTRACEINRC(5, "SKGDocument::computeMaterializedViews-drop-" % table, err);
            err = executeSqliteOrder("DROP TABLE IF EXISTS " % table);
        }
        {
            // Recreate table
            SKGTRACEINRC(5, "SKGDocument::computeMaterializedViews-create-" % table, err);
            IFOKDO(err, executeSqliteOrder("CREATE TABLE " % table % " AS SELECT * FROM " % view))
        }
    }

    return err;
}

SKGError SKGDocument::sendMessage(const QString& iMessage, MessageType iMessageType)
{
    SKGError err;
    SKGTRACEINFUNCRC(10, err);
    // Associate message with transaction
    if (!checkExistingTransaction()) {
        SKGObjectBase msg(this, "doctransactionmsg");
        err = msg.setAttribute("rd_doctransaction_id", SKGServices::intToString(getCurrentTransaction()));
        IFOKDO(err, msg.setAttribute("t_message", iMessage))
        IFOKDO(err, msg.setAttribute("t_type", iMessageType == SKGDocument::Positive ? "P" :
                                     iMessageType == SKGDocument::Information ? "I" :
                                     iMessageType == SKGDocument::Warning ? "W" :
                                     iMessageType == SKGDocument::Error ? "E" : "H"));
        IFOKDO(err, msg.save())
    } else {
        // Addition message in global variable in case of no transaction opened
        if (iMessageType != SKGDocument::Hidden && !m_unTransactionnalMessages.contains(iMessage)) {
            m_unTransactionnalMessages.push_back(iMessage);
            m_unTransactionnalMessagesTypes.push_back(iMessageType);
        }
    }
    return err;
}

SKGError SKGDocument::removeMessages(int iIdTransaction)
{
    SKGError err;
    SKGTRACEINFUNCRC(10, err);

    if (!checkExistingTransaction()) {
        err = executeSqliteOrder("DELETE FROM doctransactionmsg WHERE rd_doctransaction_id=" % SKGServices::intToString(iIdTransaction));
    }

    m_unTransactionnalMessages.clear();
    m_unTransactionnalMessagesTypes.clear();
    return err;
}

SKGError SKGDocument::getMessages(int iIdTransaction, QStringList& oMessages, QList<MessageType>& oMessageTypes, bool iAll)
{
    SKGError err;
    SKGTRACEINFUNCRC(10, err);
    oMessages = m_unTransactionnalMessages;
    oMessageTypes = m_unTransactionnalMessagesTypes;

    m_unTransactionnalMessages.clear();
    m_unTransactionnalMessagesTypes.clear();

    SKGStringListList listTmp;
    if (getDatabase()) {
        err = executeSelectSqliteOrder(
                  QString("SELECT t_message, t_type FROM doctransactionmsg WHERE ") %
                  (iAll ? "" : "t_type<>'H' AND ") %
                  "rd_doctransaction_id=" %
                  SKGServices::intToString(iIdTransaction) %
                  " ORDER BY id ASC",
                  listTmp);
    }
    int nb = listTmp.count();
    for (int i = 1; !err && i < nb ; ++i) {
        QString msg = listTmp.at(i).at(0);
        QString type = listTmp.at(i).at(1);
        if (!oMessages.contains(msg)) {
            oMessages.push_back(msg);
            oMessageTypes.push_back(type == "P" ? SKGDocument::Positive : type == "I" ? SKGDocument::Information : type == "W" ? SKGDocument::Warning : type == "E" ? SKGDocument::Error : SKGDocument::Hidden);
        }
    }
    return err;
}

SKGError SKGDocument::getModifications(int iIdTransaction, SKGObjectModificationList& oModifications)
{
    SKGError err;
    SKGTRACEINFUNCRC(10, err);
    oModifications.clear();

    SKGStringListList listTmp;
    err = executeSelectSqliteOrder(
              "SELECT i_object_id,t_object_table,t_action FROM doctransactionitem WHERE rd_doctransaction_id=" %
              SKGServices::intToString(iIdTransaction) %
              " ORDER BY id ASC",
              listTmp);
    int nb = listTmp.count();
    for (int i = 1; !err && i < nb ; ++i) {
        SKGObjectModification mod;
        mod.id = SKGServices::stringToInt(listTmp.at(i).at(0));
        mod.table = listTmp.at(i).at(1);
        QString type = listTmp.at(i).at(2);
        mod.type = (type == "D" ? I : (type == "I" ? D : U));  // Normal because in database we have to sql order to go back.
        mod.uuid = listTmp.at(i).at(0) % '-' % mod.table;

        oModifications.push_back(mod);
    }
    return err;
}

QStringList SKGDocument::getImpactedViews(const QString& iTable)
{
    SKGTRACEINFUNC(10);
    if (Q_UNLIKELY(m_ImpactedViews.count() == 0)) {
        // Get list of tables and views
        QStringList tables;
        SKGStringListList result;
        executeSelectSqliteOrder("SELECT tbl_name FROM sqlite_master WHERE tbl_name NOT LIKE '%_delete' AND type IN ('table', 'view')", result);
        int nb = result.count();
        for (int i = 1; i < nb; ++i) {
            tables.push_back(result.at(i).at(0));
        }

        // First computation
        executeSelectSqliteOrder("SELECT tbl_name, sql FROM sqlite_master WHERE tbl_name NOT LIKE '%_delete' AND type='view'", result);
        nb = result.count();
        for (int i = 1; i < nb; ++i) {
            QStringList line = result.at(i);
            QString name = line.at(0);
            QString sql = line.at(1);

            QStringList words = SKGServices::splitCSVLine(sql, ' ', false);
            words.push_back("parameters");
            int nbWords = words.count();
            for (int j = 0; j < nbWords; ++j) {
                QString word = words.at(j);
                word = word.remove(',');
                if (word.startsWith(QLatin1String("vm_"))) {
                    word.replace(0, 3, "v_");
                }
                if (word != name && tables.contains(word, Qt::CaseInsensitive)) {
                    QStringList l = m_ImpactedViews[word];
                    if (!l.contains(name)) {
                        l.push_back(name);
                    }
                    m_ImpactedViews[word] = l;
                }
            }
        }

        // Now, we have some thing like this
        // m_ImpactedViews[A]={ B, C, D}
        // m_ImpactedViews[B]={ E, F}
        // We must build m_ImpactedViews[A]={ B, C, D, E, F}
        QStringList keys = m_ImpactedViews.keys();
        foreach(const QString & k, keys) {
            QStringList l = m_ImpactedViews[k];
            for (int i = 0; i < l.count(); ++i) {  // Warning: the size of l will change in the loop
                QString item = l.at(i);
                if (m_ImpactedViews.contains(item)) {
                    foreach(const QString & name, m_ImpactedViews[item])
                    if (!l.contains(name)) {
                        l.push_back(name);
                    }
                }
            }
            m_ImpactedViews[k] = l;
        }
    }
    return m_ImpactedViews[iTable];
}

SKGError SKGDocument::groupTransactions(int iFrom, int iTo)
{
    SKGError err;
    SKGTRACEINFUNCRC(5, err);

    ++m_inundoRedoTransaction;  // It's a kind of undo redo

    // Check if a transaction is still opened
    err = checkExistingTransaction();
    IFOK(err) err.setReturnCode(ERR_ABORT).setMessage(i18nc("Something went wrong with SQL transactions", "Creation of a group of transactions is forbidden inside a transaction"));
    else {
        int iidMaster = qMax(iFrom, iTo);
        QString smin = SKGServices::intToString(qMin(iFrom, iTo));
        QString smax = SKGServices::intToString(iidMaster);

        // Get transaction
        SKGStringListList transactions;
        err = executeSelectSqliteOrder(
                  QString("SELECT id, t_name, t_mode, i_parent FROM doctransaction WHERE id BETWEEN ") %
                  smin % " AND " %
                  smax % " ORDER BY id ASC",
                  transactions);

        // Check and get main parameter for the group
        int nb = transactions.count();
        QString transactionMode;
        QString communParent;
        QString name;
        for (int i = 1; !err && i < nb; ++i) {  // We forget header
            QStringList transaction = transactions.at(i);
            QString mode = transaction.at(2);
            if (!name.isEmpty()) {
                name += ',';
            }
            name += transaction.at(1);

            if (!transactionMode.isEmpty() && mode != transactionMode) {
                err = SKGError(ERR_INVALIDARG, "Undo and Redo transactions cannot be grouped");
            } else {
                transactionMode = mode;
            }

            if (i == 1) {
                communParent = transaction.at(3);
            }
        }

        // Group
        IFOK(err) {
            err = SKGDocument::beginTransaction("#INTERNAL#");
            // Group items
            IFOKDO(err, executeSqliteOrder(
                       QString("UPDATE doctransactionitem set rd_doctransaction_id=") %
                       smax %
                       " where rd_doctransaction_id BETWEEN " %
                       smin % " AND " % smax));
            IFOKDO(err, executeSqliteOrder(
                       QString("UPDATE doctransaction set i_parent=") %
                       communParent %
                       ", t_name='" % SKGServices::stringToSqlString(name) %
                       "' where id=" % smax));

            IFOKDO(err, executeSqliteOrder(
                       QString("DELETE FROM doctransaction WHERE id BETWEEN ") %
                       smin % " AND " % SKGServices::intToString(qMax(iFrom, iTo) - 1)));

            SKGENDTRANSACTION(this,  err);
        }
    }

    --m_inundoRedoTransaction;
    return err;
}

SKGError SKGDocument::undoRedoTransaction(const UndoRedoMode& iMode)
{
    SKGError err;
    SKGTRACEINFUNCRC(5, err);
    // Check if a transaction is still opened
    err = checkExistingTransaction();
    IFOK(err) err.setReturnCode(ERR_ABORT).setMessage(i18nc("Something went wrong with SQL transactions", "Undo / Redo is forbidden inside a transaction"));
    else {
        if (iMode == SKGDocument::UNDOLASTSAVE) {
            // Create group
            SKGStringListList transactions;
            err = executeSelectSqliteOrder(
                      "SELECT id, t_savestep FROM doctransaction WHERE t_mode='U' ORDER BY id DESC",
                      transactions);
            int nb = transactions.count();
            int min = 0;
            int max = 0;
            for (int i = 1; !err && i < nb; ++i) {
                QStringList transaction = transactions.at(i);
                if (i == 1) {
                    max = SKGServices::stringToInt(transaction.at(0));
                }
                if (i != 1 && transaction.at(1) == "Y") {
                    break;
                }
                min = SKGServices::stringToInt(transaction.at(0));
            }
            if (min == 0) {
                min = max;
            }
            if (!err && min != max && min != 0) {
                err = groupTransactions(min, max);
            }
        } else {
            err = SKGError();  // To ignore error generated by checkExistingTransaction.
        }

        // Get ID of the transaction to undo
        IFOK(err) {
            QString name;
            bool saveStep = false;
            QDateTime date;
            bool refreshViews;
            int id = getTransactionToProcess(iMode, &name, &saveStep, &date, &refreshViews);
            if (id == 0) {
                // No transaction found ==> generate an error
                err = SKGError(ERR_INVALIDARG, "No transaction found. Undo / Redo impossible.");
            } else {
                // Undo transaction
                SKGTRACEL(5) << "Undoing transaction [" << id << "]- [" << name << "]..." << endl;
                SKGStringListList listSqlOrder;
                err = executeSelectSqliteOrder(
                          "SELECT t_sqlorder FROM doctransactionitem WHERE rd_doctransaction_id=" %
                          SKGServices::intToString(id) %
                          " ORDER BY id DESC",
                          listSqlOrder);
                IFOK(err) {
                    int nb = listSqlOrder.count();
                    err = SKGDocument::beginTransaction(name, nb + 3, date, refreshViews);
                    IFOK(err) {
                        ++m_inundoRedoTransaction;  // Because we will be in a undo/redo transaction
                        // Normal the first element is ignored because it is the header
                        for (int i = 1; !err && i < nb ; ++i) {
                            err = executeSqliteOrder(listSqlOrder.at(i).at(0));

                            IFOKDO(err, stepForward(i))
                        }

                        IFOK(err) {
                            // Set the NEW transaction in redo mode
                            int lastredo = getTransactionToProcess((iMode == SKGDocument::UNDO || iMode == SKGDocument::UNDOLASTSAVE  ? SKGDocument::REDO : SKGDocument::UNDO));
                            int newredo = getTransactionToProcess(iMode);
                            IFOKDO(err, executeSqliteOrder(
                                       QString("UPDATE doctransaction set t_mode=") %
                                       (iMode == SKGDocument::UNDO || iMode == SKGDocument::UNDOLASTSAVE ? "'R'" : "'U'") %
                                       ", i_parent=" %
                                       SKGServices::intToString(lastredo) %
                                       " where id=" % SKGServices::intToString(newredo)));
                            IFOKDO(err, stepForward(nb))

                            // Move messages from previous transaction to new one
                            IFOKDO(err, executeSqliteOrder(
                                       "UPDATE doctransactionmsg set rd_doctransaction_id=" %
                                       SKGServices::intToString(getCurrentTransaction()) %
                                       " where rd_doctransaction_id=" %
                                       SKGServices::intToString(id)));
                            IFOKDO(err, stepForward(nb + 1))

                            // delete treated transaction
                            IFOKDO(err, executeSqliteOrder(
                                       "DELETE from doctransaction where id="
                                       % SKGServices::intToString(id)));
                            IFOKDO(err, stepForward(nb + 2))

                            // Check that new transaction has exactly the same number of item
                            /* IFOK (err) {
                                     SKGStringListList listSqlOrder;
                                     err=executeSelectSqliteOrder(
                                                     "SELECT count(1) FROM doctransactionitem WHERE rd_doctransaction_id=" %
                                                     SKGServices::intToString(getCurrentTransaction()),
                                                     listSqlOrder);
                                     if (!err && SKGServices::stringToInt(listSqlOrder.at(1).at(0))!=nb-1) {
                                             err=SKGError(ERR_ABORT, i18nc("Error message", "Invalid number of item after undo/redo. Expected (%1) != Result (%2)",nb-1,listSqlOrder.at(1).at(0)));
                                     }
                             }*/

                            IFOKDO(err, stepForward(nb + 3))
                        }

                        SKGENDTRANSACTION(this,  err);
                        --m_inundoRedoTransaction;  // We left the undo / redo transaction
                    }
                }
            }
        }
    }

    return err;
}

int SKGDocument::getDepthTransaction() const
{
    return m_nbStepForTransaction.size();
}

int SKGDocument::getNbTransaction(const UndoRedoMode& iMode) const
{
    SKGTRACEINFUNC(10);
    int output = 0;
    if (Q_LIKELY(getDatabase())) {
        QString sqlorder = "select count(1) from doctransaction where t_mode='";
        sqlorder += (iMode == SKGDocument::UNDO || iMode == SKGDocument::UNDOLASTSAVE ? "U" : "R");
        sqlorder += '\'';
        QSqlQuery query = getDatabase()->exec(sqlorder);
        if (query.next()) {
            output = query.value(0).toInt();
        }
    }
    return output;
}

int SKGDocument::getTransactionToProcess(const UndoRedoMode& iMode, QString* oName, bool* oSaveStep, QDateTime* oDate, bool* oRefreshViews) const
{
    SKGTRACEINFUNC(10);
    // initialisation
    int output = 0;
    if (oName) {
        *oName = "";
    }
    if (Q_LIKELY(getDatabase())) {
        QString sqlorder = "select A.id , A.t_name, A.t_savestep, A.d_date, A.t_refreshviews from doctransaction A where "
                           "NOT EXISTS(select 1 from doctransaction B where B.i_parent=A.id) "
                           "and A.t_mode='";
        sqlorder += (iMode == SKGDocument::UNDO || iMode == SKGDocument::UNDOLASTSAVE ? "U" : "R");
        sqlorder += '\'';
        QSqlQuery query = getDatabase()->exec(sqlorder);
        if (query.next()) {
            output = query.value(0).toInt();
            if (oName != NULL) {
                *oName = query.value(1).toString();
            }
            if (oSaveStep != NULL) {
                *oSaveStep = (query.value(2).toString() == "Y");
            }
            if (oDate != NULL) {
                *oDate = SKGServices::stringToTime(query.value(3).toString());
            }
            if (oRefreshViews != NULL) {
                *oRefreshViews = (query.value(4).toString() == "Y");
            }
        }
    }
    return output;
}

int SKGDocument::getCurrentTransaction() const
{
    SKGTRACEINFUNC(10);
    return m_currentTransaction;
}

SKGError SKGDocument::changePassword(const QString& newPassword)
{
    SKGError err;
    SKGTRACEINFUNCRC(10, err);
    err = setParameter("SKG_PASSWORD", newPassword);
    IFOKDO(err, sendMessage(newPassword.isEmpty() ? i18nc("Inform the user that the password protection was removed", "The document password has been removed.") :
                            i18nc("Inform the user that the password was successfully changed", "The document password has been modified."), SKGDocument::Positive));
    return err;
}

SKGError SKGDocument::setLanguage(const QString& iLanguage)
{
    SKGError err;
    SKGTRACEINFUNCRC(5, err);
    QString previousLanguage = getParameter("SKG_LANGUAGE");
    if (previousLanguage != iLanguage) {
        // Save language into the document
        IFOKDO(err, beginTransaction("#INTERNAL#", 0, QDateTime::currentDateTime(), false))
        IFOKDO(err, setParameter("SKG_LANGUAGE", iLanguage))

        // Migrate view for new language
        IFOKDO(err, refreshViewsIndexesAndTriggers())

        // close temporary transaction
        SKGENDTRANSACTION(this,  err);
    }
    return err;
}

SKGError SKGDocument::initialize()
{
    SKGError err;
    SKGTRACEINFUNCRC(5, err);
    err = load("", "");
    return err;
}

SKGError SKGDocument::recover(const QString& iName, const QString& iPassword, QString& oRecoveredFile)
{
    SKGError err;
    SKGTRACEINFUNCRC(5, err);
    SKGTRACEL(10) << "Input parameter [name]=[" << iName << ']' << endl;

    QString sqliteFile = QString(iName % "_recovered.sqlite").replace(".skg_", "_");
    oRecoveredFile = QString(iName % "_recovered.skg").replace(".skg_", "_");
    err = SKGServices::cryptFile(iName, sqliteFile, iPassword, false, getDocumentHeader());
    IFOK(err) {
        QFile(oRecoveredFile).remove();
        QString cmd = "echo .dump | sqlite3 \"" % sqliteFile % "\" | sed -e 's/ROLLBACK; -- due to errors/COMMIT;/g' | sqlite3 \"" % oRecoveredFile % '"';
        QProcess p;
        p.start("sh", QStringList() << "-c" << cmd);
        if (!p.waitForFinished(1000 * 60 * 2) || p.exitCode() != 0) {
            err.setReturnCode(ERR_FAIL).setMessage(i18nc("Error message",  "The following command line failed:\n'%1'", cmd));
        }

        // Try to load the recovered file
        IFOKDO(err, load(oRecoveredFile, ""));
        IFOK(err) {
            SKGBEGINTRANSACTION(*this, i18nc("Noun", "Recovery"), err);
            IFOKDO(err, refreshViewsIndexesAndTriggers(true))
        }
        IFOKDO(err, save())

        // Reset the current document
        initialize();

        // Clean useless file
        IFOK(err) {
            // We keep only the recovered
            QFile(sqliteFile).remove();
        } else {
            // We keep the sqlite file in case of
            QFile(oRecoveredFile).remove();
            err.addError(ERR_FAIL, i18nc("Error message", "Impossible to recover this file"));
        }
    }

    return err;
}

SKGError SKGDocument::load(const QString& iName, const QString& iPassword, bool iRestoreTmpFile, bool iForceReadOnly)
{
    // Close previous document
    SKGError err;
    SKGTRACEINFUNCRC(5, err);
    SKGTRACEL(10) << "Input parameter [name]=[" << iName << ']' << endl;

    m_lastSavedTransaction = -1;  // To avoid double event emission
    err = close();
    IFOK(err) {
        if (!iName.isEmpty()) {
            // File exist
            QFileInfo fi(iName);
            m_modeReadOnly = iForceReadOnly || !fi.permission(QFile::WriteUser);

            // Temporary file
            m_temporaryFile = SKGDocument::getTemporaryFile(iName, m_modeReadOnly);
            if (!iRestoreTmpFile || !QFile(m_temporaryFile).exists()) {
                QFile::remove(m_temporaryFile);  // Must remove it to be able to copy
                err = SKGServices::cryptFile(iName, m_temporaryFile, iPassword, false, getDocumentHeader());
            } else {
                // BUG 249955: Check if password protected vvv
                // Temporary file will be loaded but first we must check if original document is password protected
                QString temporaryFile2 = m_temporaryFile % '2';
                err = SKGServices::cryptFile(iName, temporaryFile2, iPassword, false, getDocumentHeader());

                // Try an open to check if well descrypted
                IFOK(err) {
                    QSqlDatabase tryOpen(QSqlDatabase::addDatabase("QSQLITE", "tryOpen"));
                    tryOpen.setDatabaseName(temporaryFile2);
                    if (!tryOpen.open()) {
                        // Set error message
                        QSqlError sqlErr = tryOpen.lastError();
                        err = SKGError(SQLLITEERROR + sqlErr.number(), sqlErr.text());
                    }
                    IFOKDO(err, SKGServices::executeSqliteOrder(&tryOpen, "PRAGMA synchronous = OFF"))
                }
                QSqlDatabase::removeDatabase("tryOpen");
                QFile::remove(temporaryFile2);

                // To avoid deletion of temporary file during next try
                IFKO(err) m_temporaryFile = "";
                // BUG 249955: Check if password protected ^^^
            }

            // Create file database
            IFOK(err) {
                m_currentDatabase = new QSqlDatabase(QSqlDatabase::addDatabase("QSQLITE", m_databaseIdentifier));
                m_currentDatabase->setDatabaseName(m_temporaryFile);
                if (!m_currentDatabase->open()) {
                    // Set error message
                    QSqlError sqlErr = m_currentDatabase->lastError();
                    err = SKGError(SQLLITEERROR + sqlErr.number(), sqlErr.text());
                }

                m_directAccessDb = true;
                if (KUrl(iName).isLocalFile()) {
                    m_currentFileName = iName;
                }
            }
        } else {
            // Temporary file
            m_temporaryFile = QDir::tempPath() % "/skg_" % QUuid::createUuid().toString() % ".skg";

            // Create memory database
            m_currentDatabase = new QSqlDatabase(QSqlDatabase::addDatabase("QSQLITE", m_databaseIdentifier));
            m_currentDatabase->setDatabaseName(":memory:");
            if (!m_currentDatabase->open()) {
                // Set error message
                QSqlError sqlErr = m_currentDatabase->lastError();
                err = SKGError(SQLLITEERROR + sqlErr.number(), sqlErr.text());
            }

            m_directAccessDb = false;
        }

        // Check if the database is correct
        IFOK(err) {
            IFOKDO(err, executeSqliteOrder("PRAGMA journal_mode=MEMORY"))
            IFKO(err) {
                err.addError(ERR_CORRUPTION, i18nc("Error message", "Oups, this file seems to be corrupted"));
            }
        }

        // Optimization
        QStringList optimization;
        optimization << "PRAGMA case_sensitive_like=true"
                     << "PRAGMA journal_mode=MEMORY"
                     << "PRAGMA temp_store=MEMORY"
                     << "PRAGMA locking_mode=EXCLUSIVE"
                     << "PRAGMA synchronous = OFF"
                     << "PRAGMA recursive_triggers=true";
        IFOKDO(err, executeSqliteOrders(optimization))

        if (!m_directAccessDb) {
            // Create parameter and undo redo table
            /**
            * This constant is used to initialized the data model (table creation)
            */
            QStringList InitialDataModel;

            // ==================================================================
            // Table parameters
            InitialDataModel << "CREATE TABLE parameters "
                             "(id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,"
                             "t_uuid_parent TEXT NOT NULL DEFAULT '',"
                             "t_name TEXT NOT NULL,"
                             "t_value TEXT NOT NULL DEFAULT '',"
                             "b_blob BLOB,"
                             "d_lastmodifdate DATE NOT NULL DEFAULT CURRENT_TIMESTAMP,"
                             "i_tmp INTEGER NOT NULL DEFAULT 0"
                             ")"

                             // ==================================================================
                             // Table node
                             << "CREATE TABLE node ("
                             "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,"
                             "t_name TEXT NOT NULL DEFAULT '' CHECK (t_name NOT LIKE '%" % OBJECTSEPARATOR % "%'),"
                             "t_fullname TEXT,"
                             "t_icon TEXT DEFAULT '',"
                             "f_sortorder FLOAT,"
                             "t_autostart VARCHAR(1) DEFAULT 'N' CHECK (t_autostart IN ('Y', 'N')),"
                             "t_data TEXT,"
                             "rd_node_id INT CONSTRAINT fk_id REFERENCES node(id) ON DELETE CASCADE)"

                             // ==================================================================
                             // Table doctransaction
                             << "CREATE TABLE doctransaction ("
                             "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,"
                             "t_name TEXT NOT NULL,"
                             "t_mode VARCHAR(1) DEFAULT 'U' CHECK (t_mode IN ('U', 'R')),"
                             "d_date DATE NOT NULL,"
                             "t_savestep VARCHAR(1) DEFAULT 'N' CHECK (t_savestep IN ('Y', 'N')),"
                             "t_refreshviews VARCHAR(1) DEFAULT 'Y' CHECK (t_refreshviews IN ('Y', 'N')),"
                             "i_parent INTEGER)"

                             // ==================================================================
                             // Table doctransactionitem
                             << "CREATE TABLE doctransactionitem ("
                             "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "
                             "rd_doctransaction_id INTEGER NOT NULL,"
                             "i_object_id INTEGER NOT NULL,"
                             "t_object_table TEXT NOT NULL,"
                             "t_action VARCHAR(1) DEFAULT 'I' CHECK (t_action IN ('I', 'U', 'D')),"
                             "t_sqlorder TEXT NOT NULL DEFAULT '')"

                             << "CREATE TABLE doctransactionmsg ("
                             "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "
                             "rd_doctransaction_id INTEGER NOT NULL,"
                             "t_message TEXT NOT NULL DEFAULT '',"
                             "t_type VARCHAR(1) DEFAULT 'I' CHECK (t_type IN ('P', 'I', 'W', 'E', 'H')))";  // Positive, Information, Warning, Error, Hidden

            IFOKDO(err, executeSqliteOrders(InitialDataModel))
            IFOKDO(err, SKGDocument::refreshViewsIndexesAndTriggers())
        }
    }

    // migrate
    IFOK(err) {
        bool mig = false;
        err = migrate(mig);

        // To authorize manual repair of document in case of error during migration
        // the error is not caught if traces are activated
        if (err && SKGTraces::SKGLevelTrace) {
            err = sendMessage(i18nc("Popup message", "The migration failed but the document has been loaded without error because debug mode is activated"), SKGDocument::Warning);
        }

        if (!err && mig && !iName.isEmpty()) {
            err = sendMessage(i18nc("The document has been upgraded to the latest Skrooge version format", "The document has been migrated"), SKGDocument::Positive);
        }
    }

    // Optimization
    IFOK(err) {
        m_lastSavedTransaction = getTransactionToProcess(SKGDocument::UNDO);
        executeSqliteOrder("ANALYZE");
    }

    // Creation undo/redo triggers
    IFOKDO(err, createUndoRedoTemporaryTriggers())

    QString sqliteQtVersion = getParameter("SKG_SQLITE_LAST_VERSION");
    QString sqliteSystemVersion = SQLITE_VERSION;
    QProcess sqlite3Process;
    sqlite3Process.start("sqlite3", QStringList() << "-version");
    if (sqlite3Process.waitForFinished()) {
        sqliteSystemVersion = SKGServices::splitCSVLine(sqlite3Process.readAll(), ' ').value(0);
    }
    SKGTRACEL(5) << "SQLite version of Qt        :" << sqliteQtVersion << endl;
    SKGTRACEL(5) << "SQLite version of the system:" << sqliteSystemVersion << endl;
    if (!err && sqliteQtVersion != sqliteSystemVersion) {
        QString message = i18nc("Error message", "This application can not run correctly because the SQLite version of the system (%1) is not aligned with the SQLite version embedded in Qt (%2). You should rebuild Qt with the option -system-sqlite.", sqliteQtVersion, sqliteSystemVersion);
        err = sendMessage(message, Warning);
        SKGTRACE << "WARNING:" << message << endl;
    }

    // Add custom sqlite functions
    IFOK(err) {
        sqlite3* sqlite_handle = getDatabase()->driver()->handle().value<sqlite3*>();
        if (sqlite_handle) {
            sqlite3_create_function(sqlite_handle, "regexp", 2, SQLITE_UTF16, NULL, &regexpFunction, NULL, NULL);
            sqlite3_create_function(sqlite_handle, "wildcard", 2, SQLITE_UTF16, NULL, &wildcardFunction, NULL, NULL);
            sqlite3_create_function(sqlite_handle, "word", 2, SQLITE_UTF16, NULL, &wordFunction, NULL, NULL);
            sqlite3_create_function(sqlite_handle, "todate", 2, SQLITE_UTF16, NULL, &dateFunction, NULL, NULL);
            sqlite3_create_function(sqlite_handle, "period", 2, SQLITE_UTF16, NULL, &periodFunction, NULL, NULL);
            sqlite3_create_function(sqlite_handle, "upper", 1, SQLITE_UTF16, NULL, &upperFunction, NULL, NULL);
            sqlite3_create_function(sqlite_handle, "lower", 1, SQLITE_UTF16, NULL, &lowerFunction, NULL, NULL);
            sqlite3_create_function(sqlite_handle, "capitalize", 1, SQLITE_UTF16, NULL, &capitalizeFunction, NULL, NULL);
        }
    }

    if (err && !iName.isEmpty()) {
        close();
    } else {
        // Send event
        m_uniqueIdentifier = QUuid::createUuid().toString();
        Q_EMIT tableModified("", 0, false);
    }

    return err;
}

bool SKGDocument::isReadOnly() const
{
    return m_modeReadOnly;
}

bool SKGDocument::isFileModified() const
{
    // Get last executed transaction
    int last = getTransactionToProcess(SKGDocument::UNDO);
    //  if (nbStepForTransaction.size()) --last;
    return (m_lastSavedTransaction != last);
}

void SKGDocument::setFileNotModified()
{
    m_lastSavedTransaction = getTransactionToProcess(SKGDocument::UNDO);
}

QString SKGDocument::getCurrentFileName() const
{
    return m_currentFileName;
}

SKGError SKGDocument::save()
{
    SKGError err;
    SKGTRACEINFUNCRC(5, err);
    if (m_currentFileName.isEmpty()) {
        err = SKGError(ERR_INVALIDARG, i18nc("Error message: Can not save a file if it has no name yet", "Save not authorized because the file name is not yet defined"));
    } else {
        // save
        err = saveAs(m_currentFileName, true);
    }
    return err;
}

SKGError SKGDocument::saveAs(const QString& name, bool overwrite)
{
    SKGError err;
    SKGTRACEINFUNCRC(5, err);
    SKGTRACEL(10) << "Input parameter [name]=[" << name << ']' << endl;

    bool simulateFileSystemFull = !SKGServices::getEnvVariable("SKGFILESYSTEMFULL").isEmpty();

    // Check if a transaction is still opened
    err = checkExistingTransaction();
    IFOK(err) err.setReturnCode(ERR_ABORT).setMessage(i18nc("Cannot save the file while Skrooge is still performing an SQL transaction", "Save is forbidden if a transaction is still opened"));
    else {
        err = SKGError();
        if (getParameter("SKG_UNDO_CLEAN_AFTER_SAVE") == "Y") {
            err = executeSqliteOrder("delete from doctransaction");
        }

        // No transaction opened ==> it's ok
        // We mark the last transaction as a save point
        IFOKDO(err, executeSqliteOrder("update doctransaction set t_savestep='Y' where id in (select A.id from doctransaction A where "
                                       "NOT EXISTS(select 1 from doctransaction B where B.i_parent=A.id) "
                                       "and A.t_mode='U')"));
        Q_EMIT tableModified("doctransaction", 0, false);

        // Optimization
        IFOK(err) {
            err = executeSqliteOrder("VACUUM;");
            IFOK(err) {
                // Check if file already exist
                if (!overwrite && QFile(name).exists()) {
                    err.setReturnCode(ERR_INVALIDARG).setMessage(i18nc("There is already a file with the same name", "File '%1' already exist", name));
                } else {
                    // Get backup file name
                    bool backupFileMustBeRemoved = false;
                    QString backupFileName = getBackupFile(name);
                    if (backupFileName.isEmpty()) {
                        backupFileName = name % ".tmp";
                        backupFileMustBeRemoved = true;
                    }

                    // Create backup file
                    QFile::remove(backupFileName % '~');
                    QFile::rename(backupFileName, backupFileName % '~');
                    if (QFile(name).exists() && (simulateFileSystemFull || !QFile(name).copy(backupFileName))) {
                        this->sendMessage(i18nc("Error message: Could not create a backup file", "Creation of backup file %1 failed", backupFileName), Warning);
                    }

                    // Save database
                    IFOK(err) {
                        QFile::remove(name % '~');
                        QFile::rename(name, name % '~');

                        // To be sure that db is flushed
                        IFOKDO(err, executeSqliteOrder("PRAGMA synchronous = FULL"))

                        // Copy memory to tmp db
                        if (!m_directAccessDb && !err) {
                            QFile::remove(m_temporaryFile);
                            QSqlDatabase* fileDb = new QSqlDatabase(QSqlDatabase::addDatabase("QSQLITE", m_databaseIdentifier % "_tmp"));
                            fileDb->setDatabaseName(m_temporaryFile);
                            if (!fileDb->open()) {
                                // Set error message
                                QSqlError sqlErr = fileDb->lastError();
                                err = SKGError(SQLLITEERROR + sqlErr.number(), sqlErr.text());
                            } else {
                                IFOKDO(err, SKGServices::copySqliteDatabase(fileDb, m_currentDatabase, false))
                            }

                            fileDb->close();
                            delete fileDb;
                            QSqlDatabase::removeDatabase(m_databaseIdentifier % "_tmp");
                        }

                        // To simulate a file system full
                        if (!err && simulateFileSystemFull) {
                            err = SKGError(ERR_WRITEACCESS, i18nc("Error message: writing a file failed", "Write file '%1' failed", name));
                        }

                        // Crypt the file
                        IFOKDO(err, SKGServices::cryptFile(m_temporaryFile, name, getParameter("SKG_PASSWORD"), true, getDocumentHeader()))
                        if (!m_directAccessDb && !err) {
                            QFile(m_temporaryFile).remove();
                        }

                        // For performances
                        IFOKDO(err, executeSqliteOrder("PRAGMA synchronous = OFF"))
                    }

                    if (backupFileMustBeRemoved) {
                        QFile::remove(backupFileName);
                    }

                    IFOK(err) {
                        // The document is not modified
                        QString oldtemporaryFile = m_temporaryFile;
                        m_currentFileName = name;
                        m_modeReadOnly = false;
                        m_temporaryFile = getTemporaryFile(m_currentFileName);
                        if (oldtemporaryFile != m_temporaryFile) {
                            QFile(oldtemporaryFile).rename(m_temporaryFile);
                        }
                        m_lastSavedTransaction = getTransactionToProcess(SKGDocument::UNDO);

                        // Commit save
                        QFile::remove(backupFileName % '~');
                        QFile::remove(name % '~');
                    } else {
                        // Rollback file
                        QFile::remove(backupFileName);
                        QFile::rename(backupFileName % '~', backupFileName);

                        QFile::remove(name);
                        QFile::rename(name % '~', name);
                    }
                }
            }
        }

        Q_EMIT transactionSuccessfullyEnded(0);
    }
    return err;
}

SKGError SKGDocument::close()
{
    SKGTRACEINFUNC(5);
    if (getDatabase() != NULL) {
        getDatabase()->close();
        delete m_currentDatabase;  // Delete handler to avoid warning and memory leak
        QSqlDatabase::removeDatabase(m_databaseIdentifier);
    }

    if (!m_temporaryFile.isEmpty()) {
        QFile(m_temporaryFile).remove();
        m_temporaryFile = "";
    }

    // Emit events ?
    bool emitEvent = (m_lastSavedTransaction != -1);

    // Init fields
    m_currentDatabase = NULL;
    m_currentFileName = "";
    m_lastSavedTransaction = 0;
    m_nbStepForTransaction.clear();
    m_posStepForTransaction.clear();
    m_nameForTransaction.clear();

    // Send event
    if (emitEvent && qApp && !qApp->closingDown()) {
        Q_EMIT tableModified("", 0, false);
        Q_EMIT transactionSuccessfullyEnded(0);
    }

    return SKGError();
}

SKGError SKGDocument::dropViewsAndIndexes(const QStringList& iTables) const
{
    SKGError err;
    // Drop all views
    SKGStringListList list;
    err = executeSelectSqliteOrder("SELECT tbl_name, name, type FROM sqlite_master WHERE type IN ('view','index')", list);
    int nb = list.count();
    for (int i = 1; !err && i < nb; ++i) {
        QString name = list.at(i).at(1);
        QString table = SKGServices::getRealTable(list.at(i).at(0));
        QString type = list.at(i).at(2);
        if (iTables.contains(table)) {
            QString sql = "DROP " % type % " IF EXISTS " % name;
            err = this->executeSqliteOrder(sql);
        }
    }
    return err;
}

#include "skgdocument2.cpp"

SKGError SKGDocument::migrate(bool& oMigrationDone)
{
    SKGError err;
    SKGTRACEINFUNCRC(5, err);
    oMigrationDone = false;

    {
        SKGBEGINPROGRESSTRANSACTION(*this, "#INTERNAL#" % i18nc("Progression step", "Migrate document"), err, 3);
        if (getParameter("SKG_UNDO_MAX_DEPTH").isEmpty()) {
            err = setParameter("SKG_UNDO_MAX_DEPTH", SKGServices::intToString(SKG_UNDO_MAX_DEPTH));
        }

        if (getParameter("SKG_UNDO_CLEAN_AFTER_SAVE").isEmpty()) {
            err = setParameter("SKG_UNDO_CLEAN_AFTER_SAVE", "N");
        }

        QString version = getParameter("SKG_DB_VERSION");
        QString initialversion = version;
        QString lastversion = "1.6";

        if (!err && version.isEmpty()) {
            // First creation
            SKGTRACEL(10) << "Migration from 0 to " << lastversion << endl;

            // Set new version
            version = lastversion;
            IFOKDO(err, setParameter("SKG_DB_VERSION", version))

            // Set sqlite creation version
            SKGStringListList listTmp;
            IFOKDO(err, executeSelectSqliteOrder("select sqlite_version()", listTmp))
            if (!err && listTmp.count() == 2) {
                err = setParameter("SKG_SQLITE_CREATION_VERSION", listTmp.at(1).at(0));
            }
            oMigrationDone = true;
        }

        if (!err && SKGServices::stringToDouble(version) > SKGServices::stringToDouble(lastversion)) {
            err = SKGError(ERR_ABORT, i18nc("Error message", "Impossible to load a document generated by a more recent version"));
        }

        {
            // Migration steps
            if (!err && version == "0.1") {
                // Migration from version 0.1 to 0.2
                SKGTRACEL(10) << "Migration from 0.1 to 0.2" << endl;

                // ==================================================================
                // Table doctransactionmsg
                QStringList sqlOrders;
                sqlOrders << "DROP TABLE IF EXISTS doctransactionmsg"
                          << "CREATE TABLE doctransactionmsg ("
                          "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "
                          "rd_doctransaction_id INTEGER NOT NULL,"
                          "t_message TEXT NOT NULL DEFAULT '')";
                err = executeSqliteOrders(sqlOrders);

                // Set new version
                version = "0.2";
                IFOKDO(err, SKGDocument::setParameter("SKG_DB_VERSION", version))
                oMigrationDone = true;
            }
            if (!err && version == "0.2") {
                // Migration from version 0.2 to 0.3
                SKGTRACEL(10) << "Migration from 0.2 to 0.3" << endl;

                err = executeSqliteOrder("UPDATE node set f_sortorder=id");

                // Set new version
                version = "0.3";
                IFOKDO(err, SKGDocument::setParameter("SKG_DB_VERSION", version))
                oMigrationDone = true;
            }
            if (!err && version == "0.3") {
                // Migration from version 0.3 to 0.4
                SKGTRACEL(10) << "Migration from 0.3 to 0.4" << endl;

                err = executeSqliteOrder("ALTER TABLE node ADD COLUMN t_autostart VARCHAR(1) DEFAULT 'N' CHECK (t_autostart IN ('Y', 'N'))");
                IFOKDO(err, executeSqliteOrder("UPDATE node set t_autostart='Y' where t_name='" % i18nc("Verb, automatically load when the application is started", "autostart") % '\''))

                // Set new version
                version = "0.4";
                IFOKDO(err, SKGDocument::setParameter("SKG_DB_VERSION", version))
                oMigrationDone = true;
            }
            if (!err && version == "0.4") {
                // Migration from version 0.4 to 0.5
                SKGTRACEL(10) << "Migration from 0.4 to 0.5" << endl;

                err = executeSqliteOrder("ALTER TABLE doctransactionmsg ADD COLUMN t_popup VARCHAR(1) DEFAULT 'Y' CHECK (t_popup IN ('Y', 'N'))");

                // Set new version
                version = "0.5";
                IFOKDO(err, SKGDocument::setParameter("SKG_DB_VERSION", version))
                oMigrationDone = true;
            }
            if (!err && version == "0.5") {
                // Migration from version 0.5 to 0.6
                SKGTRACEL(10) << "Migration from 0.5 to 0.6" << endl;

                err = executeSqliteOrder("UPDATE node set t_autostart='N' where t_autostart NOT IN ('Y', 'N')");

                // Set new version
                version = "0.6";
                IFOKDO(err, SKGDocument::setParameter("SKG_DB_VERSION", version))
                oMigrationDone = true;
            }
            if (!err && version == "0.6") {
                // Migration from version 0.6 to 0.7
                SKGTRACEL(10) << "Migration from 0.6 to 0.7" << endl;

                err = executeSqliteOrder("ALTER TABLE parameters ADD COLUMN b_blob BLOB");

                // Set new version
                version = "0.7";
                IFOKDO(err, SKGDocument::setParameter("SKG_DB_VERSION", version))
                oMigrationDone = true;
            }
            if (!err && version == "0.7") {
                // Migration from version 0.7 to 0.8
                SKGTRACEL(10) << "Migration from 0.7 to 0.8" << endl;

                err = executeSqliteOrder("UPDATE parameters set t_name='SKG_LANGUAGE' where t_name='SKGLANGUAGE'");

                // Set new version
                version = "0.8";
                IFOKDO(err, SKGDocument::setParameter("SKG_DB_VERSION", version))
                oMigrationDone = true;
            }
            if (!err && version == "0.8") {
                SKGTRACEL(10) << "Migration from 0.8 to 0.9" << endl;

                QStringList sql;
                sql << "ALTER TABLE parameters ADD COLUMN i_tmp INTEGER NOT NULL DEFAULT 0"
                    << "UPDATE parameters set i_tmp=0";

                err = executeSqliteOrders(sql);

                // Set new version
                version = "0.9";
                IFOKDO(err, SKGDocument::setParameter("SKG_DB_VERSION", version))
                oMigrationDone = true;
            }
            if (!err && version == "0.9") {
                SKGTRACEL(10) << "Migration from 0.9 to 1.0" << endl;

                err = SKGDocument::setParameter("SKG_UNIQUE_ID", "");

                // Set new version
                version = "1.0";
                IFOKDO(err, SKGDocument::setParameter("SKG_DB_VERSION", version))
                oMigrationDone = true;
            }
            if (!err && version == "1.0") {
                // Migration from version 1.0 to 1.1
                SKGTRACEL(10) << "Migration from 1.0 to 1.1" << endl;

                err = executeSqliteOrder("ALTER TABLE node ADD COLUMN t_icon TEXT DEFAULT ''");
                IFOK(err) {
                    SKGStringListList result;
                    err = executeSelectSqliteOrder("SELECT id,t_data from node", result);
                    int nb = result.count();
                    for (int i = 1; !err && i < nb; ++i) {
                        QStringList line = result.at(i);
                        QString icon = "folder-bookmark";
                        QStringList data = SKGServices::splitCSVLine(line.at(1));
                        if (data.count() > 2) {
                            icon = data.at(2);
                        }
                        data.removeAt(2);
                        err = executeSqliteOrder("UPDATE node set t_icon='" % SKGServices::stringToSqlString(icon) %
                                                 "', t_data='" % SKGServices::stringToSqlString(SKGServices::stringsToCsv(data)) % "' where id=" % line.at(0));
                    }
                }

                // Set new version
                version = "1.1";
                IFOKDO(err, SKGDocument::setParameter("SKG_DB_VERSION", version))
                oMigrationDone = true;
            }
            if (!err && version == "1.1") {
                // Migration from version 1.1 to 1.2
                SKGTRACEL(10) << "Migration from 1.1 to 1.2" << endl;

                QStringList sql;
                sql << "ALTER TABLE doctransaction ADD COLUMN t_refreshviews VARCHAR(1) DEFAULT 'Y' CHECK (t_refreshviews IN ('Y', 'N'))"
                    << "UPDATE doctransaction set t_refreshviews='Y'";

                err = executeSqliteOrders(sql);

                // Set new version
                version = "1.2";
                IFOKDO(err, SKGDocument::setParameter("SKG_DB_VERSION", version))
                oMigrationDone = true;
            }
            if (!err && version == "1.2") {
                // Migration from version 1.2 to 1.3
                SKGTRACEL(10) << "Migration from 1.2 to 1.3" << endl;

                err = SKGDocument::refreshViewsIndexesAndTriggers();

                QStringList sql;
                sql << "DELETE FROM node WHERE (r_node_id IS NULL OR r_node_id='') AND EXISTS (SELECT 1 FROM node n WHERE n.t_name=node.t_name AND r_node_id=0)"
                    << "UPDATE node SET t_name=t_name";
                IFOKDO(err, executeSqliteOrders(sql))

                // Set new version
                version = "1.3";
                IFOKDO(err, SKGDocument::setParameter("SKG_DB_VERSION", version))
                oMigrationDone = true;
            }
            if (!err && version == "1.3") {
                // Migration from version 1.3 to 1.4
                SKGTRACEL(10) << "Migration from 1.3 to 1.4" << endl;

                QStringList sql;
                sql   << "CREATE TABLE node2 ("
                      "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,"
                      "t_name TEXT NOT NULL DEFAULT '' CHECK (t_name NOT LIKE '%" % OBJECTSEPARATOR % "%'),"
                      "t_fullname TEXT,"
                      "t_icon TEXT DEFAULT '',"
                      "f_sortorder FLOAT,"
                      "t_autostart VARCHAR(1) DEFAULT 'N' CHECK (t_autostart IN ('Y', 'N')),"
                      "t_data TEXT,"
                      "rd_node_id INT CONSTRAINT fk_id REFERENCES node(id) ON DELETE CASCADE)"

                      << "INSERT INTO node2 (id, t_name, t_fullname, t_icon, f_sortorder, t_autostart, t_data, rd_node_id) "
                      "SELECT id, t_name, t_fullname, t_icon, f_sortorder, t_autostart, t_data, r_node_id FROM node"

                      << "DROP TABLE IF EXISTS node"
                      << "ALTER TABLE node2 RENAME TO node";

                err = executeSqliteOrders(sql);

                // Set new version
                version = "1.4";
                IFOKDO(err, SKGDocument::setParameter("SKG_DB_VERSION", version))
                oMigrationDone = true;
            }
            if (!err && version == "1.4") {
                // Migration from version 1.4 to 1.5
                SKGTRACEL(10) << "Migration from 1.4 to 1.5" << endl;

                err = SKGDocument::refreshViewsIndexesAndTriggers();

                QStringList sql;
                sql << "UPDATE parameters SET t_uuid_parent='advice' WHERE t_uuid_parent='advices'";
                IFOKDO(err, executeSqliteOrders(sql))

                // Set new version
                version = "1.5";
                IFOKDO(err, SKGDocument::setParameter("SKG_DB_VERSION", version))
                oMigrationDone = true;
            }
            if (!err && version == "1.5") {
                // Migration from version 1.5 to 1.6
                SKGTRACEL(10) << "Migration from  1.5 to 1.6" << endl;

                err = SKGDocument::refreshViewsIndexesAndTriggers();

                QStringList sql;
                sql << "DROP TABLE IF EXISTS doctransactionmsg2"
                    << "CREATE TABLE doctransactionmsg2 ("
                    "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "
                    "rd_doctransaction_id INTEGER NOT NULL,"
                    "t_message TEXT NOT NULL DEFAULT '',"
                    "t_type VARCHAR(1) DEFAULT 'I' CHECK (t_type IN ('P', 'I', 'W', 'E', 'H')))"  // Positive, Information, Warning, Error, Hidden
                    << "INSERT INTO doctransactionmsg2 (id, rd_doctransaction_id, t_message, t_type) SELECT id, rd_doctransaction_id, t_message, (CASE WHEN t_popup='Y' THEN 'I' ELSE 'H' END)  FROM doctransactionmsg"

                    << "DROP TABLE IF EXISTS doctransactionmsg"
                    << "ALTER TABLE doctransactionmsg2 RENAME TO doctransactionmsg";
                IFOKDO(err, executeSqliteOrders(sql))

                // Set new version
                version = "1.6";
                IFOKDO(err, SKGDocument::setParameter("SKG_DB_VERSION", version))
                oMigrationDone = true;
            }
        }
        IFOKDO(err, stepForward(1, i18nc("Progression step", "Refresh views")))

        // Set sqlite last version
        SKGStringListList listTmp;
        IFOKDO(err, executeSelectSqliteOrder("select sqlite_version()", listTmp))
        if (!err && listTmp.count() == 2) {
            err = setParameter("SKG_SQLITE_LAST_VERSION", listTmp.at(1).at(0));
        }

        // Refresh views
        IFOKDO(err, refreshViewsIndexesAndTriggers())
        IFOKDO(err, stepForward(2, i18nc("Progression step", "Update materialized views")))

        // Refresh materialized views
        if (!err && oMigrationDone) {
            err = computeMaterializedViews();
        }
        IFOKDO(err, stepForward(3))

        IFKO(err) err.addError(ERR_FAIL, i18nc("Error message: Could not perform database migration", "Database migration from version %1 to version %2 failed", initialversion, version));
    }

    return err;
}

SKGError SKGDocument::createUndoRedoTemporaryTriggers()
{
    SKGError err;
    SKGTRACEINFUNCRC(10, err);

    // Create triggers
    QStringList tables;
    err = this->getTablesList(tables);
    int nbTables = tables.count();
    for (int i = 0; !err && i < nbTables; ++i) {
        // Get table name
        QString table = tables.at(i);

        // Do we have to treat this table
        if (!SKGListNotUndoable.contains("T." % table) && !table.startsWith(QLatin1String("vm_"))) {
            // YES
            // Get attributes name
            QStringList attributes;
            err = getAttributesList(table, attributes);

            // Build sqlorder for update and insert
            QString sqlorderForUpdate2;
            QString sqlorderForInsert1;
            QString sqlorderForInsert2;
            int nbAttributes = attributes.count();
            for (int j = 0; !err && j < nbAttributes; ++j) {
                // Get attribute
                QString att = attributes.at(j);

                // Do we have to treat this attribute
                if (!SKGListNotUndoable.contains("A." % table % '.' % att)) {
                    // Build for update
                    if (!sqlorderForUpdate2.isEmpty()) {
                        sqlorderForUpdate2 += ',';
                    }
                    sqlorderForUpdate2 += att % "='||quote(old." % att % ")||'";

                    // Build for insert part 1
                    if (!sqlorderForInsert1.isEmpty()) {
                        sqlorderForInsert1 += ',';
                    }
                    sqlorderForInsert1 += att;

                    // Build for insert part 2
                    if (!sqlorderForInsert2.isEmpty()) {
                        sqlorderForInsert2 += ',';
                    }
                    sqlorderForInsert2 += "'||quote(old." % att % ")||'";
                }
            }

            // Create specific triggers for the current transaction
            QStringList sqlOrders;
            // DROP DELETE trigger
            sqlOrders << "DROP TRIGGER IF EXISTS UR_" % table % "_IN"

                      // Create DELETE trigger
                      << "CREATE TEMP TRIGGER UR_" % table % "_IN "
                      "AFTER  INSERT ON " % table % " BEGIN "
                      "INSERT INTO doctransactionitem (rd_doctransaction_id, t_sqlorder,i_object_id,t_object_table,t_action) VALUES(0,'DELETE FROM " % table %
                      " WHERE id='||new.id,new.id,'" % table % "','D');END"

                      // DROP UPDATE trigger
                      << "DROP TRIGGER IF EXISTS UR_" % table % "_UP"

                      // Create UPDATE trigger
                      << "CREATE TEMP TRIGGER UR_" % table % "_UP "
                      "AFTER UPDATE ON " % table % " BEGIN "
                      "INSERT INTO doctransactionitem  (rd_doctransaction_id, t_sqlorder,i_object_id,t_object_table,t_action) VALUES(0,'UPDATE " % table %
                      " SET " % sqlorderForUpdate2 %
                      " WHERE id='||new.id,new.id,'" % table % "','U');END"

                      // DROP INSERT trigger
                      << "DROP TRIGGER IF EXISTS UR_" % table % "_DE"

                      // Create INSERT trigger
                      << "CREATE TEMP TRIGGER UR_" % table % "_DE "
                      "AFTER DELETE ON " % table %
                      " BEGIN "
                      "INSERT INTO doctransactionitem  (rd_doctransaction_id, t_sqlorder,i_object_id,t_object_table,t_action) VALUES(0,'INSERT INTO " % table %
                      '(' % sqlorderForInsert1 % ") VALUES(" % sqlorderForInsert2 % ")',old.id,'" % table % "','I'); END";
            err = executeSqliteOrders(sqlOrders);
        }
    }
    return err;
}

QStringList SKGDocument::getParameters(const QString& iParentUUID, const QString& iWhereClause)
{
    SKGTRACEINFUNC(10);
    QStringList output;
    QString wc = "t_uuid_parent='" % SKGServices::stringToSqlString(iParentUUID) % '\'';
    if (!iWhereClause.isEmpty()) {
        wc += " AND (" % iWhereClause % ')';
    }
    this->getDistinctValues("parameters", "t_name", wc, output);
    return output;
}

QString SKGDocument::getParameter(const QString& iName, const QString& iParentUUID)
{
    SKGTRACEINFUNC(10);
    SKGTRACEL(10) << "Input parameter [iName]=[" << iName << ']' << endl;
    QString output;

    // Get parameter
    SKGObjectBase param;
    SKGError err = getObject("parameters", "t_name='" % SKGServices::stringToSqlString(iName) %
                             "' AND t_uuid_parent='" % SKGServices::stringToSqlString(iParentUUID) % '\'', param);
    IFOK(err) {
        output = param.getAttribute("t_value");
    }
    return output;
}

QVariant SKGDocument::getParameterBlob(const QString& iName, const QString& iParentUUID)
{
    SKGTRACEINFUNC(10);
    SKGTRACEL(10) << "Input parameter [iName]=[" << iName << ']' << endl;
    QVariant output;

    QString sqlQuery = "SELECT b_blob FROM parameters WHERE t_name=? AND t_uuid_parent=?";
    QSqlQuery query(*getDatabase());
    query.prepare(sqlQuery);
    query.addBindValue(iName);
    query.addBindValue(iParentUUID);
    if (Q_LIKELY(!query.exec())) {
        QSqlError sqlError = query.lastError();
        SKGTRACE << "WARNING: " << sqlQuery << endl;
        SKGTRACE << "         returns :" << sqlError.text() << endl;
    } else {
        if (query.next()) {
            output = query.value(0);
        }
    }

    return output;
}

SKGError SKGDocument::setParameter(const QString& iName, const QString& iValue, const QString& iFileName, const QString& iParentUUID, SKGPropertyObject* oObjectCreated)
{
    SKGError err;
    SKGTRACEINFUNCRC(10, err);
    SKGTRACEL(10) << "Input parameter [iName]    =[" << iName << ']' << endl;
    SKGTRACEL(10) << "Input parameter [iValue]   =[" << iValue << ']' << endl;
    SKGTRACEL(10) << "Input parameter [iFileName]=[" << iFileName << ']' << endl;
    QVariant blob;
    QString value = iValue;
    QFile file(iFileName);
    if (file.exists()) {
        QFileInfo fileInfo(iFileName);
        if (fileInfo.isDir()) {
            value = "file://" % iFileName;
        } else {
            // Open file
            if (Q_UNLIKELY(!file.open(QIODevice::ReadOnly))) {
                err = SKGError(ERR_INVALIDARG, i18nc("Error message: Could not open a file", "Open file '%1' failed", iFileName));
            } else {
                QByteArray blob_bytes = file.readAll();
                if (!blob_bytes.count()) {
                    err = SKGError(ERR_INVALIDARG, i18nc("Error message: Could not open a file", "Open file '%1' failed", iFileName));
                } else {
                    blob = blob_bytes;
                    value = fileInfo.fileName();
                }

                // close file
                file.close();
            }
        }
    }

    IFOKDO(err, setParameter(iName, value, blob, iParentUUID, oObjectCreated))
    return err;
}

SKGError SKGDocument::setParameter(const QString& iName, const QString& iValue, const QVariant& iBlob, const QString& iParentUUID, SKGPropertyObject* oObjectCreated)
{
    SKGError err;
    SKGTRACEINFUNCRC(10, err);
    SKGTRACEL(10) << "Input parameter [iName]    =[" << iName << ']' << endl;
    SKGTRACEL(10) << "Input parameter [iValue]   =[" << iValue << ']' << endl;

    SKGPropertyObject param(this);
    IFOKDO(err, param.setName(iName))
    IFOKDO(err, param.setValue(iValue))
    IFOKDO(err, param.setParentId(iParentUUID))
    IFOKDO(err, param.save(true, oObjectCreated != NULL))

    if (!err && !iBlob.isNull()) {
        err = param.load();
        IFOK(err) {
            // Set blob
            QString sqlQuery = "UPDATE parameters SET b_blob=? WHERE id=?";
            QSqlQuery query(*getDatabase());
            query.prepare(sqlQuery);
            query.addBindValue(iBlob);
            query.addBindValue(param.getID());
            if (Q_LIKELY(!query.exec())) {
                QSqlError sqlError = query.lastError();
                QString msg = sqlQuery % ':' % sqlError.text();
                err = SKGError(SQLLITEERROR + sqlError.number(), msg);
            }
        }
    }
    if (!err && oObjectCreated != NULL) {
        *oObjectCreated = param;
    }

    return err;
}

SKGError SKGDocument::dump(int iMode)
{
    SKGError err;
    if (Q_LIKELY(getDatabase())) {
        // dump parameters
        SKGTRACE << "=== START DUMP ===" << endl;
        if (iMode & DUMPSQLITE) {
            SKGTRACE << "=== DUMPSQLITE ===" << endl;
            err.addError(dumpSelectSqliteOrder("SELECT * FROM sqlite_master order by type"));

            SKGTRACE << "=== DUMPSQLITE (TEMPORARY) ===" << endl;
            err.addError(dumpSelectSqliteOrder("SELECT * FROM sqlite_temp_master order by type"));
        }

        if (iMode & DUMPPARAMETERS) {
            SKGTRACE << "=== DUMPPARAMETERS ===" << endl;
            err.addError(dumpSelectSqliteOrder("SELECT * FROM parameters order by id"));
        }

        if (iMode & DUMPNODES) {
            SKGTRACE << "=== DUMPNODES ===" << endl;
            err.addError(dumpSelectSqliteOrder("SELECT * FROM node order by id"));
        }

        if (iMode & DUMPTRANSACTIONS) {
            // dump transaction
            SKGTRACE << "=== DUMPTRANSACTIONS ===" << endl;
            err.addError(dumpSelectSqliteOrder("SELECT * FROM doctransaction order by id"));

            // dump transaction
            SKGTRACE << "=== DUMPTRANSACTIONS (ITEMS) ===" << endl;
            err.addError(dumpSelectSqliteOrder("SELECT * FROM doctransactionitem order by rd_doctransaction_id, id"));
        }
        SKGTRACE << "=== END DUMP ===" << endl;
    }
    return err;
}

QSqlDatabase* SKGDocument::getDatabase() const
{
    return m_currentDatabase;
}

SKGError SKGDocument::getConsolidatedView(const QString& iTable,
        const QString& iAsColumn,
        const QString& iAsRow,
        const QString& iAttribute,
        const QString& iOpAtt,
        const QString& iWhereClause,
        SKGStringListList& oTable,
        const QString& iMissingValue)
{
    SKGError err;
    SKGTRACEINFUNCRC(10, err);
    SKGTRACEL(10) << "Input parameter [iTable]=[" << iTable << ']' << endl;
    SKGTRACEL(10) << "Input parameter [iAsColumn]=[" << iAsColumn << ']' << endl;
    SKGTRACEL(10) << "Input parameter [iAsRow]=[" << iAsRow << ']' << endl;
    SKGTRACEL(10) << "Input parameter [iAttribute]=[" << iAttribute << ']' << endl;
    SKGTRACEL(10) << "Input parameter [iOpAtt]=[" << iOpAtt << ']' << endl;
    SKGTRACEL(10) << "Input parameter [iWhereClause]=[" << iWhereClause << ']' << endl;
    SKGTRACEL(10) << "Input parameter [iMissingValue]=[" << iMissingValue << ']' << endl;

    // Mode
    int mode = 0;
    if (!iAsColumn.isEmpty()) {
        mode += 1;
    }
    if (!iAsRow.isEmpty()) {
        mode += 2;
    }

    oTable.clear();
    oTable.push_back(QStringList());


    QStringList titles = oTable.at(0);

    if (mode == 3) {
        titles.push_back(iAsRow % '/' % iAsColumn);
    } else {
        if (mode == 1) {
            titles.push_back(iAsColumn);

            QStringList sums;
            sums.push_back(i18nc("Noun, the numerical sum of a list of values", "Sum"));
            oTable.push_back(sums);
        } else {
            if (mode == 2) {
                titles.push_back(iAsRow);
                titles.push_back(i18nc("Noun, the numerical sum of a list of values", "Sum"));
            }
        }
    }
    oTable.removeAt(0);
    oTable.insert(0, titles);

    // Create sqlorder
    QString asColumn = iAsColumn;
    if (asColumn.startsWith(QLatin1String("p_"))) {
        QString propertyName = asColumn.right(asColumn.length() - 2);
        asColumn = "(SELECT t_value FROM parameters WHERE t_uuid_parent=" % iTable % ".id||'-" % SKGServices::getRealTable(iTable) % "' AND t_name='" % propertyName % "')";
    }
    QString asRow = iAsRow;
    if (asRow.startsWith(QLatin1String("p_"))) {
        QString propertyName = asRow.right(asRow.length() - 2);
        asRow = "(SELECT t_value FROM parameters WHERE t_uuid_parent=" % iTable % ".id||'-" % SKGServices::getRealTable(iTable) % "' AND t_name='" % propertyName % "')";
    }

    QString att = asColumn;
    if (!att.isEmpty() && !asRow.isEmpty()) {
        att += ',';
    }
    att += asRow;

    QString sort = asRow;
    if (!sort.isEmpty() && !asColumn.isEmpty()) {
        sort += ',';
    }
    sort += asColumn;

    if (!att.isEmpty()) {
        QString sql = "SELECT " % att % ',' % iOpAtt % '(' % iAttribute % ") FROM " % iTable;
        if (!iWhereClause.isEmpty()) {
            sql += " WHERE " % iWhereClause;
        }
        if (!iOpAtt.isEmpty()) {
            sql += " GROUP BY " % att;
        }
        sql += " ORDER BY " % sort;

        QHash<QString, int> cols;
        QHash<QString, int> rows;

        SKGTRACEL(10) << "sqlorder=[" << sql << ']' << endl;
        SKGStringListList listTmp;
        err = executeSelectSqliteOrder(sql, listTmp);
        int nb = listTmp.count();
        for (int i = 1; !err && i < nb; ++i) {  // Title is ignored
            QStringList line = listTmp.at(i);
            int rowindex = -1;
            int colindex = -1;
            if (mode >= 2) {
                QString rowname = line.at(mode == 3 ? 1 : 0);

                if (!rows.contains(rowname)) {
                    QStringList r;
                    r.push_back(rowname);
                    int nbx = oTable.at(0).count();
                    for (int j = 1; j < nbx; ++j) {
                        r.push_back(iMissingValue);
                    }

                    oTable.push_back(r);

                    rowindex = oTable.count() - 1;
                    rows.insert(rowname, rowindex);
                } else {
                    rowindex = rows[rowname];
                }
            } else {
                rowindex = 1;
            }

            if (mode == 1 || mode == 3) {
                QString colname = line.at(0);

                if (!cols.contains(colname)) {
                    // Search better position of this column
                    colindex = -1;
                    {
                        QHashIterator<QString, int> cols_i(cols);
                        while (cols_i.hasNext()) {
                            cols_i.next();
                            if (colname > cols_i.key() && cols_i.value() > colindex) {
                                colindex = cols_i.value();
                            }
                        }
                    }
                    if (colindex == -1) {
                        colindex = 1;
                    } else {
                        ++colindex;
                    }

                    int nbx = oTable.count();
                    for (int j = 0; j < nbx; ++j) {
                        if (j == 0) {
                            oTable[j].insert(colindex, colname);
                        } else {
                            oTable[j].insert(colindex, iMissingValue);
                        }
                    }

                    {
                        QHash<QString, int> tmp;
                        QHashIterator<QString, int> cols_i(cols);
                        while (cols_i.hasNext()) {
                            cols_i.next();
                            tmp.insert(cols_i.key(), cols_i.value() + (cols_i.value() >= colindex ? 1 : 0));
                        }

                        cols = tmp;
                    }
                    cols.insert(colname, colindex);

                } else {
                    colindex = cols[colname];
                }
            } else {
                colindex = 1;
            }

            QString sum = line.at(mode == 3 ? 2 : 1);

            oTable[rowindex][colindex] = sum;
        }

        IFSKGTRACEL(10) {
            QStringList dump2 = SKGServices::tableToDump(oTable, SKGServices::DUMP_TEXT);
            int nbl = dump2.count();
            for (int i = 0; i < nbl; ++i) {
                SKGTRACE << dump2.at(i) << endl;
            }
        }

        // Correction bug 205466 vvv
        // If some months or years are missing, we must add them.
        if (asColumn.startsWith(QLatin1String("d_"))) {
            for (int c = 1; c < oTable[0].count() - 1; ++c) {  // Dynamic size
                bool forecast = false;
                QString title = oTable.at(0).at(c);
                if (title.isEmpty()) {
                    title = "0000";
                }

                if (title.endsWith(QLatin1String("999"))) {
                    title = title.left(title.count() - 3);
                    forecast = true;
                }
                QString nextTitle = oTable.at(0).at(c + 1);
                if (nextTitle.endsWith(QLatin1String("999"))) {
                    nextTitle = nextTitle.left(nextTitle.count() - 3);
                    forecast = true;
                }

                QString dateFormat = (asColumn == "d_date" ? "yyyy-MM-dd" : (asColumn == "d_DATEMONTH" ? "yyyy-MM" : (asColumn == "d_DATEQUARTER" ? "yyyy-QM" : (asColumn == "d_DATESEMESTER" ? "yyyy-SM" : (asColumn == "d_DATEWEEK" ? "yyyy-WM" : "yyyy")))));
                QDate nextExpected = QDate::fromString(title, dateFormat);
                QString nextExpectedString;
                if (asColumn == "d_DATEWEEK") {
                    /* TODO(Stephane MANKOWSKI)
                                      QStringList items=SKGServices::splitCSVLine(oTable.at(0).at(c),'-');
                                      nextExpected=QDate(SKGServices::stringToInt(items.at(0)), 1, 1);
                                      QString w=items.at(1);
                                      w.remove('W');
                                      nextExpected=nextExpected.addDays(7*SKGServices::stringToInt(w));
                                      QString newW=SKGServices::intToString(nextExpected.weekNumber());
                                      if(newW.count()==1) newW='0'+newW;
                                      */
                    nextExpectedString = nextTitle;
                } else if (asColumn == "d_DATEMONTH") {
                    nextExpected = nextExpected.addMonths(1);
                    nextExpectedString = nextExpected.toString(dateFormat);
                } else if (asColumn == "d_DATEQUARTER") {
                    nextExpected = nextExpected.addMonths(nextExpected.month() * 3 - nextExpected.month());  // convert quater in month
                    nextExpected = nextExpected.addMonths(3);
                    nextExpectedString = nextExpected.toString("yyyy-Q") % (nextExpected.month() <= 3 ? '1' : (nextExpected.month() <= 6 ? '2' : (nextExpected.month() <= 9 ? '3' : '4')));
                } else if (asColumn == "d_DATESEMESTER") {
                    nextExpected = nextExpected.addMonths(nextExpected.month() * 6 - nextExpected.month());  // convert semester in month
                    nextExpected = nextExpected.addMonths(6);
                    nextExpectedString = nextExpected.toString("yyyy-S") % (nextExpected.month() <= 6 ? '1' : '2');
                } else if (asColumn == "d_DATEYEAR") {
                    nextExpected = nextExpected.addYears(1);
                    nextExpectedString = nextExpected.toString(dateFormat);
                } else {
                    nextExpected = nextExpected.addDays(1);
                    nextExpectedString = nextExpected.toString(dateFormat);
                }
                if (title != "0000" && nextTitle != nextExpectedString && nextTitle != title) {
                    int colindex = c + 1;
                    if (forecast) {
                        nextExpectedString += "999";
                    }

                    int nbx = oTable.count();
                    oTable[0].insert(colindex, nextExpectedString);
                    for (int j = 1; j < nbx; ++j) {
                        oTable[j].insert(colindex, iMissingValue);
                    }
                }
            }
        }
        // Correction bug 205466 ^^^
    }

    return err;
}

QList<SKGDocument::SKGModelTemplate> SKGDocument::getDisplaySchemas(const QString& iRealTable) const
{
    QList<SKGDocument::SKGModelTemplate> listSchema;

    // Build schemas
    if (iRealTable == "doctransaction") {
        SKGModelTemplate def;
        def.id = "default";
        def.name = i18nc("Noun, the default value of an item", "Default");
        def.icon = "edit-undo";
        def.schema = "t_name;t_value;d_lastmodifdate;t_savestep";
        listSchema.push_back(def);

        SKGModelTemplate minimum;
        minimum.id = "minimum";
        minimum.name = i18nc("Noun, the minimum value of an item", "Minimum");
        minimum.icon = "";
        minimum.schema = "t_name;t_value;d_lastmodifdate|N;t_savestep|N";
        listSchema.push_back(minimum);
    } else if (iRealTable == "parameters") {
        SKGModelTemplate def;
        def.id = "default";
        def.name = i18nc("Noun, the default value of an item", "Default");
        def.icon = "edit-undo";
        def.schema = "t_name;t_value";
        listSchema.push_back(def);
    } else if (iRealTable == "node") {
        SKGModelTemplate def;
        def.id = "default";
        def.name = i18nc("Noun, the default value of an item", "Default");
        def.icon = "edit-undo";
        def.schema = "t_name";
        listSchema.push_back(def);
    } else {
        SKGModelTemplate def;
        def.id = "default";
        def.name = i18nc("Noun, the default value of an item", "Default");
        def.icon = "edit-undo";
        def.schema = "";
        SKGStringListList lines;
        executeSelectSqliteOrder("PRAGMA table_info(" % iRealTable % ");", lines);
        foreach(const QStringList & line, lines) {
            if (!def.schema.isEmpty()) {
                def.schema += ';';
            }
            def.schema += line[1];
        }
        listSchema.push_back(def);
    }

    return listSchema;
}

QString SKGDocument::getDisplay(const QString& iString) const
{
    QString output = iString.toLower();

    if (output.endsWith(QLatin1String("t_name"))) {
        output = i18nc("Noun, the name of an item", "Name");
    } else if (output.endsWith(QLatin1String("d_date"))) {
        output = i18nc("Noun, the date of an item", "Date");
    } else if (output.endsWith(QLatin1String("t_savestep"))) {
        output = i18nc("Verb, save a document", "Save");
    } else if (output.endsWith(QLatin1String("t_value"))) {
        output = i18nc("Noun, the value of an item", "Value");
    } else if (output.endsWith(QLatin1String("d_lastmodifdate"))) {
        output = i18nc("Noun, date of last modification", "Last modification");
    } else if (output.startsWith(QLatin1String("p_")) || output.contains("p_")) {
        // This is a property
        int pos = iString.indexOf(".");
        if (pos != -1) {
            output = iString.right(iString.count() - pos - 1);
        }
        output = output.right(output.length() - 2);
    } else {
        output = iString;
    }
    return output;
}

QIcon SKGDocument::getIcon(const QString& iString) const
{
    QString output = iString.toLower();
    if (output.startsWith(QLatin1String("p_")) || output.contains("p_")) {
        return KIcon("feed-subscribe");
    }
    return QIcon();
}

QString SKGDocument::getRealAttribute(const QString& iString) const
{
    if (iString == iString.toLower()) {
        return iString;
    }
    return "";
}

SKGServices::AttributeType SKGDocument::getAttributeType(const QString& iAttributeName) const
{
    SKGServices::AttributeType output = SKGServices::TEXT;
    if (iAttributeName.startsWith(QLatin1String("d_"))) {
        output = SKGServices::DATE;
    } else if (iAttributeName.startsWith(QLatin1String("i_"))) {
        output = SKGServices::INTEGER;
    } else if (iAttributeName.startsWith(QLatin1String("rd_")) || iAttributeName.startsWith(QLatin1String("rc_")) || iAttributeName.startsWith(QLatin1String("r_")) || iAttributeName.startsWith(QLatin1String("id_"))) {
        output = SKGServices::LINK;
    } else if (iAttributeName.startsWith(QLatin1String("f_"))) {
        output = SKGServices::FLOAT;
    } else if (iAttributeName.startsWith(QLatin1String("b_"))) {
        output = SKGServices::BLOB;
    } else if (iAttributeName == "id") {
        output = SKGServices::ID;
    } else if (iAttributeName == "t_savestep" || iAttributeName == "t_refreshviews") {
        output = SKGServices::BOOL;
    }

    return output;
}

SKGServices::SKGUnitInfo SKGDocument::getUnit(const QString& iPrefixInCache)
{
    SKGServices::SKGUnitInfo output;
    output.Name = getCachedValue(iPrefixInCache % "UnitCache");
    output.Symbol = getCachedValue(iPrefixInCache % "UnitSymbolCache");
    QString val = getCachedValue(iPrefixInCache % "UnitValueCache");
    if (!val.isEmpty()) {
        output.Value = SKGServices::stringToDouble(val);
    } else {
        output.Value = 1;
    }
    val = getCachedValue(iPrefixInCache % "UnitDecimalCache");
    if (!val.isEmpty()) {
        output.NbDecimal = SKGServices::stringToInt(val);
    } else {
        output.NbDecimal = 2;
    }

    return output;
}

QString SKGDocument::formatMoney(double iValue, SKGServices::SKGUnitInfo iUnit, bool iHtml) const
{
    KLocale* loc = KGlobal::locale();
    QString val = (loc ? loc->formatMoney(iValue / iUnit.Value, iUnit.Symbol, iUnit.NbDecimal) : SKGServices::doubleToString(round(iValue / iUnit.Value / 100) * 100) % ' ' % iUnit.Symbol);
    if (iHtml) {
        // Get std colors
        KColorScheme scheme(QPalette::Normal);
        QString negative = scheme.foreground(KColorScheme::NegativeText).color().name();
        QString neutral = scheme.foreground(KColorScheme::NormalText).color().name();

        // Return value
        return QString("<font color=\"") %
               (iValue < 0 ? negative : neutral) %
               "\">" %
               SKGServices::stringToHtml(val) %
               "</font>";
    }
    return val;
}

QString SKGDocument::formatPercentage(double iValue, bool iInvertColors) const
{
    KLocale* loc = KGlobal::locale();

    // Get std colors
    KColorScheme scheme(QPalette::Normal);
    QString negative = scheme.foreground(KColorScheme::NegativeText).color().name();
    QString positive = scheme.foreground(KColorScheme::PositiveText).color().name();

    // Return value
    QString p = (loc ? loc->formatMoney(iValue, "%", 2) : SKGServices::doubleToString(iValue) % " %");
    if (iValue > 0) {
        p = '+' % p;
    }
    if (p.count() > 10 || isnan(iValue) || isinf(iValue)) {
        p = QChar(8734);
    }
    return "<font color=\"" %
           QString((iValue < 0 && !iInvertColors) || (iValue >= 0 && iInvertColors) ? negative : positive) %
           "\">" % SKGServices::stringToHtml(p) %
           "</font>";
}

QString SKGDocument::getFileExtension() const
{
    return "skgc";
}

QString SKGDocument::getDocumentHeader() const
{
    return "SKG";
}

void SKGDocument::addValueInCache(const QString& iKey, const QString& iValue)
{
    m_cache[iKey] = iValue;
}

QString SKGDocument::getCachedValue(const QString& iKey) const
{
    return m_cache[iKey];
}

void SKGDocument::setBackupParameters(const QString& iPrefix, const QString& iSuffix)
{
    m_backupPrefix = iPrefix;
    m_backupSuffix = iSuffix;
}

QString SKGDocument::getCurrentTemporaryFile()
{
    return m_temporaryFile;
}

QString SKGDocument::getTemporaryFile(const QString iFileName, bool iForceReadOnly)
{
    QString output;
    QFileInfo fi(iFileName);
    QFileInfo di(fi.dir().path());
    if (iForceReadOnly || !KUrl(iFileName).isLocalFile() || !di.permission(QFile::WriteUser)) {
        output = QDir::tempPath();
    } else {
        output = fi.absolutePath();
    }
    return output += "/." % fi.fileName() % ".wrk";
}

QString SKGDocument::getBackupFile(const QString iFileName) const
{
    QString output;
    if (!m_backupPrefix.isEmpty() || !m_backupSuffix.isEmpty()) {
        QFileInfo fi(iFileName);
        output = m_backupPrefix % fi.fileName() % m_backupSuffix;
        output = output.replace("<DATE>", SKGServices::dateToSqlString(QDateTime::currentDateTime().date()));
        output = output.replace("<TIME>", SKGServices::timeToString(QDateTime::currentDateTime()));
        if (!QFileInfo(output).isAbsolute()) {
            output = fi.absolutePath() % '/' % output;
        }
    }

    return output;
}

SKGError SKGDocument::getObjects(const QString& iTable, const QString& iWhereClause, SKGObjectBase::SKGListSKGObjectBase& oListObject)
{
    SKGError err;

    // Initialisation
    oListObject.clear();

    // Execute sqlorder
    SKGStringListList result;
    err = executeSelectSqliteOrder(
              QString("SELECT * FROM " % iTable %
                      (!iWhereClause.isEmpty() ? QString(" WHERE " % iWhereClause) : "")),
              result);

    // Create output
    IFOK(err) {
        SKGStringListListIterator itrow = result.begin();
        QStringList columns = *(itrow);
        ++itrow;
        for (; !err && itrow != result.end(); ++itrow) {
            QStringList values = *(itrow);
            SKGObjectBase tmp(this, iTable);
            err = tmp.setAttributes(columns, values);
            oListObject.push_back(tmp);
        }
    }
    return err;
}

SKGError SKGDocument::existObjects(const QString& iTable, const QString& iWhereClause, bool& oExist) const
{
    SKGError err;

    // Initialisation
    oExist = false;

    // Execute sqlorder
    SKGStringListList result;
    err = executeSelectSqliteOrder(
              "SELECT EXISTS(SELECT 1 FROM " % iTable % " WHERE " %
              (!iWhereClause.isEmpty() ?  iWhereClause  : "1=1") % ')',
              result);

    // Create output
    IFOK(err) oExist = (result.at(1).at(0) == "1");
    return err;
}

SKGError SKGDocument::getNbObjects(const QString& iTable, const QString& iWhereClause, int& oNbObjects) const
{
    SKGError err;

    // Initialisation
    oNbObjects = 0;

    // Execute sqlorder
    SKGStringListList result;
    err = executeSelectSqliteOrder(
              QString("SELECT count(1) FROM " % iTable %
                      (!iWhereClause.isEmpty() ? QString(" WHERE " % iWhereClause) : "")),
              result);

    // Create output
    IFOK(err) oNbObjects = SKGServices::stringToInt(result.at(1).at(0));
    return err;
}

SKGError SKGDocument::getObject(const QString& iTable, const QString& iWhereClause, SKGObjectBase& oObject)
{
    SKGObjectBase::SKGListSKGObjectBase temporaryResult;
    oObject.resetID();
    SKGError err = SKGDocument::getObjects(iTable, iWhereClause, temporaryResult);
    IFOK(err) {
        int size = temporaryResult.size();
        if (Q_UNLIKELY(size > 1)) {
            err = SKGError(ERR_INVALIDARG, i18nc("Error message: We expected only one object in the result, but got more", "More than one object returned in '%1' for '%2'", iTable, iWhereClause));
        } else {
            if (Q_UNLIKELY(size == 0)) {
                err = SKGError(ERR_INVALIDARG, i18nc("Error message: We expected at least one object in the result, but got none", "No object returned in '%1' for '%2'", iTable, iWhereClause));
            } else {
                oObject = *(temporaryResult.begin());
            }
        }
    }
    return err;
}

SKGError SKGDocument::getObject(const QString& iTable, int iId, SKGObjectBase& oObject)
{
    return getObject(iTable, "id=" % SKGServices::intToString(iId), oObject);
}

SKGError SKGDocument::getTablesList(QStringList& oResult) const
{
    return getDistinctValues("sqlite_master", "name",
                             "type='table' AND name NOT LIKE 'sqlite_%'",
                             oResult);
}

SKGError SKGDocument::getDistinctValues(const QString& iTable, const QString& iAttribute, const QString& iWhereClause, QStringList& oResult) const
{
    SKGError err;
    _SKGTRACEINFUNCRC(10, err);
    // initialisation
    oResult.clear();

    // Search
    SKGStringListList temporaryResult;
    err = executeSelectSqliteOrder(
              "SELECT DISTINCT " % iAttribute %
              " FROM " % iTable % " WHERE (" %
              (!iWhereClause.isEmpty() ? iWhereClause : "1=1") %
              ") ORDER BY " % iAttribute
              // Correction bug 202167 vvv
              % " COLLATE NOCASE"
              // Correction bug 202167 ^^^
              , temporaryResult);
    IFOK(err) {
        SKGStringListListIterator it = temporaryResult.begin();
        ++it;  // to forget column name
        for (; it != temporaryResult.end(); ++it) {
            oResult.push_back(*(it->begin()));
        }
    }

    return err;
}

SKGError SKGDocument::getDistinctValues(const QString& iTable, const QString& iAttribute, QStringList& oResult) const
{
    return getDistinctValues(iTable, iAttribute,
                             iAttribute % " IS NOT NULL AND " % iAttribute % "!=''",
                             oResult);
}

SKGError SKGDocument::executeSqliteOrder(const QString& iSqlOrder, int* iLastId) const
{
    SKGError err;
    _SKGTRACEINFUNCRC(10, err);
    err = SKGServices::executeSqliteOrder(getDatabase(), iSqlOrder, iLastId);
    return err;
}

SKGError SKGDocument::executeSqliteOrders(const QStringList& iSqlOrders) const
{
    SKGError err;
    _SKGTRACEINFUNCRC(10, err);
    err = SKGServices::executeSqliteOrders(getDatabase(), iSqlOrders);
    return err;
}

SKGError SKGDocument::executeSqliteOrder(const QString& iSqlOrder, const QMap< QString, QVariant >& iBind, int* iLastId) const
{
    SKGError err;
    _SKGTRACEINFUNCRC(10, err);
    err = SKGServices::executeSqliteOrder(getDatabase(), iSqlOrder, iBind, iLastId);
    return err;
}

SKGError SKGDocument::dumpSelectSqliteOrder(const QString& iSqlOrder, QTextStream* oStream, SKGServices::DumpMode iMode) const
{
    SKGError err;
    _SKGTRACEINFUNCRC(10, err);
    err = SKGServices::dumpSelectSqliteOrder(getDatabase(), iSqlOrder, oStream, iMode);
    return err;
}

SKGError SKGDocument::dumpSelectSqliteOrder(const QString& iSqlOrder, QString& oResult, SKGServices::DumpMode iMode) const
{
    SKGError err;
    _SKGTRACEINFUNCRC(10, err);
    err = SKGServices::dumpSelectSqliteOrder(getDatabase(), iSqlOrder, oResult, iMode);
    return err;
}

SKGError SKGDocument::dumpSelectSqliteOrder(const QString& iSqlOrder, QStringList& oResult, SKGServices::DumpMode iMode) const
{
    SKGError err;
    _SKGTRACEINFUNCRC(10, err);
    err = SKGServices::dumpSelectSqliteOrder(getDatabase(), iSqlOrder, oResult, iMode);
    return err;
}

SKGError SKGDocument::executeSingleSelectSqliteOrder(const QString& iSqlOrder, QString& oResult) const
{
    SKGError err;
    _SKGTRACEINFUNCRC(10, err);
    oResult.clear();
    err = SKGServices::executeSingleSelectSqliteOrder(getDatabase(), iSqlOrder, oResult);
    return err;
}

SKGError SKGDocument::executeSelectSqliteOrder(const QString& iSqlOrder, SKGStringListList& oResult) const
{
    SKGError err;
    _SKGTRACEINFUNCRC(10, err);
    oResult.clear();
    err = SKGServices::executeSelectSqliteOrder(getDatabase(), iSqlOrder, oResult);
    return err;
}

SKGError SKGDocument::getAttributesDescription(const QString& iTable, SKGServices::SKGAttributesList& oResult) const
{
    SKGError err;
    _SKGTRACEINFUNCRC(10, err);
    // initialisation
    oResult.clear();

    // Search
    SKGStringListList temporaryResult;
    err = this->executeSelectSqliteOrder("PRAGMA table_info( " % iTable % " );", temporaryResult);
    IFOK(err) {
        int nblines = temporaryResult.count();
        QString realTable = SKGServices::getRealTable(iTable);

        for (int i = 1; i < nblines; ++i) {  // the first one is ignored because it is the headers
            QStringList line = temporaryResult.at(i);

            SKGServices::SKGAttributeInfo attribute;
            attribute.name = line[1];

            QString attname = realTable % '.' % attribute.name;
            attribute.display = getDisplay(attname);
            if (attribute.display == attname) {
                attribute.display = "";
            }
            attribute.icon = getIcon(attname);
            attribute.type = getAttributeType(attribute.name);
            attribute.notnull = (line[3] == "0");
            attribute.defaultvalue = line[4];
            oResult.push_back(attribute);
        }
    }

    return err;
}

SKGError SKGDocument::getAttributesList(const QString& iTable, QStringList& oResult) const
{
    SKGError err;
    _SKGTRACEINFUNCRC(10, err);
    oResult.clear();
    SKGServices::SKGAttributesList attDesc;
    err = getAttributesDescription(iTable, attDesc);
    int nblines = attDesc.count();
    for (int i = 0; !err && i < nblines; ++i) {
        oResult.push_back(attDesc.at(i).name);
    }
    return err;
}

SKGReport* SKGDocument::getReport()
{
    return new SKGReport(this);
}

SKGError SKGDocument::copyToJson(QString& oDocument) const
{
    SKGError err;
    SKGTRACEINFUNCRC(10, err);
    QVariantMap doc;

    // Copy the tables
    QStringList listTables = getDatabase()->tables();
    int nb = listTables.count();
    for (int i = 0; !err && i < nb; ++i) {
        QString tableName = listTables.at(i);
        if (Q_UNLIKELY(!tableName.startsWith(QLatin1String("sqlite_")) && !tableName.startsWith(QLatin1String("vm_")))) {
            QVariantList list;

            SKGStringListList listRows;
            err = SKGServices::executeSelectSqliteOrder(getDatabase(), "SELECT * FROM " % tableName, listRows);
            int nbRows = listRows.count();
            if (Q_LIKELY(nbRows)) {
                QVariantMap item;
                QStringList titles = listRows.at(0);
                for (int j = 1; !err && j < nbRows; ++j) {  // Forget title
                    QStringList values = listRows.at(j);

                    int nbVals = values.count();
                    for (int k = 0; k < nbVals; ++k) {
                        QString t = titles.at(k);
                        SKGServices::AttributeType type = getAttributeType(t);
                        if (type == SKGServices::ID || type == SKGServices::INTEGER || type == SKGServices::LINK) {
                            item.insert(t, SKGServices::stringToInt(values.at(k)));
                        } else if (type == SKGServices::FLOAT) {
                            item.insert(t, SKGServices::stringToDouble(values.at(k)));
                        } else if (type == SKGServices::BOOL) {
                            item.insert(t, values.at(k) == "Y");
                        } else {
                            item.insert(t, values.at(k));
                        }
                    }

                    list << item;
                }
            }
            doc.insert(tableName, list);
        }
    }

    QJson::Serializer serializer;
    serializer.setIndentMode(QJson::IndentMinimum);
    bool ok;
    oDocument = serializer.serialize(doc, &ok);
    if (Q_UNLIKELY(!ok)) {
        err = SKGError(ERR_FAIL, serializer.errorMessage());
    }
    return err;
}

#include "skgdocument.moc"
