////////////////////////////////////////////////////////////////////////////////////////
//
//  Copyright 2026 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_ABSTRACT_OVITO_CLASS(BasePipelineSource);
DEFINE_REFERENCE_FIELD(BasePipelineSource, dataCollection);
DEFINE_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");

/******************************************************************************
* Post-processes the DataCollection generated by the data source and updates
* the internal master data collection.
******************************************************************************/
void BasePipelineSource::postprocessDataCollection(Future<PipelineFlowState>& stateFuture, const PipelineEvaluationRequest& request)
{
    // Register the task to indicate in the UI that this pipeline stage is currently performing some work.
    if(!request.interactiveMode())
        registerActiveFuture(stateFuture);

    stateFuture.postprocess(ObjectExecutor(this), [this, request](Future<PipelineFlowState> future) -> PipelineFlowState {
        OVITO_ASSERT(future.isFinished() && !future.isCanceled());

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

                // Display status of the source in the UI.
                setStatusIfCurrentFrame(state.status(), request);

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

                    // Adopt the generated data collection as our new master data collection (only if it is for the current animation time).
                    AnimationTime currentTime = this_task::ui()->datasetContainer().currentAnimationTime();
                    if(state.stateValidity().contains(currentTime)) {
                        setDataCollectionFrame(std::clamp(animationTimeToSourceFrame(currentTime), 0, std::max(numberOfSourceFrames() - 1, 0)));
                        setDataCollection(state.data());
                    }
                }

                return state;
            }
            catch(const Exception&) {
                throw;  // Pass through regular exceptions.
            }
            catch(const OperationCanceled&) {
                throw;  // Pass through regular exceptions.
            }
            catch(const std::bad_alloc&) {
                throw Exception(tr("Not enough memory."));
            }
            catch(const std::exception& ex) {
                OVITO_ASSERT_MSG(false, "BasePipelineSource::postprocessDataCollection()", "Caught an unexpected exception type during source evaluation.");
                throw Exception(tr("A non-standard exception occurred: %1").arg(QString::fromLatin1(ex.what())));
            }
            catch(...) {
                OVITO_ASSERT_MSG(false, "BasePipelineSource::postprocessDataCollection()", "Caught an unknown exception type during source evaluation.");
                throw Exception(tr("An unknown type of exception occurred."));
            }
        }
        catch(Exception& ex) {
            setStatusIfCurrentFrame(ex, request);
            if(request.throwOnError())
                throw;
            ex.prependToMessage(tr("Pipeline source reported: "));
            return PipelineFlowState(dataCollection(), PipelineStatus(ex, QStringLiteral(" ")));
        }
    });
}

/******************************************************************************
* Gets called by the PipelineCache whenever it returns a pipeline state from the cache.
******************************************************************************/
PipelineEvaluationResult BasePipelineSource::postprocessCachedState(const PipelineEvaluationRequest& request, const PipelineFlowState& cachedState)
{
    PipelineFlowState state = cachedState;
    setStatusIfCurrentFrame(state.status(), request);

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

        // Adopt the generated data collection as our new master data collection (only if it is for the current animation time).
        AnimationTime currentTime = this_task::ui()->datasetContainer().currentAnimationTime();
        if(state.stateValidity().contains(currentTime)) {
            setDataCollectionFrame(std::clamp(animationTimeToSourceFrame(currentTime), 0, numberOfSourceFrames() - 1));
            setDataCollection(state.data());
        }
    }

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

/******************************************************************************
* Is called when a RefTarget referenced by this object generated an event.
******************************************************************************/
bool BasePipelineSource::referenceEvent(RefTarget* source, const ReferenceEvent& event)
{
    if(event.type() == ReferenceEvent::TargetChanged && source == dataCollection() && !event.sender()->isBeingLoaded()) {
        // 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() == DataObject::VisualElementModified && source == dataCollection()) {
        // Set dirty flag when user modifies one 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);
}

/******************************************************************************
* Replaces all references to the given visual element in the pipeline with new compatible objects.
******************************************************************************/
void BasePipelineSource::replaceVisualElement(DataVis* visElement, const std::function<OORef<DataVis>(const QString&)>& getReplacement)
{
    DataOORef<const DataCollection> dc = dataCollection();
    if(!dc)
        return;

    // Temporarily detach data collection from the BasePipelineSource to ignore change signals sent by the data collection.
    setDataCollection(nullptr);

    // Make the replacement in all of the data collection's data objects.
    bool wasReplaced = dc->replaceVisualElement(visElement, getReplacement);

    // Re-attach the updated data collection to the pipeline source.
    setDataCollection(std::move(dc));

    if(wasReplaced) {

        class ModifiedDataCollectionOperation : public UndoableOperation
        {
        private:
            OORef<BasePipelineSource> _source;
        public:
            ModifiedDataCollectionOperation(BasePipelineSource* source) : _source(source) {}
            virtual void undo() override {
                if(_source->dataCollectionFrame() >= 0) {
                    _source->pipelineCache().overrideCache(_source->dataCollection(), _source->frameTimeInterval(_source->dataCollectionFrame()));
                }
                _source->notifyDependents(ReferenceEvent::InteractiveStateAvailable);
                _source->notifyTargetChanged();
            }
        };

        pushIfUndoRecording<ModifiedDataCollectionOperation>(this);

        _userHasChangedDataCollection.set(this, PROPERTY_FIELD(userHasChangedDataCollection), true);

        // 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 know that its input has changed.
        notifyDependents(ReferenceEvent::InteractiveStateAvailable);
        notifyTargetChanged();
    }
}

}   // End of namespace
