//  ************************************************************************************************
//
//  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/IntensityDataItem.h"
#include "GUI/Model/Job/FitParameterContainerItem.h"
#include "GUI/Model/Job/FitSuiteItem.h"
#include "GUI/Model/Job/JobItem.h"
#include "GUI/View/Fit/FitLog.h"
#include "GUI/View/Fit/FitObjectiveBuilder.h"
#include "GUI/View/Fit/FitWorkerLauncher.h"
#include "GUI/View/Fit/GUIFitObserver.h"

namespace {

const bool use_fit_objective = true;
}

FitSessionController::FitSessionController(QObject* parent)
    : QObject(parent)
    , m_jobItem(nullptr)
    , m_runFitManager(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_runFitManager, &FitWorkerLauncher::fittingStarted, this,
            &FitSessionController::onFittingStarted);
    connect(m_runFitManager, &FitWorkerLauncher::fittingFinished, this,
            &FitSessionController::onFittingFinished);
    connect(m_runFitManager, &FitWorkerLauncher::fittingError, this,
            &FitSessionController::onFittingError);
}

FitSessionController::~FitSessionController() = default;

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

    m_jobItem = jobItem;
    ASSERT(m_jobItem);

    // 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_jobItem->fitSuiteItem(), &FitSuiteItem::updateIntervalChanged, m_observer.get(),
            &GUIFitObserver::setInterval, Qt::UniqueConnection);
}

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

    try {
        m_objectiveBuilder = std::make_unique<FitObjectiveBuilder>(m_jobItem);
        m_observer->setInterval(m_jobItem->fitSuiteItem()->updateInterval());
        m_objectiveBuilder->attachObserver(m_observer);
        m_observer->finishedPlotting();
        m_runFitManager->runFitting(m_objectiveBuilder);
    } catch (std::exception& e) {
        m_jobItem->setStatus(JobStatus::Failed);
        m_fitlog->append(e.what(), FitLogLevel::Error);
        emit fittingError(QString::fromStdString(e.what()));
    }
}

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

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

void FitSessionController::onObserverUpdate()
{
    auto progressInfo = m_observer->progressInfo();
    m_jobItem->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_jobItem->setStatus(JobStatus::Fitting);
    m_jobItem->setProgress(0);
    m_jobItem->setBeginTime(m_runFitManager->fitStart());
    m_jobItem->setEndTime(QDateTime());

    emit fittingStarted();
}

void FitSessionController::onFittingFinished()
{
    if (m_jobItem->status() != JobStatus::Failed)
        m_jobItem->setStatus(JobStatus::Completed);
    m_jobItem->setEndTime(m_runFitManager->fitEnd());
    m_jobItem->setProgress(100);

    if (m_jobItem->isCompleted())
        m_fitlog->append("Done", FitLogLevel::Success);

    // TODO: notify FitComparisonWidget/FitComparisonWidget1D in a proper way by fittingFinished();
    // emit fittingFinished();
    emit m_jobItem->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_jobItem->setEndTime(m_runFitManager->fitEnd());

    emit fittingError(message);
}

void FitSessionController::updateIterationCount(const FitProgressInfo& info)
{
    FitSuiteItem* fitSuiteItem = m_jobItem->fitSuiteItem();
    // FIXME FitFlowWidget 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_jobItem->fitParameterContainerItem();
    fitParContainer->setValuesInParameterContainer(info.parValues(),
                                                   m_jobItem->parameterContainerItem());
}

void FitSessionController::updateLog(const FitProgressInfo& info)
{
    QString message = QString("NCalls:%1 chi2:%2 \n").arg(info.iterationCount()).arg(info.chi2());
    FitParameterContainerItem* fitParContainer = m_jobItem->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);
}
