//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      GUI/Model/Par/ParameterTreeItems.cpp
//! @brief     Implements classes ParameterLabelItem, ParameterItem, ParameterContainerItem.
//!
//! @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/Par/ParameterTreeItems.h"
#include "Base/Util/Assert.h"
#include "GUI/Model/Util/UtilXML.h"
#include <memory>
#include <utility>

namespace {
namespace Tag {

const QString BackupValues("BackupValues"); // moved to ParameterBackup class since v22.0
const QString BackupValue("BackupValue");   // moved to ParameterBackup class since v22.0
const QString Backups("Backups");
const QString Backup("Backup");
const QString ParameterLabels("ParameterLabels");
const QString ParameterLabel("ParameterLabel");
const QString CurrentIndex("CurrentIndex");

} // namespace Tag

ParameterItem* findParameterItem(QObject* item, const QString& link)
{
    ASSERT(item);
    if (auto* parameter = dynamic_cast<ParameterItem*>(item))
        if (parameter->link() == link)
            return parameter;

    for (auto* child : item->children())
        if (auto* p = findParameterItem(child, link))
            return p;

    return nullptr;
}

} // namespace

//  ************************************************************************************************
//  class ParameterLabelItem
//  ************************************************************************************************

ParameterLabelItem::ParameterLabelItem(QString title, QObject* parent)
    : QObject(parent)
    , m_title(std::move(title))
{
}

void ParameterLabelItem::writeTo(QXmlStreamWriter* w) const
{
    w->writeStartElement(Tag::ParameterLabel);
    XML::writeAttribute(w, XML::Attrib::name, m_title);
    XML::writeAttribute(w, XML::Attrib::collapsed, m_collapsed);
    XML::writeAttribute(w, XML::Attrib::version, uint(1));
    w->writeEndElement();
}

void ParameterLabelItem::readFrom(QXmlStreamReader* r)
{
    while (r->readNextStartElement()) {
        QString tag = r->name().toString();

        if (tag == Tag::ParameterLabel) {
            const uint version = XML::readUIntAttribute(r, XML::Attrib::version);
            Q_UNUSED(version)

            QString title;
            XML::readAttribute(r, XML::Attrib::name, &title);
            if (title == m_title)
                XML::readAttribute(r, XML::Attrib::collapsed, &m_collapsed);
            XML::gotoEndElementOfTag(r, tag);
            break;
        }
    }
}

//  ************************************************************************************************
//  class ParameterItem
//  ************************************************************************************************

ParameterItem::ParameterItem(QObject* parent)
    : QObject(parent)
{
}

double ParameterItem::valueOfLink() const
{
    return getPropertyValue();
}

//! Sets current value to the original PropertyItem of SampleItem/InstrumentItem.

void ParameterItem::propagateValueToLink(double newValue)
{
    setPropertyValue(newValue);
}

void ParameterItem::linkToProperty(DoubleProperty& d)
{
    getPropertyValue = [&d] { return d.value(); };
    setPropertyValue = [&d](double v) { d.setValue(v); };
    m_d = &d; // make a copy
}

RealLimits ParameterItem::limitsOfLink() const
{
    return m_d->limits();
}

int ParameterItem::decimalsOfLink() const
{
    return m_d->decimals();
}

QString ParameterItem::link() const
{
    return m_d->uid();
}

QString ParameterItem::titleForFitItem() const
{
    QString result = m_title;

    auto* p = parent();
    while (p) {
        if (const auto* pLabel = dynamic_cast<ParameterLabelItem*>(p))
            result.push_front(pLabel->title() + "/");
        p = p->parent();
    }

    return result;
}

//  ************************************************************************************************
//  class ParameterContainerItem
//  ************************************************************************************************

ParameterContainerItem::ParameterContainerItem()
{
    m_parameter_tree_root = std::make_unique<QObject>();
}

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

    // selected index
    w->writeStartElement(Tag::CurrentIndex);
    XML::writeAttribute(w, XML::Attrib::value, m_current_index);
    w->writeEndElement();

    // backups
    w->writeStartElement(Tag::Backups);
    for (const auto& backup : m_backup_values) {
        w->writeStartElement(Tag::Backup);
        backup->writeTo(w);
        w->writeEndElement();
    }
    w->writeEndElement();

    // labels
    w->writeStartElement(Tag::ParameterLabels);
    QVector<ParameterLabelItem*> list = m_parameter_tree_root->findChildren<ParameterLabelItem*>();
    for (const auto& item : list)
        item->writeTo(w);
    w->writeEndElement();
}

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

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

        // selected index
        if (tag == Tag::CurrentIndex) {
            XML::readAttribute(r, XML::Attrib::value, &m_current_index);
            XML::gotoEndElementOfTag(r, tag);

            // backups
        } else if (tag == Tag::Backups) {
            m_backup_values.clear();
            while (r->readNextStartElement()) {
                QString backupTag = r->name().toString();

                if (backupTag == Tag::Backup) {
                    m_backup_values.push_back(new ParameterBackup);
                    m_backup_values.back()->readFrom(r);
                    XML::gotoEndElementOfTag(r, backupTag);

                } else
                    throw std::runtime_error("Cannot read parameter tree backups.");
            }
            XML::gotoEndElementOfTag(r, tag);

            // old backup: compatibility with pre-22.0
        } else if (version == 1 && tag == Tag::BackupValues) {
            ASSERT(m_backup_values.empty());
            addBackupValues("Backup values from pre-22.0");
            m_backup_values.back()->readValues(r);
            XML::gotoEndElementOfTag(r, tag);

            // labels
        } else if (tag == Tag::ParameterLabels) {
            auto list = m_parameter_tree_root->findChildren<ParameterLabelItem*>();
            for (const auto& item : list)
                item->readFrom(r);
            XML::gotoEndElementOfTag(r, tag);

        } else
            r->skipCurrentElement();
    }
}

QStringList ParameterContainerItem::backupTitles() const
{
    QStringList result;
    for (const auto& b : m_backup_values)
        result.append(b->title());
    return result;
}

void ParameterContainerItem::addBackupValue(QObject* item)
{
    ASSERT(item);
    if (auto* parameter = dynamic_cast<ParameterItem*>(item))
        m_backup_values.back()->setValue(parameter->link(), parameter->valueOfLink());

    for (auto* child : item->children())
        addBackupValue(child);
}

void ParameterContainerItem::addBackupValues(const QString& title)
{
    m_backup_values.push_back(new ParameterBackup(title));
    for (auto* child : m_parameter_tree_root->children())
        addBackupValue(child);
}

void ParameterContainerItem::restoreBackupValue(QObject* item, int index)
{
    if (index < 0 || index > (int)m_backup_values.size())
        return;

    ASSERT(item);
    if (auto* parameter = dynamic_cast<ParameterItem*>(item))
        if (m_backup_values.at(index)->contains(parameter->link()))
            parameter->propagateValueToLink(m_backup_values.at(index)->value(parameter->link()));

    for (auto* child : item->children())
        restoreBackupValue(child, index);
}

void ParameterContainerItem::restoreBackupValues(int index)
{
    for (auto* child : m_parameter_tree_root->children())
        restoreBackupValue(child, index);
}

void ParameterContainerItem::deleteBackupValues(int index)
{
    m_backup_values.release_at(index);
}

ParameterItem* ParameterContainerItem::findParameterItem(const QString& link) const
{
    return ::findParameterItem(m_parameter_tree_root.get(), link);
}

QObject* ParameterContainerItem::parameterTreeRoot()
{
    return m_parameter_tree_root.get();
}
