//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      GUI/Model/Device/RealItem.cpp
//! @brief     Implements class RealItem
//!
//! @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/Device/RealItem.h"
#include "Base/Axis/Scale.h"
#include "Device/Coord/ICoordSystem.h"
#include "Device/Data/DataUtil.h"
#include "Device/Data/Datafield.h"
#include "GUI/Model/Axis/AmplitudeAxisItem.h"
#include "GUI/Model/Data/DataItemUtil.h"
#include "GUI/Model/Data/IntensityDataItem.h"
#include "GUI/Model/Data/ProjectionItems.h"
#include "GUI/Model/Data/SpecularDataItem.h"
#include "GUI/Model/Device/InstrumentItems.h"
#include "GUI/Support/Util/ItemFileNameUtil.h"
#include "GUI/Support/XML/Backup.h"
#include "GUI/Support/XML/DeserializationException.h"

namespace {

namespace Tag {

const QString InstrumentId("InstrumentId");
const QString Data("Data");
const QString Name("Name");
const QString NativeData("NativeData");
const QString BinaryData("BinaryData");
const QString NativeDataUnits("NativeDataUnits");
const QString PresentationType("PresentationType");

} // namespace Tag

//! Creates and inserts a data item except if such item with same tag already exists.
//! Checks for rank compatibility if already existing.
//! No further initialization (like clearing the data etc).

void initDataItem(const std::size_t rank, std::unique_ptr<DataItem>& dataItem)
{
    // TODO: initDataItem can be replaced by a template which accepts only
    // SpecularDataItem or IntensityDataItem.

    ASSERT(rank == 1 || rank == 2);

    if (dataItem) {
        if ((rank == 1 && !dynamic_cast<const SpecularDataItem*>(dataItem.get()))
            || (rank == 2 && !dynamic_cast<const IntensityDataItem*>(dataItem.get())))
            throw std::runtime_error("Error in RealItem::initDataItem: trying to set data "
                                     "incompatible with underlying data item");
    } else {
        if (rank == 1)
            dataItem = std::make_unique<SpecularDataItem>();
        else // if rank == 2
            dataItem = std::make_unique<IntensityDataItem>();
        ASSERT(dataItem
               && "Assertion failed in RealItem::initDataItem: inserting data item failed.");
    }
}

} // namespace


RealItem::RealItem() = default;

RealItem::~RealItem() = default;

QString RealItem::realItemName() const
{
    return m_name;
}

void RealItem::setRealItemName(const QString& name)
{
    m_name = name;
    updateDataFileName();
}

QString RealItem::presentationType() const
{
    return m_presentationType;
}

void RealItem::setPresentationType(const QString& type)
{
    m_presentationType = type;
}

DataItem* RealItem::dataItem() const
{
    return m_dataItem.get();
}

IntensityDataItem* RealItem::intensityDataItem() const
{
    return dynamic_cast<IntensityDataItem*>(dataItem());
}

SpecularDataItem* RealItem::specularDataItem() const
{
    return dynamic_cast<SpecularDataItem*>(dataItem());
}

void RealItem::initItemWithRank(int rank)
{
    initDataItem(rank, m_dataItem);
}

bool RealItem::isIntensityData() const
{
    return intensityDataItem() != nullptr;
}

bool RealItem::isSpecularData() const
{
    return specularDataItem() != nullptr;
}

bool RealItem::hasNativeData() const
{
    return (nativeDataItem() != nullptr) && (nativeDataItem()->c_field() != nullptr);
}

DataItem* RealItem::nativeDataItem() const
{
    return m_nativeDataItem.get();
}

DataItem* RealItem::initNativeData()
{
    const size_t rank = isSpecularData() ? 1 : 2;
    initDataItem(rank, m_nativeDataItem);
    if (dataItem()->c_field())
        nativeDataItem()->setDatafield(dataItem()->c_field()->clone());
    updateDataFileName();
    return m_nativeDataItem.get();
}

void RealItem::removeNativeData()
{
    ASSERT(isSpecularData()); // not implemented for intensityDataItem

    if (nativeDataItem() != nullptr)
        nativeDataItem()->setDatafield(nullptr);
}

QString RealItem::nativeDataUnits() const
{
    return m_nativeDataUnits;
}

void RealItem::setNativeDataUnits(const QString& units)
{
    m_nativeDataUnits = units;
}

bool RealItem::holdsDimensionalData() const
{
    return nativeDataUnits() != "nbins";
}

const Datafield* RealItem::nativeDatafield() const
{
    return hasNativeData() ? nativeDataItem()->c_field() : nullptr;
}

//! takes ownership of data

void RealItem::setNativeDatafield(Datafield* data)
{
    nativeDataItem()->setDatafield(data); // takes ownership of odata
}

//! Sets Datafield to underlying item. Creates it if not existing.

void RealItem::setDatafield(Datafield* data)
{
    ASSERT(data && "Assertion failed in RealItem::setDatafield: passed data is nullptr");

    initDataItem(data->rank(), m_dataItem);

    dataItem()->setDatafield(data);
}

void RealItem::setNativeFileName(const QString& filename)
{
    m_nativeFileName = filename;
    updateDataFileName();
}

QString RealItem::nativeFileName() const
{
    return m_nativeFileName;
}

QString RealItem::instrumentId() const
{
    return m_instrumentId;
}

void RealItem::setInstrumentId(const QString& id)
{
    m_instrumentId = id;
}

void RealItem::linkToInstrument(const InstrumentItem* instrument)
{
    if (instrument) {
        m_instrumentId = instrument->id();
        updateToInstrument(instrument);
    } else
        unlinkFromInstrument();
}

void RealItem::unlinkFromInstrument()
{
    m_instrumentId = QString{};
    updateToInstrument(nullptr);
}

bool RealItem::rotationAffectsSetup() const
{
    if (!isIntensityData()) // rotation only for 2D items possible
        return false;

    const bool hasLinkToInstrument = !instrumentId().isEmpty();
    if (hasLinkToInstrument)
        return true;

    if (intensityDataItem()->hasMasks())
        return true;

    if (intensityDataItem()->hasProjections())
        return true;

    return false;
}

void RealItem::rotateData()
{
    if (!isIntensityData()) // rotation only for 2D items possible
        return;

    // -- first break instrument link, clear masks and projections
    unlinkFromInstrument();

    if (MaskContainerItem* maskContainer = intensityDataItem()->maskContainerItem())
        maskContainer->clear();

    if (ProjectionContainerItem* projectionsContainer =
            intensityDataItem()->projectionContainerItem()) {
        projectionsContainer->clear();
    }

    // -- now rotate data
    const Datafield* input = intensityDataItem()->c_field();
    Datafield* output = DataUtil::Data::createRearrangedDataSet(*input, 1).release();

    // upd AxesItems
    intensityDataItem()->xAxisItem()->setBinCount(output->xAxis().size());
    intensityDataItem()->yAxisItem()->setBinCount(output->yAxis().size());

    // apply rotated data
    intensityDataItem()->setDatafield(output);
    intensityDataItem()->setAxesRangeToData();
}

//! Updates the name of file to store intensity data.

void RealItem::updateDataFileName()
{
    if (DataItem* item = dataItem())
        item->setFileName(GUI::Model::FilenameUtil::realDataFileName(realItemName()));

    if (DataItem* item = nativeDataItem())
        item->setFileName(GUI::Model::FilenameUtil::nativeDataFileName(realItemName()));
}

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

    // NOTE: The ordering of the XML elements is important in initialization

    // instrument id
    w->writeStartElement(Tag::InstrumentId);
    XML::writeAttribute(w, XML::Attrib::value, m_instrumentId);
    w->writeEndElement();

    // name
    w->writeStartElement(Tag::Name);
    XML::writeAttribute(w, XML::Attrib::value, m_name);
    w->writeEndElement();

    // presentation type
    w->writeStartElement(Tag::PresentationType);
    XML::writeAttribute(w, XML::Attrib::value, m_presentationType);
    w->writeEndElement();

    // native data units
    w->writeStartElement(Tag::NativeDataUnits);
    XML::writeAttribute(w, XML::Attrib::value, m_nativeDataUnits);
    w->writeEndElement();

    // data
    if (m_dataItem) {
        w->writeStartElement(Tag::Data);
        XML::writeAttribute(w, XML::Attrib::type, m_dataItem->TYPE);
        m_dataItem->writeTo(w);
        w->writeEndElement();
    }

    // native data
    if (m_nativeDataItem) {
        w->writeStartElement(Tag::NativeData);
        XML::writeAttribute(w, XML::Attrib::type, m_nativeDataItem->TYPE);
        m_nativeDataItem->writeTo(w);
        w->writeEndElement();
    }
}

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

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

        // instrument id
        if (tag == Tag::InstrumentId) {
            XML::readAttribute(r, XML::Attrib::value, &m_instrumentId);
            XML::gotoEndElementOfTag(r, Tag::InstrumentId);

            // name
        } else if (tag == Tag::Name) {
            XML::readAttribute(r, XML::Attrib::value, &m_name);
            XML::gotoEndElementOfTag(r, tag);

            // presentation type
        } else if (tag == Tag::PresentationType) {
            XML::readAttribute(r, XML::Attrib::value, &m_presentationType);
            XML::gotoEndElementOfTag(r, tag);

            // data
        } else if (tag == Tag::Data) {
            QString type;
            XML::readAttribute(r, XML::Attrib::type, &type);
            if (type == SpecularDataItem::M_TYPE)
                initItemWithRank(1);
            else if (type == IntensityDataItem::M_TYPE)
                initItemWithRank(2);
            else
                ASSERT(false);

            m_dataItem->readFrom(r);
            XML::gotoEndElementOfTag(r, tag);

            // native data
        } else if (tag == Tag::NativeData) {
            ASSERT(m_dataItem); // read 'm_dataItem' before
            initNativeData()->readFrom(r);
            XML::gotoEndElementOfTag(r, tag);

            // native data units
        } else if (tag == Tag::NativeDataUnits) {
            XML::readAttribute(r, XML::Attrib::value, &m_nativeDataUnits);
            XML::gotoEndElementOfTag(r, tag);

        } else
            r->skipCurrentElement();
    }
}

void RealItem::writeDataFiles(const QString& projectDir) const
{
    if (m_dataItem)
        m_dataItem->saveDatafield(projectDir);

    if (m_nativeDataItem)
        m_nativeDataItem->saveDatafield(projectDir);
}

QString RealItem::readDataFiles(const QString& projectDir, MessageService* messageService)
{
    QString dataError, nativeDataError;

    if (m_dataItem)
        dataError = m_dataItem->loadDatafield(messageService, projectDir);

    if (m_nativeDataItem)
        nativeDataError = m_nativeDataItem->loadDatafield(messageService, projectDir);

    // return error message
    if (dataError.isEmpty() && nativeDataError.isEmpty())
        return {};
    else if (!dataError.isEmpty() && nativeDataError.isEmpty())
        return (dataError);
    else if (dataError.isEmpty() && !nativeDataError.isEmpty())
        return (nativeDataError);
    else
        return (dataError + "\n" + nativeDataError);
}

void RealItem::copyTo(RealItem* const realdata_dst) const
{
    GUI::Util::copyContents(this, realdata_dst);

    if (m_dataItem)
        realdata_dst->dataItem()->setDatafield(dataItem()->c_field()->clone());

    if (m_nativeDataItem)
        realdata_dst->nativeDataItem()->setDatafield(nativeDataItem()->c_field()->clone());
}

std::vector<int> RealItem::shape() const
{
    const auto* data_item = dataItem();
    if (!data_item) {
        ASSERT(data_item);
        return {};
    }
    return data_item->shape();
}

MaskContainerItem* RealItem::maskContainerItem()
{
    if (auto* intensity_data = intensityDataItem())
        return intensity_data->maskContainerItem();
    return nullptr;
}

void RealItem::updateToInstrument(const InstrumentItem* instrument)
{
    DataItem* data_item = dataItem();
    if (!data_item)
        return;

    if (instrument) {
        // To keep the same units after linking, just comment lines.
        // To switch to certain units on linking, uncomment lines.
        // dataItem()->setCurrentCoord(Coords::DEGREES);
        // const auto converter = instrument->createCoordSystem();
        // dataItem()->updateCoords(*converter);
        return;
    }

    // instrument == nullptr => unlinking => going back to native data
    if (isSpecularData()) {
        if (hasNativeData()) {
            std::unique_ptr<Datafield> native_data(nativeDataItem()->c_field()->clone());
            const QString units_label = nativeDataUnits();
        } else {
            specularDataItem()->setDatafield(nullptr);
        }
    } else {
        auto* native_data_item = nativeDataItem();
        auto* data_source = native_data_item ? native_data_item : data_item;

        std::unique_ptr<Datafield> native_data(data_source->c_field()->clone());
        const QString units_label = nativeDataUnits();
    }
}
