//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      GUI/Model/Tune/FitParameterContainerItem.cpp
//! @brief     Implements class FitParameterContainerItem.
//!
//! @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/Model/Tune/FitParameterContainerItem.h"
#include "Base/Util/Assert.h"
#include "Fit/Param/Parameters.h"
#include "GUI/Model/Par/ParameterTreeItems.h"
#include "GUI/Model/Util/UtilXML.h"

namespace {
namespace Tag {

const QString FitParameter("FitParameter");

} // namespace Tag
} // namespace

FitParameterContainerItem::FitParameterContainerItem(QObject* parent)
    : QObject(parent)
{
    setObjectName("FitParameterContainer");
}

void FitParameterContainerItem::createFitParameter(ParameterItem* parameterItem)
{
    ASSERT(parameterItem);

    // prevents from making empty fit params without links
    FitParameterItem* oldFitPar = oldFitParameterItemToBeRemoved(parameterItem);
    if (!oldFitPar)
        removeLink(parameterItem);

    auto* fitPar = createBareFitParameterItem();

    fitPar->setStartValue(parameterItem->valueOfLink());
    fitPar->initMinMaxValues(parameterItem->limitsOfLink());
    fitPar->addLinkItem(parameterItem->titleForFitItem(), parameterItem->link());

    if (oldFitPar)
        removeFitParameter(oldFitPar);

    emit fitItemChanged();
}

void FitParameterContainerItem::removeFitParameter(FitParameterItem* fitPar)
{
    m_fit_parameter_items.delete_element(fitPar);
    updateFitParameterNames();
}

void FitParameterContainerItem::addToFitParameter(ParameterItem* parameterItem,
                                                  const QString& fitParName)
{
    ASSERT(parameterItem);

    // prevents from making empty fit params without links
    FitParameterItem* oldFitPar = oldFitParameterItemToBeRemoved(parameterItem);
    // prevents from moving tree parameter into its own fit parameter
    if (oldFitPar && oldFitPar->displayName() == fitParName)
        return;

    if (!oldFitPar)
        removeLink(parameterItem);

    for (FitParameterItem* fitPar : fitParameterItems()) {
        if (fitPar->displayName() == fitParName) {
            fitPar->addLinkItem(parameterItem->titleForFitItem(), parameterItem->link());
            break;
        }
    }

    if (oldFitPar)
        removeFitParameter(oldFitPar);

    emit fitItemChanged();
}

//! Returns FitParameterItem for given link (path in model)

FitParameterItem* FitParameterContainerItem::fitParameterItem(const QString& link) const
{
    for (FitParameterItem* item : fitParameterItems())
        if (item->links().contains(link))
            return item;

    return nullptr;
}

FitParameterItem*
FitParameterContainerItem::fitParameterItem(const ParameterItem* parameterItem) const
{
    return fitParameterItem(parameterItem->link());
}

QVector<FitParameterItem*> FitParameterContainerItem::fitParameterItems() const
{
    return {m_fit_parameter_items.begin(), m_fit_parameter_items.end()};
}

QStringList FitParameterContainerItem::fitParameterNames() const
{
    QStringList result;
    for (FitParameterItem* item : fitParameterItems())
        result.append(item->displayName());
    return result;
}

void FitParameterContainerItem::removeLink(const ParameterItem* parameterItem) const
{
    if (FitParameterItem* fitParItem = fitParameterItem(parameterItem))
        fitParItem->removeLink(parameterItem->link());
}

FitParameterItem* FitParameterContainerItem::createBareFitParameterItem()
{
    auto* fitPar = new FitParameterItem(this);
    m_fit_parameter_items.push_back(fitPar);
    updateFitParameterNames();
    return fitPar;
}

bool FitParameterContainerItem::isEmpty() const
{
    return fitParameterItems().isEmpty();
}

//! Propagate values to the corresponding parameter tree items of parameterContainer.

void FitParameterContainerItem::setValuesInParameterContainer(
    const std::vector<double>& values, ParameterContainerItem* parameterContainer) const
{
    ASSERT(parameterContainer);

    int index(0);
    for (auto* fitPar : fitParameterItems()) {
        auto link_list = fitPar->linkItems();
        if (link_list.empty())
            continue;
        for (FitParameterLinkItem* linkItem : link_list)
            if (auto* itemInTuningTree = parameterContainer->findParameterItem(linkItem->link()))
                itemInTuningTree->propagateValueToLink(values[index]);

        index++;
    }
}

void FitParameterContainerItem::pullValuesFromParameterContainer(
    ParameterContainerItem* parameterContainer)
{
    ASSERT(parameterContainer);

    for (auto* fitPar : fitParameterItems()) {
        auto link_list = fitPar->linkItems();
        if (!link_list.empty()) {
            FitParameterLinkItem* linkItem = link_list.last(); // update from last added link
            if (auto* itemInTuningTree = parameterContainer->findParameterItem(linkItem->link()))
                fitPar->setStartValue(itemInTuningTree->valueOfLink());
        }
    }
}

mumufit::Parameters FitParameterContainerItem::createParameters() const
{
    mumufit::Parameters result;

    int index(0);
    for (auto* fitPar : fitParameterItems()) {
        if (!fitPar->isValid())
            throw std::runtime_error(
                QString("FitParameterContainerItem::createParameters(): invalid starting value "
                        "or (min, max) range in fitting parameter par %1")
                    .arg(index)
                    .toLatin1());
        double startValue = fitPar->startValue();
        AttLimits limits = fitPar->attLimits();
        QString name = QString("par%1").arg(index);
        result.add(mumufit::Parameter(name.toStdString(), startValue, limits));
        ++index;
    }

    return result;
}

void FitParameterContainerItem::writeTo(QXmlStreamWriter* w) const
{
    XML::writeAttribute(w, XML::Attrib::version, uint(1));

    // fit parameters
    for (const auto* fitPar : m_fit_parameter_items) {
        w->writeStartElement(Tag::FitParameter);
        XML::writeAttribute(w, XML::Attrib::name, fitPar->displayName());
        fitPar->writeTo(w);
        w->writeEndElement();
    }
}

void FitParameterContainerItem::readFrom(QXmlStreamReader* r)
{
    const uint version = XML::readUIntAttribute(r, XML::Attrib::version);
    Q_UNUSED(version)

    while (r->readNextStartElement()) {
        QString tag = r->name().toString();

        // fit parameter
        if (tag == Tag::FitParameter) {
            createBareFitParameterItem()->readFrom(r);
            XML::gotoEndElementOfTag(r, tag);

        } else
            r->skipCurrentElement();
    }
}

FitParameterItem*
FitParameterContainerItem::oldFitParameterItemToBeRemoved(ParameterItem* parameterItem) const
{
    if (auto* fitPar = fitParameterItem(parameterItem))
        if (fitPar->linkItems().size() == 1)
            return fitPar;
    return nullptr;
}

void FitParameterContainerItem::updateFitParameterNames()
{
    int size = fitParameterItems().size();
    for (int i = 0; i < size; i++) {
        FitParameterItem* item = fitParameterItems()[i];
        if (size > 1)
            item->setDisplayName("par" + QString::number(i));
        else
            item->setDisplayName("par");
    }
}
