/* -*- 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 <svl/undo.hxx>
#include <rtl/math.hxx>
#include <osl/time.h>

#include <rangelst.hxx>
#include <docsh.hxx>
#include <document.hxx>
#include <reffact.hxx>
#include <docfunc.hxx>
#include <scresid.hxx>
#include <strings.hrc>

#include <random>

#include <RandomNumberGeneratorDialog.hxx>

namespace
{

const sal_Int64 DIST_UNIFORM             = 0;
const sal_Int64 DIST_NORMAL              = 1;
const sal_Int64 DIST_CAUCHY              = 2;
const sal_Int64 DIST_BERNOULLI           = 3;
const sal_Int64 DIST_BINOMIAL            = 4;
const sal_Int64 DIST_CHI_SQUARED         = 5;
const sal_Int64 DIST_GEOMETRIC           = 6;
const sal_Int64 DIST_NEGATIVE_BINOMIAL   = 7;
const sal_Int64 DIST_UNIFORM_INTEGER     = 8;
const sal_Int64 DIST_POISSON             = 9;

const sal_Int64 PRECISION   = 10000;
const sal_Int64 DIGITS      = 4;

}

ScRandomNumberGeneratorDialog::ScRandomNumberGeneratorDialog(
                    SfxBindings* pSfxBindings, SfxChildWindow* pChildWindow,
                    weld::Window* pParent, ScViewData& rViewData)
    : ScAnyRefDlgController(pSfxBindings, pChildWindow, pParent,
                          u"modules/scalc/ui/randomnumbergenerator.ui"_ustr,
                          u"RandomNumberGeneratorDialog"_ustr)
    , mrViewData(rViewData)
    , mrDoc(rViewData.GetDocument())
    , mbDialogLostFocus(false)
    , mxInputRangeText(m_xBuilder->weld_label(u"cell-range-label"_ustr))
    , mxInputRangeEdit(new formula::RefEdit(m_xBuilder->weld_entry(u"cell-range-edit"_ustr)))
    , mxInputRangeButton(new formula::RefButton(m_xBuilder->weld_button(u"cell-range-button"_ustr)))
    , mxDistributionCombo(m_xBuilder->weld_combo_box(u"distribution-combo"_ustr))
    , mxParameter1Text(m_xBuilder->weld_label(u"parameter1-label"_ustr))
    , mxParameter1Value(m_xBuilder->weld_spin_button(u"parameter1-spin"_ustr))
    , mxParameter2Text(m_xBuilder->weld_label(u"parameter2-label"_ustr))
    , mxParameter2Value(m_xBuilder->weld_spin_button(u"parameter2-spin"_ustr))
    , mxSeed(m_xBuilder->weld_spin_button(u"seed-spin"_ustr))
    , mxEnableSeed(m_xBuilder->weld_check_button(u"enable-seed-check"_ustr))
    , mxDecimalPlaces(m_xBuilder->weld_spin_button(u"decimal-places-spin"_ustr))
    , mxEnableRounding(m_xBuilder->weld_check_button(u"enable-rounding-check"_ustr))
    , mxButtonApply(m_xBuilder->weld_button(u"apply"_ustr))
    , mxButtonOk(m_xBuilder->weld_button(u"ok"_ustr))
    , mxButtonClose(m_xBuilder->weld_button(u"close"_ustr))
{
    mxInputRangeEdit->SetReferences(this, mxInputRangeText.get());
    mxInputRangeButton->SetReferences(this, mxInputRangeEdit.get());

    Init();
    GetRangeFromSelection();
}

ScRandomNumberGeneratorDialog::~ScRandomNumberGeneratorDialog()
{
}

void ScRandomNumberGeneratorDialog::Init()
{
    mxButtonOk->connect_clicked( LINK( this, ScRandomNumberGeneratorDialog, OkClicked ) );
    mxButtonClose->connect_clicked( LINK( this, ScRandomNumberGeneratorDialog, CloseClicked ) );
    mxButtonApply->connect_clicked( LINK( this, ScRandomNumberGeneratorDialog, ApplyClicked ) );

    mxInputRangeEdit->SetGetFocusHdl(LINK( this, ScRandomNumberGeneratorDialog, GetEditFocusHandler ));
    mxInputRangeButton->SetGetFocusHdl(LINK( this, ScRandomNumberGeneratorDialog, GetButtonFocusHandler ));

    mxInputRangeEdit->SetLoseFocusHdl (LINK( this, ScRandomNumberGeneratorDialog, LoseEditFocusHandler ));
    mxInputRangeButton->SetLoseFocusHdl (LINK( this, ScRandomNumberGeneratorDialog, LoseButtonFocusHandler ));

    mxInputRangeEdit->SetModifyHdl( LINK( this, ScRandomNumberGeneratorDialog, InputRangeModified ));
    mxParameter1Value->connect_value_changed( LINK( this, ScRandomNumberGeneratorDialog, Parameter1ValueModified ));
    mxParameter2Value->connect_value_changed( LINK( this, ScRandomNumberGeneratorDialog, Parameter2ValueModified ));

    mxDistributionCombo->connect_changed( LINK( this, ScRandomNumberGeneratorDialog, DistributionChanged ));

    mxEnableSeed->connect_toggled( LINK( this, ScRandomNumberGeneratorDialog, CheckChanged ));
    mxEnableRounding->connect_toggled( LINK( this, ScRandomNumberGeneratorDialog, CheckChanged ));

    DistributionChanged(*mxDistributionCombo);
    CheckChanged(*mxEnableSeed);
}

void ScRandomNumberGeneratorDialog::GetRangeFromSelection()
{
    mrViewData.GetSimpleArea(maInputRange);
    OUString aCurrentString(maInputRange.Format(mrDoc, ScRefFlags::RANGE_ABS_3D, mrDoc.GetAddressConvention()));
    mxInputRangeEdit->SetText( aCurrentString );
}

void ScRandomNumberGeneratorDialog::SetActive()
{
    if ( mbDialogLostFocus )
    {
        mbDialogLostFocus = false;
        if( mxInputRangeEdit )
            mxInputRangeEdit->GrabFocus();
    }
    else
    {
        m_xDialog->grab_focus();
    }
    RefInputDone();
}

void ScRandomNumberGeneratorDialog::Close()
{
    DoClose( ScRandomNumberGeneratorDialogWrapper::GetChildWindowId() );
}

void ScRandomNumberGeneratorDialog::SetReference( const ScRange& rReferenceRange, ScDocument& rDoc )
{
    if (!mxInputRangeEdit->GetWidget()->get_sensitive())
        return;

    if ( rReferenceRange.aStart != rReferenceRange.aEnd )
        RefInputStart(mxInputRangeEdit.get());

    maInputRange = rReferenceRange;

    OUString aReferenceString(maInputRange.Format(rDoc, ScRefFlags::RANGE_ABS_3D, rDoc.GetAddressConvention()));
    mxInputRangeEdit->SetRefString( aReferenceString );

    mxButtonApply->set_sensitive(true);
    mxButtonOk->set_sensitive(true);
}

void ScRandomNumberGeneratorDialog::SelectGeneratorAndGenerateNumbers()
{
    if (!maInputRange.IsValid())
        return;

    sal_Int64 aSelectedId = mxDistributionCombo->get_active_id().toInt64();

    sal_uInt32 seedValue;

    if( mxEnableSeed->get_active() )
    {
        seedValue = mxSeed->get_value();
    }
    else
    {
        TimeValue now;
        osl_getSystemTime(&now);
        seedValue = now.Nanosec;
    }

    std::mt19937 seed(seedValue);

    sal_Int64 parameterInteger1 = mxParameter1Value->get_value();
    sal_Int64 parameterInteger2 = mxParameter2Value->get_value();

    double parameter1 = parameterInteger1 / static_cast<double>(PRECISION);
    double parameter2 = parameterInteger2 / static_cast<double>(PRECISION);

    std::optional<sal_Int8> aDecimalPlaces;
    if (mxEnableRounding->get_active())
    {
        aDecimalPlaces = static_cast<sal_Int8>(mxDecimalPlaces->get_value());
    }

    switch(aSelectedId)
    {
        case DIST_UNIFORM:
        {
            std::uniform_real_distribution<> distribution(parameter1, parameter2);
            GenerateNumbers(distribution, seed, STR_DISTRIBUTION_UNIFORM_REAL, aDecimalPlaces);
            break;
        }
        case DIST_UNIFORM_INTEGER:
        {
            std::uniform_int_distribution<sal_Int64> distribution(parameterInteger1, parameterInteger2);
            GenerateNumbers(distribution, seed, STR_DISTRIBUTION_UNIFORM_INTEGER, aDecimalPlaces);
            break;
        }
        case DIST_NORMAL:
        {
            std::normal_distribution<> distribution(parameter1, parameter2);
            GenerateNumbers(distribution, seed, STR_DISTRIBUTION_NORMAL, aDecimalPlaces);
            break;
        }
        case DIST_CAUCHY:
        {
            std::cauchy_distribution<> distribution(parameter1);
            GenerateNumbers(distribution, seed, STR_DISTRIBUTION_CAUCHY, aDecimalPlaces);
            break;
        }
        case DIST_BERNOULLI:
        {
            std::bernoulli_distribution distribution(parameter1);
            GenerateNumbers(distribution, seed, STR_DISTRIBUTION_BERNOULLI, aDecimalPlaces);
            break;
        }
        case DIST_BINOMIAL:
        {
            std::binomial_distribution<> distribution(parameterInteger2, parameter1);
            GenerateNumbers(distribution, seed, STR_DISTRIBUTION_BINOMIAL, aDecimalPlaces);
            break;
        }
        case DIST_CHI_SQUARED:
        {
            std::chi_squared_distribution<> distribution(parameter1);
            GenerateNumbers(distribution, seed, STR_DISTRIBUTION_CHI_SQUARED, aDecimalPlaces);
            break;
        }
        case DIST_GEOMETRIC:
        {
            std::geometric_distribution<> distribution(parameter1);
            GenerateNumbers(distribution, seed, STR_DISTRIBUTION_GEOMETRIC, aDecimalPlaces);
            break;
        }
        case DIST_NEGATIVE_BINOMIAL:
        {
            std::negative_binomial_distribution<> distribution(parameterInteger2, parameter1);
            GenerateNumbers(distribution, seed, STR_DISTRIBUTION_NEGATIVE_BINOMIAL, aDecimalPlaces);
            break;
        }
        case DIST_POISSON:
        {
            std::poisson_distribution<> distribution(parameter1);
            GenerateNumbers(distribution, seed, STR_DISTRIBUTION_POISSON, aDecimalPlaces);
            break;
        }
    }
}

template<class DIST>
void ScRandomNumberGeneratorDialog::GenerateNumbers(DIST& distribution, std::mt19937& seed, TranslateId pDistributionStringId, std::optional<sal_Int8> aDecimalPlaces)
{
    OUString aUndo = ScResId(STR_UNDO_DISTRIBUTION_TEMPLATE);
    OUString aDistributionName = ScResId(pDistributionStringId);
    aUndo = aUndo.replaceAll("%1",  aDistributionName);

    ScDocShell* pDocShell = mrViewData.GetDocShell();
    SfxUndoManager* pUndoManager = pDocShell->GetUndoManager();
    pUndoManager->EnterListAction( aUndo, aUndo, 0, mrViewData.GetViewShell()->GetViewShellId() );

    SCROW nRowStart = maInputRange.aStart.Row();
    SCROW nRowEnd   = maInputRange.aEnd.Row();
    SCCOL nColStart = maInputRange.aStart.Col();
    SCCOL nColEnd   = maInputRange.aEnd.Col();
    SCTAB nTabStart = maInputRange.aStart.Tab();
    SCTAB nTabEnd   = maInputRange.aEnd.Tab();

    std::vector<double> aVals;
    aVals.reserve(nRowEnd - nRowStart + 1);

    for (SCROW nTab = nTabStart; nTab <= nTabEnd; ++nTab)
    {
        for (SCCOL nCol = nColStart; nCol <= nColEnd; ++nCol)
        {
            aVals.clear();

            ScAddress aPos(nCol, nRowStart, nTab);
            for (SCROW nRow = nRowStart; nRow <= nRowEnd; ++nRow)
            {

                if (aDecimalPlaces)
                    aVals.push_back(rtl::math::round(distribution(seed), *aDecimalPlaces));
                else
                    aVals.push_back(distribution(seed));
            }

            pDocShell->GetDocFunc().SetValueCells(aPos, aVals, true);
        }
    }

    pUndoManager->LeaveListAction();

    pDocShell->PostPaint( maInputRange, PaintPartFlags::Grid );
}

IMPL_LINK_NOARG( ScRandomNumberGeneratorDialog, OkClicked, weld::Button&, void )
{
    ApplyClicked(*mxButtonApply);
    CloseClicked(*mxButtonClose);
}

IMPL_LINK_NOARG( ScRandomNumberGeneratorDialog, ApplyClicked, weld::Button&, void )
{
    SelectGeneratorAndGenerateNumbers();
}

IMPL_LINK_NOARG( ScRandomNumberGeneratorDialog, CloseClicked, weld::Button&, void )
{
    response(RET_CLOSE);
}

IMPL_LINK_NOARG(ScRandomNumberGeneratorDialog, GetEditFocusHandler, formula::RefEdit&, void)
{
    mxInputRangeEdit->SelectAll();
}

IMPL_LINK_NOARG(ScRandomNumberGeneratorDialog, GetButtonFocusHandler, formula::RefButton&, void)
{
    mxInputRangeEdit->SelectAll();
}

IMPL_LINK_NOARG(ScRandomNumberGeneratorDialog, LoseEditFocusHandler, formula::RefEdit&, void)
{
    mbDialogLostFocus = !m_xDialog->has_toplevel_focus();
}

IMPL_LINK_NOARG(ScRandomNumberGeneratorDialog, LoseButtonFocusHandler, formula::RefButton&, void)
{
    mbDialogLostFocus = !m_xDialog->has_toplevel_focus();
}

IMPL_LINK_NOARG(ScRandomNumberGeneratorDialog, InputRangeModified, formula::RefEdit&, void)
{
    ScRangeList aRangeList;
    bool bValid = ParseWithNames( aRangeList, mxInputRangeEdit->GetText(), mrDoc);
    const ScRange* pRange = (bValid && aRangeList.size() == 1) ? &aRangeList[0] : nullptr;
    if (pRange)
    {
        maInputRange = *pRange;
        mxButtonApply->set_sensitive(true);
        mxButtonOk->set_sensitive(true);
        // Highlight the resulting range.
        mxInputRangeEdit->StartUpdateData();
    }
    else
    {
        maInputRange = ScRange( ScAddress::INITIALIZE_INVALID);
        mxButtonApply->set_sensitive(false);
        mxButtonOk->set_sensitive(false);
    }
}

IMPL_LINK_NOARG(ScRandomNumberGeneratorDialog, Parameter1ValueModified, weld::SpinButton&, void)
{
    sal_Int64 aSelectedId = mxDistributionCombo->get_active_id().toInt64();
    if (aSelectedId == DIST_UNIFORM ||
        aSelectedId == DIST_UNIFORM_INTEGER)
    {
        sal_Int64 min = mxParameter1Value->get_value();
        sal_Int64 max = mxParameter2Value->get_value();
        if(min > max)
        {
            mxParameter2Value->set_value(min);
        }
    }
}

IMPL_LINK_NOARG(ScRandomNumberGeneratorDialog, Parameter2ValueModified, weld::SpinButton&, void)
{
    sal_Int64 aSelectedId = mxDistributionCombo->get_active_id().toInt64();
    if (aSelectedId == DIST_UNIFORM ||
        aSelectedId == DIST_UNIFORM_INTEGER)
    {
        sal_Int64 min = mxParameter1Value->get_value();
        sal_Int64 max = mxParameter2Value->get_value();
        if(min > max)
        {
            mxParameter1Value->set_value(max);
        }
    }
}

IMPL_LINK_NOARG(ScRandomNumberGeneratorDialog, CheckChanged, weld::Toggleable&, void)
{
    mxSeed->set_sensitive(mxEnableSeed->get_active());
    mxDecimalPlaces->set_sensitive(mxEnableRounding->get_active());
}

IMPL_LINK_NOARG(ScRandomNumberGeneratorDialog, DistributionChanged, weld::ComboBox&, void)
{
    sal_Int64 aSelectedId = mxDistributionCombo->get_active_id().toInt64();

    mxParameter1Value->set_range(SAL_MIN_INT32, SAL_MAX_INT32);
    mxParameter2Value->set_range(SAL_MIN_INT32, SAL_MAX_INT32);

    mxParameter1Value->set_digits(DIGITS);
    mxParameter1Value->set_increments(PRECISION, PRECISION * 10);

    mxParameter2Value->set_digits(DIGITS);
    mxParameter2Value->set_increments(PRECISION, PRECISION * 10);

    switch(aSelectedId)
    {
        case DIST_UNIFORM:
        {
            mxParameter1Text->set_label(ScResId(STR_RNG_PARAMETER_MINIMUM));
            mxParameter2Text->set_label(ScResId(STR_RNG_PARAMETER_MAXIMUM));
            mxParameter2Text->show();
            mxParameter2Value->show();
            break;
        }
        case DIST_UNIFORM_INTEGER:
        {
            mxParameter1Text->set_label(ScResId(STR_RNG_PARAMETER_MINIMUM));
            mxParameter1Value->set_digits(0);
            mxParameter1Value->set_increments(1, 10);

            mxParameter2Text->set_label(ScResId(STR_RNG_PARAMETER_MAXIMUM));
            mxParameter2Value->set_digits(0);
            mxParameter2Value->set_increments(1, 10);

            mxParameter2Text->show();
            mxParameter2Value->show();
            break;
        }
        case DIST_NORMAL:
        {
            mxParameter1Text->set_label(ScResId(STR_RNG_PARAMETER_MEAN));
            mxParameter2Text->set_label(ScResId(STR_RNG_PARAMETER_STANDARD_DEVIATION));
            mxParameter2Text->show();
            mxParameter2Value->show();
            break;
        }
        case DIST_CAUCHY:
        {
            mxParameter1Text->set_label(ScResId(STR_RNG_PARAMETER_STANDARD_MEDIAN));
            mxParameter2Text->set_label(ScResId(STR_RNG_PARAMETER_STANDARD_SIGMA));
            mxParameter2Text->show();
            mxParameter2Value->show();
            break;
        }
        case DIST_BERNOULLI:
        case DIST_GEOMETRIC:
        {
            mxParameter1Text->set_label(ScResId(STR_RNG_PARAMETER_STANDARD_PROBABILITY));
            mxParameter1Value->set_range(0, PRECISION);
            mxParameter1Value->set_increments(1000, 10000);

            mxParameter2Text->hide();
            mxParameter2Value->hide();
            break;
        }
        case DIST_BINOMIAL:
        case DIST_NEGATIVE_BINOMIAL:
        {
            mxParameter1Text->set_label(ScResId(STR_RNG_PARAMETER_STANDARD_PROBABILITY));
            mxParameter1Value->set_range(0, PRECISION);
            mxParameter1Value->set_increments(1000, 10000);

            mxParameter2Text->set_label(ScResId(STR_RNG_PARAMETER_STANDARD_NUMBER_OF_TRIALS));
            mxParameter2Value->set_digits(0);
            mxParameter2Value->set_increments(1, 10);
            mxParameter2Value->set_min(0);

            mxParameter2Text->show();
            mxParameter2Value->show();
            break;
        }
        case DIST_CHI_SQUARED:
        {
            mxParameter1Text->set_label(ScResId(STR_RNG_PARAMETER_STANDARD_NU_VALUE));
            // tdf#165846 - ensure degrees of freedom (nu value) is greater than 0
            mxParameter1Value->set_min(1);
            mxParameter1Value->set_value(PRECISION);

            mxParameter2Text->hide();
            mxParameter2Value->hide();
            break;
        }
        case DIST_POISSON:
        {
            mxParameter1Text->set_label(ScResId(STR_RNG_PARAMETER_MEAN));
            mxParameter1Value->set_value(PRECISION);
            mxParameter1Value->set_increments(1000, 10000);
            mxParameter1Value->set_min(1000);
            mxParameter2Text->hide();
            mxParameter2Value->hide();
            break;
        }
    }
}

/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
