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

#include "GUI/Model/Model/FitParameterModel.h"
#include "Base/Util/Assert.h"
#include "GUI/Model/Job/FitParameterContainerItem.h"
#include "GUI/Model/Job/JobItem.h"
#include "GUI/Model/Model/FitParameterHelper.h"
#include <QColor>
#include <QMimeData>

FitParameterModel::FitParameterModel(FitParameterContainerItem* fitParContainer, JobItem* jobItem)
    : QAbstractItemModel()
    , m_fitParameterContainer(fitParContainer)
    , m_jobItem(jobItem)
{
    addColumn(COL_NAME, "Name", "Name of fit parameter");
    addColumn(COL_TYPE, "Type", "Fit parameter limits type");
    addColumn(COL_VALUE, "Value", "Starting value of fit parameter");
    addColumn(COL_MIN, "Min", "Lower bound on fit parameter value");
    addColumn(COL_MAX, "Max", "Upper bound on fit parameter value");

    connectContainer();
    connect(fitParContainer, &QObject::destroyed, this,
            [this]() { m_fitParameterContainer = nullptr; });
    connect(jobItem, &QObject::destroyed, this, [this]() { m_jobItem = nullptr; });
}

Qt::ItemFlags FitParameterModel::flags(const QModelIndex& index) const
{
    if (!m_fitParameterContainer)
        return Qt::NoItemFlags;

    Qt::ItemFlags result = Qt::ItemIsEnabled | Qt::ItemIsSelectable;
    if (QObject* item = itemForIndex(index)) {

        bool isEditable = true;
        if (auto* enablableItem = dynamic_cast<FitEditableDoubleItem*>(item))
            isEditable = enablableItem->isEnabled();

        if (isEditable && index.column() != COL_NAME)
            result |= Qt::ItemIsEditable;

        if (dynamic_cast<FitParameterLinkItem*>(item->parent()) && index.column() == COL_NAME)
            result |= Qt::ItemIsDragEnabled;
        const bool allow_one_fit_parameter_to_have_more_than_one_link = true;
        if (allow_one_fit_parameter_to_have_more_than_one_link) {
            // drop is allowed to fit parameter container, and, to FitParameterItem itself.
            // (i.e. we can have more than one link in single FitParameterItem)
            if (dynamic_cast<FitParameterItem*>(item)
                || dynamic_cast<FitParameterContainerItem*>(item)) {
                result |= Qt::ItemIsDropEnabled;
            }
        } else {
            // drop is allowed only to fit parameter container
            // (i.e. only one link is allowed in FitParameterItem)
            if (dynamic_cast<FitParameterContainerItem*>(item))
                result |= Qt::ItemIsDropEnabled;
        }
    }
    return result;
}

QModelIndex FitParameterModel::index(int row, int column, const QModelIndex& parent) const
{
    if (!m_fitParameterContainer || row < 0 || column < 0 || column >= columnCount(QModelIndex())
        || (parent.isValid() && parent.column() != COL_NAME))
        return QModelIndex();

    QObject* parent_item = itemForIndex(parent);
    ASSERT(parent_item);

    if (dynamic_cast<FitParameterContainerItem*>(parent_item)) {
        QVector<FitParameterItem*> fitParamItems = m_fitParameterContainer->fitParameterItems();
        if (row < fitParamItems.size()) {
            FitParameterItem* fitParItem = fitParamItems[row];
            QObject* itemToPack = nullptr;
            switch (column) {
            case COL_NAME:
                itemToPack = fitParItem;
                break;
            case COL_TYPE:
                itemToPack = fitParItem->typeItem();
                break;
            case COL_VALUE:
                itemToPack = fitParItem->startValueItem();
                break;
            case COL_MIN:
                itemToPack = fitParItem->minimumItem();
                break;
            case COL_MAX:
                itemToPack = fitParItem->maximumItem();
                break;
            default:
                itemToPack = nullptr;
            }
            return createIndex(row, column, itemToPack);
        }
    } else if (column == COL_NAME && dynamic_cast<FitParameterItem*>(parent_item)) {
        auto* fitPar = dynamic_cast<FitParameterItem*>(parent_item);
        QVector<FitParameterLinkItem*> links = fitPar->linkItems();
        if (row < links.size())
            if (FitParameterLinkItem* linkItem = links.at(row))
                return createIndex(row, column, linkItem->linkItem());
    }
    return QModelIndex();
}

QModelIndex FitParameterModel::parent(const QModelIndex& child) const
{
    if (!m_fitParameterContainer)
        return QModelIndex();

    if (!child.isValid())
        return QModelIndex();

    if (QObject* child_item = itemForIndex(child)) {
        if (QObject* parent_item = child_item->parent()) {
            if (auto* fitLinkItem = dynamic_cast<FitParameterLinkItem*>(parent_item)) {
                auto* fitPar = dynamic_cast<FitParameterItem*>(fitLinkItem->parent());
                ASSERT(fitPar);
                int row = m_fitParameterContainer->fitParameterItems().indexOf(fitPar);
                return createIndex(row, 0, fitPar);
            }
        }
    }
    return QModelIndex();
}

int FitParameterModel::rowCount(const QModelIndex& parent) const
{
    if (!m_fitParameterContainer)
        return 0;

    if (parent.isValid() && parent.column() != COL_NAME)
        return 0;

    QObject* parent_item = itemForIndex(parent);

    if (dynamic_cast<FitParameterContainerItem*>(parent_item))
        return m_fitParameterContainer->fitParameterItems().size();

    if (auto* fitPar = dynamic_cast<FitParameterItem*>(parent_item))
        return fitPar->linkItems().size();
    return 0;
}

int FitParameterModel::columnCount(const QModelIndex& parent) const
{
    if (!m_fitParameterContainer)
        return 0;

    if (parent.isValid() && parent.column() != COL_NAME)
        return 0;

    if (!parent.isValid())
        return NUM_COLUMNS;

    if (parent.isValid())
        if (QObject* parentItem = itemForIndex(parent))
            if (auto* fitPar = dynamic_cast<FitParameterItem*>(parentItem))
                return !fitPar->linkItems().empty();
    return 0;
}

QVariant FitParameterModel::data(const QModelIndex& index, int role) const
{
    if (!m_fitParameterContainer)
        return QVariant();

    if (!index.isValid() || index.column() < 0 || index.column() >= NUM_COLUMNS)
        return QVariant();

    if (QObject* item = itemForIndex(index)) {
        if (role == Qt::DisplayRole || role == Qt::EditRole) {
            if (auto* fitPar = dynamic_cast<FitParameterItem*>(item))
                return fitPar->displayName();
            if (auto* linkItem = dynamic_cast<FitParameterLinkItem*>(item->parent()))
                return linkItem->title();
            return valueOfItem(item);
        }

        bool isEditable = true;
        if (auto* enablableItem = dynamic_cast<FitEditableDoubleItem*>(item))
            isEditable = enablableItem->isEnabled();
        if (role == Qt::ForegroundRole && !isEditable)
            return QVariant(QColor(Qt::gray));
        if (role == Qt::ToolTipRole && dynamic_cast<FitParameterLinkItem*>(item))
            return valueOfItem(item);
    }
    return QVariant();
}

bool FitParameterModel::setData(const QModelIndex& index, const QVariant& value, int role)
{
    if (!m_fitParameterContainer)
        return false;

    if (!index.isValid())
        return false;
    if (QObject* item = itemForIndex(index)) {
        if (role == Qt::EditRole) {
            if (auto* fitParItem = dynamic_cast<FitParameterItem*>(item->parent())) {
                if (index.column() == COL_TYPE)
                    fitParItem->setTypeCombo(value.value<ComboProperty>());
                else if (index.column() == COL_VALUE)
                    fitParItem->setStartValue(value.toDouble());
                else if (index.column() == COL_MIN)
                    fitParItem->setMinimum(value.toDouble());
                else if (index.column() == COL_MAX)
                    fitParItem->setMaximum(value.toDouble());
            } else
                setValueOfItem(item, value);
            emit dataChanged(index, index);
            return true;
        }
    }
    return false;
}

QStringList FitParameterModel::mimeTypes() const
{
    QStringList types;
    types << XML::LinkMimeType;
    return types;
}

QMimeData* FitParameterModel::mimeData(const QModelIndexList& indexes) const
{
    auto* mimeData = new QMimeData();
    QModelIndex index = indexes.first();
    if (index.isValid()) {
        QObject* item = static_cast<QObject*>(index.internalPointer());
        if (auto* linkItem = dynamic_cast<LinkItem*>(item)) {
            QString path = linkItem->link();
            ASSERT(m_jobItem);
            auto* parameterItem = FitParameterHelper::getParameterItem(m_jobItem, path);
            QByteArray data;
            data.setNum(reinterpret_cast<qlonglong>(parameterItem));
            mimeData->setData(XML::LinkMimeType, data);
        }
    }
    return mimeData;
}

bool FitParameterModel::canDropMimeData(const QMimeData* data, Qt::DropAction action, int row,
                                        int column, const QModelIndex& parent) const
{
    Q_UNUSED(data);
    Q_UNUSED(action);
    Q_UNUSED(row);
    bool drop_is_possible(false);
    if (parent.isValid())
        drop_is_possible = true;
    if (!parent.isValid() && row == -1 && column == -1)
        drop_is_possible = true;
    return drop_is_possible;
}

bool FitParameterModel::dropMimeData(const QMimeData* data, Qt::DropAction action, int row,
                                     int column, const QModelIndex& parent)
{
    Q_UNUSED(action);
    Q_UNUSED(row);
    Q_UNUSED(column);

    if (parent.isValid()) {
        QObject* parentItem = static_cast<QObject*>(parent.internalPointer());
        if (auto* fitPar = dynamic_cast<FitParameterItem*>(parentItem)) {
            ASSERT(fitPar);
            ParameterItem* parItem =
                reinterpret_cast<ParameterItem*>(data->data(XML::LinkMimeType).toULongLong());
            ASSERT(parItem);
            m_fitParameterContainer->addToFitParameter(parItem, fitPar->displayName());
        }
    } else {
        ParameterItem* parItem =
            reinterpret_cast<ParameterItem*>(data->data(XML::LinkMimeType).toULongLong());
        ASSERT(parItem);
        m_fitParameterContainer->createFitParameter(parItem);
    }
    return true;
}

QVariant FitParameterModel::headerData(int section, Qt::Orientation orientation, int role) const
{
    if (role == Qt::DisplayRole && orientation == Qt::Horizontal)
        return m_columnNames.value(section);
    if (role == Qt::ToolTipRole)
        return m_columnToolTips.value(section);
    return QVariant();
}

void FitParameterModel::onFitItemChanged()
{
    if (!m_fitParameterContainer)
        return;
    beginResetModel();
    endResetModel();
}

void FitParameterModel::connectContainer() const
{
    if (!m_fitParameterContainer)
        return;

    connect(m_fitParameterContainer, &FitParameterContainerItem::fitItemChanged, this,
            &FitParameterModel::onFitItemChanged, Qt::UniqueConnection);
}

void FitParameterModel::addColumn(FitParameterModel::EColumn id, const QString& name,
                                  const QString& tooltip)
{
    m_columnNames[id] = name;
    m_columnToolTips[id] = tooltip;
}

QModelIndex FitParameterModel::indexOfItem(QObject* item) const
{
    if (!m_fitParameterContainer)
        return QModelIndex();

    ASSERT(item);

    if (QObject* parent_item = item->parent()) {
        if (dynamic_cast<FitParameterContainerItem*>(parent_item)) {
            if (auto* fitPar = dynamic_cast<FitParameterItem*>(item)) {
                int row = m_fitParameterContainer->fitParameterItems().indexOf(fitPar);
                return createIndex(row, 0, fitPar);
            }
        } else if (auto* fitParam = dynamic_cast<FitParameterItem*>(parent_item)) {
            std::optional<int> col;
            if (item == fitParam->typeItem())
                col = COL_TYPE;
            else if (item == fitParam->startValueItem())
                col = COL_VALUE;
            else if (item == fitParam->minimumItem())
                col = COL_MIN;
            else if (item == fitParam->maximumItem())
                col = COL_MAX;
            if (col) {
                int row = m_fitParameterContainer->fitParameterItems().indexOf(fitParam);
                return createIndex(row, col.value(), item);
            }
        } else if (auto* fitLink = dynamic_cast<FitParameterLinkItem*>(parent_item)) {
            auto* fitPar = dynamic_cast<FitParameterItem*>(fitLink->parent());
            int index = fitPar->linkItems().indexOf(fitLink);
            return createIndex(index, 0, item);
        }
    }
    return QModelIndex();
}

QObject* FitParameterModel::itemForIndex(const QModelIndex& index) const
{
    if (!m_fitParameterContainer)
        return nullptr;

    if (index.isValid())
        return static_cast<QObject*>(index.internalPointer());

    return m_fitParameterContainer;
}

QVariant FitParameterModel::valueOfItem(QObject* item) const
{
    if (auto* type = dynamic_cast<FitTypeItem*>(item))
        return type->type().variant();
    if (auto* doubleVal = dynamic_cast<FitDoubleItem*>(item))
        return doubleVal->value();
    if (auto* link = dynamic_cast<LinkItem*>(item))
        return link->link();

    return QVariant();
}

void FitParameterModel::setValueOfItem(QObject* item, const QVariant& value)
{
    if (auto* type = dynamic_cast<FitTypeItem*>(item))
        type->setType(value.value<ComboProperty>());
    if (auto* doubleVal = dynamic_cast<FitDoubleItem*>(item))
        doubleVal->setValue(value.toDouble());
    if (auto* link = dynamic_cast<LinkItem*>(item))
        link->setLink(value.toString());
}
