////////////////////////////////////////////////////////////////////////////////////////
//
//  Copyright 2023 OVITO GmbH, Germany
//
//  This file is part of OVITO (Open Visualization Tool).
//
//  OVITO is free software; you can redistribute it and/or modify it either under the
//  terms of the GNU General Public License version 3 as published by the Free Software
//  Foundation (the "GPL") or, at your option, under the terms of the MIT License.
//  If you do not alter this notice, a recipient may use your version of this
//  file under either the GPL or the MIT License.
//
//  You should have received a copy of the GPL along with this program in a
//  file LICENSE.GPL.txt.  You should have received a copy of the MIT License along
//  with this program in a file LICENSE.MIT.txt
//
//  This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND,
//  either express or implied. See the GPL or the MIT License for the specific language
//  governing rights and limitations.
//
////////////////////////////////////////////////////////////////////////////////////////

#include <ovito/core/Core.h>
#include <ovito/core/dataset/animation/AnimationSettings.h>
#include <ovito/core/dataset/DataSetContainer.h>
#include <ovito/core/app/Application.h>
#include <ovito/core/app/UserInterface.h>
#include "BasePipelineSource.h"

namespace Ovito {

IMPLEMENT_OVITO_CLASS(BasePipelineSource);
DEFINE_REFERENCE_FIELD(BasePipelineSource, dataCollection);
DEFINE_RUNTIME_PROPERTY_FIELD(BasePipelineSource, dataCollectionFrame);
DEFINE_PROPERTY_FIELD(BasePipelineSource, userHasChangedDataCollection);
SET_PROPERTY_FIELD_LABEL(BasePipelineSource, dataCollection, "Data");
SET_PROPERTY_FIELD_LABEL(BasePipelineSource, dataCollectionFrame, "Active frame index");

/******************************************************************************
* Constructor.
******************************************************************************/
BasePipelineSource::BasePipelineSource(ObjectInitializationFlags flags) : PipelineNode(flags),
    _dataCollectionFrame(-1),
    _userHasChangedDataCollection(false)
{
}

/******************************************************************************
* Post-processes the DataCollection generated by the data source and updates
* the internal master data collection.
******************************************************************************/
Future<PipelineFlowState> BasePipelineSource::postprocessDataCollection(int animationFrame, TimeInterval frameInterval, Future<PipelineFlowState> future, bool throwOnError)
{
    return std::move(future).then(*this, [this, animationFrame, frameInterval, throwOnError](Future<PipelineFlowState> future) -> PipelineFlowState {
        OVITO_ASSERT(future.isFinished() && !future.isCanceled());
        OVITO_ASSERT(ExecutionContext::current().isValid());

        try {
            PipelineFlowState state = future.result();
            setStatus(state.status());

            // Check if the generated pipeline state is valid.
            if(state.data() && state.status().type() != PipelineStatus::Error) {

                // In GUI mode, create editable proxy objects for the data objects in the generated collection.
                if(Application::instance()->guiMode()) {
                    _updatingEditableProxies = true;
                    ConstDataObjectPath dataPath = { state.data() };
                    state.data()->updateEditableProxies(state, dataPath);
                    _updatingEditableProxies = false;
                }

                // Adopt the generated data collection as our new master data collection (only if it is for the current animation time).
                if(state.stateValidity().contains(ExecutionContext::current().ui().datasetContainer().currentAnimationTime())) {
                    setDataCollectionFrame(animationFrame);
                    setDataCollection(state.data());
                    notifyDependents(ReferenceEvent::PreliminaryStateAvailable);
                }
            }

            return state;
        }
        catch(Exception& ex) {
            if(throwOnError)
                throw;
            setStatus(ex);
            ex.prependToMessage(tr("Pipeline source reported: "));
            return PipelineFlowState(dataCollection(), PipelineStatus(ex, QStringLiteral(" ")), frameInterval);
        }
        catch(const std::bad_alloc&) {
            setStatus(PipelineStatus(PipelineStatus::Error, tr("Not enough memory.")));
            return PipelineFlowState(dataCollection(), status(), frameInterval);
        }
        catch(...) {
            if(throwOnError)
                throw;
            OVITO_ASSERT_MSG(false, "BasePipelineSource::postprocessDataCollection()", "Caught an unexpected exception type during source function execution.");
            setStatus(PipelineStatus(PipelineStatus::Error, tr("Unknown exception caught during execution of pipeline source function.")));
            return PipelineFlowState(dataCollection(), status(), frameInterval);
        }
    });
}

/******************************************************************************
* Gets called by the PipelineCache whenever it returns a pipeline state from the cache.
******************************************************************************/
Future<PipelineFlowState> BasePipelineSource::postprocessCachedState(const PipelineEvaluationRequest& request, const PipelineFlowState& cachedState)
{
    OVITO_ASSERT(ExecutionContext::current().isValid());

    PipelineFlowState state = cachedState;
    setStatus(state.status());

    if(state.data() && state.status().type() != PipelineStatus::Error) {
        UndoSuspender noUndo;

        // In GUI mode, create editable proxy objects for the data objects in the generated collection.
        if(Application::instance()->guiMode()) {
            _updatingEditableProxies = true;
            ConstDataObjectPath dataPath = { state.data() };
            state.data()->updateEditableProxies(state, dataPath);
            _updatingEditableProxies = false;
        }

        // Adopt the generated data collection as our new master data collection (only if it is for the current animation time).
        if(state.stateValidity().contains(ExecutionContext::current().ui().datasetContainer().currentAnimationTime())) {
            setDataCollectionFrame(animationTimeToSourceFrame(request.time()));
            setDataCollection(state.data());
        }
    }

    return PipelineNode::postprocessCachedState(request, state);
}

/******************************************************************************
* Is called when a RefTarget referenced by this object has generated an event.
******************************************************************************/
bool BasePipelineSource::referenceEvent(RefTarget* source, const ReferenceEvent& event)
{
    if(event.type() == ReferenceEvent::TargetChanged && source == dataCollection() && !_updatingEditableProxies && !event.sender()->isBeingLoaded()) {
        if(ExecutionContext::isInteractive()) {

            // The user has modified one of the editable proxy objects attached to the data collection.
            // Apply the changes made to the proxy objects to the actual data objects.
            UndoSuspender noUndo;
            PipelineFlowState state(dataCollection(), PipelineStatus::Success);
            _updatingEditableProxies = true;
            _userHasChangedDataCollection.set(this, PROPERTY_FIELD(userHasChangedDataCollection), true);
            // Temporarily detach data collection from the BasePipelineSource to ignore change signals sent by the data collection.
            setDataCollection(nullptr);
            ConstDataObjectPath dataPath = { state.data() };
            state.data()->updateEditableProxies(state, dataPath);
            // Re-attach the updated data collection to the pipeline source.
            setDataCollection(state.data());
            _updatingEditableProxies = false;

            // Invalidate pipeline cache, except at the current animation time.
            // Here we use the updated data collection.
            if(dataCollectionFrame() >= 0) {
                pipelineCache().overrideCache(dataCollection(), frameTimeInterval(dataCollectionFrame()));
            }
            // Let downstream pipeline now that its input has changed.
            notifyDependents(ReferenceEvent::PreliminaryStateAvailable);
            notifyTargetChanged();
        }
        else {
            // When the data collection was modified by a script, then we simply invalidate the pipeline cache
            // and inform the scene that the pipeline must be re-evaluated.
            pipelineCache().invalidate();
            notifyTargetChanged();
        }
    }
    else if(event.type() == ReferenceEvent::VisualElementModified && source == dataCollection()) {
        // Set dirty flag when user modifies on of the visual elements associated with the current data collection.
        _userHasChangedDataCollection.set(this, PROPERTY_FIELD(userHasChangedDataCollection), true);
    }
    return PipelineNode::referenceEvent(source, event);
}

/******************************************************************************
* Computes the time interval covered on the timeline by the given source animation frame.
******************************************************************************/
TimeInterval BasePipelineSource::frameTimeInterval(int frame) const
{
    return TimeInterval(
        sourceFrameToAnimationTime(frame),
        std::max(sourceFrameToAnimationTime(frame + 1) - 1, sourceFrameToAnimationTime(frame)));
}

/******************************************************************************
* Throws away the master data collection maintained by the source.
******************************************************************************/
void BasePipelineSource::discardDataCollection()
{
    class ResetDataCollectionOperation : public UndoableOperation
    {
    private:
        OORef<BasePipelineSource> _source;
    public:
        ResetDataCollectionOperation(BasePipelineSource* source) : _source(source) {}
        virtual void undo() override {
            _source->setDataCollectionFrame(-1);
            _source->pipelineCache().invalidate();
            _source->notifyTargetChanged();
        }
    };

    pushIfUndoRecording<ResetDataCollectionOperation>(this);

    // Throw away cached frame data and notify pipeline that an update is in order.
    setDataCollection(nullptr);
    setDataCollectionFrame(-1);
    pipelineCache().invalidate();

    // Reset flag that keeps track of user modifications to the data collection.
    _userHasChangedDataCollection.set(this, PROPERTY_FIELD(userHasChangedDataCollection), false);

    notifyTargetChanged();

    pushIfUndoRecording<ResetDataCollectionOperation>(this);
}

}   // End of namespace
