/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
 * This file is part of the LibreOffice project.
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 *
 */
 
#include <sal/config.h>
 
#include <com/sun/star/frame/XModel.hpp>
#include <com/sun/star/uno/XComponentContext.hpp>
#include <com/sun/star/container/XIndexAccess.hpp>
#include <com/sun/star/sheet/XSpreadsheetDocument.hpp>
#include <com/sun/star/sheet/XSpreadsheet.hpp>
#include <com/sun/star/sheet/XSolver.hpp>
#include <com/sun/star/sheet/XSolverDescription.hpp>
#include <com/sun/star/table/CellAddress.hpp>
#include <com/sun/star/table/CellContentType.hpp>
#include <com/sun/star/table/XCell.hpp>
#include <com/sun/star/lang/XServiceInfo.hpp>
 
#include <rtl/math.hxx>
#include <cppuhelper/implbase.hxx>
#include <cppuhelper/supportsservice.hxx>
 
#include <comphelper/broadcasthelper.hxx>
#include <comphelper/propertycontainer.hxx>
#include <comphelper/proparrhlp.hxx>
 
#include <cmath>
#include <vector>
#include <limits>
#include <chrono>
#include <random>
#include <o3tl/make_unique.hxx>
 
#include <unotools/resmgr.hxx>
 
#include "DifferentialEvolution.hxx"
#include "ParticelSwarmOptimization.hxx"
 
#include <strings.hrc>
 
using namespace css;
 
namespace
{
struct Bound
{
    double lower;
    double upper;
 
    Bound()
        // float bounds should be low/high enough for all practical uses
        // otherwise we are too far away from the solution
        : lower(std::numeric_limits<float>::lowest())
        , upper(std::numeric_limits<float>::max())
    {
    }
 
    void updateBound(sheet::SolverConstraintOperator eOp, double fValue)
    {
        if (eOp == sheet::SolverConstraintOperator_LESS_EQUAL)
        {
            // if we set the bound multiple times use the one which includes both values
            // for example bound values 100, 120, 150 -> use 100 -> the lowest one
            if (fValue < upper)
                upper = fValue;
        }
        else if (eOp == sheet::SolverConstraintOperator_GREATER_EQUAL)
        {
            if (fValue > lower)
                lower = fValue;
        }
        else if (eOp == sheet::SolverConstraintOperator_EQUAL)
        {
            lower = fValue;
            upper = fValue;
        }
    }
};
 
enum
{
    PROP_NONNEGATIVE,
    PROP_INTEGER,
    PROP_TIMEOUT,
    PROP_ALGORITHM,
};
 
} // end anonymous namespace
 
typedef cppu::WeakImplHelper<sheet::XSolver, sheet::XSolverDescription, lang::XServiceInfo>
    SwarmSolver_Base;
 
class SwarmSolver : public comphelper::OMutexAndBroadcastHelper,
                    public comphelper::OPropertyContainer,
                    public comphelper::OPropertyArrayUsageHelper<SwarmSolver>,
                    public SwarmSolver_Base
{
private:
    uno::Reference<sheet::XSpreadsheetDocument> mxDocument;
    table::CellAddress maObjective;
    uno::Sequence<table::CellAddress> maVariables;
    uno::Sequence<sheet::SolverConstraint> maConstraints;
    bool mbMaximize;
 
    // set via XPropertySet
    bool mbNonNegative;
    bool mbInteger;
    sal_Int32 mnTimeout;
    sal_Int32 mnAlgorithm;
 
    // results
    bool mbSuccess;
    double mfResultValue;
 
    uno::Sequence<double> maSolution;
    OUString maStatus;
 
    std::vector<Bound> maBounds;
    std::vector<sheet::SolverConstraint> maNonBoundedConstraints;
 
private:
    static OUString getResourceString(const char* pId);
 
    uno::Reference<table::XCell> getCell(const table::CellAddress& rPosition);
    void setValue(const table::CellAddress& rPosition, double fValue);
    double getValue(const table::CellAddress& rPosition);
 
public:
    SwarmSolver()
        : OPropertyContainer(GetBroadcastHelper())
        , mbMaximize(true)
        , mbNonNegative(false)
        , mbInteger(false)
        , mnTimeout(60000)
        , mnAlgorithm(0)
        , mbSuccess(false)
        , mfResultValue(0.0)
    {
        registerProperty("NonNegative", PROP_NONNEGATIVE, 0, &mbNonNegative,
                         cppu::UnoType<decltype(mbNonNegative)>::get());
        registerProperty("Integer", PROP_INTEGER, 0, &mbInteger,
                         cppu::UnoType<decltype(mbInteger)>::get());
        registerProperty("Timeout", PROP_TIMEOUT, 0, &mnTimeout,
                         cppu::UnoType<decltype(mnTimeout)>::get());
        registerProperty("Algorithm", PROP_ALGORITHM, 0, &mnAlgorithm,
                         cppu::UnoType<decltype(mnAlgorithm)>::get());
    }
 
    DECLARE_XINTERFACE()
    DECLARE_XTYPEPROVIDER()
 
    virtual uno::Reference<beans::XPropertySetInfo> SAL_CALL getPropertySetInfo() override
    {
        return createPropertySetInfo(getInfoHelper());
    }
    // OPropertySetHelper
    virtual cppu::IPropertyArrayHelper& SAL_CALL getInfoHelper() override
    {
        return *getArrayHelper();
    }
    // OPropertyArrayUsageHelper
    virtual cppu::IPropertyArrayHelper* createArrayHelper() const override
    {
        uno::Sequence<beans::Property> aProperties;
        describeProperties(aProperties);
        return new cppu::OPropertyArrayHelper(aProperties);
    }
 
    // XSolver
    virtual uno::Reference<sheet::XSpreadsheetDocument> SAL_CALL getDocument() override
    {
        return mxDocument;
    }
    virtual void SAL_CALL
    setDocument(const uno::Reference<sheet::XSpreadsheetDocument>& rDocument) override
    {
        mxDocument = rDocument;
    }
 
    virtual table::CellAddress SAL_CALL getObjective() override { return maObjective; }
    virtual void SAL_CALL setObjective(const table::CellAddress& rObjective) override
    {
        maObjective = rObjective;
    }
 
    virtual uno::Sequence<table::CellAddress> SAL_CALL getVariables() override
    {
        return maVariables;
    }
    virtual void SAL_CALL setVariables(const uno::Sequence<table::CellAddress>& rVariables) override
    {
        maVariables = rVariables;
    }
 
    virtual uno::Sequence<sheet::SolverConstraint> SAL_CALL getConstraints() override
    {
        return maConstraints;
    }
    virtual void SAL_CALL
    setConstraints(const uno::Sequence<sheet::SolverConstraint>& rConstraints) override
    {
        maConstraints = rConstraints;
    }
 
    virtual sal_Bool SAL_CALL getMaximize() override { return mbMaximize; }
    virtual void SAL_CALL setMaximize(sal_Bool bMaximize) override { mbMaximize = bMaximize; }
 
    virtual sal_Bool SAL_CALL getSuccess() override { return mbSuccess; }
    virtual double SAL_CALL getResultValue() override { return mfResultValue; }
 
    virtual uno::Sequence<double> SAL_CALL getSolution() override { return maSolution; }
 
    virtual void SAL_CALL solve() override;
 
    // XSolverDescription
    virtual OUString SAL_CALL getComponentDescription() override
    {
        return SwarmSolver::getResourceString(RID_SWARM_SOLVER_COMPONENT);
    }
 
    virtual OUString SAL_CALL getStatusDescription() override { return maStatus; }
 
    virtual OUString SAL_CALL getPropertyDescription(const OUString& rPropertyName) override
    {
        const char* pResId = nullptr;
        switch (getInfoHelper().getHandleByName(rPropertyName))
        {
            case PROP_NONNEGATIVE:
                pResId = RID_PROPERTY_NONNEGATIVE;
                break;
            case PROP_INTEGER:
                pResId = RID_PROPERTY_INTEGER;
                break;
            case PROP_TIMEOUT:
                pResId = RID_PROPERTY_TIMEOUT;
                break;
            case PROP_ALGORITHM:
                pResId = RID_PROPERTY_ALGORITHM;
                break;
            default:
                break;
        }
        return SwarmSolver::getResourceString(pResId);
    }
 
    // XServiceInfo
    virtual OUString SAL_CALL getImplementationName() override
    {
        return OUString("com.sun.star.comp.Calc.SwarmSolver");
    }
 
    sal_Bool SAL_CALL supportsService(const OUString& rServiceName) override
    {
        return cppu::supportsService(this, rServiceName);
    }
 
    uno::Sequence<OUString> SAL_CALL getSupportedServiceNames() override
    {
        uno::Sequence<OUString> aServiceNames{ "com.sun.star.sheet.Solver" };
        return aServiceNames;
    }
 
private:
    void applyVariables(std::vector<double> const& rVariables);
    bool doesViolateConstraints();
 
public:
    double calculateFitness(std::vector<double> const& rVariables);
    size_t getDimensionality();
    void initializeVariables(std::vector<double>& rVariables, std::mt19937& rGenerator);
    double clampVariable(size_t nVarIndex, double fValue);
    double boundVariable(size_t nVarIndex, double fValue);
};
 
OUString SwarmSolver::getResourceString(const char* pId)
{
    if (!pId)
        return OUString();
 
    return Translate::get(pId, Translate::Create("scc"));
}
 
uno::Reference<table::XCell> SwarmSolver::getCell(const table::CellAddress& rPosition)
{
    uno::Reference<container::XIndexAccess> xSheets(mxDocument->getSheets(), uno::UNO_QUERY);
    uno::Reference<sheet::XSpreadsheet> xSheet(xSheets->getByIndex(rPosition.Sheet),
                                               uno::UNO_QUERY);
    return xSheet->getCellByPosition(rPosition.Column, rPosition.Row);
}
 
void SwarmSolver::setValue(const table::CellAddress& rPosition, double fValue)
{
    getCell(rPosition)->setValue(fValue);
}
 
double SwarmSolver::getValue(const table::CellAddress& rPosition)
{
    return getCell(rPosition)->getValue();
}
 
IMPLEMENT_FORWARD_XINTERFACE2(SwarmSolver, SwarmSolver_Base, OPropertyContainer)
IMPLEMENT_FORWARD_XTYPEPROVIDER2(SwarmSolver, SwarmSolver_Base, OPropertyContainer)
 
void SwarmSolver::applyVariables(std::vector<double> const& rVariables)
{
    for (sal_Int32 i = 0; i < maVariables.getLength(); ++i)
    {
        setValue(maVariables[i], rVariables[i]);
    }
}
 
double SwarmSolver::calculateFitness(std::vector<double> const& rVariables)
{
    applyVariables(rVariables);
 
    if (doesViolateConstraints())
        return std::numeric_limits<float>::lowest();
 
    double x = getValue(maObjective);
 
    if (mbMaximize)
        return x;
    else
        return -x;
}
 
void SwarmSolver::initializeVariables(std::vector<double>& rVariables, std::mt19937& rGenerator)
{
    int nTry = 1;
    bool bConstraintsOK = false;
 
    while (!bConstraintsOK && nTry < 10)
    {
        size_t noVariables(maVariables.getLength());
 
        rVariables.resize(noVariables);
 
        for (size_t i = 0; i < noVariables; ++i)
        {
            Bound const& rBound = maBounds[i];
            if (mbInteger)
            {
                sal_Int64 intLower(rBound.lower);
                sal_Int64 intUpper(rBound.upper);
                std::uniform_int_distribution<sal_Int64> random(intLower, intUpper);
                rVariables[i] = double(random(rGenerator));
            }
            else
            {
                std::uniform_real_distribution<double> random(rBound.lower, rBound.upper);
                rVariables[i] = random(rGenerator);
            }
        }
 
        applyVariables(rVariables);
 
        bConstraintsOK = !doesViolateConstraints();
        nTry++;
    }
}
 
double SwarmSolver::clampVariable(size_t nVarIndex, double fValue)
{
    Bound const& rBound = maBounds[nVarIndex];
    double fResult = std::max(std::min(fValue, rBound.upper), rBound.lower);
 
    if (mbInteger)
        return std::trunc(fResult);
 
    return fResult;
}
 
double SwarmSolver::boundVariable(size_t nVarIndex, double fValue)
{
    Bound const& rBound = maBounds[nVarIndex];
    // double fResult = std::max(std::min(fValue, rBound.upper), rBound.lower);
    double fResult = fValue;
    while (fResult < rBound.lower || fResult > rBound.upper)
    {
        if (fResult < rBound.lower)
            fResult = rBound.upper - (rBound.lower - fResult);
        if (fResult > rBound.upper)
            fResult = (fResult - rBound.upper) + rBound.lower;
    }
 
    if (mbInteger)
        return std::trunc(fResult);
 
    return fResult;
}
 
size_t SwarmSolver::getDimensionality() { return maVariables.getLength(); }
 
bool SwarmSolver::doesViolateConstraints()
{
    for (sheet::SolverConstraint& rConstraint : maNonBoundedConstraints)
    {
        double fLeftValue = getValue(rConstraint.Left);
        double fRightValue = 0.0;
 
        table::CellAddress aCellAddress;
 
        if (rConstraint.Right >>= aCellAddress)
        {
            fRightValue = getValue(aCellAddress);
        }
        else if (rConstraint.Right >>= fRightValue)
        {
            // empty
        }
        else
        {
            return false;
        }
 
        sheet::SolverConstraintOperator eOp = rConstraint.Operator;
        switch (eOp)
        {
            case sheet::SolverConstraintOperator_LESS_EQUAL:
            {
                if (fLeftValue > fRightValue)
                    return true;
            }
            break;
            case sheet::SolverConstraintOperator_GREATER_EQUAL:
            {
                if (fLeftValue < fRightValue)
                    return true;
            }
            break;
            case sheet::SolverConstraintOperator_EQUAL:
            {
                if (!rtl::math::approxEqual(fLeftValue, fRightValue))
                    return true;
            }
            break;
            default:
                break;
        }
    }
    return false;
}
 
template <typename SwarmAlgorithm> class SwarmRunner
{
private:
    SwarmAlgorithm& mrAlgorithm;
    double mfTimeout;
 
    static constexpr size_t mnPopulationSize = 40;
    static constexpr int constNumberOfGenerationsWithoutChange = 50;
 
    std::chrono::high_resolution_clock::time_point maStart;
    std::chrono::high_resolution_clock::time_point maEnd;
 
public:
    SwarmRunner(SwarmAlgorithm& rAlgorithm)
        : mrAlgorithm(rAlgorithm)
        , mfTimeout(5000)
    {
    }
 
    void setTimeout(double fTimeout) { mfTimeout = fTimeout; }
 
    std::vector<double> const& solve()
    {
        using std::chrono::duration_cast;
        using std::chrono::high_resolution_clock;
        using std::chrono::milliseconds;
 
        mrAlgorithm.initialize();
 
        maEnd = maStart = high_resolution_clock::now();
 
        int nLastChange = 0;
 
        while ((mrAlgorithm.getGeneration() - nLastChange) < constNumberOfGenerationsWithoutChange
               && duration_cast<milliseconds>(maEnd - maStart).count() < mfTimeout)
        {
            bool bChange = mrAlgorithm.next();
 
            if (bChange)
                nLastChange = mrAlgorithm.getGeneration();
 
            maEnd = high_resolution_clock::now();
        }
        return mrAlgorithm.getResult();
    }
};
 
void SAL_CALL SwarmSolver::solve()
{
    uno::Reference<frame::XModel> xModel(mxDocument, uno::UNO_QUERY_THROW);
 
    maStatus.clear();
    mbSuccess = false;
 
    maBounds.resize(maVariables.getLength());
 
    xModel->lockControllers();
 
    if (mbNonNegative)
    {
        for (Bound& rBound : maBounds)
            rBound.lower = 0;
    }
 
    // Determine variable bounds
    for (sheet::SolverConstraint const& rConstraint : maConstraints)
    {
        table::CellAddress aLeftCellAddress = rConstraint.Left;
        sheet::SolverConstraintOperator eOp = rConstraint.Operator;
 
        size_t index = 0;
        bool bFoundVariable = false;
        for (table::CellAddress& rVariableCell : maVariables)
        {
            if (aLeftCellAddress == rVariableCell)
            {
                bFoundVariable = true;
                table::CellAddress aCellAddress;
                double fValue;
 
                if (rConstraint.Right >>= aCellAddress)
                {
                    uno::Reference<table::XCell> xCell = getCell(aCellAddress);
                    if (xCell->getType() == table::CellContentType_VALUE)
                    {
                        maBounds[index].updateBound(eOp, xCell->getValue());
                    }
                    else
                    {
                        maNonBoundedConstraints.push_back(rConstraint);
                    }
                }
                else if (rConstraint.Right >>= fValue)
                {
                    maBounds[index].updateBound(eOp, fValue);
                }
            }
            index++;
        }
        if (!bFoundVariable)
            maNonBoundedConstraints.push_back(rConstraint);
    }
 
    std::vector<double> aSolution;
 
    if (mnAlgorithm == 0)
    {
        DifferentialEvolutionAlgorithm<SwarmSolver> aDE(*this, 50);
        SwarmRunner<DifferentialEvolutionAlgorithm<SwarmSolver>> aEvolution(aDE);
        aEvolution.setTimeout(mnTimeout);
        aSolution = aEvolution.solve();
    }
    else
    {
        ParticleSwarmOptimizationAlgorithm<SwarmSolver> aPSO(*this, 100);
        SwarmRunner<ParticleSwarmOptimizationAlgorithm<SwarmSolver>> aSwarmSolver(aPSO);
        aSwarmSolver.setTimeout(mnTimeout);
        aSolution = aSwarmSolver.solve();
    }
 
    xModel->unlockControllers();
 
    mbSuccess = true;
 
    maSolution.realloc(aSolution.size());
    std::copy(aSolution.begin(), aSolution.end(), maSolution.begin());
}
 
extern "C" SAL_DLLPUBLIC_EXPORT uno::XInterface*
com_sun_star_comp_Calc_SwarmSolver_get_implementation(uno::XComponentContext*,
                                                      uno::Sequence<uno::Any> const&)
{
    return cppu::acquire(new SwarmSolver());
}
 
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */

V614 Uninitialized variable 'fValue' used. Consider checking the second actual argument of the 'updateBound' function.