//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      GUI/View/Fit/FitSessionController.cpp
//! @brief     Implements class FitSessionController.
//!
//! @homepage  http://www.bornagainproject.org
//! @license   GNU General Public License v3 or higher (see COPYING)
//! @copyright Forschungszentrum Jülich GmbH 2018
//! @authors   Scientific Computing Group at MLZ (see CITATION, AUTHORS)
//
//  ************************************************************************************************

#include "GUI/View/Fit/FitSessionController.h"
#include "Base/Util/Assert.h"
#include "GUI/Model/Data/Data2DItem.h"
#include "GUI/Model/Job/BatchInfo.h"
#include "GUI/Model/Job/JobItem.h"
#include "GUI/Model/Job/JobStatus.h"
#include "GUI/Model/Tune/FitParameterContainerItem.h"
#include "GUI/Model/Tune/FitSuiteItem.h"
#include "GUI/View/FitControl/FitObjectiveBuilder.h"
#include "GUI/View/FitControl/FitWorkerLauncher.h"
#include "GUI/View/FitControl/GUIFitObserver.h"
#include "GUI/View/FitMessage/FitLog.h"

namespace {

const bool use_fit_objective = true;
} // namespace

FitSessionController::FitSessionController(QObject* parent)
    : QObject(parent)
    , m_job_item(nullptr)
    , m_run_fit_manager(new FitWorkerLauncher(this))
    , m_observer(new GUIFitObserver)
    , m_fitlog(new FitLog(this))
    , m_block_progress_update(false)
{
    connect(m_observer.get(), &GUIFitObserver::updateReady, this,
            &FitSessionController::onObserverUpdate);

    connect(m_run_fit_manager, &FitWorkerLauncher::fittingStarted, this,
            &FitSessionController::onFittingStarted);
    connect(m_run_fit_manager, &FitWorkerLauncher::fittingFinished, this,
            &FitSessionController::onFittingFinished);
    connect(m_run_fit_manager, &FitWorkerLauncher::fittingError, this,
            &FitSessionController::onFittingError);
}

FitSessionController::~FitSessionController() = default;

void FitSessionController::setJobItem(JobItem* jobItem)
{
    if (m_job_item && m_job_item != jobItem)
        throw std::runtime_error("JobItem was already set");

    m_job_item = jobItem;
    ASSERT(m_job_item);

    // no need to unsubscribe from jobItem on jobItem destroy. FitSessionManager deletes
    // controller right after the jobItem.

    // Propagates update interval from FitSuiteItem to fit observer.
    connect(m_job_item->fitSuiteItem(), &FitSuiteItem::updateIntervalChanged, m_observer.get(),
            &GUIFitObserver::setInterval, Qt::UniqueConnection);
}

void FitSessionController::onStartFittingRequest()
{
    if (!m_job_item)
        return;

    try {
        m_objective_builder = std::make_unique<FitObjectiveBuilder>(m_job_item);
        m_observer->setInterval(m_job_item->fitSuiteItem()->updateInterval());
        m_objective_builder->attachObserver(m_observer);
        m_observer->finishedPlotting();
        m_run_fit_manager->runFitting(m_objective_builder);
    } catch (std::exception& e) {
        m_job_item->setFailed();
        m_fitlog->append(e.what(), FitLogLevel::Error);
        emit fittingError(QString::fromStdString(e.what()));
    }
}

FitLog* FitSessionController::fitLog()
{
    return m_fitlog.get();
}

void FitSessionController::onStopFittingRequest()
{
    m_run_fit_manager->interruptFitting();
}

void FitSessionController::onObserverUpdate()
{
    auto progressInfo = m_observer->progressInfo();
    m_job_item->simulatedDataItem()->setRawDataVector(progressInfo.simValues());

    updateIterationCount(progressInfo);

    if (!use_fit_objective)
        updateFitParameterValues(progressInfo);

    updateLog(progressInfo);

    if (!progressInfo.logInfo().empty())
        m_fitlog->append(progressInfo.logInfo(), FitLogLevel::Default);

    m_observer->finishedPlotting();
}

void FitSessionController::onFittingStarted()
{
    m_fitlog->clearLog();

    m_job_item->batchInfo()->setStatus(JobStatus::Fitting);
    m_job_item->batchInfo()->setProgress(0);
    m_job_item->batchInfo()->setBeginTime(m_run_fit_manager->fitStart());
    m_job_item->batchInfo()->setEndTime(QDateTime());

    emit fittingStarted();
}

void FitSessionController::onFittingFinished()
{
    if (!isFailed(m_job_item->batchInfo()->status()))
        m_job_item->batchInfo()->setStatus(JobStatus::Completed);
    m_job_item->batchInfo()->setEndTime(m_run_fit_manager->fitEnd());
    m_job_item->batchInfo()->setProgress(100);

    if (isCompleted(m_job_item->batchInfo()->status()))
        m_fitlog->append("Done", FitLogLevel::Success);

    // TODO: notify Fit2DFrame/Fit1DFrame in a proper way by fittingFinished();
    emit fittingFinished();
    emit m_job_item->simulatedDataItem()->datafieldChanged();
}

void FitSessionController::onFittingError(const QString& text)
{
    QString message;
    message.append("Current settings cause fitting failure.\n\n");
    message.append(text);
    m_fitlog->append(message.toStdString(), FitLogLevel::Error);
    m_job_item->batchInfo()->setEndTime(m_run_fit_manager->fitEnd());

    emit fittingError(message);
}

void FitSessionController::updateIterationCount(const FitProgressInfo& info)
{
    FitSuiteItem* fitSuiteItem = m_job_item->fitSuiteItem();
    // FIXME ProgressCanvas updates chi2 and n_iteration on P_ITERATION_COUNT change
    // The order of two lines below is important
    fitSuiteItem->setChi2(info.chi2());
    fitSuiteItem->setIterationCount(info.iterationCount());
}

void FitSessionController::updateFitParameterValues(const FitProgressInfo& info)
{
    FitParameterContainerItem* fitParContainer =
        m_job_item->fitSuiteItem()->fitParameterContainerItem();
    fitParContainer->setValuesInParameterContainer(info.parValues(),
                                                   m_job_item->parameterContainerItem());
}

void FitSessionController::updateStartValuesFromTree()
{
    FitParameterContainerItem* fitParContainer =
        m_job_item->fitSuiteItem()->fitParameterContainerItem();
    fitParContainer->pullValuesFromParameterContainer(m_job_item->parameterContainerItem());
}

void FitSessionController::updateLog(const FitProgressInfo& info)
{
    QString message = QString("NCalls:%1 chi2:%2 \n").arg(info.iterationCount()).arg(info.chi2());
    FitParameterContainerItem* fitParContainer =
        m_job_item->fitSuiteItem()->fitParameterContainerItem();
    int index(0);
    for (FitParameterItem* item : fitParContainer->fitParameterItems()) {
        if (item->linkItems().empty())
            continue;
        QString parinfo =
            QString("      %1 %2\n").arg(item->displayName()).arg(info.parValues()[index++]);
        message.append(parinfo);
    }
    m_fitlog->append(message.toStdString(), FitLogLevel::Default);
}
