package CH.ifa.draw.framework;

import java.awt.EventQueue;
import java.lang.reflect.InvocationTargetException;
import java.util.Hashtable;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

import de.renew.draw.storables.ontology.Drawing;
import de.renew.draw.ui.api.UndoRedoApi;
import de.renew.draw.ui.ontology.DrawingEditor;
import de.renew.draw.ui.ontology.undoredo.SnapshotHistory;

/**
 * Manages undo and redo histories for each drawing.
 * <p>
 * The undo support works as follows:
 * </p>
 * <p>
 * If a drawing is created/loaded, an undo history
 * should immediately be created by {@link #newUndoHistory}.
 * The {@link CH.ifa.draw.application.DrawApplication}
 * does this by default when a drawing is added.
 * </p>
 * <p>
 * Each action/command/tool/handle/... which modifies
 * the drawing should <UL>
 * <LI> prepare an undo snapshot <b>before</b> the modifications
 *      are applied to the drawing ({@link #prepareUndoSnapshot}) </LI>
 * <LI> commit the snapshot after the changes took place
 *      ({@link #commitUndoSnapshot}) </LI>
 * <LI> just omit the 2nd step if no changes were made. </LI>
 * </UL>
 * <p>
 * For commands and tools, there exist classes which can be
 * inherited ({@link UndoableCommand}, {@link CH.ifa.draw.standard.UndoableTool}).
 * They provide a default behavior which implements the steps above.
 * For Handles, the implementation of {@link CH.ifa.draw.standard.AbstractHandle}
 * also provides a default undo support.
 * </p>
 * <p>
 * Undo and redo snapshots are restored by the methods
 * {@link #restoreUndoSnapshot} and {@link #restoreRedoSnapshot}.
 * The {@link CH.ifa.draw.standard.UndoRedoCommand}
 * uses these methods.
 * </p>
 * UndoRedoManager.java
 * Created: Wed Jan 31  2001
 * @author Michael Duvigneau
 * @author Julia Hagemeister
 * @deprecated This class is not to be used externally. Please use the {@link de.renew.draw.ui.api.UndoRedoApi} instead.
 */
@Deprecated(since = "5.0", forRemoval = true)
@SuppressWarnings("removal")
public class UndoRedoManager implements de.renew.draw.ui.ontology.undoredo.UndoRedoManager {

    /**
     * The maximum number of actions which can be undone.
     * This value is used twice per each drawing (undo and redo).
     **/
    private static final int UNDOSTEPS = 20;

    /**
     * The time in which changes are accumulated in milliseconds (only for
     * accumulated snapshots).
     */
    private static final int ACCUMULATIONTIME = 800;

    /**
     * Contains a SnapshotHistory-Object for each drawing.
     * <p>
     * The history object should be instantiated and added to
     * the table immediately when a drawing is added.
     * </p>
     **/
    private final Hashtable<Drawing, SnapshotHistory> _undoHistoryTable = new Hashtable<>();

    /**
     * Contains a SnapshotHistory-Object for each drawing.
     * {@link #_undoHistoryTable}
     **/
    private final Hashtable<Drawing, SnapshotHistory> _redoHistoryTable = new Hashtable<>();

    /**
     * Refers to the drawing editor, needed to give feedback to the user.
     **/
    private final DrawingEditor _editor;

    /**
     * futures of commit tasks for accumulated snapshots
     */
    private final Hashtable<Drawing, ScheduledFuture<?>> _accumulationCommitFutures =
        new Hashtable<>();

    /**
     * scheduler for accumulated snapshots
     */
    private final ScheduledExecutorService _scheduledThreadPool;
    private static final org.apache.log4j.Logger LOGGER =
        org.apache.log4j.Logger.getLogger(UndoRedoManager.class);

    /**
     * Init. an undo redo manager to manage these operations.
     * @param editor an instance of DrawingEditor
     * @deprecated This constructor is not to be used externally and will later be hidden.
     */
    @Deprecated(since = "5.0", forRemoval = true)
    public UndoRedoManager(DrawingEditor editor) {
        this._editor = editor;
        this._scheduledThreadPool = Executors.newScheduledThreadPool(0);
    }

    /**
     * Takes a snapshot of the given drawing and
     * remembers it until it will be added by {@link #commitUndoSnapshot}.
     * Any previously prepared snapshot will be forgotten.
     * @param drawing drawing to snapshotted
     * @deprecated This method is not to be used externally.
     * Please use the method {@link de.renew.draw.ui.api.UndoRedoApi#prepareUndoSnapshot(Drawing)} instead.
     * If you want to prepare the undo snapshot for the current drawing, use {@link de.renew.draw.ui.api.UndoRedoApi#prepareUndoSnapshot()} instead.
     */
    @Deprecated(since = "5.0", forRemoval = true)
    public void prepareUndoSnapshot(Drawing drawing) {
        commitPendingAccumulatedUndoSnapshot(drawing);
        SnapshotHistory undoHistory = getUndoHistory(drawing);
        if (undoHistory != null) {
            undoHistory.prepareSnapshot();
        }
    }

    /**
     * Takes the last prepared snapshot and
     * appends it to the undo history of the given drawing.
     * The redo history is cleared.
     * @deprecated This method is not to be used externally.
     * Please use the method {@link UndoRedoApi#commitUndoSnapshot(Drawing)} instead.
     * If you want to commit the undo snapshot for the current drawing, use {@link UndoRedoApi#commitUndoSnapshot()} instead.
     */
    @Deprecated(since = "5.0", forRemoval = true)
    public void commitUndoSnapshot(Drawing drawing) {
        SnapshotHistory undoHistory = getUndoHistory(drawing);
        SnapshotHistory redoHistory = getRedoHistory(drawing);
        if (undoHistory != null) {
            undoHistory.commitSnapshot();
            redoHistory.clear();
            _editor.menuStateChanged();
        }
    }

    /**
     * Restores the drawing to the state saved by the last
     * call to {@link #commitUndoSnapshot}.
     * Additional calls to this method will restore more
     * undo snapshots step by step, until the history is empty.
     * @param drawing drawing whose undo history should be used for restoration.
     * The effect can be undone by a call to {@link #restoreUndoSnapshot}.
     * @deprecated This method is not to be used externally.
     * Please use the method {@link UndoRedoApi#restoreUndoSnapshot(Drawing)} instead.
     * If you want to prepare the undo snapshot for the current drawing, use {@link UndoRedoApi#restoreUndoSnapshot()} instead.
     */
    @Deprecated(since = "5.0", forRemoval = true)
    public void restoreUndoSnapshot(Drawing drawing) {
        SnapshotHistory undoHistory = getUndoHistory(drawing);
        SnapshotHistory redoHistory = getRedoHistory(drawing);
        commitPendingAccumulatedUndoSnapshot(drawing);
        if (!undoHistory.isEmpty()) {
            redoHistory.takeSnapshot();
            undoHistory.restoreSnapshot();

            /* editor.menuStateChanged(); not needed because drawing changed*/
            _editor.showStatus("Undone.");
        } else {
            _editor.showStatus("Nothing to undo.");
        }
    }

    /**
     * Restores the drawing to the state it had before the last undo.
     * @param drawing drawing to be restored
     * @deprecated This method is not to be used externally and will later be hidden.
     */
    @Deprecated(since = "5.0", forRemoval = true)
    public void restoreRedoSnapshot(Drawing drawing) {
        SnapshotHistory undoHistory = getUndoHistory(drawing);
        SnapshotHistory redoHistory = getRedoHistory(drawing);
        commitPendingAccumulatedUndoSnapshot(drawing);
        if (redoHistory != null) {
            if (!redoHistory.isEmpty()) {
                undoHistory.takeSnapshot();
                redoHistory.restoreSnapshot();

                // editor.menuStateChanged(); not needed because drawing changed
                _editor.showStatus("Redone.");
            } else {
                _editor.showStatus("Nothing to redo.");
            }
        }
    }

    /**
     * Returns the undo history for the given drawing.
     * May return <code>null</code> if there is no history kept for the drawing.
     * @param drawing drawing with undo history
     * @return undo history for given drawing
     * @deprecated This method is not to be used externally and will later be hidden.
     */
    @Deprecated(since = "5.0", forRemoval = true)
    public SnapshotHistory getUndoHistory(Drawing drawing) {
        return _undoHistoryTable.get(drawing);
    }

    /**
     * Returns the redo history for the given drawing.
     * May return <code>null</code> if there is no history kept for the drawing.
     * @param drawing drawing with redo history
     * @return the redo history of given drawing
     * @deprecated This method is not to be used externally and will later be hidden.
     */
    @Deprecated(since = "5.0", forRemoval = true)
    public SnapshotHistory getRedoHistory(Drawing drawing) {
        return _redoHistoryTable.get(drawing);
    }

    /**
     * Clears undo <b>and</b> redo history for the given drawing.
     * @param drawing drawing with undo and redo history
     * @deprecated This method is not to be used externally.
     * Please use the method {@link UndoRedoApi#clearUndoRedoHistory(Drawing)} instead.
     * If you want to prepare the undo snapshot for the current drawing, use {@link UndoRedoApi#clearUndoRedoHistory()}  instead.
     */
    @Deprecated(since = "5.0", forRemoval = true)
    public void clearUndoHistory(Drawing drawing) {
        SnapshotHistory undoHistory = _undoHistoryTable.get(drawing);
        SnapshotHistory redoHistory = _redoHistoryTable.get(drawing);
        if (undoHistory != null) {
            undoHistory.clear();
            redoHistory.clear();
            _editor.menuStateChanged();
        }
        cancelPendingAccumulatedUndoSnapshot(drawing);
    }

    /**
     * Enables the undo/redo history management for the given drawing.
     * @param drawing drawing given
     * @deprecated This method is not to be used externally and will later be hidden.
     */
    @Deprecated(since = "5.0", forRemoval = true)
    public void newUndoHistory(Drawing drawing) {
        SnapshotHistory undoHistory = _undoHistoryTable.get(drawing);
        if (undoHistory == null) {
            _undoHistoryTable
                .put(drawing, new CH.ifa.draw.framework.SnapshotHistory(drawing, UNDOSTEPS));
            _redoHistoryTable
                .put(drawing, new CH.ifa.draw.framework.SnapshotHistory(drawing, UNDOSTEPS));
        }
    }

    /**
     * Prohibits the management of undo and redo snapshots
     * for the given drawing.
     * @param drawing drawing with enabled management
     * @deprecated This method is not to be used externally and will later be hidden.
     */
    @Deprecated(since = "5.0", forRemoval = true)
    public void removeUndoHistory(Drawing drawing) {
        _undoHistoryTable.remove(drawing);
        _redoHistoryTable.remove(drawing);
        cancelPendingAccumulatedUndoSnapshot(drawing);
        _editor.menuStateChanged();
    }

    /**
     * If an accumulated undo snapshot is pending, this method cancels it.
     * The method has no effect otherwise.
     */
    private void cancelPendingAccumulatedUndoSnapshot(Drawing drawing) {
        if (_accumulationCommitFutures.containsKey(drawing)) {
            ScheduledFuture<?> future = _accumulationCommitFutures.remove(drawing);
            future.cancel(true);
        }
    }

    /**
     * Check if an accumulated undo snapshot is pending.
     */
    private boolean isAccumulatedUndoSnapshotPending(Drawing drawing) {
        boolean result = false;
        if (_accumulationCommitFutures.containsKey(drawing)) {
            ScheduledFuture<?> future = _accumulationCommitFutures.get(drawing);
            if (!future.isCancelled() && !future.isDone()) {
                result = true;
            }
        }
        return result;
    }

    /**
     * Commits a pending accumulated undo snapshot if it's pending. Does
     * nothing if no commit is pending.
     * @param drawing drawing with snapshot made
     */
    private void commitPendingAccumulatedUndoSnapshot(Drawing drawing) {
        if (isAccumulatedUndoSnapshotPending(drawing)) {
            cancelPendingAccumulatedUndoSnapshot(drawing);
            commitUndoSnapshot(drawing);
        }
    }

    /**
     * Prepares an undo snapshot only if no snapshot commit task is pending.
     * @param drawing drawing with snapshot
     * @deprecated This method is not to be used externally and will later be hidden.
     */
    @Deprecated(since = "5.0", forRemoval = true)
    public void prepareAccumulatedUndoSnapshot(Drawing drawing) {
        if (!isAccumulatedUndoSnapshotPending(drawing)) {
            SnapshotHistory undoHistory = getUndoHistory(drawing);
            if (undoHistory != null) {
                undoHistory.prepareSnapshot();
            }
        }
    }

    /**
     * Triggers an accumulated undo snapshot for scheduled commitment. If this
     * method is called multiple times in a certain time range
     * ({@link CH.ifa.draw.framework.UndoRedoManager#ACCUMULATIONTIME}) a
     * previously scheduled commit task is canceled.
     * @param drawing drawing with snapshot
     * @deprecated This method is not to be used externally and will later be hidden.
     */
    @Deprecated(since = "5.0", forRemoval = true)
    public void triggerAccumulatedUndoSnapshot(final Drawing drawing) {
        cancelPendingAccumulatedUndoSnapshot(drawing);
        final Runnable commitSnapshotTask = () -> {
            commitUndoSnapshot(drawing);
            LOGGER.debug("Commiting accumulated snapshot.");
        };
        Runnable awtCommitSnapshotTask = () -> {
            try {
                EventQueue.invokeAndWait(commitSnapshotTask);
            } catch (InvocationTargetException ite) {
                Throwable targetException = ite.getTargetException();
                if (targetException instanceof RuntimeException) {
                    throw (RuntimeException) targetException;
                }
            } catch (InterruptedException e) {
                LOGGER.debug("Scheduled accumulated snapshot task was canceled.");
            }
        };

        ScheduledFuture<?> future = _scheduledThreadPool
            .schedule(awtCommitSnapshotTask, ACCUMULATIONTIME, TimeUnit.MILLISECONDS);
        _accumulationCommitFutures.put(drawing, future);
    }
}