/***************************************************************************
*   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 defines classes SKGUnitObject.
*
* @author Stephane MANKOWSKI / Guillaume DE BURE
*/
#include "skgunitobject.h"

#include <qsavefile.h>
#include <qregexp.h>
#include <qfileinfo.h>
#include <qfile.h>
#include <qdir.h>
#include <qmath.h>
#include <qdesktopservices.h>
#include <qstandardpaths.h>
#include <qdiriterator.h>
#include <qprocess.h>

#include <kconfiggroup.h>
#include <kconfig.h>
#include <klocalizedstring.h>

#include "skgunitvalueobject.h"
#include "skgdocumentbank.h"
#include "skgoperationobject.h"
#include "skgtransactionmng.h"
#include "skgtraces.h"

SKGUnitObject::SKGUnitObject() : SKGUnitObject(nullptr)
{}

SKGUnitObject::SKGUnitObject(SKGDocument* iDocument, int iID) : SKGNamedObject(iDocument, QStringLiteral("v_unit"), iID)
{}

SKGUnitObject::~SKGUnitObject()
    = default;

SKGUnitObject::SKGUnitObject(const SKGUnitObject& iObject) = default;

SKGUnitObject::SKGUnitObject(const SKGNamedObject& iObject)
{
    if (iObject.getRealTable() == QStringLiteral("unit")) {
        copyFrom(iObject);
    } else {
        *this = SKGNamedObject(iObject.getDocument(), QStringLiteral("v_unit"), iObject.getID());
    }
}

SKGUnitObject::SKGUnitObject(const SKGObjectBase& iObject)
{
    if (iObject.getRealTable() == QStringLiteral("unit")) {
        copyFrom(iObject);
    } else {
        *this = SKGNamedObject(iObject.getDocument(), QStringLiteral("v_unit"), iObject.getID());
    }
}

const SKGUnitObject& SKGUnitObject::operator= (const SKGObjectBase& iObject)
{
    copyFrom(iObject);
    return *this;
}

QString SKGUnitObject::getWhereclauseId() const
{
    QString output = SKGObjectBase::getWhereclauseId();
    if (output.isEmpty()) {
        QString name = getName();
        if (!name.isEmpty()) {
            output = "t_name='" % SKGServices::stringToSqlString(name) % '\'';
        }

        QString symbol = getSymbol();
        if (!symbol.isEmpty()) {
            if (!output.isEmpty()) {
                output += QStringLiteral(" OR ");
            }
            output += "t_symbol='" % SKGServices::stringToSqlString(symbol) % '\'';
        }
        if (!output.isEmpty()) {
            output = '(' % output % ')';
        }
    }

    return output;
}

QList<SKGServices::SKGUnitInfo> SKGUnitObject::currencies;

QStringList SKGUnitObject::getListofKnownCurrencies(bool iIncludingObsolete)
{
    SKGTRACEINFUNC(10);
    if (currencies.isEmpty()) {
        // Search currencies
        const QStringList dirs = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("skrooge/currency/"), QStandardPaths::LocateDirectory);
        for (const auto& dir : dirs) {
            auto listDesktopFiles = QDir(dir).entryList(QStringList() << QStringLiteral("*.desktop"));
            for (const auto& path : qAsConst(listDesktopFiles)) {
                // Read the file
                QFileInfo file(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("skrooge/currency/") + path));
                KConfig cgFile(file.absoluteFilePath());
                KConfigGroup cg(&cgFile, QStringLiteral("Currency Code"));

                SKGServices::SKGUnitInfo unit;
                unit.Name = cg.readEntry(QStringLiteral("Name"), QString()) + QStringLiteral(" (") + cg.readEntry(QStringLiteral("CurrencyCodeIsoAlpha3"), QString()) + QStringLiteral(")");
                unit.Symbol = cg.readEntry(QStringLiteral("CurrencyUnitSymbolDefault"), QString());
                if (unit.Symbol.isEmpty()) {
                    unit.Symbol = cg.readEntry(QStringLiteral("CurrencyCodeIsoAlpha3"), file.baseName());
                }
                unit.Value = 1;
                unit.NbDecimal = cg.readEntry(QStringLiteral("CurrencyDecimalPlacesDisplay"), 2);
                unit.Source = QStringLiteral("GrandTrunk");
                unit.Date = cg.readEntry(QStringLiteral("CurrencyIntroducedDate"), QDate::currentDate());
                unit.Obsolete = (cg.readEntry(QStringLiteral("CurrencySuspendedDate"), cg.readEntry(QStringLiteral("CurrencyWithdrawnDate"), QDate())) != QDate());
                currencies.push_back(unit);
            }
        }

        // Add other units
        {
            // CAC40
            SKGServices::SKGUnitInfo info;
            info.Name = i18nc("Noun, a currency", "CAC 40");
            info.Symbol = info.Name;
            info.Country = i18nc("Noun, a country", "France");
            info.Date = QDate(1987, 1, 1);
            info.Internet = QStringLiteral("^FCHI");
            info.Source = QStringLiteral("Yahoo");
            info.NbDecimal = 2;
            info.Value = -1;
            currencies.push_back(info);
        }
        {
            // NASDAQ
            SKGServices::SKGUnitInfo info;
            info.Name = i18nc("Noun, a currency", "NASDAQ");
            info.Symbol = info.Name;
            info.Country = i18nc("Noun, a country", "United States");
            info.Date = QDate(1971, 2, 5);
            info.Internet = QStringLiteral("^IXIC");
            info.Source = QStringLiteral("Yahoo");
            info.NbDecimal = 2;
            info.Value = -1;
            currencies.push_back(info);
        }
        {
            // Dow Jones
            SKGServices::SKGUnitInfo info;
            info.Name = i18nc("Noun, a currency", "Dow Jones (DJIA)");
            info.Symbol = QStringLiteral("DJIA");
            info.Country = i18nc("Noun, a country", "United States");
            info.Date = QDate(1884, 1, 1);
            info.Internet = QStringLiteral("^DJI");
            info.Source = QStringLiteral("Yahoo");
            info.NbDecimal = 2;
            info.Value = -1;
            currencies.push_back(info);
        }
        {
            // SBF 120
            SKGServices::SKGUnitInfo info;
            info.Name = i18nc("Noun, a currency", "SBF 120");
            info.Symbol = info.Name;
            info.Country = i18nc("Noun, a country", "France");
            info.Date = QDate(1990, 12, 31);
            info.Internet = QStringLiteral("^SBF120");
            info.Source = QStringLiteral("Yahoo");
            info.NbDecimal = 2;
            info.Value = -1;
            currencies.push_back(info);
        }
        {
            // S&P 500
            SKGServices::SKGUnitInfo info;
            info.Name = i18nc("Noun, a currency", "S&P 500");
            info.Symbol = info.Name;
            info.Country = i18nc("Noun, a country", "United States");
            info.Date = QDate(1920, 1, 1);
            info.Internet = QStringLiteral("^GSPC");
            info.Source = QStringLiteral("Yahoo");
            info.NbDecimal = 2;
            info.Value = -1;
            currencies.push_back(info);
        }
        {
            // FTSE 100
            SKGServices::SKGUnitInfo info;
            info.Name = i18nc("Noun, a currency", "FTSE 100");
            info.Symbol = info.Name;
            info.Country = i18nc("Noun, a country", "England");
            info.Date = QDate(1984, 1, 3);
            info.Internet = QStringLiteral("^FTSE");
            info.Source = QStringLiteral("Yahoo");
            info.NbDecimal = 2;
            info.Value = -1;
            currencies.push_back(info);
        }
        {
            // DAX
            SKGServices::SKGUnitInfo info;
            info.Name = i18nc("Noun, a currency", "DAX");
            info.Symbol = info.Name;
            info.Country = i18nc("Noun, a country", "Germany");
            info.Date = QDate(1920, 1, 1);
            info.Internet = QStringLiteral("^GDAXI");
            info.Source = QStringLiteral("Yahoo");
            info.NbDecimal = 2;
            info.Value = -1;
            currencies.push_back(info);
        }
        {
            // NIKKEI 225
            SKGServices::SKGUnitInfo info;
            info.Name = i18nc("Noun, a currency", "NIKKEI 225");
            info.Symbol = info.Name;
            info.Country = i18nc("Noun, a country", "Japan");
            info.Date = QDate(1920, 1, 1);
            info.Internet = QStringLiteral("^N225");
            info.Source = QStringLiteral("Yahoo");
            info.NbDecimal = 2;
            info.Value = -1;
            currencies.push_back(info);
        }
        {
            // HANG SENG
            SKGServices::SKGUnitInfo info;
            info.Name = i18nc("Noun, a currency", "HANG SENG");
            info.Symbol = info.Name;
            info.Country = i18nc("Noun, a country", "China");
            info.Date = QDate(1920, 1, 1);
            info.Internet = QStringLiteral("^HSI");
            info.Source = QStringLiteral("Yahoo");
            info.NbDecimal = 2;
            info.Value = -1;
            currencies.push_back(info);
        }
        {
            // STRAITS TIMES
            SKGServices::SKGUnitInfo info;
            info.Name = i18nc("Noun, a currency", "STRAITS TIMES");
            info.Symbol = info.Name;
            info.Date = QDate(1920, 1, 1);
            info.Country = i18nc("Noun, a country", "Singapore");
            info.Internet = QStringLiteral("^STI");
            info.Source = QStringLiteral("Yahoo");
            info.NbDecimal = 2;
            info.Value = -1;
            currencies.push_back(info);
        }
        {
            // BITCOIN
            SKGServices::SKGUnitInfo info;
            info.Name = i18nc("Noun, a currency", "Bitcoin");
            info.Symbol = QStringLiteral("BTC");
            info.Date = QDate(2009, 2, 4);
            info.Country = i18nc("Noun, the country of bitcoin", "Internet");
            info.Internet = QStringLiteral("mtgoxUSD");
            info.Source = QStringLiteral("BitcoinAverage");
            info.Parent = QStringLiteral("USD");
            info.NbDecimal = 4;
            info.Value = -1;
            currencies.push_back(info);
        }
    }
    QStringList output;
    output.reserve(currencies.count());
    for (const auto& unit : qAsConst(currencies)) {
        if (iIncludingObsolete || !unit.Obsolete) {
            output.push_back(unit.Name);
        }
    }
    output.sort();
    return output;
}

QString SKGUnitObject::getInternationalCode(const QString& iUnitName)
{
    SKGTRACEINFUNC(10);
    QString output = iUnitName;
    QRegExp rx(QStringLiteral(".*\\(([^\\(\\)]+)\\)[^\\(\\)]*"));
    if (rx.indexIn(iUnitName) != -1) {
        output = rx.cap(1);
    }

    return output;
}

SKGServices::SKGUnitInfo SKGUnitObject::getUnitInfo()
{
    SKGTRACEINFUNC(10);
    SKGServices::SKGUnitInfo info;
    info.Name = getName();
    info.Value = getAmount();
    info.NbDecimal = getNumberDecimal();
    info.Symbol = getSymbol();
    info.Country = getCountry();
    info.Internet = getInternetCode();
    info.Date = QDate::currentDate();

    return info;
}

SKGServices::SKGUnitInfo SKGUnitObject::getUnitInfo(const QString& iUnitName)
{
    SKGTRACEINFUNC(10);
    SKGServices::SKGUnitInfo info;
    if (currencies.isEmpty()) {
        getListofKnownCurrencies(false);
    }
    QString isoCode = getInternationalCode(iUnitName);
    for (const auto& unit : qAsConst(currencies)) {
        if (getInternationalCode(unit.Name) == isoCode) {
            info = unit;
            break;
        }
    }
    return info;
}

SKGError SKGUnitObject::createCurrencyUnit(SKGDocumentBank* iDocument, const QString& iUnitName, SKGUnitObject& oUnit)
{
    SKGError err;
    SKGTRACEINFUNCRC(10, err);
    if (iDocument) {
        SKGUnitObject parentUnit;
        oUnit = SKGUnitObject(iDocument);

        SKGUnitObject::UnitType type = SKGUnitObject::CURRENCY;
        SKGServices::SKGUnitInfo prim = iDocument->getPrimaryUnit();
        SKGServices::SKGUnitInfo seco = iDocument->getSecondaryUnit();

        // Get information on the unit
        SKGServices::SKGUnitInfo info = getUnitInfo(iUnitName);
        if (info.Name.isEmpty()) {
            err = SKGError(ERR_INVALIDARG, i18nc("Error message", "Unknown unit '%1'", iUnitName));
        }
        if (!err && !info.Parent.isEmpty()) {
            err = createCurrencyUnit(iDocument, info.Parent, parentUnit);
        }

        // Set the type
        if (info.Name == info.Symbol) {
            // This is an index
            type = SKGUnitObject::INDEX;
        } else if (!info.Parent.isEmpty()) {
            // This is a secondary unit
            type = (seco.Symbol.isEmpty() || seco.Symbol == info.Symbol  ? SKGUnitObject::SECONDARY : SKGUnitObject::CURRENCY);
        } else {
            // As primary
            type = (prim.Symbol.isEmpty() || prim.Symbol == info.Symbol ? SKGUnitObject::PRIMARY : SKGUnitObject::CURRENCY);
        }

        // Point on primary unit
        if (info.Value == 1 && !err && (type == SKGUnitObject::CURRENCY || type == SKGUnitObject::SECONDARY)) {
            SKGUnitObject primunit(iDocument);
            err = primunit.setSymbol(prim.Symbol);
            IFOKDO(err, primunit.load())
            IFOK(err) {
                QString codeprimunit = getInternationalCode(primunit.getName());
                QString codeunit = getInternationalCode(info.Name);
                if (!codeprimunit.isEmpty()) {
                    info.Internet = codeunit % '/' % codeprimunit;
                    info.Value = -1;

                    parentUnit = SKGUnitObject(iDocument);
                    err = parentUnit.setSymbol(prim.Symbol);
                    IFOKDO(err, parentUnit.load())
                }
            }
        }

        IFOKDO(err, oUnit.setName(info.Name))
        if (!err && oUnit.exist()) {
            err = oUnit.load();
        }
        IFOKDO(err, oUnit.setType(type))
        IFOKDO(err, oUnit.setSymbol(info.Symbol))
        IFOKDO(err, oUnit.setInternetCode(info.Internet))
        IFOKDO(err, oUnit.setDownloadSource(info.Source))
        IFOKDO(err, oUnit.setCountry(info.Country))
        IFOKDO(err, oUnit.setNumberDecimal(info.NbDecimal))
        if (!err && parentUnit.exist()) {
            err = oUnit.setUnit(parentUnit);
        }
        IFOKDO(err, oUnit.save())

        // Creation of the value
        if (info.Value > 0) {
            SKGUnitValueObject unitValue;
            IFOKDO(err, oUnit.addUnitValue(unitValue))
            IFOKDO(err, unitValue.setDate(info.Date))
            IFOKDO(err, unitValue.setQuantity(info.Value))
            IFOKDO(err, unitValue.save())
        }
    }
    return err;
}

double SKGUnitObject::convert(double iValue, const SKGUnitObject& iUnitFrom, const SKGUnitObject& iUnitTo, const QDate iDate)
{
    double output = iValue;
    if (iUnitFrom != iUnitTo) {
        double valFrom = iUnitFrom.getAmount(iDate);
        double valTo = iUnitTo.getAmount(iDate);
        output = iValue * valFrom / valTo;
    }
    return output;
}

SKGError SKGUnitObject::setSymbol(const QString& iSymbol)
{
    return setAttribute(QStringLiteral("t_symbol"), iSymbol);
}

QString SKGUnitObject::getSymbol() const
{
    return getAttribute(QStringLiteral("t_symbol"));
}

QString SKGUnitObject::getDownloadSource() const
{
    return getAttribute(QStringLiteral("t_source"));
}

SKGError SKGUnitObject::setDownloadSource(const QString& iSource)
{
    return setAttribute(QStringLiteral("t_source"), iSource);
}

SKGError SKGUnitObject::setNumberDecimal(int iNb)
{
    return setAttribute(QStringLiteral("i_nbdecimal"), SKGServices::intToString(iNb));
}

int SKGUnitObject::getNumberDecimal() const
{
    return SKGServices::stringToInt(getAttribute(QStringLiteral("i_nbdecimal")));
}

SKGError SKGUnitObject::setCountry(const QString& iCountry)
{
    return setAttribute(QStringLiteral("t_country"), iCountry);
}

QString SKGUnitObject::getCountry() const
{
    return getAttribute(QStringLiteral("t_country"));
}

SKGError SKGUnitObject::setInternetCode(const QString& iCode)
{
    return setAttribute(QStringLiteral("t_internet_code"), iCode);
}

QString SKGUnitObject::getInternetCode() const
{
    return getAttribute(QStringLiteral("t_internet_code"));
}

SKGError SKGUnitObject::setType(SKGUnitObject::UnitType iType)
{
    SKGError err;
    if (getAttribute(QStringLiteral("t_type")).isEmpty() || this->getType() != iType) {
        // Guaranty that PRIMARY and SECONDARY is unique
        if (iType == PRIMARY || iType == SECONDARY) {
            // Set old SECONDARY as CURRENCY
            err = getDocument()->executeSqliteOrder(QStringLiteral("UPDATE unit SET t_type='C' WHERE t_type='2'"));

            // Set old PRIMARY as SECONDARY
            if (!err && iType == PRIMARY) {
                err = getDocument()->executeSqliteOrder(QStringLiteral("UPDATE unit SET t_type='2' WHERE t_type='1'"));
            }
        }
    }
    IFOKDO(err, setAttribute(QStringLiteral("t_type"), (iType == CURRENCY ? QStringLiteral("C") : (iType == PRIMARY ? QStringLiteral("1") : (iType == SECONDARY ? QStringLiteral("2") : (iType == SHARE ? QStringLiteral("S") : (iType == INDEX ? QStringLiteral("I") : QStringLiteral("O"))))))))
    return err;
}

SKGUnitObject::UnitType SKGUnitObject::getType() const
{
    QString typeString = getAttribute(QStringLiteral("t_type"));
    return (typeString == QStringLiteral("C") ? CURRENCY : (typeString == QStringLiteral("S") ? SHARE : (typeString == QStringLiteral("1") ? PRIMARY : (typeString == QStringLiteral("2") ? SECONDARY : (typeString == QStringLiteral("I") ? INDEX : OBJECT)))));
}

SKGError SKGUnitObject::getUnit(SKGUnitObject& oUnit) const
{
    SKGError err;
    if (getDocument()) {
        err = getDocument()->getObject(QStringLiteral("v_unit"), "id=" % getAttribute(QStringLiteral("rd_unit_id")), oUnit);
    }
    return err;
}

SKGError SKGUnitObject::setUnit(const SKGUnitObject& iUnit)
{
    SKGError err;
    if (*this == iUnit && iUnit.getID() != 0) {
        err = SKGError(ERR_INVALIDARG, i18nc("Error message", "Reference unit of a unit cannot be itself."));
    } else {
        err = setAttribute(QStringLiteral("rd_unit_id"), SKGServices::intToString(iUnit.getID()));
    }
    return err;
}

SKGError SKGUnitObject::removeUnit()
{
    return setAttribute(QStringLiteral("rd_unit_id"), QStringLiteral("0"));
}

SKGError SKGUnitObject::addUnitValue(SKGUnitValueObject& oUnitValue)
{
    SKGError err;
    if (getID() == 0) {
        err = SKGError(ERR_FAIL, i18nc("Error message", "%1 failed because linked object is not yet saved in the database.", QStringLiteral("SKGUnitObject::addUnitValue")));
    } else {
        oUnitValue = SKGUnitValueObject(static_cast<SKGDocumentBank*>(getDocument()));
        err = oUnitValue.setAttribute(QStringLiteral("rd_unit_id"), SKGServices::intToString(getID()));
    }
    return err;
}

SKGError SKGUnitObject::simplify()
{
    SKGListSKGObjectBase values;
    SKGError err = getUnitValues(values);
    int nb = values.count();
    if (!err && nb) {
        QHash<QString, SKGListSKGObjectBase> groups;

        SKGBEGINPROGRESSTRANSACTION(*getDocument(), "#INTERNAL#" % i18nc("Progression step", "Simplify unit"), err, 2);
        // Build groups
        {
            SKGBEGINPROGRESSTRANSACTION(*getDocument(), "#INTERNAL#" % i18nc("Progression step", "Analyze unit"), err, nb);
            QDate limit1 = QDate::currentDate().addMonths(-3);
            QDate limit2 = QDate::currentDate().addYears(-1);
            QDate limit3 = QDate::currentDate().addYears(-3);
            for (int i = nb - 1; !err && i >= 0 ; --i) {
                SKGUnitValueObject v(values.at(i));
                QDate date = v.getDate();
                if (date >= limit1) {
                    // No simplification ==> nothing to do
                } else if (date >= limit2) {
                    // Simplification by group of 30 days
                    QString key = limit1.addDays(30 * (limit1.daysTo(date) / 30)).toString();
                    SKGListSKGObjectBase group = groups[key];
                    group.push_back(v);
                    groups[key] = group;

                } else if (date >= limit3) {
                    // Simplification by group of 2 months
                    QString key = limit2.addDays(60 * (limit2.daysTo(date) / 60)).toString();
                    SKGListSKGObjectBase group = groups[key];
                    group.push_back(v);
                    groups[key] = group;
                } else {
                    // Simplification by group of 6 months
                    QString key = limit3.addDays(180 * (limit2.daysTo(date) / 180)).toString();
                    SKGListSKGObjectBase group = groups[key];
                    group.push_back(v);
                    groups[key] = group;
                }

                IFOKDO(err, getDocument()->stepForward(nb - i))
            }
        }
        IFOKDO(err, getDocument()->stepForward(1))

        // Simplify
        {
            QList<QString> keys = groups.keys();
            nb = keys.count();
            SKGBEGINPROGRESSTRANSACTION(*getDocument(), "#INTERNAL#" % i18nc("Progression step", "Remove useless values"), err, nb);
            for (int i = 0; !err && i < nb ; ++i) {
                QString k = keys.at(i);
                SKGListSKGObjectBase group = groups[k];

                // Simplify the group
                int nb2 = group.count();

                // Compute min and max
                double min = 10e20;
                double max = 0;
                for (int j = 0; j < nb2; ++j) {
                    SKGUnitValueObject v1(group.at(j));
                    double y1 = v1.getQuantity();
                    min = qMin(y1, min);
                    max = qMax(y1, max);
                }

                // Simplify
                for (int j = 1; !err && j < nb2 - 1; ++j) {
                    SKGUnitValueObject v1(group.at(j));
                    double y1 = v1.getQuantity();
                    if (y1 != min && y1 != max) {
                        err = v1.remove();
                    }
                }

                IFOKDO(err, getDocument()->stepForward(i + 1))
            }
        }
        IFOKDO(err, getDocument()->stepForward(2))
    }
    return err;
}

SKGError SKGUnitObject::getUnitValues(SKGListSKGObjectBase& oUnitValueList) const
{
    SKGError err = getDocument()->getObjects(QStringLiteral("v_unitvalue"),
                   "rd_unit_id=" % SKGServices::intToString(getID()) % " ORDER BY d_date",
                   oUnitValueList);
    return err;
}

SKGError SKGUnitObject::getLastUnitValue(SKGUnitValueObject& oUnitValue) const
{
    return SKGObjectBase::getDocument()->getObject(QStringLiteral("v_unitvalue"),
            "rd_unit_id=" % SKGServices::intToString(getID()) % " AND d_date=(select MAX(u2.d_date) from unitvalue u2 where u2.rd_unit_id=" % SKGServices::intToString(getID()) % ')',
            oUnitValue);
}

SKGError SKGUnitObject::getUnitValue(const QDate iDate, SKGUnitValueObject& oUnitValue) const
{
    QString ids = SKGServices::intToString(getID());
    QString dates = SKGServices::dateToSqlString(QDateTime(iDate));
    SKGError err = SKGObjectBase::getDocument()->getObject(QStringLiteral("v_unitvalue"),
                   "rd_unit_id=" % ids % " AND d_date<='" % dates %
                   "' AND  ABS(strftime('%s','" % dates %
                   "')-strftime('%s',d_date))=(select MIN(ABS(strftime('%s','" % dates %
                   "')-strftime('%s',u2.d_date))) from unitvalue u2 where u2.rd_unit_id=" % ids %
                   " AND u2.d_date<='" % dates % "')",
                   oUnitValue);

    // If not found then get first
    IFKO(err) err = SKGObjectBase::getDocument()->getObject(QStringLiteral("v_unitvalue"),
                    "rd_unit_id=" % SKGServices::intToString(getID()) % " AND d_date=(select MIN(d_date) from unitvalue where rd_unit_id=" %
                    SKGServices::intToString(getID()) % ')',
                    oUnitValue);
    return err;
}

double SKGUnitObject::getAmount(const QDate iDate) const
{
    SKGTRACEINFUNC(10);
    double output = 0;
    if (getType() == SKGUnitObject::PRIMARY) {
        output = 1.0;
    } else if (getDocument()) {
        // Search result in cache
        QString ids = SKGServices::intToString(getID());
        QString dates = SKGServices::dateToSqlString(QDateTime(iDate));
        QString key = "unitvalue-" % ids % '-' % dates;
        QString val = getDocument()->getCachedValue(key);
        if (val.isEmpty()) {
            // Get quantity
            double quantity = 1;
            SKGUnitValueObject uv;
            if (getUnitValue(iDate, uv).isSucceeded()) {
                quantity = uv.getQuantity();
            }

            SKGUnitObject unit;
            double coef = 1;
            if (getUnit(unit).isSucceeded()) {
                if (unit != *this) {
                    coef = unit.getAmount(iDate);
                }
            }

            output = coef * quantity;
            (static_cast<SKGDocument*>(getDocument()))->addValueInCache(key, SKGServices::doubleToString(output));

            if (getAttribute(QStringLiteral("i_NBVALUES")) == QStringLiteral("1")) {
                // Store value for this symbol for all date
                (static_cast<SKGDocument*>(getDocument()))->addValueInCache("unitvalue-" % ids, SKGServices::doubleToString(output));
            }
        } else {
            output = SKGServices::stringToDouble(val);
        }
    }
    return output;
}

double SKGUnitObject::getDailyChange(const QDate iDate) const
{
    double output = 0;
    SKGStringListList result;
    SKGError err = getDocument()->executeSelectSqliteOrder(
                       "SELECT d_date, f_quantity from unitvalue where rd_unit_id=" %
                       SKGServices::intToString(getID()) %
                       " AND d_date<='" % SKGServices::dateToSqlString(QDateTime(iDate)) %
                       "' ORDER BY d_date DESC LIMIT 2",
                       result);
    if (!err && result.count() == 3) {
        double v2 = SKGServices::stringToDouble(result.at(1).at(1));
        double v1 = SKGServices::stringToDouble(result.at(2).at(1));

        QDate d2 = SKGServices::stringToTime(result.at(1).at(0)).date();
        QDate d1 = SKGServices::stringToTime(result.at(2).at(0)).date();

        output = 100 * (qExp(qLn(v2 / v1) / (SKGServices::nbWorkingDays(d1, d2))) - 1);
    }
    return output;
}

SKGError SKGUnitObject::split(double iRatio) const
{
    SKGError err;
    if (iRatio > 0) {
        err = getDocument()->executeSqliteOrder("UPDATE unitvalue SET f_quantity=f_quantity/" % SKGServices::doubleToString(iRatio) %
                                                " WHERE rd_unit_id=" % SKGServices::intToString(getID()));
        IFOKDO(err, getDocument()->executeSqliteOrder("UPDATE suboperation SET f_value=f_value*" % SKGServices::doubleToString(iRatio) %
                " WHERE rd_operation_id IN (SELECT id FROM operation WHERE rc_unit_id=" % SKGServices::intToString(getID()) % ')'));
    } else {
        err = SKGError(ERR_INVALIDARG, i18nc("Error message", "Invalid ratio. Ratio must be greater than 0."));
    }
    return err;
}

QStringList SKGUnitObject::downloadSources()
{
    QStringList sources;
    QString a = QStringLiteral("skrooge/quotes");
    const auto list = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, a, QStandardPaths::LocateDirectory);
    for (const auto& dir : list) {
        QDirIterator it(dir, QStringList() << QStringLiteral("*.txt"));
        while (it.hasNext()) {
            QFileInfo f(it.next());
            QString file2 = f.completeBaseName();
            if (!sources.contains(file2)) {
                sources.push_back(file2);
            }
        }
    }

    return sources;
}

SKGError SKGUnitObject::addSource(const QString& iNewSource, bool iOpenSource)
{
    SKGError err;

    QString path = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation);
    QDir(path).mkpath(QStringLiteral("skrooge/quotes/"));

    QString newfile = path + "/skrooge/quotes/" % iNewSource % ".txt";

    // Create the new file
    QSaveFile file(newfile);
    // Check if file already existing
    if (!QFile(newfile).exists()) {
        // Creation of the template
        if (!file.open(QIODevice::WriteOnly)) {
            err.setReturnCode(ERR_INVALIDARG).setMessage(i18nc("Error message",  "Save file '%1' failed", newfile));
        } else {
            QTextStream out(&file);

            out << "#" << i18nc("Description test for a text file used to define a source of download",
                                "The URL or the SCRIPT of the source. %1 will be replaced by the internet code of the unit", "%1") << endl;
            out << "#" << i18nc("Description test for a text file used to define a source of download",
                                "%1 will be replaced by the current day in format yyyy-MM-dd", "%2") << endl;
            out << "#" << i18nc("Description test for a text file used to define a source of download",
                                "%1 will be replaced by the previous date in format yyyy-MM-dd", "%3") << endl;
            out << "url=http://server/?s=%1" << endl;
            out << "or" << endl;
            out << "script=mydownloadscript %1" << endl << endl;


            out << "#" << i18nc("Description test for a text file used to define a source of download",
                                "The mode (HTML or CSV or CSVR). In HTML mode, only one value will be extracted from downloaded page. In CSV mode, a value per line will be extracted. CSVR means CSV in reverse mode.") << endl;
            out << "mode=CSV, CSVR or or HTML" << endl << endl;

            out << "#" << i18nc("Description test for a text file used to define a source of download",
                                "The regular expression for the price (see %1)", "http://doc.qt.io/qt-5/qregexp.html") << endl;
            out << "price=" << endl << endl;

            out << "#" << i18nc("Description test for a text file used to define a source of download",
                                "The regular expression for the date (see %1)", "http://doc.qt.io/qt-5/qregexp.html") << endl;
            out << "date=" << endl << endl;

            out << "#" << i18nc("Description test for a text file used to define a source of download",
                                "The format of the date (see %1) or \"UNIX\" for unix time", "http://doc.qt.io/qt-5/qdate.html#fromString-1") << endl;
            out << "dateformat=yyyy-MM-dd" << endl;

            // Close file
            file.commit();
        }
    }

    // Open the created or already existing file
    if (iOpenSource) {
        QDesktopServices::openUrl(QUrl::fromLocalFile(newfile));
    }
    return err;
}

SKGError SKGUnitObject::deleteSource(const QString& iSource)
{
    SKGError err;

    QString fileName = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + QStringLiteral("skrooge/quotes/") % iSource % ".txt";

    // Delete the file
    QFile file(fileName);
    if (!file.remove()) {
        err.setReturnCode(ERR_INVALIDARG).setMessage(i18nc("Error message",  "Deletion of '%1' failed", fileName));
    }
    return err;
}

bool SKGUnitObject::isWritable(const QString& iSource)
{
    QString fileName = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + QStringLiteral("skrooge/quotes/") % iSource % ".txt";
    return QFileInfo(fileName).isWritable();
}

SKGError SKGUnitObject::downloadUnitValue(UnitDownloadMode iMode, int nbMaxValues)
{
    SKGError err;
    SKGTRACEINFUNCRC(10, err);

    QString unitname = getName();
    QString code = getInternetCode();
    bool invert = code.contains(QStringLiteral(" /"));
    code.remove(QStringLiteral(" /"));
    QString source = getDownloadSource();
    SKGDocumentBank* doc = static_cast<SKGDocumentBank*>(getDocument());
    if (!code.isEmpty() && doc) {
        // Get last date
        QDate firstDate;
        SKGStringListList result;
        doc->executeSelectSqliteOrder("SELECT MAX(d_date) FROM unitvalue where rd_unit_id=" % SKGServices::intToString(getID()), result);
        if (result.count() == 2) {
            firstDate = SKGServices::stringToTime(result.at(1).at(0)).date().addDays(-1);
        }

        if (code.startsWith(QLatin1String("="))) {
            // MODE FORMULAR
            // Set 1st january if date is not found
            if (!firstDate.isValid()) {
                firstDate.setDate(QDate::currentDate().year(), 1, 1);
            }

            // Compute yearly rate
            double rate = SKGServices::stringToDouble(code.right(code.length() - 1));

            // Compute rate for step
            double step = (iMode == LAST_MONTHLY || iMode == ALL_MONTHLY ? 12.0 : (iMode == LAST_WEEKLY || iMode == ALL_WEEKLY ? 365.0 / 7.0 : 365));
            double rate2 = 100.0 * (qExp(qLn(1 + rate / 100.0) / step) - 1.0);

            // Get last value
            SKGStringListList result2;
            double value = 100;
            doc->executeSelectSqliteOrder("SELECT f_quantity FROM unitvalue where rd_unit_id=(SELECT id from unit where t_name='" % SKGServices::stringToSqlString(unitname) % "') AND d_date='" % SKGServices::dateToSqlString(QDateTime(firstDate)) % '\'', result2);
            if (result2.count() == 2) {
                value = SKGServices::stringToDouble(result2.at(1).at(0));
            }

            // Compute and add values
            while (!err && firstDate <= QDate::currentDate()) {
                // Compute next date and value
                if (iMode == LAST_MONTHLY || iMode == ALL_MONTHLY) {
                    firstDate = firstDate.addMonths(1);
                } else if (iMode == LAST_WEEKLY || iMode == ALL_WEEKLY) {
                    firstDate = firstDate.addDays(7);
                } else {
                    firstDate = firstDate.addDays(1);
                }

                value *= (1 + rate2 / 100.0);

                // Create value
                SKGUnitValueObject val;
                err = addUnitValue(val);
                IFOKDO(err, val.setDate(firstDate))
                IFOKDO(err, val.setQuantity(value))
                IFOKDO(err, val.save())
            }
        } else {
            // Quote download

            // Set 1st january 1970 if date is not found
            if (!firstDate.isValid()) {
                firstDate.setDate(1970, 1, 1);
            }

            QString interval = QStringLiteral("1d");
            bool last = (iMode == LAST);
            if (last) {
                interval = QStringLiteral("1d");
            } else if (iMode == LAST_MONTHLY) {
                interval = QStringLiteral("1mo");
            } else if (iMode == LAST_WEEKLY) {
                interval = QStringLiteral("1wk");
            } else if (iMode == LAST_DAILY) {
                interval = QStringLiteral("1d");
            } else if (iMode == ALL_MONTHLY) {
                firstDate = QDate(1970, 01, 01);
                interval = QStringLiteral("1mo");
            } else if (iMode == ALL_WEEKLY) {
                firstDate = QDate(1970, 01, 01);
                interval = QStringLiteral("1wk");
            } else if (iMode == ALL_DAILY) {
                firstDate = QDate(1970, 01, 01);
                interval = QStringLiteral("1d");
            }

            QString fileName = QStandardPaths::locate(QStandardPaths::GenericDataLocation, "skrooge/quotes/" % source % ".txt");
            if (fileName.isEmpty()) {
                err = SKGError(ERR_FAIL, i18nc("Error message", "Source of download %1 is not installed.", source));
            } else {
                // Read source file
                QHash< QString, QString > properties;
                err = SKGServices::readPropertyFile(fileName, properties);

                IFOK(err) {
                    bool modeScript = false;
                    QString path = properties[QStringLiteral("url")];
                    if (path.isEmpty()) {
                        path = properties[QStringLiteral("script")];
                        modeScript = true;
                    }
                    path = path.replace(QStringLiteral("%1"), code);
                    QString mode = properties[QStringLiteral("mode")].toUpper();
                    QString price = properties[QStringLiteral("price")].replace(QStringLiteral("%1"), code);
                    QString date = properties[QStringLiteral("date")].replace(QStringLiteral("%1"), code);
                    QString dateFormat = properties[QStringLiteral("dateformat")];

                    path = path.replace(QStringLiteral("%2"), QDate::currentDate().toString(QStringLiteral("yyyy-MM-dd")));
                    path = path.replace(QStringLiteral("%3"), firstDate.toString(QStringLiteral("yyyy-MM-dd")));
                    path = path.replace(QStringLiteral("%4"), interval);


                    SKGTRACEL(1) << "path=[" << path << "]" << endl;
                    SKGTRACEL(1) << "mode=[" << mode << "]" << endl;
                    SKGTRACEL(1) << "price=[" << price << "]" << endl;
                    SKGTRACEL(1) << "date=[" << date << "]" << endl;
                    SKGTRACEL(1) << "dateFormat=[" << dateFormat << "]" << endl;

                    // Download url
                    QByteArray stream;
                    if (modeScript) {
                        QProcess p;
                        p.start(path);
                        if (p.waitForFinished(1000 * 60 * 2) && p.exitCode() == 0) {
                            stream = p.readAllStandardOutput();
                        } else {
                            err.setReturnCode(ERR_FAIL).setMessage(i18nc("Error message",  "The following command line failed with code %2:\n'%1'", path, p.exitCode()));
                        }
                    } else {
                        SKGServices::downloadToStream(QUrl::fromUserInput(path), stream);
                    }
                    if (!err && !stream.isEmpty()) {
                        SKGTRACEL(1) << "stream=[" << stream << "]" << endl;

                        // Parse data
                        QRegExp priceRegExp(price, Qt::CaseInsensitive);
                        QRegExp dateRegExp(date, Qt::CaseInsensitive);

                        QStringList lines = (mode.startsWith(QLatin1String("CSV")) ? SKGServices::splitCSVLine(stream, '\n') : QStringList() << QStringLiteral("") << stream);
                        int nb = lines.count();
                        int nbLoaded = 0;
                        for (int i = 1; i < nb && !err && (i < nbMaxValues || nbMaxValues == 0); ++i) {
                            QString data = lines.at(mode == QStringLiteral("CSVR") ? nb - i : i).trimmed();
                            if (!data.isEmpty()) {
                                SKGTRACEL(1) << "Downloaded data from [" << path << "]=[" << data << "]" << endl;

                                double val = 0.0;
                                if (priceRegExp.indexIn(data) > -1) {
                                    val = SKGServices::stringToDouble(priceRegExp.cap(1));
                                }
                                QString date2;
                                if (dateRegExp.indexIn(data) > -1) {
                                    date2 = dateRegExp.cap(1);
                                }
                                SKGTRACEL(1) << "Price found=[" << val << "]" << endl;
                                SKGTRACEL(1) << "Date found=[" << date2 << "]" << endl;

                                // Set value
                                if (val) {
                                    QDate ds;
                                    if (dateFormat == QStringLiteral("UNIX")) {
                                        ds = QDateTime::fromTime_t(SKGServices::stringToInt(date2)).date();
                                    } else {
                                        ds = QDate::fromString(date2, dateFormat);
                                        // Try with an english locale
                                        if (!ds.isValid()) {
                                            QLocale en(QStringLiteral("en_EN"));
                                            ds = en.toDate(date2, dateFormat);
                                        }
                                    }

                                    if (!ds.isValid()) {
                                        ds = QDate::currentDate();
                                        SKGTRACE << "WARNING:" << date2 << " not well parsed with format " << dateFormat << endl;
                                    }
                                    if (!dateFormat.contains(QStringLiteral("yyyy")) && ds.year() < 2000) {
                                        ds = ds.addYears(100);
                                    }

                                    // Creation or update of the value
                                    SKGUnitValueObject value;
                                    IFOKDO(err, addUnitValue(value))
                                    IFOKDO(err, value.setDate(ds))
                                    IFOKDO(err, value.setQuantity(invert && val != 0 ? 1 / val : val))
                                    IFOKDO(err, value.save())

                                    if (last) {
                                        break;
                                    }

                                    nbLoaded++;
                                }
                            }
                            if (nbLoaded == 0 && !data.isEmpty()) {
                                err = doc->sendMessage(i18nc("Information message", "Price not found for '%1' with regular expression '%2' in line '%3'", getName(), SKGServices::stringToHtml(price), data), SKGDocument::Warning);    // TODO(Stephane MANKOWSKI) does not work with html
                            }
                        }
                    } else {
                        doc->sendMessage(i18nc("An information message", "Open url '%1' failed", path), SKGDocument::Warning);
                    }
                }
            }
        }
    }

    IFKO(err) {
        err.addError(ERR_FAIL, i18nc("Error message", "Impossible to download unit %1 with Internet code %2.", unitname, code));
    }

    return err;
}

SKGError SKGUnitObject::getUrl(QUrl& oUrl) const
{
    SKGError err;
    SKGTRACEINFUNCRC(10, err);

    QString url;
    QString code = getInternetCode();
    code.remove(QStringLiteral(" /"));
    QString source = getDownloadSource();
    if (!code.isEmpty()) {
        if (code.startsWith(QLatin1String("="))) {
            // MODE FORMULAR

        } else if (source.isEmpty() || source == i18nc("Native download source (Yahoo)", "Yahoo")) {
            // NATIVE MODE
            url = "http://ichart.finance.yahoo.com/table.csv?s=" % code % "&ignore=.csv";
        } else {
            // ALTERNATIVE MODE
            QString fileName = QStandardPaths::locate(QStandardPaths::GenericDataLocation, "skrooge/quotes/" % source % ".txt");
            if (fileName.isEmpty()) {
                err = SKGError(ERR_FAIL, i18nc("Error message", "Source of download %1 is not installed.", source));
            } else {
                // Read source file
                QHash< QString, QString > properties;
                err = SKGServices::readPropertyFile(fileName, properties);

                IFOK(err) {
                    url = properties[QStringLiteral("url")].replace(QStringLiteral("%1"), code);
                }
            }
        }
    }

    IFOK(err) {
        oUrl = QUrl(url);
    }

    return err;
}

SKGError SKGUnitObject::openURL() const
{
    QUrl url;
    SKGError err = getUrl(url);

    IFKO(err) {
        err.addError(ERR_FAIL, i18nc("Error message", "Impossible to open unit %1 with Internet code %2.", getName(), getInternetCode()));
    } else {
        QDesktopServices::openUrl(url);
    }

    return err;
}

SKGError SKGUnitObject::getOperations(SKGObjectBase::SKGListSKGObjectBase& oOperations) const
{
    SKGError err = getDocument()->getObjects(QStringLiteral("v_operation"),
                   "rc_unit_id=" % SKGServices::intToString(getID()),
                   oOperations);
    return err;
}

SKGError SKGUnitObject::merge(const SKGUnitObject& iUnit)
{
    SKGError err;

    SKGObjectBase::SKGListSKGObjectBase ops;
    IFOKDO(err, iUnit.getOperations(ops))
    int nb = ops.count();
    for (int i = 0; !err && i < nb; ++i) {
        SKGOperationObject op(ops.at(i));
        err = op.setUnit(*this);
        IFOKDO(err, op.save(true, false))
    }

    IFOKDO(err, iUnit.remove(false))
    return err;
}
