PrometheusEditSet.java

/*
 * Prometheus: Application Framework
 * Copyright 2012-2026. Tony Washer
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License.  You may obtain a copy
 * of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package io.github.tonywasher.joceanus.prometheus.views;

import io.github.tonywasher.joceanus.oceanus.base.OceanusException;
import io.github.tonywasher.joceanus.oceanus.event.OceanusEventManager;
import io.github.tonywasher.joceanus.oceanus.event.OceanusEventRegistrar;
import io.github.tonywasher.joceanus.oceanus.event.OceanusEventRegistrar.OceanusEventProvider;
import io.github.tonywasher.joceanus.oceanus.format.OceanusDataFormatter;
import io.github.tonywasher.joceanus.oceanus.logger.OceanusLogManager;
import io.github.tonywasher.joceanus.oceanus.logger.OceanusLogger;
import io.github.tonywasher.joceanus.oceanus.profile.OceanusProfile;
import io.github.tonywasher.joceanus.metis.data.MetisDataEditState;
import io.github.tonywasher.joceanus.metis.field.MetisFieldItem;
import io.github.tonywasher.joceanus.metis.field.MetisFieldSet;
import io.github.tonywasher.joceanus.metis.list.MetisListKey;
import io.github.tonywasher.joceanus.metis.ui.MetisErrorPanel;
import io.github.tonywasher.joceanus.metis.viewer.MetisViewerErrorList;
import io.github.tonywasher.joceanus.prometheus.data.PrometheusDataItem;
import io.github.tonywasher.joceanus.prometheus.data.PrometheusDataList;
import io.github.tonywasher.joceanus.prometheus.data.PrometheusDataList.PrometheusDataListSet;
import io.github.tonywasher.joceanus.prometheus.data.PrometheusDataResource;
import io.github.tonywasher.joceanus.prometheus.data.PrometheusDataSet;

import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * Provides control of a set of update-able DataLists.
 */
public class PrometheusEditSet
        implements MetisFieldItem, OceanusEventProvider<PrometheusDataEvent>, PrometheusDataListSet {
    /**
     * Report fields.
     */
    @SuppressWarnings("rawtypes")
    private static final MetisFieldSet<PrometheusEditSet> FIELD_DEFS = MetisFieldSet.newFieldSet(PrometheusEditSet.class);

    /*
     * Declare Fields.
     */
    static {
        FIELD_DEFS.declareLocalField(PrometheusDataResource.DATASET_VERSION, PrometheusEditSet::getVersion);
    }

    /**
     * Logger.
     */
    private static final OceanusLogger LOGGER = OceanusLogManager.getLogger(PrometheusEditSet.class);

    /**
     * The Event Manager.
     */
    private final OceanusEventManager<PrometheusDataEvent> theEventManager;

    /**
     * Report fields.
     */
    @SuppressWarnings("rawtypes")
    private final MetisFieldSet<PrometheusEditSet> theLocalFields;

    /**
     * The entry map.
     */
    private final Map<MetisListKey, PrometheusEditEntry<?>> theMap;

    /**
     * The DataControl.
     */
    private final PrometheusDataControl theControl;

    /**
     * The DataSet.
     */
    private PrometheusDataSet theDataSet;

    /**
     * The version.
     */
    private int theVersion;

    /**
     * Are we editing?
     */
    private Boolean itemEditing = Boolean.FALSE;

    /**
     * Constructor for an update list.
     *
     * @param pControl the Data Control
     */
    public PrometheusEditSet(final PrometheusDataControl pControl) {
        this(pControl, pControl.getData());
    }

    /**
     * Constructor for an update list.
     *
     * @param pControl the Data Control
     * @param pDataSet the DataSet
     */
    public PrometheusEditSet(final PrometheusDataControl pControl,
                             final PrometheusDataSet pDataSet) {
        /* Store the Control */
        theControl = pControl;
        theDataSet = pDataSet;

        /* Create event manager */
        theEventManager = new OceanusEventManager<>();

        /* Create local fields */
        theLocalFields = MetisFieldSet.newFieldSet(this);

        /* Create the map */
        theMap = new LinkedHashMap<>();
    }

    @SuppressWarnings("rawtypes")
    @Override
    public MetisFieldSet<PrometheusEditSet> getDataFieldSet() {
        return theLocalFields;
    }

    @Override
    public String formatObject(final OceanusDataFormatter pFormatter) {
        return getDataFieldSet().getName();
    }

    @Override
    public OceanusEventRegistrar<PrometheusDataEvent> getEventRegistrar() {
        return theEventManager.getEventRegistrar();
    }

    /**
     * Set editing flag.
     *
     * @param pFlag the editing flag
     */
    public void setEditing(final Boolean pFlag) {
        itemEditing = pFlag;
    }

    /**
     * Is the item editing?
     *
     * @return true/false
     */
    public Boolean isEditing() {
        return itemEditing;
    }

    /**
     * Obtain the version.
     *
     * @return the version
     */
    public int getVersion() {
        return theVersion;
    }

    /**
     * Obtain the dataSet.
     *
     * @return the dataSet
     */
    public PrometheusDataSet getDataSet() {
        return theDataSet;
    }

    /**
     * Set the dataSet.
     *
     * @param pDataSet the dataSet
     */
    public void setDataSet(final PrometheusDataSet pDataSet) {
        theDataSet = pDataSet;
    }

    /**
     * Register an entry for a class.
     *
     * @param <T>       the object type
     * @param pDataType the data type
     * @return the list class entry
     */
    public <T extends PrometheusDataItem> PrometheusEditEntry<T> registerType(final MetisListKey pDataType) {
        /* Locate any existing entry */
        @SuppressWarnings("unchecked") final PrometheusEditEntry<T> myEntry = (PrometheusEditEntry<T>) theMap.computeIfAbsent(pDataType, t -> {
            /* Not found , so add it */
            final PrometheusEditEntry<T> myNewEntry = new PrometheusEditEntry<>(t);
            theLocalFields.declareLocalField(myNewEntry.getName(), n -> myNewEntry);
            return myNewEntry;
        });

        /* Update list to null and return */
        myEntry.setDataList(null);
        return myEntry;
    }

    /**
     * Obtain the list for a class.
     * <p>
     * Will look first for the list in the updateSet and then in the underlying data.
     *
     * @param <L>       the list type
     * @param pDataType the data type
     * @param pClass    the list class
     * @return the list
     */
    @Override
    public <L extends PrometheusDataList<?>> L getDataList(final MetisListKey pDataType,
                                                           final Class<L> pClass) {
        /* Locate an existing entry */
        final PrometheusEditEntry<?> myEntry = theMap.get(pDataType);

        /* Cast correctly */
        return myEntry != null
                ? pClass.cast(myEntry.getDataList())
                : theDataSet.getDataList(pDataType, pClass);
    }

    @Override
    public boolean hasDataType(final MetisListKey pDataType) {
        return theMap.containsKey(pDataType);
    }

    /**
     * Set the editEntry for a type.
     *
     * @param <T>       the dataType
     * @param pDataType the data type
     * @param pList     the list
     */
    public <T extends PrometheusDataItem> void setEditEntryList(final MetisListKey pDataType,
                                                                final PrometheusDataList<T> pList) {
        @SuppressWarnings("unchecked") final PrometheusEditEntry<T> myEntry = (PrometheusEditEntry<T>) theMap.get(pDataType);
        myEntry.setDataList(pList);
    }

    /**
     * Obtain an iterator over the listKeys.
     *
     * @return the iterator
     */
    public Iterator<MetisListKey> keyIterator() {
        return theMap.keySet().iterator();
    }

    /**
     * Obtain an iterator over the listKeys.
     *
     * @return the iterator
     */
    public Iterator<PrometheusEditEntry<?>> listIterator() {
        return theMap.values().iterator();
    }

    /**
     * Increment Version.
     */
    public void incrementVersion() {
        /* Obtain the active profile */
        final OceanusProfile myTask = theControl.getActiveTask();
        final OceanusProfile mySubTask = myTask == null
                ? theControl.getNewProfile("incrementVersion")
                : myTask.startTask("incrementVersion");

        /* Increment the version */
        theVersion++;

        /* Loop through the items in the list */
        for (PrometheusEditEntry<?> myEntry : theMap.values()) {
            /* Access list */
            final PrometheusDataList<?> myDataList = myEntry.getDataList();

            /* Increment the version if the list exists */
            if (myDataList != null) {
                /* Note the new step */
                mySubTask.startTask(myDataList.listName());

                /* Set the new version */
                myDataList.setVersion(theVersion);

                /* postProcess */
                if (Boolean.FALSE.equals(itemEditing)) {
                    myDataList.postProcessOnUpdate();
                }
            }
        }

        /* Complete the task */
        mySubTask.end();
    }

    /**
     * Rewind items to the required version.
     *
     * @param pVersion the version to rewind to
     */
    private void rewindToVersion(final int pVersion) {
        /* Obtain the active profile */
        final OceanusProfile myTask = theControl.getActiveTask();
        OceanusProfile mySubTask = myTask.startTask("reWindToVersion");

        /* Record the version */
        theVersion = pVersion;

        /* Loop through the items in the list */
        Iterator<PrometheusEditEntry<?>> myIterator = theMap.values().iterator();
        while (myIterator.hasNext()) {
            /* Access list */
            final PrometheusEditEntry<?> myEntry = myIterator.next();
            final PrometheusDataList<?> myDataList = myEntry.getDataList();

            /* If the list exists */
            if (myDataList != null) {
                /* Note the new step */
                mySubTask.startTask(myDataList.listName());

                /* Rewind the version */
                myDataList.rewindToVersion(theVersion);
            }
        }

        /* Need to validate after full reWind to avoid false errors */
        mySubTask = myTask.startTask("postProcess");
        myIterator = theMap.values().iterator();
        while (myIterator.hasNext()) {
            /* Access list */
            final PrometheusEditEntry<?> myEntry = myIterator.next();
            final PrometheusDataList<?> myDataList = myEntry.getDataList();

            /* If the list exists */
            if (myDataList != null) {
                /* Note the new step */
                mySubTask.startTask(myDataList.listName());

                /* postProcess */
                if (Boolean.FALSE.equals(itemEditing)) {
                    myDataList.postProcessOnUpdate();
                }
            }
        }

        /* Note the new step */
        mySubTask = myTask.startTask("Notify");

        /* Fire that we have rewound the updateSet */
        theEventManager.fireEvent(PrometheusDataEvent.DATACHANGED);

        /* Complete the task */
        mySubTask.end();
    }

    /**
     * Undo changes in a viewSet.
     */
    private void undoLastChange() {
        /* Ignore if we have no changes */
        if (theVersion == 0) {
            return;
        }

        /* Decrement version */
        theVersion--;

        /* Rewind to the version */
        rewindToVersion(theVersion);
    }

    /**
     * Reset changes in a viewSet.
     */
    private void resetChanges() {
        /* Ignore if we have no changes */
        if (theVersion == 0) {
            return;
        }

        /* Decrement version */
        theVersion = 0;

        /* Rewind to the version */
        rewindToVersion(theVersion);
    }

    /**
     * Apply changes in a ViewSet into the core data.
     */
    private void applyChanges() {
        /* Obtain the active profile */
        final OceanusProfile myTask = theControl.getActiveTask();
        final OceanusProfile mySubTask = myTask.startTask("applyChanges");

        /* Validate the changes */
        validate();

        /* Reject request if there are errors */
        if (hasErrors()) {
            /* We have finished */
            mySubTask.startTask("Notify");

            /* Fire that we have rewound the updateSet */
            theEventManager.fireEvent(PrometheusDataEvent.DATACHANGED);

            /* Complete the task */
            mySubTask.end();
            return;
        }

        /* Apply the changes */
        boolean bSuccess = prepareChanges();

        /* analyse the data */
        if (bSuccess) {
            /* Analyse the applied changes */
            bSuccess = theControl.analyseData(false);
        }

        /* If we were successful */
        if (bSuccess) {
            /* Commit the changes */
            commitChanges();

            /* Refresh views */
            theControl.refreshViews();

            /* else we failed */
        } else {
            /* RollBack the changes */
            rollBackChanges();

            /* Re-analyse the data */
            theControl.analyseData(true);
        }

        /* Complete the task */
        mySubTask.end();
    }

    /**
     * Prepare changes in a ViewSet back into the core data.
     *
     * @return success true/false
     */
    private boolean prepareChanges() {
        /* Obtain the active profile */
        final OceanusProfile myTask = theControl.getActiveTask();
        final OceanusProfile mySubTask = myTask.startTask("prepareChanges");
        boolean bSuccess = true;

        /* Protect against exceptions */
        try {
            /* Loop through the items in the list */
            for (PrometheusEditEntry<?> myEntry : theMap.values()) {
                /* Note the new step */
                mySubTask.startTask(myEntry.getName());

                /* Prepare changes for the entry */
                myEntry.prepareChanges();
            }

        } catch (OceanusException e) {
            LOGGER.error("Failed to prepare changes", e);
            bSuccess = false;
        }

        /* Complete the task */
        mySubTask.end();
        return bSuccess;
    }

    /**
     * Commit changes in a ViewSet back into the core data.
     */
    private void commitChanges() {
        /* Obtain the active profile */
        final OceanusProfile myTask = theControl.getActiveTask();
        final OceanusProfile mySubTask = myTask.startTask("commitChanges");

        /* Loop through the items in the list */
        for (PrometheusEditEntry<?> myEntry : theMap.values()) {
            /* Note the new step */
            mySubTask.startTask(myEntry.getName());

            /* Commit changes for the entry */
            myEntry.commitChanges();
        }

        /* Increment the version and notify listeners */
        theControl.incrementVersion();
        theVersion = 0;

        /* Complete the task */
        mySubTask.end();
    }

    /**
     * RollBack changes in a ViewSet back into the core data.
     */
    private void rollBackChanges() {
        /* Obtain the active profile */
        final OceanusProfile myTask = theControl.getActiveTask();
        final OceanusProfile mySubTask = myTask.startTask("rollBackChanges");

        /* Loop through the items in the list */
        for (PrometheusEditEntry<?> myEntry : theMap.values()) {
            /* Note the new step */
            mySubTask.startTask(myEntry.getName());

            /* RollBack changes for the entry */
            myEntry.rollBackChanges();
        }

        /* Complete the task */
        mySubTask.end();
    }

    /**
     * Has this UpdateSet got updates?
     *
     * @return true/false
     */
    public boolean hasUpdates() {
        /* We have changes if version is non-zero */
        return theVersion != 0;
    }

    /**
     * Has this UpdateSet got errors?
     *
     * @return true/false
     */
    public boolean hasErrors() {
        /* Loop through the items in the list */
        for (PrometheusEditEntry<?> myEntry : theMap.values()) {
            /* Access entry */
            final PrometheusDataList<?> myDataList = myEntry.getDataList();

            /* Determine whether there are errors */
            if (myDataList != null && myDataList.hasErrors()) {
                return true;
            }
        }

        /* Return to caller */
        return false;
    }

    /**
     * Validate the updateSet.
     */
    public void validate() {
        /* Obtain the active profile */
        final OceanusProfile myTask = theControl.getActiveTask();
        final OceanusProfile mySubTask = myTask.startTask("validate");

        /* Loop through the items in the list */
        for (PrometheusEditEntry<?> myEntry : theMap.values()) {
            /* Access list */
            final PrometheusDataList<?> myDataList = myEntry.getDataList();

            /* if list exists */
            if (myDataList != null) {
                /* Note the new step */
                mySubTask.startTask(myDataList.listName());

                /* Validate */
                myDataList.validate();
            }
        }

        /* Complete the task */
        mySubTask.end();
    }

    /**
     * Get the edit state of this set of tables.
     *
     * @return the edit state
     */
    public MetisDataEditState getEditState() {
        /* Loop through the items in the list */
        final Iterator<PrometheusEditEntry<?>> myIterator = theMap.values().iterator();
        MetisDataEditState myState = MetisDataEditState.CLEAN;
        while (myIterator.hasNext()) {
            /* Access list */
            final PrometheusEditEntry<?> myEntry = myIterator.next();
            final PrometheusDataList<?> myDataList = myEntry.getDataList();

            /* Combine states if list exists */
            if (myDataList != null) {
                myState = myState.combineState(myDataList.getEditState());
            }
        }

        /* Return the state */
        return myState;
    }

    /**
     * Process Save command.
     *
     * @param pCmd   the command.
     * @param pError the error panel
     */
    public void processCommand(final PrometheusUIEvent pCmd,
                               final MetisErrorPanel pError) {
        /* Create a new profile */
        final OceanusProfile myTask = theControl.getNewProfile("EditCommand");

        /* Switch on command */
        switch (pCmd) {
            case OK:
                applyChanges();
                break;
            case UNDO:
                undoLastChange();
                break;
            case RESET:
                resetChanges();
                break;
            default:
                break;
        }

        /* Access any error */
        final MetisViewerErrorList myErrors = theControl.getErrors();

        /* Show the error */
        if (!myErrors.isEmpty()) {
            pError.setErrors(myErrors);
        }

        /* Complete the task */
        myTask.end();
    }

    /**
     * Process Edit command.
     *
     * @param pCmd     the command.
     * @param pVersion the version
     * @param pError   the error panel
     */
    public void processEditCommand(final PrometheusUIEvent pCmd,
                                   final int pVersion,
                                   final MetisErrorPanel pError) {
        /* Create a new profile */
        final OceanusProfile myTask = theControl.getNewProfile("ItemCommand");

        /* Switch on command */
        switch (pCmd) {
            case OK:
                condenseHistory(pVersion);
                break;
            case UNDO:
                undoLastChange();
                break;
            case REWIND:
                rewindToVersion(pVersion);
                break;
            default:
                break;
        }

        /* Access any error */
        final MetisViewerErrorList myErrors = theControl.getErrors();

        /* Show the error */
        if (!myErrors.isEmpty()) {
            pError.setErrors(myErrors);
        }

        /* Complete the task */
        myTask.end();
    }

    /**
     * Condense history.
     *
     * @param pNewVersion the new maximum version
     */
    private void condenseHistory(final int pNewVersion) {
        /* Obtain the active profile */
        final OceanusProfile myTask = theControl.getActiveTask();
        final OceanusProfile mySubTask = myTask.startTask("condenseHistory");

        /* Loop through the items in the list */
        for (PrometheusEditEntry<?> myEntry : theMap.values()) {
            /* Access list */
            final PrometheusDataList<?> myDataList = myEntry.getDataList();

            /* Condense history in the list */
            if (myDataList != null) {
                /* Note the new step */
                final OceanusProfile myListTask = mySubTask.startTask(myDataList.listName());
                myListTask.startTask("Condense");

                /* Condense history */
                myDataList.condenseHistory(pNewVersion);

                /* postProcess */
                if (Boolean.FALSE.equals(itemEditing)) {
                    myListTask.startTask("postProcess");
                    myDataList.postProcessOnUpdate();
                }
            }
        }

        /* Store version */
        theVersion = pNewVersion;

        /* Fire that we have added to updateSet */
        theEventManager.fireEvent(PrometheusDataEvent.DATACHANGED);

        /* Complete the task */
        mySubTask.end();
    }
}