//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      GUI/Model/Job/JobItem.cpp
//! @brief     Implements class JobItem
//!
//! @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/Job/JobItem.h"
#include "Device/Coord/ICoordSystem.h"
#include "Device/Data/Datafield.h"
#include "Device/Detector/SimulationAreaIterator.h" // roiIndex
#include "Device/Histo/SimulationResult.h"
#include "GUI/Model/Data/DataItemUtil.h"
#include "GUI/Model/Data/IntensityDataItem.h"
#include "GUI/Model/Data/SpecularDataItem.h"
#include "GUI/Model/Device/BackgroundItems.h"
#include "GUI/Model/Device/InstrumentItems.h"
#include "GUI/Model/Device/RealItem.h"
#include "GUI/Model/Job/FitParameterContainerItem.h"
#include "GUI/Model/Job/FitSuiteItem.h"
#include "GUI/Model/Job/MinimizerItem.h"
#include "GUI/Model/Job/ParameterTreeItems.h"
#include "GUI/Model/Sample/SampleItem.h"
#include "GUI/Support/Util/ItemFileNameUtil.h"
#include <stdexcept> // domain_error

namespace {
namespace Tag {

const QString SimulationOptions("SimulationOptions");
const QString ParameterContainer("ParameterContainer");
const QString Sample("Sample");
const QString Instrument("Instrument");
const QString Name("Name");
const QString Identifier("Id");
const QString Comments("Comments");
const QString PresentationType("PresentationType");
const QString SliderRange("SliderRange");
const QString Activity("Activity");
const QString Duration("Duration");
const QString BeginTime("Begin");
const QString EndTime("End");
const QString Status("Status");
const QString Progress("Progress");
const QString RealItem("RealItem");
const QString SimulatedData("SimulatedData");
const QString FitSuite("FitSuite");

} // namespace Tag
} // namespace

JobItem::JobItem()
    : m_simulationOptionsItem(std::make_unique<SimulationOptionsItem>())
    , m_parameterContainer(std::make_unique<ParameterContainerItem>())
    , m_sampleItem(std::make_unique<SampleItem>())
{
}

JobItem::~JobItem() = default;

QString JobItem::jobName() const
{
    return m_name;
}

void JobItem::setJobName(const QString& name)
{
    m_name = name;
    updateDataFileName();
    emit jobNameChanged(name);
}

JobStatus JobItem::status() const
{
    return m_status;
}

void JobItem::setStatus(JobStatus status)
{
    m_status = status;
    if (status == JobStatus::Failed) {
        if (DataItem* dataItem = simulatedDataItem()) {
            if (Datafield* df = dataItem->p_field())
                df->setAllTo(0.0);
            emit dataItem->datafieldChanged();
        }
    }
    emit jobStatusChanged(status);
}

bool JobItem::isIdle() const
{
    return status() == JobStatus::Idle;
}

bool JobItem::isRunning() const
{
    return status() == JobStatus::Running;
}

bool JobItem::isCompleted() const
{
    return status() == JobStatus::Completed;
}

bool JobItem::isCanceled() const
{
    return status() == JobStatus::Canceled;
}

bool JobItem::isFailed() const
{
    return status() == JobStatus::Failed;
}

bool JobItem::isFitting() const
{
    return status() == JobStatus::Fitting;
}

bool JobItem::isValidForFitting()
{
    return bool(m_realItem);
}

QDateTime JobItem::beginTime() const
{
    return m_beginTime;
}

void JobItem::setBeginTime(const QDateTime& begin_time)
{
    m_beginTime = begin_time;
    emit jobBeginTimeChanged(begin_time);
}

QDateTime JobItem::endTime() const
{
    return m_endTime;
}

void JobItem::setEndTime(const QDateTime& end_time)
{
    m_endTime = end_time;
    emit jobEndTimeChanged(end_time);
}

std::optional<size_t> JobItem::duration() const
{
    QDateTime begin_time = beginTime();
    QDateTime end_time = endTime();
    if (begin_time.isValid() && end_time.isValid() && begin_time < end_time)
        return begin_time.msecsTo(end_time);
    return std::nullopt;
}

QString JobItem::comments() const
{
    return m_comments;
}

void JobItem::setComments(const QString& comments)
{
    m_comments = comments;
    emit jobCommentsChanged(comments);
}

int JobItem::progress() const
{
    return m_progress;
}

void JobItem::setProgress(int progress)
{
    m_progress = progress;
    emit jobProgressChanged(progress);
}

bool JobItem::runImmediately() const
{
    return simulationOptionsItem().runImmediately();
}

bool JobItem::runInBackground() const
{
    return !simulationOptionsItem().runImmediately();
}

bool JobItem::isSpecularJob() const
{
    return instrumentItem()->is<SpecularInstrumentItem>();
}

bool JobItem::isIntensityJob() const
{
    return instrumentItem()->is<GISASInstrumentItem>()
           || instrumentItem()->is<OffspecInstrumentItem>()
           || instrumentItem()->is<DepthprobeInstrumentItem>();
}

SampleItem* JobItem::sampleItem()
{
    return m_sampleItem.get();
}

void JobItem::copySampleIntoJob(const SampleItem* sample)
{
    m_sampleItem->initFrom(sample);
}

InstrumentItem* JobItem::instrumentItem() const
{
    return m_instrument.currentItem();
}

void JobItem::copyInstrumentIntoJob(const InstrumentItem* instrument)
{
    m_instrument.setCurrentItem(instrument->createItemCopy());
    m_instrument.currentItem()->setId(QUuid::createUuid().toString());
}

const SimulationOptionsItem& JobItem::simulationOptionsItem() const
{
    return *m_simulationOptionsItem;
}

void JobItem::copySimulationOptionsIntoJob(const SimulationOptionsItem& options)
{
    m_simulationOptionsItem = std::make_unique<SimulationOptionsItem>(options);
}

void JobItem::setResults(const SimulationResult& result)
{
    auto coord = simulatedDataItem()->currentCoord();
    auto converter = instrumentItem()->createCoordSystem();
    GUI::Model::DataItemUtil::updateAxesTitle(simulatedDataItem(), *converter, coord);
    simulatedDataItem()->setDatafield(
        new Datafield(converter->convertedAxes(coord), result.flatVector()));
    updateDataFileName();
}

FitSuiteItem* JobItem::fitSuiteItem()
{
    return m_fitSuiteItem.get();
}

FitSuiteItem* JobItem::createFitSuiteItem()
{
    if (m_fitSuiteItem)
        throw std::runtime_error("Will not create a second FitSuiteItem.");

    m_fitSuiteItem = std::make_unique<FitSuiteItem>();
    return m_fitSuiteItem.get();
}

void JobItem::createFitContainers()
{
    FitSuiteItem* fitSuiteItem = createFitSuiteItem();

    fitSuiteItem->createFitParametersContainerItem();
    fitSuiteItem->createMinimizerContainerItem();
}

ParameterContainerItem* JobItem::parameterContainerItem()
{
    return m_parameterContainer.get();
}

FitParameterContainerItem* JobItem::fitParameterContainerItem()
{
    if (FitSuiteItem* item = fitSuiteItem())
        return item->fitParameterContainerItem();

    return nullptr;
}

DataItem* JobItem::createNewDataItem()
{
    DataItem* dataItem;
    if (isSpecularJob())
        dataItem = new SpecularDataItem();
    else if (isIntensityJob())
        dataItem = new IntensityDataItem();
    else
        ASSERT(false);

    return dataItem;
}

void JobItem::createSimulatedDataItem()
{
    ASSERT(!simulatedDataItem());
    m_simulatedDataItem.reset(createNewDataItem());

    // Set default axes units for simulated data.
    // Can be overriden by units from RealItem
    if (instrumentItem()->is<SpecularInstrumentItem>())
        m_simulatedDataItem->setCurrentCoord(Coords::QSPACE);
    else if (instrumentItem()->is<GISASInstrumentItem>())
        m_simulatedDataItem->setCurrentCoord(Coords::QSPACE);
    else if (instrumentItem()->is<OffspecInstrumentItem>())
        m_simulatedDataItem->setCurrentCoord(Coords::DEGREES); // Coords::QSPACE is unsupported
    else if (instrumentItem()->is<DepthprobeInstrumentItem>())
        m_simulatedDataItem->setCurrentCoord(Coords::QSPACE);
    else
        ASSERT(false);
}

IntensityDataItem* JobItem::intensityDataItem()
{
    return dynamic_cast<IntensityDataItem*>(m_simulatedDataItem.get());
}

DataItem* JobItem::simulatedDataItem()
{
    return m_simulatedDataItem.get();
}

DataItem* JobItem::createDiffDataItem()
{
    ASSERT(!diffDataItem());
    m_diffDataItem.reset(createNewDataItem());

    // use the same axes units as for real data
    ASSERT(m_realItem);
    Coords coords = m_realItem->dataItem()->currentCoord();
    m_diffDataItem->setCurrentCoord(coords);

    // update axes labels
    const auto converter = instrumentItem()->createCoordSystem();
    ASSERT(converter);
    GUI::Model::DataItemUtil::updateAxesTitle(diffDataItem(), *converter, coords);

    if (isSpecularJob())
        dynamic_cast<SpecularDataItem*>(diffDataItem())->setDiffPlotStyle();

    return m_diffDataItem.get();
}

DataItem* JobItem::diffDataItem()
{
    return m_diffDataItem.get();
}

RealItem* JobItem::createRealItem()
{
    ASSERT(!realItem());
    m_realItem = std::make_unique<RealItem>();
    return m_realItem.get();
}

void JobItem::copyRealItemIntoJob(const RealItem* srcRealItem)
{
    createRealItem();
    srcRealItem->copyTo(realItem());

    // override axes units of simulated data
    ASSERT(m_simulatedDataItem);
    m_simulatedDataItem->setCurrentCoord(m_realItem->dataItem()->currentCoord());

    if (isSpecularJob())
        m_realItem->specularDataItem()->setRealPlotStyle();
}

RealItem* JobItem::realItem()
{
    return m_realItem.get();
}

void JobItem::adjustReaDataToJobInstrument()
{
    // update stored instrument id without updating rest of real data
    realItem()->setInstrumentId(instrumentItem()->id());

    if (instrumentItem()->is<GISASInstrumentItem>()) {
        // Temporary conversion of real units to degrees before copying masks to instrument.
        // It is not clear why we need to do this, but otherwise the result is incorrect.
        // Seems that degrees are default units in detector, so we should use the same units here,
        // but if we change 'converter->defaultUnits()' to, for example, radians, here we still
        // should use 'Coords::DEGREES'. So the reason lies deeper.
        Coords backup_coord = m_realItem->dataItem()->currentCoord();
        m_realItem->dataItem()->setCurrentCoord(Coords::DEGREES);
        const auto converter = instrumentItem()->createCoordSystem();
        realItem()->dataItem()->updateCoords(*converter);

        importMasksFromRealItem(); // Copy masks and ROI from RealItem on board of instrument.

        // convert units back
        m_realItem->dataItem()->setCurrentCoord(backup_coord);
        realItem()->dataItem()->updateCoords(*converter);

        cropRealData(); // Crop RealItem to the region of interest.
    }
}

void JobItem::importMasksFromRealItem()
{
    if (auto* iiI = dynamic_cast<GISASInstrumentItem*>(instrumentItem()))
        if (const auto* container = realItem()->maskContainerItem())
            iiI->importMasks(container);
}

//! Crops RealItem to the region of interest.
void JobItem::cropRealData()
{
    auto* iiI = dynamic_cast<GISASInstrumentItem*>(instrumentItem());
    ASSERT(iiI);

    // Adjust real data to the size of region of interest
    IntensityDataItem* intensityItem = realItem()->intensityDataItem();

    std::unique_ptr<Datafield> origData(intensityItem->c_field()->clone());

    const auto converter = iiI->createCoordSystem();
    ASSERT(converter);

    // (re)create zero-valued Datafield. Axes are derived from the current units
    GUI::Model::DataItemUtil::createDefaultDetectorMap(intensityItem, *converter);

    iiI->normalDetector()->iterateOverNonMaskedPoints([&](IDetector::const_iterator it) {
        Datafield* cropped_data = intensityItem->p_field();
        (*cropped_data)[it.roiIndex()] = (*origData)[it.detectorIndex()];
    });

    intensityItem->updateDataRange();
}

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

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

    // simulation options
    w->writeStartElement(Tag::SimulationOptions);
    m_simulationOptionsItem->writeTo(w);
    w->writeEndElement();

    // instrument
    w->writeStartElement(Tag::Instrument);
    m_instrument.writeTo(w);
    w->writeEndElement();

    // sample
    w->writeStartElement(Tag::Sample);
    m_sampleItem->writeTo(w);
    w->writeEndElement();

    // parameters
    w->writeStartElement(Tag::ParameterContainer);
    m_parameterContainer->writeTo(w);
    w->writeEndElement();

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

    // identifier
    w->writeStartElement(Tag::Identifier);
    XML::writeAttribute(w, XML::Attrib::value, m_identifier);
    w->writeEndElement();

    // activity
    w->writeStartElement(Tag::Activity);
    XML::writeAttribute(w, XML::Attrib::value, m_activity);
    w->writeEndElement();

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

    // slider range
    w->writeStartElement(Tag::SliderRange);
    XML::writeAttribute(w, XML::Attrib::value, m_sliderRange);
    w->writeEndElement();

    // begin time
    w->writeStartElement(Tag::BeginTime);
    XML::writeAttribute(w, XML::Attrib::value, m_beginTime.toString(Qt::ISODateWithMs));
    w->writeEndElement();

    // end time
    w->writeStartElement(Tag::EndTime);
    XML::writeAttribute(w, XML::Attrib::value, m_endTime.toString(Qt::ISODateWithMs));
    w->writeEndElement();

    // status
    w->writeStartElement(Tag::Status);
    XML::writeAttribute(w, XML::Attrib::value, jobStatusToString(m_status));
    w->writeEndElement();

    // progress
    w->writeStartElement(Tag::Progress);
    XML::writeAttribute(w, XML::Attrib::value, m_progress);
    w->writeEndElement();

    // comments
    w->writeStartElement(Tag::Comments);
    XML::writeAttribute(w, XML::Attrib::value, m_comments);
    w->writeEndElement();

    // real item
    if (m_realItem) {
        w->writeStartElement(Tag::RealItem);
        m_realItem->writeTo(w);
        w->writeEndElement();
    }

    // simulated data
    if (m_simulatedDataItem) {
        w->writeStartElement(Tag::SimulatedData);
        m_simulatedDataItem->writeTo(w);
        w->writeEndElement();
    }

    // fit suite
    if (m_fitSuiteItem) {
        w->writeStartElement(Tag::FitSuite);
        m_fitSuiteItem->writeTo(w);
        w->writeEndElement();
    }
}

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

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

        // simulation options
        if (tag == Tag::SimulationOptions) {
            m_simulationOptionsItem->readFrom(r);
            XML::gotoEndElementOfTag(r, tag);

            // instrument
        } else if (tag == Tag::Instrument) {
            m_instrument.readFrom(r);
            XML::gotoEndElementOfTag(r, tag);

            // parameters
        } else if (tag == Tag::ParameterContainer) {
            m_parameterContainer->readFrom(r);
            XML::gotoEndElementOfTag(r, tag);

            // sample
        } else if (tag == Tag::Sample) {
            m_sampleItem->readFrom(r);
            XML::gotoEndElementOfTag(r, tag);

            // activity
        } else if (tag == Tag::Activity) {
            XML::readAttribute(r, XML::Attrib::value, &m_activity);
            XML::gotoEndElementOfTag(r, tag);

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

            // slider range
        } else if (tag == Tag::SliderRange) {
            XML::readAttribute(r, XML::Attrib::value, &m_sliderRange);
            XML::gotoEndElementOfTag(r, tag);

            // comments
        } else if (tag == Tag::Comments) {
            XML::readAttribute(r, XML::Attrib::value, &m_comments);
            XML::gotoEndElementOfTag(r, tag);

            // status
        } else if (tag == Tag::Status) {
            QString status_str;
            XML::readAttribute(r, XML::Attrib::value, &status_str);
            m_status = jobStatusFromString(status_str);
            XML::gotoEndElementOfTag(r, tag);

            // progress
        } else if (tag == Tag::Progress) {
            QString progress_str;
            XML::readAttribute(r, XML::Attrib::value, &progress_str);
            m_progress = progress_str.toInt();
            XML::gotoEndElementOfTag(r, tag);

            // begin time
        } else if (tag == Tag::BeginTime) {
            QString begin_t_str;
            XML::readAttribute(r, XML::Attrib::value, &begin_t_str);
            m_beginTime = QDateTime::fromString(begin_t_str, Qt::ISODateWithMs);
            XML::gotoEndElementOfTag(r, tag);

            // end time
        } else if (tag == Tag::EndTime) {
            QString end_t_str;
            XML::readAttribute(r, XML::Attrib::value, &end_t_str);
            m_endTime = QDateTime::fromString(end_t_str, Qt::ISODateWithMs);
            XML::gotoEndElementOfTag(r, tag);

            // identifier
        } else if (tag == Tag::Identifier) {
            XML::readAttribute(r, XML::Attrib::value, &m_identifier);
            XML::gotoEndElementOfTag(r, tag);

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

            // real item
        } else if (tag == Tag::RealItem) {
            createRealItem();
            m_realItem->readFrom(r);
            createDiffDataItem()->copyXYRangesFromItem(m_realItem->dataItem());

            if (isIntensityJob())
                dynamic_cast<IntensityDataItem*>(diffDataItem())
                    ->setCurrentGradient(m_realItem->intensityDataItem()->currentGradient());

            importMasksFromRealItem();
            XML::gotoEndElementOfTag(r, tag);

            // simulated data
        } else if (tag == Tag::SimulatedData) {
            createSimulatedDataItem();
            m_simulatedDataItem->readFrom(r);
            XML::gotoEndElementOfTag(r, tag);

            // fit suite
        } else if (tag == Tag::FitSuite) {
            createFitSuiteItem()->readFrom(r);
            XML::gotoEndElementOfTag(r, tag);

        } else
            r->skipCurrentElement();
    }
}

void JobItem::writeDataFiles(const QString& projectDir) const
{
    if (m_realItem)
        m_realItem->writeDataFiles(projectDir);

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

void JobItem::readDataFiles(const QString& projectDir, MessageService* messageService)
{
    QString realError, simError, errorMessage;

    if (m_realItem)
        realError = m_realItem->readDataFiles(projectDir, messageService);

    if (m_simulatedDataItem)
        simError = m_simulatedDataItem->loadDatafield(messageService, projectDir);

    // Handling corrupted file on disk
    if (!realError.isEmpty() || !simError.isEmpty()) {
        errorMessage = QString("Load of the data from disk failed with:\n");

        if (!realError.isEmpty() && simError.isEmpty())
            errorMessage += QString("'%1'").arg(realError);
        else if (realError.isEmpty() && !simError.isEmpty())
            errorMessage += QString("'%1'").arg(simError);
        else
            errorMessage += QString("'%1',\n'%2'").arg(realError, simError);
    }

    // Handling previous crash of GUI during job run
    if (m_status == JobStatus::Running) {
        if (errorMessage.isEmpty())
            errorMessage = "Possible GUI crash while job was running";
        else
            errorMessage += "\n. Also possibly GUI crashed while job was running";
    }

    if (!errorMessage.isEmpty()) {
        setComments(errorMessage);
        m_status = JobStatus::Failed;
    }
}
//! Updates the name of file to store intensity data.

void JobItem::updateDataFileName()
{
    if (DataItem* item = simulatedDataItem())
        item->setFileName(GUI::Model::FilenameUtil::jobResultsFileName(jobName()));

    if (RealItem* real = realItem()) {
        if (DataItem* item = real->dataItem())
            item->setFileName(GUI::Model::FilenameUtil::jobReferenceFileName(jobName()));

        if (DataItem* item = real->nativeDataItem())
            item->setFileName(GUI::Model::FilenameUtil::jobNativeDataFileName(identifier()));
    }
}
