//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      GUI/Model/Sample/SampleItem.cpp
//! @brief     Implements class SampleItem.
//!
//! @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/Sample/SampleItem.h"
#include "Base/Util/Vec.h"
#include "GUI/Model/Sample/LayerItem.h"
#include "GUI/Model/Sample/LayerStackItem.h"
#include "GUI/Model/Sample/ParticleLayoutItem.h"
#include "GUI/Model/ToCore/SampleToCore.h"
#include "GUI/Model/Type/PredefinedColors.h"
#include "GUI/Model/Util/Backup.h"
#include "GUI/Model/Util/UtilXML.h"
#include <random>
#include <set>

namespace {
namespace Tag {

const QString Name("Name");
const QString Description("Description");
const QString MaterialsSet("MaterialsSet");
const QString OuterStack("OuterStack");
const QString ExternalField("ExternalField");
const QString ExpandInfoGroupbox("ExpandInfoGroupbox");
const QString ShowRoughness("ShowRoughness");

} // namespace Tag
} // namespace

SampleItem::SampleItem()
    : NamedItem("Sample")
    , m_outer_stack(std::make_unique<LayerStackItem>(&m_materials, 1))
{
    m_external_field.init("External field", "A/m", "External field (A/m)", "extField");
}

SampleItem::~SampleItem() = default;

SampleItem* SampleItem::clone() const
{
    auto* result = new SampleItem;
    GUI::Util::copyContents(this, result);
    return result;
}

std::vector<ItemWithMaterial*> SampleItem::itemsWithMaterial() const
{
    std::vector<ItemWithMaterial*> result;
    std::vector<LayerItem*> layers = uniqueLayerItems();
    for (LayerItem* l : layers)
        Vec::concat(result, l->itemsWithMaterial());
    return result;
}

void SampleItem::addStandardMaterials()
{
    // add only non-existing materials
    QString name = materialMap.key(DefaultMaterials::Default);
    if (!m_materials.materialItemFromName(name))
        m_materials.addRefractiveMaterialItem(name, 1e-3, 1e-5);

    name = materialMap.key(DefaultMaterials::Vacuum);
    if (!m_materials.materialItemFromName(name))
        m_materials.addRefractiveMaterialItem(name, 0.0, 0.0);

    name = materialMap.key(DefaultMaterials::Particle);
    if (!m_materials.materialItemFromName(name))
        m_materials.addRefractiveMaterialItem(name, 6e-4, 2e-8);

    name = materialMap.key(DefaultMaterials::Core);
    if (!m_materials.materialItemFromName(name))
        m_materials.addRefractiveMaterialItem(name, 2e-4, 1e-8);

    name = materialMap.key(DefaultMaterials::Substrate);
    if (!m_materials.materialItemFromName(name))
        m_materials.addRefractiveMaterialItem(name, 6e-6, 2e-8);
}

void SampleItem::setOuterStackItem(LayerStackItem* outer_stack)
{
    m_outer_stack.reset(outer_stack); // gets ownership
    updateTopBottom();
}

std::vector<LayerItem*> SampleItem::uniqueLayerItems() const
{
    return m_outer_stack->uniqueLayerItems();
}

std::vector<ItemWithLayers*> SampleItem::componentItems() const
{
    return m_outer_stack->componentItems();
}

int SampleItem::indexOfComponent(const ItemWithLayers* item) const
{
    return m_outer_stack->indexOfComponent(item);
}

LayerStackItem* SampleItem::parentOfComponent(const ItemWithLayers* searchedItem)
{
    return m_outer_stack->parentOfComponent(searchedItem);
}

LayerItem* SampleItem::createLayerItemAt(LayerStackItem& parentStack, int index)
{
    LayerItem* layer = parentStack.createLayerItemAt(index);
    updateTopBottom();
    return layer;
}

LayerStackItem* SampleItem::createLayerStackItemAt(LayerStackItem& parentStack, int index)
{
    return parentStack.createLayerStackItemAt(index);
}

void SampleItem::updateTopBottom()
{
    std::vector<ItemWithLayers*> non_empty_outer_components = componentItems();
    // erase empty stacks
    for (int i = non_empty_outer_components.size() - 1; i >= 0; i--) {
        if (const auto* stack = dynamic_cast<const LayerStackItem*>(non_empty_outer_components[i]))
            if (stack->uniqueLayerItems().empty() || (stack->numberOfPeriods() == 0))
                non_empty_outer_components.erase(non_empty_outer_components.begin() + i);
    }

    std::vector<LayerItem*> layers = uniqueLayerItems();
    if (layers.empty())
        return;

    // Only layers, belonging to the outer stack can be considered as ambient or substrate
    auto* ambient = dynamic_cast<LayerItem*>(non_empty_outer_components.front());
    auto* substrate = dynamic_cast<LayerItem*>(non_empty_outer_components.back());

    for (LayerItem* l : layers) {
        l->setIsAmbient(l == ambient);
        l->setIsSubstrate(l == substrate);
    }
}

void SampleItem::removeComponent(const ItemWithLayers* component)
{
    LayerStackItem* containingStack = parentOfComponent(component);
    if (!containingStack)
        return;

    containingStack->removeComponent(component);
    updateTopBottom();
}

void SampleItem::moveComponent(ItemWithLayers* component, ItemWithLayers* aboveThisComponent)
{
    LayerStackItem* containingStack = parentOfComponent(component);
    ASSERT(containingStack);

    containingStack->moveComponent(component, aboveThisComponent);
    updateTopBottom();
}

void SampleItem::writeTo(QXmlStreamWriter* w) const
{
    XML::writeTaggedValue(w, Tag::Name, name());
    XML::writeTaggedValue(w, Tag::Description, description());
    XML::writeTaggedElement(w, Tag::ExternalField, m_external_field);
    XML::writeTaggedElement(w, Tag::MaterialsSet, m_materials);
    XML::writeTaggedElement(w, Tag::OuterStack, *m_outer_stack);
    XML::writeTaggedValue(w, Tag::ExpandInfoGroupbox, expandInfo);
    XML::writeTaggedValue(w, Tag::ShowRoughness, showRoughness);
}

void SampleItem::readFrom(QXmlStreamReader* r)
{
    while (r->readNextStartElement()) {
        QString tag = r->name().toString();
        if (tag == Tag::Name)
            setName(XML::readTaggedString(r, tag));
        else if (tag == Tag::Description)
            setDescription(XML::readTaggedString(r, tag));
        else if (tag == Tag::ExternalField)
            XML::readTaggedElement(r, tag, m_external_field);
        else if (tag == Tag::MaterialsSet)
            XML::readTaggedElement(r, tag, m_materials);
        else if (tag == Tag::OuterStack)
            XML::readTaggedElement(r, tag, *m_outer_stack);
        else if (tag == Tag::ExpandInfoGroupbox)
            expandInfo = XML::readTaggedBool(r, tag);
        else if (tag == Tag::ShowRoughness)
            showRoughness = XML::readTaggedBool(r, tag);
        else
            r->skipCurrentElement();
    }
    updateTopBottom();
}

void SampleItem::updateDefaultLayerColors()
{
    const auto& colors = GUI::Colors::layerDefaults();

    int col = 0;
    std::vector<LayerItem*> layers = uniqueLayerItems();
    for (auto* l : layers) {
        if (!l->color().isValid())
            l->setColor(colors[col]);
        col = (col + 1) % colors.size();
    }
}

void SampleItem::adjustLayerSeeds(bool update_all) const
{
    unsigned rng_seed =
        static_cast<unsigned>(std::chrono::system_clock::now().time_since_epoch().count());
    std::mt19937 gen(rng_seed);
    std::uniform_int_distribution<int> distr(0, std::numeric_limits<int>::max());

    std::vector<int> old_seeds = seeds;

    std::vector<LayerItem*> layers = m_outer_stack->unwrappedLayerItems();
    if (old_seeds.size() != layers.size()) {
        update_all = true;
        seeds.clear();
        seeds.resize(layers.size(), 0);
    }
    std::set<int> used_seeds;

    const int i_bottom = layers.size() - 1;

    seeds[i_bottom] = update_all ? distr(gen) : old_seeds[i_bottom];
    used_seeds.insert(seeds[i_bottom]);

    for (int i = i_bottom - 1; i >= 0; i--) {
        const RoughnessItem* roughness = layers[i]->roughnessSelection().certainItem();
        const bool hasCrosscorr = roughness
                                  && (roughness->certainCrosscorrModel()
                                      || dynamic_cast<const LinearGrowthRoughnessItem*>(roughness));

        // preliminary assign already existing seed
        seeds[i] = update_all ? 0 : old_seeds[i];

        // change the seed to the proper one if needed
        if (hasCrosscorr)
            seeds[i] = seeds[i + 1];
        else if (update_all || used_seeds.count(seeds[i])) {
            seeds[i] = distr(gen);
            used_seeds.insert(seeds[i]);
        }
    }
}

void SampleItem::adjustLayoutSeeds() const
{
    std::vector<LayerItem*> layers = m_outer_stack->unwrappedLayerItems();
    for (const auto* lr : layers)
        for (const auto* layout : lr->layoutItems())
            layout->updateSeed();
}
