//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      GUI/View/Sample/SampleEditorController.cpp
//! @brief     Implements class SampleEditorController.
//!
//! @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/View/Sample/SampleEditorController.h"
#include "Base/Util/Assert.h"
#include "Base/Util/Vec.h"
#include "GUI/Model/Material/MaterialsSet.h"
#include "GUI/Model/Project/ProjectDocument.h"
#include "GUI/Model/Sample/CompoundItem.h"
#include "GUI/Model/Sample/CoreAndShellItem.h"
#include "GUI/Model/Sample/InterferenceItems.h"
#include "GUI/Model/Sample/LayerItem.h"
#include "GUI/Model/Sample/LayerStackItem.h"
#include "GUI/Model/Sample/MesocrystalItem.h"
#include "GUI/Model/Sample/ParticleItem.h"
#include "GUI/Model/Sample/ParticleLayoutItem.h"
#include "GUI/Model/Sample/SampleItem.h"
#include "GUI/Model/Util/Backup.h"
#include "GUI/View/Sample/CompoundForm.h"
#include "GUI/View/Sample/CoreAndShellForm.h"
#include "GUI/View/Sample/InterferenceForm.h"
#include "GUI/View/Sample/LatticeTypeSelectionForm.h"
#include "GUI/View/Sample/LayerForm.h"
#include "GUI/View/Sample/LayerStackForm.h"
#include "GUI/View/Sample/MaterialInplaceForm.h"
#include "GUI/View/Sample/MesocrystalForm.h"
#include "GUI/View/Sample/ParticleLayoutForm.h"
#include "GUI/View/Sample/SampleForm.h"
#include <QLabel>

SampleEditorController::SampleEditorController(SampleItem* multi)
    : m_sample_item(multi)
    , m_sample_form(nullptr)
{
}

void SampleEditorController::setSampleForm(SampleForm* view)
{
    m_sample_form = view;
}

void SampleEditorController::addLayerItem(LayerStackForm& parentStackForm,
                                          const ItemWithLayers* before)
{
    LayerStackItem& parentStack = parentStackForm.stackItem();
    const int at_index = parentStack.indexOfComponent(before) + 1;
    QColor color = findColor(at_index); // before adding layer
    LayerItem* new_layer = m_sample_item->createLayerItemAt(parentStack, at_index);
    new_layer->setMaterial(materialModel()->defaultMaterialItem());
    new_layer->setColor(color);
    onComponentAdded(new_layer);
}

void SampleEditorController::addLayerStackItem(LayerStackForm& parentStackForm,
                                               const ItemWithLayers* before)
{
    LayerStackItem& parentStack = parentStackForm.stackItem();
    const int at_index = parentStack.indexOfComponent(before) + 1;
    LayerStackItem* new_stack = m_sample_item->createLayerStackItemAt(parentStack, at_index);
    onComponentAdded(new_stack);
}

void SampleEditorController::duplicateItemWithLayers(const ItemWithLayers* component)
{
    LayerStackItem* parentStack = m_sample_item->parentOfComponent(component);

    // case of the outer stack
    if (!parentStack)
        return;

    LayerStackForm* parentStackForm = m_sample_form->formOfStackItem(parentStack);
    ASSERT(parentStackForm);

    const int at_index = parentStack->indexOfComponent(component) + 1;

    if (const auto* layer = dynamic_cast<const LayerItem*>(component)) {
        QColor color = findColor(at_index); // before adding layer
        LayerItem* new_layer = m_sample_item->createLayerItemAt(*parentStack, at_index);
        new_layer->setMaterial(materialModel()->defaultMaterialItem());
        GUI::Util::copyContents(layer, new_layer);
        new_layer->setColor(color);
        onComponentAdded(new_layer);

    } else if (const auto* stack = dynamic_cast<const LayerStackItem*>(component)) {
        LayerStackItem* new_stack = m_sample_item->createLayerStackItemAt(*parentStack, at_index);
        GUI::Util::copyContents(stack, new_stack);
        onComponentAdded(new_stack);

    } else
        ASSERT_NEVER;
}

void SampleEditorController::onComponentAdded(ItemWithLayers* component)
{
    LayerStackItem* parentStack = m_sample_item->parentOfComponent(component);
    ASSERT(parentStack);

    LayerStackForm* parentStackForm = m_sample_form->formOfStackItem(parentStack);
    ASSERT(parentStackForm);

    parentStackForm->addComponentForm(component);
    m_sample_form->updateRowVisibilities();
    emit gDoc->sampleChanged();
}

QColor SampleEditorController::findColor(size_t atIndex)
{
    std::vector<LayerItem*> uniqueLayerItems = m_sample_item->uniqueLayerItems();

    QColor result;
    auto unusedColors = GUI::Util::Layer::predefinedLayerColors();
    for (auto* l : uniqueLayerItems)
        unusedColors.removeAll(l->color());
    if (!unusedColors.isEmpty())
        result = unusedColors.first();
    else {
        // search for a color which has been used the less, and which is not the same as in the
        // layers above and below
        QMap<QString, int> usage;
        for (auto* l : uniqueLayerItems)
            usage[l->color().name()] += 1;

        auto sortedByUsage = GUI::Util::Layer::predefinedLayerColors();
        std::stable_sort(
            sortedByUsage.begin(), sortedByUsage.end(),
            [&](const QColor& a, const QColor& b) { return usage[a.name()] < usage[b.name()]; });

        const QColor above = (atIndex > 0) ? uniqueLayerItems[atIndex - 1]->color() : QColor();
        const QColor below =
            (atIndex < uniqueLayerItems.size()) ? uniqueLayerItems[atIndex]->color() : QColor();

        for (const auto& col : sortedByUsage)
            if (col != above && col != below) {
                result = col;
                break;
            }
    }
    return result;
}

void SampleEditorController::removeItemWithLayers(ItemWithLayers* component)
{
    LayerStackItem* parentStack = m_sample_item->parentOfComponent(component);

    // case of the outer stack
    if (!parentStack)
        return;

    LayerStackForm* parentStackForm = m_sample_form->formOfStackItem(parentStack);
    ASSERT(parentStackForm);

    emit aboutToRemoveItem(component);
    parentStackForm->removeComponentForm(component);
    m_sample_item->removeComponent(component);
    m_sample_form->updateRowVisibilities();
    emit gDoc->sampleChanged();
}

void SampleEditorController::onLayoutAdded(LayerForm* layerForm, ParticleLayoutItem* layout)
{
    layerForm->onLayoutAdded(layout);

    for (auto* layoutForms : layerForm->findChildren<ParticleLayoutForm*>())
        layoutForms->updateTitle(layerForm->layerItem());

    emit gDoc->sampleChanged();
}

void SampleEditorController::addLayoutItem(LayerForm* layerForm)
{
    auto* newLayout = layerForm->layerItem()->addLayoutItem();
    onLayoutAdded(layerForm, newLayout);
}

void SampleEditorController::duplicateLayoutItem(LayerForm* layerForm, ParticleLayoutItem* layout)
{
    auto* newLayout = layerForm->layerItem()->addLayoutItem();
    GUI::Util::copyContents(layout, newLayout);
    onLayoutAdded(layerForm, newLayout);
}

void SampleEditorController::removeLayoutItem(LayerForm* layerForm, ParticleLayoutItem* layout)
{
    emit aboutToRemoveItem(layout);
    layerForm->onAboutToRemoveLayout(layout);
    layerForm->layerItem()->removeLayoutItem(layout);

    for (auto* layoutForm : layerForm->findChildren<ParticleLayoutForm*>())
        layoutForm->updateTitle(layerForm->layerItem());

    emit gDoc->sampleChanged();
}

void SampleEditorController::onParticleLayoutAdded(ParticleLayoutItem* layout,
                                                   ItemWithParticles* newItem)
{
    emit gDoc->sampleChanged();

    //  search for particle layout widget for notification
    ASSERT(m_sample_form);
    for (auto* w : m_sample_form->findChildren<ParticleLayoutForm*>())
        if (w->layoutItem() == layout)
            w->onParticleAdded(newItem);
}

void SampleEditorController::addParticleLayoutItem(ParticleLayoutItem* layout,
                                                   FormfactorCatalog::Type formFactorType)
{
    auto* newParticle = createAndInitItem(formFactorType);
    layout->addItemWithParticleSelection(newParticle);
    onParticleLayoutAdded(layout, newParticle);
}

void SampleEditorController::addParticleLayoutItem(ParticleLayoutItem* layout,
                                                   ParticleCatalog::Type type)
{
    auto* newItem = createAndInitItem(type);
    layout->addItemWithParticleSelection(newItem);
    onParticleLayoutAdded(layout, newItem);
}

void SampleEditorController::duplicateItemWithParticles(ItemWithParticles* item)
{
    auto type = ParticleCatalog::type(item);
    auto* newItem = createAndInitItem(type);
    GUI::Util::copyContents(item, newItem);

    if (ParticleLayoutItem* parent_layout = parentLayoutItem(item)) {
        parent_layout->addItemWithParticleSelection(newItem);
        onParticleLayoutAdded(parent_layout, newItem);
    } else if (CompoundItem* parent_compound = parentCompoundItem(item)) {
        parent_compound->addItemWithParticleSelection(newItem);
        onParticleCompoundAdded(parent_compound, newItem);
    } else
        ASSERT_NEVER;
}

void SampleEditorController::onParticleCompoundAdded(CompoundItem* composition,
                                                     ItemWithParticles* newItem)
{
    emit gDoc->sampleChanged();

    //  search for composition widget for notification
    ASSERT(m_sample_form);
    for (auto* c : m_sample_form->findChildren<CompoundForm*>())
        if (c->compositionItem() == composition)
            c->onParticleAdded(newItem);
}

void SampleEditorController::addCompoundItem(CompoundItem* compositionItem,
                                             ParticleCatalog::Type type)
{
    auto* newItem = createAndInitItem(type);
    compositionItem->addItemWithParticleSelection(newItem);
    onParticleCompoundAdded(compositionItem, newItem);
}

void SampleEditorController::addCompoundItem(CompoundItem* compositionItem,
                                             FormfactorCatalog::Type formFactorType)
{
    auto* newParticle = createAndInitItem(formFactorType);
    compositionItem->addItemWithParticleSelection(newParticle);
    onParticleCompoundAdded(compositionItem, newParticle);
}

ItemWithParticles*
SampleEditorController::createAndInitItem(FormfactorCatalog::Type formFactorType) const
{
    auto* newParticle = new ParticleItem(materialModel());
    newParticle->setFormfactor(FormfactorCatalog::create(formFactorType));
    newParticle->setMaterial(materialModel()->defaultParticleMaterialItem());
    return newParticle;
}

ItemWithParticles* SampleEditorController::createAndInitItem(ParticleCatalog::Type type) const
{
    auto* newItem = ParticleCatalog::create(type, materialModel());

    if (auto* p = dynamic_cast<ItemWithMaterial*>(newItem))
        p->setMaterial(materialModel()->defaultMaterialItem());

    if (auto* cs = dynamic_cast<CoreAndShellItem*>(newItem)) {
        cs->createCoreItem(materialModel());
        cs->createShellItem(materialModel());
        cs->coreItem()->setFormfactor(new CylinderItem());
        cs->shellItem()->setFormfactor(new CylinderItem());
    }

    if (auto* meso = dynamic_cast<MesocrystalItem*>(newItem); meso && meso->basisItem())
        if (auto* p = dynamic_cast<ItemWithMaterial*>(meso->basisItem()))
            p->setMaterial(materialModel()->defaultMaterialItem());

    return newItem;
}

void SampleEditorController::setCoreFormfactor(CoreAndShellForm* widget,
                                               FormfactorCatalog::Type type)
{
    auto* particleCoreShell = widget->coreShellItem();

    if (particleCoreShell->coreItem() == nullptr)
        particleCoreShell->createCoreItem(materialModel());

    particleCoreShell->coreItem()->setFormfactor(FormfactorCatalog::create(type));
    widget->createCoreWidgets();
    emit gDoc->sampleChanged();
}

void SampleEditorController::setShellFormfactor(CoreAndShellForm* widget,
                                                FormfactorCatalog::Type type)
{
    auto* particleCoreShell = widget->coreShellItem();

    if (particleCoreShell->shellItem() == nullptr)
        particleCoreShell->createShellItem(materialModel());

    particleCoreShell->shellItem()->setFormfactor(FormfactorCatalog::create(type));
    widget->createShellWidgets();
    emit gDoc->sampleChanged();
}

ParticleLayoutItem* SampleEditorController::parentLayoutItem(ItemWithParticles* item)
{
    for (auto* layoutForm : m_sample_form->findChildren<ParticleLayoutForm*>())
        if (Vec::containsPtr(item, layoutForm->layoutItem()->itemsWithParticles()))
            return layoutForm->layoutItem();
    return nullptr;
}

CompoundItem* SampleEditorController::parentCompoundItem(ItemWithParticles* item)
{
    for (auto* compoundForm : m_sample_form->findChildren<CompoundForm*>())
        if (Vec::containsPtr(item, compoundForm->compositionItem()->itemsWithParticles()))
            return compoundForm->compositionItem();
    return nullptr;
}

void SampleEditorController::removeParticle(ItemWithParticles* itemToRemove)
{
    ASSERT(m_sample_form);

    for (auto* layoutForm : m_sample_form->findChildren<ParticleLayoutForm*>())
        if (Vec::containsPtr(itemToRemove, layoutForm->layoutItem()->itemsWithParticles())) {
            layoutForm->onAboutToRemoveParticle(itemToRemove);

            emit aboutToRemoveItem(itemToRemove);
            layoutForm->layoutItem()->removeItemWithParticle(itemToRemove);
            emit gDoc->sampleChanged();
            return;
        }

    for (auto* c : m_sample_form->findChildren<CompoundForm*>())
        if (Vec::containsPtr(itemToRemove, c->compositionItem()->itemsWithParticles())) {
            c->onAboutToRemoveParticle(itemToRemove);

            emit aboutToRemoveItem(itemToRemove);
            c->compositionItem()->removeItemWithParticle(itemToRemove);
            emit gDoc->sampleChanged();
            return;
        }
}

MaterialsSet* SampleEditorController::materialModel() const
{
    return &m_sample_item->materialModel();
}

void SampleEditorController::selectMaterial(ItemWithMaterial* item,
                                            const QString& newMaterialIdentifier)
{
    item->setMaterial(newMaterialIdentifier);

    //  update Layer title
    ASSERT(m_sample_form);
    for (auto* c : m_sample_form->findChildren<LayerForm*>())
        if (c->layerItem() == item)
            c->updateTitle();

    // #baLayerEditor notify all material users (update link info)
    emit gDoc->sampleChanged();
}

void SampleEditorController::setMaterialValue(ItemWithMaterial* item, double value,
                                              DoubleProperty& d)
{
    d.setDVal(value);
    emit gDoc->sampleChanged();

    // -- notify all other users of this material (update values in the UI)
    ASSERT(m_sample_form);
    for (auto* c : m_sample_form->findChildren<MaterialInplaceForm*>())
        if (c->itemWithMaterial() != item
            && c->itemWithMaterial()->materialIdentifier() == item->materialIdentifier())
            c->updateValues();
    emit gDoc->sampleChanged();
}


//! notify the containing particle layout UI about changed value
void SampleEditorController::setDensityRelatedValue(InterferenceItem* interferenceItem)
{
    ASSERT(m_sample_form);
    for (auto* c : m_sample_form->findChildren<ParticleLayoutForm*>())
        if (c->layoutItem()->interferenceSelection().certainItem() == interferenceItem) {
            c->updateDensityValue();
            break;
        }
}

void SampleEditorController::onStoppedToMoveComponent(QWidget* widgetToMove,
                                                      QWidget* moveAboveThisWidget)
{
    auto* formToMove = dynamic_cast<LayerContainerForm*>(widgetToMove);
    ASSERT(formToMove);
    auto* itemToMove = formToMove->item();

    LayerStackItem* parentStack = m_sample_item->parentOfComponent(itemToMove);
    ASSERT(parentStack);

    LayerStackForm* parentStackForm = m_sample_form->formOfStackItem(parentStack);
    ASSERT(parentStackForm);

    const LayerContainerForm* moveAboveThisLayerForm =
        parentStackForm->findNextComponentForm(moveAboveThisWidget);

    auto* moveAboveThisItem = moveAboveThisLayerForm ? moveAboveThisLayerForm->item() : nullptr;

    m_sample_item->moveComponent(itemToMove, moveAboveThisItem);
    parentStackForm->onComponentMoved(itemToMove);
    m_sample_form->updateRowVisibilities();

    emit gDoc->sampleChanged();
}

void SampleEditorController::setMesocrystalBasis(MesocrystalForm* widget,
                                                 ParticleCatalog::Type type)
{
    auto* meso = widget->mesocrystalItem();
    meso->setBasisItem(createAndInitItem(type));
    widget->createBasisWidgets();
    emit gDoc->sampleChanged();
}

void SampleEditorController::setMesocrystalBasis(MesocrystalForm* widget,
                                                 FormfactorCatalog::Type type)
{
    auto* meso = widget->mesocrystalItem();
    meso->setBasisItem(createAndInitItem(type));
    widget->createBasisWidgets();
    emit gDoc->sampleChanged();
}

void SampleEditorController::selectInterference(InterferenceForm* widget, int newIndex)
{
    widget->layoutItem()->interferenceSelection().setCertainIndex(newIndex);
    widget->onInterferenceTypeChanged();

    // Disable/enable total density property in the particle layout, depending on type of
    // interference function.
    QWidget* parent = widget->parentWidget();
    while (parent != nullptr && dynamic_cast<ParticleLayoutForm*>(parent) == nullptr)
        parent = parent->parentWidget();

    if (auto* particleLayoutForm = dynamic_cast<ParticleLayoutForm*>(parent)) {
        particleLayoutForm->updateDensityEnabling();
        particleLayoutForm->updateDensityValue();
    }

    emit gDoc->sampleChanged();
}
