/*******************************************************************************
 * Copyright (c) 2011, 2018 Wind River Systems and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     Wind River Systems - initial API and implementation
 *     IBM Corporation - bug fixing
 *******************************************************************************/
package org.eclipse.debug.internal.ui.viewers.model;

import java.io.IOException;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.ISafeRunnable;
import org.eclipse.core.runtime.ListenerList;
import org.eclipse.core.runtime.SafeRunner;
import org.eclipse.debug.internal.ui.DebugUIPlugin;
import org.eclipse.debug.internal.ui.viewers.model.provisional.IElementCompareRequest;
import org.eclipse.debug.internal.ui.viewers.model.provisional.IElementMementoProvider;
import org.eclipse.debug.internal.ui.viewers.model.provisional.IElementMementoRequest;
import org.eclipse.debug.internal.ui.viewers.model.provisional.IModelDelta;
import org.eclipse.debug.internal.ui.viewers.model.provisional.IModelDeltaVisitor;
import org.eclipse.debug.internal.ui.viewers.model.provisional.IStateUpdateListener;
import org.eclipse.debug.internal.ui.viewers.model.provisional.ITreeModelViewer;
import org.eclipse.debug.internal.ui.viewers.model.provisional.IViewerUpdate;
import org.eclipse.debug.internal.ui.viewers.model.provisional.IViewerUpdateListener;
import org.eclipse.debug.internal.ui.viewers.model.provisional.ModelDelta;
import org.eclipse.jface.viewers.ITreeSelection;
import org.eclipse.jface.viewers.TreePath;
import org.eclipse.jface.viewers.TreeSelection;
import org.eclipse.ui.IMemento;
import org.eclipse.ui.XMLMemento;

/**
 * Class containing logic to save and restore expanded state of the tree model
 * viewer.
 * <p>
 * When the input to the viewer is changes, the tree model viewer attempts to
 * save the expansion state of elements as well as currently selected element and
 * scroll position.  Each expanded element is queried for its memento and all the
 * collected mementos are saved into a delta tree then serialized by the viewer.<br>
 * When a new input is set to the viewer, the viewer compares the input's memento
 * with the stored mementos and if a match is found, it attempts to restore the
 * previous expansion state to the viewer.  As elements are revealed and realized
 * in the viewer, the element's memento is compared against the memento stored in
 * the saved state delta.  If matching elements are found in the delta, the expansion
 * and selection state is then restored to those elements.
 * </p><p>
 * Additionally to saving restoring state on input change, the viewer also
 * saves/restores elements' state when the model requests viewer to refresh model
 * structure.  Since the viewer items are matched to the model elements using items'
 * indexes, inserting or removing elements in model can cause the expansion state
 * of elements to shift after a refresh.  To compensate for this, the viewer saves
 * the elements before a refresh is performed into a delta, but without encoding
 * elements using mementos.  As the refresh of the tree progresses, the save state
 * is restored to the tree and elements are expanded or collapsed as needed to
 * compensate for changes in model structure.
 * </p>
 * @see TreeModelContentProvider
 */
class ViewerStateTracker {

    // State update type constants used in notifying listeners
    static final int STATE_SAVE_SEQUENCE_BEGINS = 4;
    static final int STATE_SAVE_SEQUENCE_COMPLETE = 5;
    static final int STATE_RESTORE_SEQUENCE_BEGINS = 6;
    static final int STATE_RESTORE_SEQUENCE_COMPLETE = 7;

    /**
     * Dummy marker element used in the state delta. The marker indicates that a
     * given element in the pending state delta has been removed. It replaces
     * the original element so that it may optionally be garbage collected.
     */
    private final static String ELEMENT_REMOVED = "ELEMENT_REMOVED"; //$NON-NLS-1$

    /**
     * Collector of memento encoding requests.
     */
    interface IElementMementoCollector {

        /**
         * Adds the request to this manager.
         *
         * @param request to add
         */
        void addRequest(ElementMementoRequest request);

        /**
         * Notification the request is complete.
         *
         * @param request that was completed
         */
        void requestComplete(ElementMementoRequest request);

        /**
         * Process the queued requests. Accepts no more new requests.
         */
        void processReqeusts();

        /**
         * Cancels the requests in progress.
         */
        void cancel();
    }

    /**
     * LRU cache for viewer states
     */
	class LRUMap<K, V> extends LinkedHashMap<K, V> {
        private static final long serialVersionUID = 1L;

        private int fMaxSize;

        LRUMap(int maxSize) {
            super();
            fMaxSize = maxSize;
        }

        @Override
		protected boolean removeEldestEntry(Entry<K, V> eldest) {
            return size() > fMaxSize;
        }
    }

    /**
     * Content provider that is using this state tracker.
     */
    private TreeModelContentProvider fContentProvider;

    ViewerStateTracker(TreeModelContentProvider contentProvider) {
        fContentProvider = contentProvider;
    }

    /**
     * Map of viewer states keyed by viewer input mementos
     */
	private Map<String, ModelDelta> fViewerStates = new LRUMap<>(20);

    /**
     * Pending viewer state to be restored
     */
    private ModelDelta fPendingState = null;

    /**
     * Flag indicating that the content provider is performing
     * state restore operations.
     */
    private boolean fInStateRestore = false;

    /**
     * State update listeners
     */
	private ListenerList<IStateUpdateListener> fStateUpdateListeners = new ListenerList<>();

    /**
     * Postpone restoring REVEAL element until the current updates are complete.
     * See bug 324100
     */
    protected PendingRevealDelta fPendingSetTopItem = null;

    /**
     * Set of IMementoManager's that are currently saving state
     */
	private Set<IElementMementoCollector> fPendingStateSaves = new HashSet<>();

    /**
     * Used to queue a viewer input for state restore
     */
    private Object fQueuedRestore = null;

    /**
     * Object used to key compare requests in map.
     */
    private static class CompareRequestKey {

        CompareRequestKey(TreePath path, IModelDelta delta) {
            fPath = path;
            fDelta = delta;
        }

        TreePath fPath;
        IModelDelta fDelta;

        @Override
		public boolean equals(Object obj) {
            if (obj instanceof CompareRequestKey) {
                CompareRequestKey key = (CompareRequestKey) obj;
                return key.fDelta.equals(fDelta) && key.fPath.equals(fPath);
            }
            return false;
        }

        @Override
		public int hashCode() {
            return fDelta.hashCode() + fPath.hashCode();
        }
    }

    /**
     * Compare requests that are currently running.
     */
	private Map<CompareRequestKey, ElementCompareRequest> fCompareRequestsInProgress = new LinkedHashMap<>();


    /**
     * Cancels pending updates.
     */
    void dispose() {
        Assert.isTrue( fContentProvider.getViewer().getDisplay().getThread() == Thread.currentThread() );
		for (IElementMementoCollector emc : fPendingStateSaves) {
			emc.cancel();
        }
        fStateUpdateListeners.clear();
		for (ElementCompareRequest ecr : fCompareRequestsInProgress.values()) {
			ecr.cancel();
        }
        fCompareRequestsInProgress.clear();

        if (fPendingSetTopItem != null) {
            fPendingSetTopItem.dispose();
        }
    }

    /**
     * Restores viewer state for the given input
     *
     * @param input
     *            viewer input
     */
    private void startRestoreViewerState(final Object input) {
        Assert.isTrue( fContentProvider.getViewer().getDisplay().getThread() == Thread.currentThread() );

        fPendingState = null;
        final IElementMementoProvider defaultProvider = ViewerAdapterService.getMementoProvider(input);
        if (defaultProvider != null) {
            // build a model delta representing expansion and selection state
            final ModelDelta delta = new ModelDelta(input, IModelDelta.NO_CHANGE);
            final XMLMemento inputMemento = XMLMemento.createWriteRoot("VIEWER_INPUT_MEMENTO"); //$NON-NLS-1$
            final IElementMementoCollector manager = new IElementMementoCollector() {

                private IElementMementoRequest fRequest;

                /*
                 * (non-Javadoc)
                 *
                 * @see
                 * org.eclipse.debug.internal.ui.viewers.model.provisional.viewers
                 * .
                 * IMementoManager#requestComplete(org.eclipse.debug.internal.ui
                 * .viewers.model.provisional.IElementMementoRequest)
                 */
                @Override
				public void requestComplete(ElementMementoRequest request) {
                    if (fContentProvider.isDisposed()) {
						return;
					}

                    notifyStateUpdate(input, TreeModelContentProvider.UPDATE_COMPLETE, request);

                    if (!request.isCanceled() && (request.getStatus() == null || request.getStatus().isOK())) {
                        XMLMemento keyMemento = (XMLMemento) delta.getElement();
                        StringWriter writer = new StringWriter();
                        try {
                            keyMemento.save(writer);
                            final String keyMementoString = writer.toString();
                            ModelDelta stateDelta = fViewerStates.get(keyMementoString);
                            if (stateDelta != null) {
                                if (DebugUIPlugin.DEBUG_STATE_SAVE_RESTORE && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(fContentProvider.getPresentationContext()))  {
                                	DebugUIPlugin.trace("STATE RESTORE INPUT COMARE ENDED : " + fRequest + " - MATCHING STATE FOUND"); //$NON-NLS-1$ //$NON-NLS-2$
                                }

                                // Process start of restore in an async cycle because we may still be inside inputChanged()
                                // call. I.e. the "input.equals(fContentProvider.getViewer().getInput())" test may fail.
								fContentProvider.getViewer().getDisplay().asyncExec(() -> {
									if (!fContentProvider.isDisposed()
											&& input.equals(fContentProvider.getViewer().getInput())) {
										ModelDelta stateDelta2 = fViewerStates.remove(keyMementoString);
										if (stateDelta2 != null) {
											if (DebugUIPlugin.DEBUG_STATE_SAVE_RESTORE
													&& DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(
															fContentProvider.getPresentationContext())) {
												DebugUIPlugin.trace("STATE RESTORE BEGINS"); //$NON-NLS-1$
												DebugUIPlugin.trace("\tRESTORE: " + stateDelta2.toString()); //$NON-NLS-1$
												notifyStateUpdate(input, STATE_RESTORE_SEQUENCE_BEGINS, null);
											}
											stateDelta2.setElement(input);
											fPendingState = stateDelta2;
											doInitialRestore(fPendingState);
										}
									} else {
										if (DebugUIPlugin.DEBUG_STATE_SAVE_RESTORE
												&& DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(
														fContentProvider.getPresentationContext())) {
											DebugUIPlugin.trace("STATE RESTORE CANCELED."); //$NON-NLS-1$
										}
									}
								});
                            } else {
                                if (DebugUIPlugin.DEBUG_STATE_SAVE_RESTORE && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(fContentProvider.getPresentationContext()))  {
                                	DebugUIPlugin.trace("STATE RESTORE INPUT COMARE ENDED : " + fRequest + " - NO MATCHING STATE"); //$NON-NLS-1$ //$NON-NLS-2$
                                }
                            }
                        } catch (IOException e) {
                            DebugUIPlugin.log(e);
                        }
                    } else {
                        notifyStateUpdate(input, STATE_RESTORE_SEQUENCE_BEGINS, null);
                    }
                }

                /*
                 * (non-Javadoc)
                 *
                 * @see
                 * org.eclipse.debug.internal.ui.viewers.model.provisional.viewers
                 * .IMementoManager#processReqeusts()
                 */
                @Override
				public void processReqeusts() {
                    notifyStateUpdate(input, STATE_RESTORE_SEQUENCE_BEGINS, null);
                    if (DebugUIPlugin.DEBUG_STATE_SAVE_RESTORE && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(fContentProvider.getPresentationContext())) {
                    	DebugUIPlugin.trace("STATE RESTORE INPUT COMARE BEGIN : " + fRequest); //$NON-NLS-1$
                    }
                    notifyStateUpdate(input, TreeModelContentProvider.UPDATE_BEGINS, fRequest);
                    defaultProvider.encodeElements(new IElementMementoRequest[] { fRequest });
                }

                /*
                 * (non-Javadoc)
                 *
                 * @see
                 * org.eclipse.debug.internal.ui.viewers.model.provisional.viewers
                 * .
                 * IMementoManager#addRequest(org.eclipse.debug.internal.ui.viewers
                 * .model.provisional.IElementMementoRequest)
                 */
                @Override
				public void addRequest(ElementMementoRequest req) {
                    fRequest = req;
                }

                @Override
				public void cancel() {
                    // not used
                }

            };
            manager.addRequest(
                new ElementMementoRequest(fContentProvider, fContentProvider.getViewer().getInput(), manager,
                    delta.getElement(), fContentProvider.getViewerTreePath(delta), inputMemento, delta));
            manager.processReqeusts();
        } else {
            if (DebugUIPlugin.DEBUG_STATE_SAVE_RESTORE && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(fContentProvider.getPresentationContext()))  {
            	DebugUIPlugin.trace("STATE RESTORE: No input memento provider"); //$NON-NLS-1$
            }
        }
    }

    /**
     * Appends the state of the given subtree to the current pending delta.
     * @param path Path to subtree to restore.
     */
    void appendToPendingStateDelta(final TreePath path) {
        if (fContentProvider.getViewer() == null)
		 {
			return; // Not initialized yet.
		}

        if (DebugUIPlugin.DEBUG_STATE_SAVE_RESTORE && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(fContentProvider.getPresentationContext()))  {
        	DebugUIPlugin.trace("STATE APPEND BEGIN: " + path.getLastSegment()); //$NON-NLS-1$
        }

        // build a model delta representing expansion and selection state
        final ModelDelta appendDeltaRoot = new ModelDelta(fContentProvider.getViewer().getInput(), IModelDelta.NO_CHANGE);
        ModelDelta delta = appendDeltaRoot;
        for (int i = 0; i < path.getSegmentCount(); i++) {
            delta = delta.addNode(path.getSegment(i), IModelDelta.NO_CHANGE);
        }

        if (!fContentProvider.getViewer().saveElementState(path, delta, IModelDelta.COLLAPSE | IModelDelta.EXPAND | IModelDelta.SELECT)) {
            // Path to save the state was not found or there was no
            // (expansion) state to save!  Abort.
            if (DebugUIPlugin.DEBUG_STATE_SAVE_RESTORE && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(fContentProvider.getPresentationContext())) {
            	DebugUIPlugin.trace("STATE APPEND CANCEL: Element " + path.getLastSegment() + " not found."); //$NON-NLS-1$ //$NON-NLS-2$
            }
            return;
        }

        // Append a marker CONTENT flag to all the delta nodes that contain the
        // EXPAND node. These
        // markers are used by the restore logic to know when a delta node can
        // be removed.
		delta.accept((d, depth) -> {
			if ((d.getFlags() & IModelDelta.EXPAND) != 0) {
				((ModelDelta) d).setFlags(d.getFlags() | IModelDelta.CONTENT);
			}
			return true;
		});

        if (DebugUIPlugin.DEBUG_STATE_SAVE_RESTORE && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(fContentProvider.getPresentationContext())) {
        	DebugUIPlugin.trace("\tAPPEND DELTA: " + appendDeltaRoot); //$NON-NLS-1$
        }

        if (fPendingState != null) {
            // If the restore for the current input was never completed,
            // preserve
            // that restore along with the restore that was completed.
            if (DebugUIPlugin.DEBUG_STATE_SAVE_RESTORE && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(fContentProvider.getPresentationContext())) {
            	DebugUIPlugin.trace("\tAPPEND OUTSTANDING RESTORE: " + fPendingState); //$NON-NLS-1$
            }

            // If the append delta is generated for a sub-tree, copy the pending delta
            // attributes into the pending delta.
            if (path.getSegmentCount() > 0) {
				fPendingState.accept((pendingDeltaNode, depth) -> {
					TreePath pendingDeltaPath = fContentProvider.getViewerTreePath(pendingDeltaNode);
					if (path.startsWith(pendingDeltaPath, null)) {
						ModelDelta appendDelta = findDeltaForPath(appendDeltaRoot, pendingDeltaPath);
						appendDelta.setFlags(pendingDeltaNode.getFlags());
						appendDelta.setChildCount(pendingDeltaNode.getChildCount());
						appendDelta.setIndex(pendingDeltaNode.getIndex());
						return true;
					}
					return false;
				});
            }

            // Copy the pending state into the new appended state.
			fPendingState.accept((pendingDeltaNode, depth) -> {
				// Skip the top element
				if (pendingDeltaNode.getParentDelta() == null) {
					return true;
				}

				// Find the node in the save delta which is the parent
				// of to the current pending delta node.
				// If the parent node cannot be found, it means that
				// most likely the user collapsed the parent node before
				// the children were ever expanded.
				// If the pending state node already exists in the parent
				// node, it is already processed and we can skip it.
				// If the pending state node does not contain any flags,
				// we can also skip it.
				ModelDelta saveDeltaNode = findSubDeltaParent(appendDeltaRoot, pendingDeltaNode);
				if (saveDeltaNode != null && !isDeltaInParent(pendingDeltaNode, saveDeltaNode)
						&& pendingDeltaNode.getFlags() != IModelDelta.NO_CHANGE) {
					saveDeltaNode.setChildCount(pendingDeltaNode.getParentDelta().getChildCount());
					copyIntoDelta(pendingDeltaNode, saveDeltaNode);
				} else {
					if (DebugUIPlugin.DEBUG_STATE_SAVE_RESTORE
							&& DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(fContentProvider.getPresentationContext())) {
						DebugUIPlugin.trace("\tSKIPPED: " + pendingDeltaNode.getElement()); //$NON-NLS-1$
					}
				}

				// If the pending delta node has a memento element, its
				// children should also be mementos therefore the copy
				// delta operation should have added all the children
				// of this pending delta node into the save delta.
				if (pendingDeltaNode.getElement() instanceof IMemento) {
					return false;
				} else {
					return pendingDeltaNode.getChildCount() > 0;
				}
			});
        }

        if (appendDeltaRoot.getChildDeltas().length > 0) {
            // Set the new delta root as the pending state delta.
            if (fPendingState == null) {
                notifyStateUpdate(appendDeltaRoot.getElement(), STATE_RESTORE_SEQUENCE_BEGINS, null);
            }
            fPendingState = appendDeltaRoot;
            if (DebugUIPlugin.DEBUG_STATE_SAVE_RESTORE && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(fContentProvider.getPresentationContext())) {
            	DebugUIPlugin.trace("STATE APPEND COMPLETE " + fPendingState); //$NON-NLS-1$
            }
        } else {
            if (DebugUIPlugin.DEBUG_STATE_SAVE_RESTORE && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(fContentProvider.getPresentationContext())) {
            	DebugUIPlugin.trace("STATE APPEND CANCELED: No Data"); //$NON-NLS-1$
            }
        }
    }

    /**
     * Saves the viewer's state for the previous input. * @param oldInput
     * @param input the {@link ModelDelta} input
     */
    protected void saveViewerState(Object input) {
		for (Iterator<ElementCompareRequest> itr = fCompareRequestsInProgress.values().iterator(); itr.hasNext();) {
            itr.next().cancel();
            itr.remove();
        }

        IElementMementoProvider stateProvider = ViewerAdapterService.getMementoProvider(input);
        if (stateProvider != null) {
            if (DebugUIPlugin.DEBUG_STATE_SAVE_RESTORE && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(fContentProvider.getPresentationContext())) {
            	DebugUIPlugin.trace("STATE SAVE BEGIN: " + input); //$NON-NLS-1$
            }

            // build a model delta representing expansion and selection state
            final ModelDelta saveDeltaRoot = new ModelDelta(input, IModelDelta.NO_CHANGE);
            buildViewerState(saveDeltaRoot);
            if (DebugUIPlugin.DEBUG_STATE_SAVE_RESTORE && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(fContentProvider.getPresentationContext())) {
            	DebugUIPlugin.trace("\tSAVE DELTA FROM VIEW:\n" + saveDeltaRoot); //$NON-NLS-1$
            }

            // check if pending restore reveal
            if (fPendingSetTopItem != null) {
                // set back the pending reveal flag
                ModelDelta revealDelta = fPendingSetTopItem.getDelta();
                revealDelta.setFlags(revealDelta.getFlags() | IModelDelta.REVEAL);

                fPendingSetTopItem.dispose();

                ModelDelta saveDeltaNode = findSubDeltaParent(saveDeltaRoot, revealDelta);
                if (saveDeltaNode != null) {
                    clearRevealFlag(saveDeltaRoot);
            		ModelDelta child = saveDeltaNode.getChildDelta(revealDelta.getElement(), revealDelta.getIndex());
            		if (child != null) {
            		    child.setFlags(child.getFlags() | IModelDelta.REVEAL);
            		} else {
                        // the node should be added if not found
                        saveDeltaNode.setChildCount(revealDelta.getParentDelta().getChildCount());
                        copyIntoDelta(revealDelta, saveDeltaNode);
            		}
                } else {
                    if (DebugUIPlugin.DEBUG_STATE_SAVE_RESTORE && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(fContentProvider.getPresentationContext())) {
                    	DebugUIPlugin.trace("\tSKIPPED: " + revealDelta.getElement()); //$NON-NLS-1$
                    }
                }
            }

            if (fPendingState != null) {
                // If the restore for the current input was never completed,
                // preserve
                // that restore along with the restore that was completed.
                if (DebugUIPlugin.DEBUG_STATE_SAVE_RESTORE && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(fContentProvider.getPresentationContext())) {
                	DebugUIPlugin.trace("\tSAVE OUTSTANDING RESTORE: " + fPendingState); //$NON-NLS-1$
                }

				IModelDeltaVisitor pendingStateVisitor = (pendingDeltaNode, depth) -> {
					// Ignore the top element.
					if (pendingDeltaNode.getParentDelta() == null) {
						return true;
					}

					// Find the node in the save delta which is the parent
					// of to the current pending delta node.
					// If the parent node cannot be found, it means that
					// most likely the user collapsed the parent node before
					// the children were ever expanded.
					// If the pending state node already exists in the
					// parent
					// node, it is already processed and we can skip it.
					// If the pending state node does not contain any flags,
					// we can also skip it.
					ModelDelta saveDeltaNode = findSubDeltaParent(saveDeltaRoot, pendingDeltaNode);
					if (saveDeltaNode != null && !isDeltaInParent(pendingDeltaNode, saveDeltaNode)
							&& pendingDeltaNode.getFlags() != IModelDelta.NO_CHANGE) {
						// There should be only one delta element with
						// the REVEAL flag in the entire save delta. The
						// reveal flag in the pending delta trumps the one
						// in the save delta because most likely the restore
						// operation did not yet complete the reveal
						// operation.
						if ((pendingDeltaNode.getFlags() & IModelDelta.REVEAL) != 0) {
							clearRevealFlag(saveDeltaRoot);
						}
						saveDeltaNode.setChildCount(pendingDeltaNode.getParentDelta().getChildCount());
						copyIntoDelta(pendingDeltaNode, saveDeltaNode);
					} else {
						if (DebugUIPlugin.DEBUG_STATE_SAVE_RESTORE && DebugUIPlugin
								.DEBUG_TEST_PRESENTATION_ID(fContentProvider.getPresentationContext())) {
							DebugUIPlugin.trace("\tSKIPPED: " + pendingDeltaNode.getElement()); //$NON-NLS-1$
						}
					}

					// If the pending delta node has a memento element, its
					// children should also be mementos therefore the copy
					// delta operation should have added all the children
					// of this pending delta node into the save delta.
					if (pendingDeltaNode.getElement() instanceof IMemento) {
						return false;
					} else {
						return pendingDeltaNode.getChildCount() > 0;
					}
				};
                fPendingState.accept(pendingStateVisitor);
            }

            if (saveDeltaRoot.getChildDeltas().length > 0) {
                // encode delta with mementos in place of elements, in non-UI
                // thread
                encodeDelta(saveDeltaRoot, stateProvider);
            } else {
                if (DebugUIPlugin.DEBUG_STATE_SAVE_RESTORE && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(fContentProvider.getPresentationContext())) {
                	DebugUIPlugin.trace("STATE SAVE CANCELED, NO DATA"); //$NON-NLS-1$
                }
            }
        }
    }

    private void clearRevealFlag(ModelDelta saveRootDelta) {
		IModelDeltaVisitor clearDeltaVisitor = (delta, depth) -> {
			if ((delta.getFlags() & IModelDelta.REVEAL) != 0) {
				((ModelDelta) delta).setFlags(delta.getFlags() & ~IModelDelta.REVEAL);
			}
			return true;
		};
        saveRootDelta.accept(clearDeltaVisitor);
    }

    private ModelDelta findSubDeltaParent(ModelDelta destinationDeltaRoot, IModelDelta subDelta) {
        // Create a path of elements to the sub-delta.
		LinkedList<IModelDelta> deltaPath = new LinkedList<>();
        IModelDelta delta = subDelta;
        while (delta.getParentDelta() != null) {
            delta = delta.getParentDelta();
            deltaPath.addFirst(delta);
        }

        // For each element in the path of the sub-delta, find the corresponding
        // element in the destination delta
		Iterator<IModelDelta> itr = deltaPath.iterator();
        // Skip the root element
        itr.next();
        ModelDelta saveDelta = destinationDeltaRoot;
        while (saveDelta != null && itr.hasNext()) {
            IModelDelta itrDelta = itr.next();
            saveDelta = saveDelta.getChildDelta(itrDelta.getElement(), itrDelta.getIndex());
        }
        return saveDelta;
    }

    private ModelDelta findDeltaForPath(ModelDelta root, TreePath path) {
        ModelDelta delta = root;
        for (int i = 0; i < path.getSegmentCount(); i++) {
            delta = delta.getChildDelta(path.getSegment(i));
            if (delta == null) {
                return null;
            }
        }
        return delta;
    }

    private boolean isDeltaInParent(IModelDelta delta, ModelDelta destParent) {
        return destParent.getChildDelta(delta.getElement(), delta.getIndex()) != null;
    }

    private void copyIntoDelta(IModelDelta delta, ModelDelta destParent) {
        ModelDelta newDelta = destParent.addNode(delta.getElement(), delta.getIndex(), delta.getFlags(), delta
            .getChildCount());
        for (int i = 0; i < delta.getChildDeltas().length; i++) {
            copyIntoDelta(delta.getChildDeltas()[i], newDelta);
        }
    }

    /**
     * Encodes delta elements into mementos using the given provider.
     * @param rootDelta the {@link ModelDelta} to encode
     * @param defaultProvider the default provider to use when processing the given delta
     *
     */
    protected void encodeDelta(final ModelDelta rootDelta, final IElementMementoProvider defaultProvider) {
        final Object input = rootDelta.getElement();
        final XMLMemento inputMemento = XMLMemento.createWriteRoot("VIEWER_INPUT_MEMENTO"); //$NON-NLS-1$
        final XMLMemento childrenMemento = XMLMemento.createWriteRoot("CHILDREN_MEMENTO"); //$NON-NLS-1$
        final IElementMementoCollector manager = new IElementMementoCollector() {

            /**
             * list of memento fRequests
             */
			private List<IElementMementoRequest> fRequests = new ArrayList<>();

            /**
             * Flag indicating whether the encoding of delta has been canceled.
             */
            private boolean fCanceled = false;

            /*
             * (non-Javadoc)
             *
             * @see
             * org.eclipse.debug.internal.ui.viewers.model.provisional.viewers
             * .IMementoManager
             * #requestComplete(org.eclipse.debug.internal.ui.viewers
             * .model.provisional.IElementMementoRequest)
             */
            @Override
			public void requestComplete(ElementMementoRequest request) {
                Assert.isTrue( fContentProvider.getViewer().getDisplay().getThread() == Thread.currentThread() );

                notifyStateUpdate(input, TreeModelContentProvider.UPDATE_COMPLETE, request);
                if (DebugUIPlugin.DEBUG_STATE_SAVE_RESTORE && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(fContentProvider.getPresentationContext())) {
                	DebugUIPlugin.trace("\tSTATE END: " + request); //$NON-NLS-1$
                }

                if (!request.isCanceled() && (request.getStatus() == null || request.getStatus().isOK())) {
                    boolean requestsComplted = false;
                    if (!fCanceled) {
                        fRequests.remove(request);
                        requestsComplted = fRequests.isEmpty();
                    }
                    if (requestsComplted) {
                        XMLMemento keyMemento = (XMLMemento) rootDelta.getElement();
                        StringWriter writer = new StringWriter();
                        try {
                            keyMemento.save(writer);
                            fViewerStates.put(writer.toString(), rootDelta);
                        } catch (IOException e) {
                            DebugUIPlugin.log(e);
                        }
                        if (DebugUIPlugin.DEBUG_STATE_SAVE_RESTORE && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(fContentProvider.getPresentationContext())) {
                        	DebugUIPlugin.trace("STATE SAVE COMPLETED: " + rootDelta); //$NON-NLS-1$
                        }
                        stateSaveComplete(input, this);
                    }
                } else {
                    cancel();
                }
            }

            @Override
			public void cancel() {
                Assert.isTrue( fContentProvider.getViewer().getDisplay().getThread() == Thread.currentThread() );

                if (fCanceled) {
                    return;
                }

                fCanceled = true;
				for (IElementMementoRequest req : fRequests) {
                    req.cancel();
                }
                fRequests.clear();
                if (DebugUIPlugin.DEBUG_STATE_SAVE_RESTORE && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(fContentProvider.getPresentationContext())) {
                	DebugUIPlugin.trace("STATE SAVE ABORTED: " + rootDelta.getElement()); //$NON-NLS-1$
                }
                stateSaveComplete(input, this);
            }

            /*
             * (non-Javadoc)
             *
             * @see
             * org.eclipse.debug.internal.ui.viewers.model.provisional.viewers
             * .IMementoManager#processReqeusts()
             */
            @Override
			public void processReqeusts() {
                Assert.isTrue( fContentProvider.getViewer().getDisplay().getThread() == Thread.currentThread() );

				Map<IElementMementoProvider, List<IElementMementoRequest>> providers = new HashMap<>();
				for (IElementMementoRequest request : fRequests) {
                    notifyStateUpdate(input, TreeModelContentProvider.UPDATE_BEGINS, request);
                    IElementMementoProvider provider = ViewerAdapterService.getMementoProvider(request.getElement());
                    if (provider == null) {
                        provider = defaultProvider;
                    }
					List<IElementMementoRequest> reqs = providers.get(provider);
                    if (reqs == null) {
						reqs = new ArrayList<>();
                        providers.put(provider, reqs);
                    }
                    reqs.add(request);
                }
				for (Entry<IElementMementoProvider, List<IElementMementoRequest>> entry : providers.entrySet()) {
                    IElementMementoProvider provider = entry.getKey();
					List<IElementMementoRequest> reqs = entry.getValue();
					provider.encodeElements(reqs.toArray(new IElementMementoRequest[reqs.size()]));
                }
            }

            /*
             * (non-Javadoc)
             *
             * @see
             * org.eclipse.debug.internal.ui.viewers.model.provisional.viewers
             * .IMementoManager
             * #addRequest(org.eclipse.debug.internal.ui.viewers.
             * model.provisional.IElementMementoRequest)
             */
            @Override
			public void addRequest(ElementMementoRequest request) {
                Assert.isTrue( fContentProvider.getViewer().getDisplay().getThread() == Thread.currentThread() );

                fRequests.add(request);
            }

        };
		IModelDeltaVisitor visitor = (delta, depth) -> {
			// Add the CONTENT flag to all nodes with an EXPAND flag.
			// During restoring, this flag is used as a marker indicating
			// whether all the content of a given element has been
			// retrieved.
			if ((delta.getFlags() | IModelDelta.EXPAND) != 0) {
				((ModelDelta) delta).setFlags(delta.getFlags() | IModelDelta.CONTENT);
			}

			// This is the root element, save the root element memento in 'inputMemento'.
			if (delta.getParentDelta() == null) {
				manager.addRequest(new ElementMementoRequest(fContentProvider, input, manager, delta.getElement(),
						fContentProvider.getViewerTreePath(delta), inputMemento, (ModelDelta) delta));
			} else {
				// If this is another node element, save the memento to a children memento.
				if (!(delta.getElement() instanceof XMLMemento)) {
					manager.addRequest(new ElementMementoRequest(fContentProvider, input, manager, delta.getElement(),
							fContentProvider.getViewerTreePath(delta), childrenMemento.createChild("CHILD_ELEMENT"), //$NON-NLS-1$
							(ModelDelta) delta));
				}
			}
			return true;
		};
        rootDelta.accept(visitor);
        stateSaveStarted(input, manager);
        manager.processReqeusts();
    }

    /**
     * Called when a state save is starting.
     * @param input the {@link ModelDelta} input
     * @param manager the manager to notify
     */
    private void stateSaveStarted(Object input, IElementMementoCollector manager) {
        Assert.isTrue( fContentProvider.getViewer().getDisplay().getThread() == Thread.currentThread() );

        notifyStateUpdate(input, STATE_SAVE_SEQUENCE_BEGINS, null);
        fPendingStateSaves.add(manager);
    }

    /**
     * Called when a state save is complete.
     * @param input the {@link ModelDelta} input
     * @param manager the manager to notify
     */
    private void stateSaveComplete(Object input, IElementMementoCollector manager) {
        Assert.isTrue( fContentProvider.getViewer().getDisplay().getThread() == Thread.currentThread() );

        notifyStateUpdate(input, STATE_SAVE_SEQUENCE_COMPLETE, null);
        fPendingStateSaves.remove(manager);
        if (fQueuedRestore != null) {
            Object temp = fQueuedRestore;
            fQueuedRestore = null;
            restoreViewerState(temp);
        }
    }

    /**
     * Returns whether any state saving is in progress.
     *
     * @return whether any state saving is in progress
     */
    private boolean isSavingState() {
        Assert.isTrue( fContentProvider.getViewer().getDisplay().getThread() == Thread.currentThread() );

        return !fPendingStateSaves.isEmpty();
    }

    /**
     * Restores the viewer state unless a save is taking place. If a save is
     * taking place, the restore is queued.
     *
     * @param input
     *            viewer input
     */
    protected void restoreViewerState(final Object input) {
        Assert.isTrue( fContentProvider.getViewer().getDisplay().getThread() == Thread.currentThread() );

        fPendingState = null;
        if (isSavingState()) {
            fQueuedRestore = input;
        } else {
            startRestoreViewerState(input);
        }
    }


    public void cancelRestore(final TreePath path, final int flags) {
        if (fInStateRestore) {
            // If we are currently processing pending state already, ignore
            // cancelRestore requests.  These requests may be triggered in the viewer
            // by changes to the tree state (Bug 295585).
            return;
        }

        if ((flags & IModelDelta.REVEAL) != 0 && fPendingSetTopItem != null) {
            fPendingSetTopItem.dispose();
            return;
        }

        // Nothing else to do
        if (fPendingState == null) {
            return;
        }

        if ((flags & (IModelDelta.SELECT | IModelDelta.REVEAL)) != 0) {
            // If we're canceling reveal and this is waiting for updates to complete
            // then just cancel it and return

            // If we're canceling select or reveal, cancel it for all of pending deltas
            final int mask = flags & (IModelDelta.SELECT | IModelDelta.REVEAL);
			fPendingState.accept((delta, depth) -> {
				int deltaFlags = delta.getFlags();
				int newFlags = deltaFlags & ~mask;
				if (DebugUIPlugin.DEBUG_STATE_SAVE_RESTORE
						&& DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(fContentProvider.getPresentationContext())) {
					if (deltaFlags != newFlags) {
						DebugUIPlugin.trace(
								"\tCANCEL: " + delta.getElement() + "(" + Integer.toHexString(deltaFlags & mask) + ")"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
					}
				}
				((ModelDelta) delta).setFlags(newFlags);
				return true;
			});
        }
        if ((flags & ~(IModelDelta.SELECT | IModelDelta.REVEAL)) != 0) {
            final int mask = flags & ~(IModelDelta.SELECT | IModelDelta.REVEAL);
            // For other flags (EXPAND/COLLAPSE), cancel only from the matching path.
			fPendingState.accept((delta, depth) -> {
				if (depth < path.getSegmentCount()) {
					// Descend until we reach a matching depth.
					TreePath deltaPath1 = fContentProvider.getViewerTreePath(delta);
					if (path.startsWith(deltaPath1, null)) {
						return true;
					} else {
						return false;
					}
				} else if (depth == path.getSegmentCount()) {
					TreePath deltaPath2 = fContentProvider.getViewerTreePath(delta);
					if (deltaPath2.equals(path)) {
						int deltaFlags = delta.getFlags();
						int newFlags = deltaFlags & ~mask;
						if (DebugUIPlugin.DEBUG_STATE_SAVE_RESTORE && DebugUIPlugin
								.DEBUG_TEST_PRESENTATION_ID(fContentProvider.getPresentationContext())) {
							if (deltaFlags != newFlags) {
								DebugUIPlugin.trace("\tCANCEL: " + delta.getElement() + "(" //$NON-NLS-1$ //$NON-NLS-2$
										+ Integer.toHexString(deltaFlags & mask) + ")"); //$NON-NLS-1$
							}
						}
						((ModelDelta) delta).setFlags(newFlags);
						if ((flags & IModelDelta.EXPAND) != 0) {
							// Descend delta to clear the EXPAND flags of a canceled expand
							return true;
						}
					}
					return false;
				} else {
					// We're clearing out flags of a matching sub-tree
					// assert (flags & IModelDelta.EXPAND) != 0;

					if (DebugUIPlugin.DEBUG_STATE_SAVE_RESTORE
							&& DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(fContentProvider.getPresentationContext())) {
						if (delta.getFlags() != IModelDelta.NO_CHANGE) {
							DebugUIPlugin.trace("\tCANCEL: " + delta.getElement() + "(" //$NON-NLS-1$ //$NON-NLS-2$
									+ Integer.toHexString(delta.getFlags()) + ")"); //$NON-NLS-1$
						}
					}
					((ModelDelta) delta).setFlags(IModelDelta.NO_CHANGE);
					return true;
				}
			});
        }
    }


    /**
     * Perform any restoration required for the given tree path.
     * <p>
     * This method is called after every viewer update completion to continue
     * restoring the expansion state that was previously saved.
     *
     * @param path the tree path to update
     * @param modelIndex the index in the current model
     * @param knowsHasChildren if the content provider knows it has children already
     * @param knowsChildCount if the content provider knows the current child count already
     * @param checkChildrenRealized if any realized children should be checked or not
     */
    void restorePendingStateOnUpdate(final TreePath path, final int modelIndex, final boolean knowsHasChildren,
        final boolean knowsChildCount, final boolean checkChildrenRealized)
    {
        Assert.isTrue( fContentProvider.getViewer().getDisplay().getThread() == Thread.currentThread() );

        if (fPendingState == null) {
            return;
        }

		IModelDeltaVisitor visitor = (delta, depth) -> {

			Object element = delta.getElement();
			Object potentialMatch = depth != 0 ? path.getSegment(depth - 1) : fContentProvider.getViewer().getInput();
			// Only process if the depth in the delta matches the tree path.
			if (depth == path.getSegmentCount()) {
				if (element instanceof IMemento) {
					IElementMementoProvider provider = ViewerAdapterService.getMementoProvider(potentialMatch);
					if (provider == null) {
						provider = ViewerAdapterService.getMementoProvider(fContentProvider.getViewer().getInput());
					}
					if (provider != null) {
						CompareRequestKey key = new CompareRequestKey(path, delta);
						ElementCompareRequest existingRequest = fCompareRequestsInProgress.get(key);
						if (existingRequest != null) {
							// Check all the running compare updates for a
							// matching tree path.
							// If found, just update the flags.
							existingRequest.setKnowsHasChildren(knowsHasChildren);
							existingRequest.setKnowsChildCount(knowsChildCount);
							existingRequest.setCheckChildrenRealized(checkChildrenRealized);
						} else {
							// Start a new compare request
							ElementCompareRequest compareRequest = new ElementCompareRequest(fContentProvider,
									fContentProvider.getViewer().getInput(), potentialMatch, path, (IMemento) element,
									(ModelDelta) delta, modelIndex, knowsHasChildren, knowsChildCount,
									checkChildrenRealized);
							fCompareRequestsInProgress.put(key, compareRequest);
							if (DebugUIPlugin.DEBUG_STATE_SAVE_RESTORE && DebugUIPlugin
									.DEBUG_TEST_PRESENTATION_ID(fContentProvider.getPresentationContext())) {
								DebugUIPlugin.trace("\tSTATE BEGIN: " + compareRequest); //$NON-NLS-1$
							}
							notifyStateUpdate(element, TreeModelContentProvider.UPDATE_BEGINS, compareRequest);
							provider.compareElements(new IElementCompareRequest[] { compareRequest });
						}
					}
				} else if (element.equals(potentialMatch)) {
					// Element comparison already succeeded, and it matches
					// our element.
					// Call restore with delta to process the delta flags.
					restorePendingStateNode((ModelDelta) delta, knowsHasChildren, knowsChildCount,
							checkChildrenRealized);
				}
				return false;
			}
			// Only follow the paths that match the delta.
			return element.equals(potentialMatch);
		};

        try {
            fInStateRestore = true;
            fPendingState.accept(visitor);
        }
        finally {
            fInStateRestore = false;
        }
        checkIfRestoreComplete();
    }

    /**
     * Checks whether restoring pending state is already complete.
     */
    void checkIfRestoreComplete() {
        Assert.isTrue( fContentProvider.getViewer().getDisplay().getThread() == Thread.currentThread() );

        if (fPendingState == null) {
            return;
        }

        /**
         * Used to determine when restoration delta has been processed
         */
        class CheckState implements IModelDeltaVisitor {
            private boolean complete = true;

            /*
             * (non-Javadoc)
             *
             * @see
             * org.eclipse.debug.internal.ui.viewers.provisional.IModelDeltaVisitor
             * #visit(org.eclipse.debug.internal.ui.viewers.provisional.IModelDelta,
             * int)
             */
            @Override
			public boolean visit(IModelDelta delta, int depth) {
                // Filter out the CONTENT flags from the delta flags, the content
                // flag is only used as a marker indicating that all the sub-elements
                // of a given delta have been retrieved.
                int flags = (delta.getFlags() & ~IModelDelta.CONTENT);

                if (flags != IModelDelta.NO_CHANGE) {
                    IModelDelta parentDelta = delta.getParentDelta();
                    // Remove the delta if :
                    // - The parent delta has no more flags on it (the content flag is removed as well),
                    // which means that parent element's children have been completely exposed.
                    // - There are no more pending updates for the element.
                    // - If element is a memento, there are no state requests pending.
                    if (parentDelta != null && parentDelta.getFlags() == IModelDelta.NO_CHANGE) {
                        TreePath deltaPath = fContentProvider.getViewerTreePath(delta);
                        if ( !fContentProvider.areElementUpdatesPending(deltaPath) &&
                             (!(delta.getElement() instanceof IMemento) || !areMementoUpdatesPending(delta)) )
                        {
                            removeDelta(delta);
                            return false;
                        }
                    }

                    if (flags != IModelDelta.REVEAL || (delta.getElement() instanceof IMemento)) {
                        complete = false;
                        return false;
                    }
                }
                return true;
            }

            public boolean isComplete() {
                return complete;
            }

            private boolean areMementoUpdatesPending(IModelDelta delta) {
				for (CompareRequestKey key : fCompareRequestsInProgress.keySet()) {
                    if (delta.getElement().equals(key.fDelta.getElement())) {
                        return true;
                    }
                }
                return false;
            }

            private void removeDelta(IModelDelta delta) {
                if (DebugUIPlugin.DEBUG_STATE_SAVE_RESTORE && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(fContentProvider.getPresentationContext())) {
                	DebugUIPlugin.trace("\tRESTORE REMOVED: " + delta.getElement()); //$NON-NLS-1$
                }

				delta.accept((_visitorDelta, depth) -> {
					ModelDelta visitorDelta = (ModelDelta) _visitorDelta;
					visitorDelta.setElement(ELEMENT_REMOVED);
					visitorDelta.setFlags(IModelDelta.NO_CHANGE);
					return true;
				});

            }
        }

        CheckState state = new CheckState();
        fPendingState.accept(state);
        if (state.isComplete()) {
            // notify restore complete if REVEAL was restored also, otherwise
            // postpone until then.
            if (fPendingSetTopItem == null) {
                if (DebugUIPlugin.DEBUG_STATE_SAVE_RESTORE && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(fContentProvider.getPresentationContext())) {
                	DebugUIPlugin.trace("STATE RESTORE COMPELTE: " + fPendingState); //$NON-NLS-1$
                }

                notifyStateUpdate(fPendingState.getElement(), STATE_RESTORE_SEQUENCE_COMPLETE, null);
            }

            fPendingState = null;
        }
    }

    /**
     * Restores the pending state in the given delta node.  This method is called
     * once the state tracker has found the element which matches the element in
     * the given delta node.
     * @param delta the {@link ModelDelta} to restore from
     * @param knowsHasChildren if the content provider has computed its children
     * @param knowsChildCount if the content provider has already computed the child count
     * @param checkChildrenRealized if any realized children should be checked
     */
    void restorePendingStateNode(final ModelDelta delta, boolean knowsHasChildren, boolean knowsChildCount, boolean checkChildrenRealized) {
        final TreePath treePath = fContentProvider.getViewerTreePath(delta);
        final IInternalTreeModelViewer viewer = fContentProvider.getViewer();

        // Attempt to expand the node only if the children are known.
        if (knowsHasChildren) {
            if ((delta.getFlags() & IModelDelta.EXPAND) != 0) {
                if (DebugUIPlugin.DEBUG_STATE_SAVE_RESTORE && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(fContentProvider.getPresentationContext())) {
                	DebugUIPlugin.trace("\tRESTORE EXPAND: " + treePath.getLastSegment()); //$NON-NLS-1$
                }
                viewer.expandToLevel(treePath, 1);
                delta.setFlags(delta.getFlags() & ~IModelDelta.EXPAND);
            }
            if ((delta.getFlags() & IModelDelta.COLLAPSE) != 0) {
                if (DebugUIPlugin.DEBUG_STATE_SAVE_RESTORE && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(fContentProvider.getPresentationContext())) {
                	DebugUIPlugin.trace("\tRESTORE COLLAPSE: " + treePath.getLastSegment()); //$NON-NLS-1$
                }
                // Check auto-expand before collapsing an element (bug 335734)
                int autoexpand = fContentProvider.getViewer().getAutoExpandLevel();
                if (autoexpand != ITreeModelViewer.ALL_LEVELS && autoexpand < (treePath.getSegmentCount() + 1)) {
                    fContentProvider.getViewer().setExpandedState(treePath, false);
                }
                delta.setFlags(delta.getFlags() & ~IModelDelta.COLLAPSE);
            }
        }

        if ((delta.getFlags() & IModelDelta.SELECT) != 0) {
            delta.setFlags(delta.getFlags() & ~IModelDelta.SELECT);
            if (DebugUIPlugin.DEBUG_STATE_SAVE_RESTORE && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(fContentProvider.getPresentationContext())) {
            	DebugUIPlugin.trace("\tRESTORE SELECT: " + treePath.getLastSegment()); //$NON-NLS-1$
            }
            ITreeSelection currentSelection = (ITreeSelection)viewer.getSelection();
            if (currentSelection == null || currentSelection.isEmpty()) {
                viewer.setSelection(new TreeSelection(treePath), false, false);
            } else {
                TreePath[] currentPaths = currentSelection.getPaths();
                boolean pathInSelection = false;
                for (int i = 0; i < currentPaths.length; i++) {
                    if (currentPaths[i].equals(treePath)) {
                        pathInSelection = true;
                        break;
                    }
                }
                // Only set the selection if the element is not yet in
                // selection.  Otherwise the setSelection() call will
                // update selection listeners needlessly.
                if (!pathInSelection) {
                    TreePath[] newPaths = new TreePath[currentPaths.length + 1];
                    System.arraycopy(currentPaths, 0, newPaths, 0, currentPaths.length);
                    newPaths[newPaths.length - 1] = treePath;
                    viewer.setSelection(new TreeSelection(newPaths), false, false);
                }
            }
        }

        if ((delta.getFlags() & IModelDelta.REVEAL) != 0) {
            delta.setFlags(delta.getFlags() & ~IModelDelta.REVEAL);
            // Look for the reveal flag in the child deltas.  If
            // A child delta has the reveal flag, do not set the
            // top element yet.
            boolean setTopItem = true;
            IModelDelta[] childDeltas = delta.getChildDeltas();
            for (int i = 0; i < childDeltas.length; i++) {
                IModelDelta childDelta = childDeltas[i];
                int modelIndex = childDelta.getIndex();
                if (modelIndex >= 0 && (childDelta.getFlags() & IModelDelta.REVEAL) != 0) {
                    setTopItem = false;
                }
            }

            if (setTopItem) {
                Assert.isTrue(fPendingSetTopItem == null);

                fPendingSetTopItem = new PendingRevealDelta(treePath, delta);
                viewer.addViewerUpdateListener(fPendingSetTopItem);
            }
        }

        // If we know the child count of the element, look for the reveal
        // flag in the child deltas.  For the children with reveal flag start
        // a new update.
        // If the child delta's index is out of range, strip the reveal flag
        // since it is no longer applicable.
        if (knowsChildCount) {
            int childCount = viewer.getChildCount(treePath);
            if (childCount >= 0) {
                ModelDelta[] childDeltas = (ModelDelta[])delta.getChildDeltas();
                for (int i = 0; i < childDeltas.length; i++) {
                    ModelDelta childDelta = childDeltas[i];
                    int modelIndex = childDelta.getIndex();
                    if (modelIndex >= 0 && (childDelta.getFlags() & IModelDelta.REVEAL) != 0) {
                        if (modelIndex < childCount) {
                            fContentProvider.doUpdateElement(treePath, modelIndex);
                        } else {
                            childDelta.setFlags(childDelta.getFlags() & ~IModelDelta.REVEAL);
                        }
                    }
                }
            }
        }

        // Some children of this element were just updated.  If all its
        // children are now realized, clear out any elements that still
        // have flags, because they represent elements that were removed.
        if ((checkChildrenRealized &&
             !fContentProvider.areChildrenUpdatesPending(treePath) &&
             fContentProvider.getViewer().getElementChildrenRealized(treePath)) ||
            (knowsHasChildren && !viewer.getHasChildren(treePath)) )
        {
            if (DebugUIPlugin.DEBUG_STATE_SAVE_RESTORE && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(fContentProvider.getPresentationContext())) {
            	DebugUIPlugin.trace("\tRESTORE CONTENT: " + treePath.getLastSegment()); //$NON-NLS-1$
            }
            delta.setFlags(delta.getFlags() & ~IModelDelta.CONTENT);
        }
    }

    /**
     * Utility that reveals the saved top item in the viewer.  It listens for
     * all content updates to complete in order to avoid having the desired top item
     * scroll out as view content is filled in.
     * <br>
     * Revealing some elements can trigger expanding some of elements
     * that have been just revealed. Therefore, we have to check one
     * more time after the new triggered updates are completed if we
     * have to set again the top index
     */
    private class PendingRevealDelta implements IViewerUpdateListener {

        private final TreePath fPathToReveal;
        private final ModelDelta fRevealDelta;

        PendingRevealDelta(TreePath pathToReveal, ModelDelta revealDelta) {
            fPathToReveal = pathToReveal;
            fRevealDelta = revealDelta;
        }

        /**
         * Counter that tracks how many time the viewer updates were completed.
         */
        private int fCounter = 0;
        private Object fModelInput = fPendingState.getElement();

        @Override
		public void viewerUpdatesComplete() {
            Assert.isTrue( fContentProvider.getViewer().getDisplay().getThread() == Thread.currentThread() );

            IInternalTreeModelViewer viewer = fContentProvider.getViewer();
            if (viewer == null || fPendingSetTopItem != this) {
                return;
            }

            TreePath topPath = viewer.getTopElementPath();
            if (!fPathToReveal.equals(topPath)) {
                TreePath parentPath = fPathToReveal.getParentPath();
                int index = viewer.findElementIndex(parentPath, fPathToReveal.getLastSegment());
                if (index >= 0) {
                    if (DebugUIPlugin.DEBUG_STATE_SAVE_RESTORE && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(fContentProvider.getPresentationContext())) {
                    	DebugUIPlugin.trace("\tRESTORE REVEAL: " + fPathToReveal.getLastSegment()); //$NON-NLS-1$
                    }
                    viewer.reveal(parentPath, index);

                }
            }

            fCounter++;
            // in case the pending state was already set to null, we assume that
            // all others elements are restored, so we don't expect that REVEAL will
            // trigger other updates
            if (fCounter > 1 || fPendingState == null) {
                dispose();
            }
        }

        @Override
		public void viewerUpdatesBegin() {}
        @Override
		public void updateStarted(IViewerUpdate update) {}
        @Override
		public void updateComplete(IViewerUpdate update) {}

        /**
         * Returns delta that is used to reveal the item.
         * @return delta to be revealed.
         */
        public ModelDelta getDelta() {
            return fRevealDelta;
        }

        /**
         * Resets the item
         */
        public void dispose() {
            // top item is set
            fPendingSetTopItem = null;

            IInternalTreeModelViewer viewer = fContentProvider.getViewer();
            if (viewer == null) {
				return;
			}

            // remove myself as viewer update listener
            viewer.removeViewerUpdateListener(this);

            if (fPendingState == null) {
                if (DebugUIPlugin.DEBUG_STATE_SAVE_RESTORE && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(fContentProvider.getPresentationContext())) {
                	DebugUIPlugin.trace("STATE RESTORE COMPELTE: " + fPendingState); //$NON-NLS-1$
                }
                notifyStateUpdate(fModelInput, STATE_RESTORE_SEQUENCE_COMPLETE, null);
            } else {
                checkIfRestoreComplete();
            }
        }

    }

    /**
     * Restore selection/expansion based on items already in the viewer
     * @param delta the {@link ModelDelta} to restore from
     */
    protected void doInitialRestore(ModelDelta delta) {
        // Find the reveal delta and mark nodes on its path
        // to reveal as elements are updated.
        markRevealDelta(delta);

        // Restore visible items.
        // Note (Pawel Piech): the initial list of items is normally
        // empty, so in most cases the code below does not do anything.
        // Instead doRestore() is called when various updates complete.
        int count = fContentProvider.getViewer().getChildCount(TreePath.EMPTY);
        for (int i = 0; i < count; i++) {
            Object data = fContentProvider.getViewer().getChildElement(TreePath.EMPTY, i);
            if (data != null) {
                restorePendingStateOnUpdate(new TreePath(new Object[]{data}), i, false, false, false);
            }
        }

    }

    /**
     * Finds the delta with the reveal flag, then it walks up this
     * delta and marks all the parents of it with the reveal flag.
     * These flags are then used by the restore logic to restore
     * and reveal all the nodes leading up to the element that should
     * be ultimately at the top.
     * @param rootDelta Delta to search
     * @return The node just under the rootDelta which contains
     * the reveal flag.  <code>null</code> if no reveal flag was found.
     */
    private ModelDelta markRevealDelta(ModelDelta rootDelta) {
        final ModelDelta[] revealDelta = new ModelDelta[1];
		IModelDeltaVisitor visitor = (delta, depth) -> {
			if ((delta.getFlags() & IModelDelta.REVEAL) != 0) {
				revealDelta[0] = (ModelDelta) delta;
			}
			// Keep recursing only if we haven't found our delta yet.
			return revealDelta[0] == null;
		};

        rootDelta.accept(visitor);
        if (revealDelta[0] != null) {
            ModelDelta parentDelta = (ModelDelta)revealDelta[0].getParentDelta();
            while(parentDelta.getParentDelta() != null) {
                revealDelta[0] = parentDelta;
                revealDelta[0].setFlags(revealDelta[0].getFlags() | IModelDelta.REVEAL);
                parentDelta = (ModelDelta)parentDelta.getParentDelta();
            }
        }
        return revealDelta[0];
    }

    /**
     * Builds a delta with the given root delta for expansion/selection state.
     *
     * @param delta
     *            root delta
     */
    private void buildViewerState(ModelDelta delta) {
        IInternalTreeModelViewer viewer = fContentProvider.getViewer();
        viewer.saveElementState(TreeModelContentProvider.EMPTY_TREE_PATH, delta, IModelDelta.SELECT | IModelDelta.EXPAND);

        // Add memento for top item if it is mapped to an element.  The reveal memento
        // is in its own path to avoid requesting unnecessary data when restoring it.
        TreePath topElementPath = viewer.getTopElementPath();
        if (topElementPath != null) {
            ModelDelta parentDelta = delta;
            TreePath parentPath = TreeModelContentProvider.EMPTY_TREE_PATH;
            for (int i = 0; i < topElementPath.getSegmentCount(); i++) {
                Object element = topElementPath.getSegment(i);
                int index = viewer.findElementIndex(parentPath, element);
                ModelDelta childDelta = parentDelta.getChildDelta(element);
                if (childDelta == null) {
                    parentDelta = parentDelta.addNode(element, index, IModelDelta.NO_CHANGE);
                } else {
                    parentDelta = childDelta;
                }
                parentPath = parentPath.createChildPath(element);
            }
            parentDelta.setFlags(parentDelta.getFlags() | IModelDelta.REVEAL);
        }
    }

    /**
     * Cancels any outstanding compare requests for given element and its children.
     * @param path Path of element to cancel updates for.
     */
    void cancelStateSubtreeUpdates(TreePath path) {
		for (Iterator<CompareRequestKey> itr = fCompareRequestsInProgress.keySet().iterator(); itr.hasNext();) {
			CompareRequestKey key = itr.next();
            if (key.fPath.startsWith(path, null)) {
                ElementCompareRequest compareRequest = fCompareRequestsInProgress.get(key);
                compareRequest.cancel();
                itr.remove();
            }
        }
    }

    void compareFinished(ElementCompareRequest request, ModelDelta delta) {
        notifyStateUpdate(request.getViewerInput(), TreeModelContentProvider.UPDATE_COMPLETE, request);
        if (DebugUIPlugin.DEBUG_STATE_SAVE_RESTORE && DebugUIPlugin.DEBUG_TEST_PRESENTATION_ID(fContentProvider.getPresentationContext())) {
        	DebugUIPlugin.trace("\tSTATE END: " + request + " = " + false); //$NON-NLS-1$ //$NON-NLS-2$
        }

        fCompareRequestsInProgress.remove(new CompareRequestKey(request.getElementPath(), delta));
        if (!request.isCanceled()) {
            if (request.isEqual()) {
                delta.setElement(request.getElement());
                restorePendingStateNode(delta, request.knowsHasChildren(), request.knowChildCount(), request.checkChildrenRealized());
            } else if (request.getModelIndex() != -1) {
                // Comparison failed.
                // Check if the delta has a reveal flag, and if its index
                // matches the index of the element that it was compared
                // against. If this is the case, strip the reveal flag from
                // the delta as it is most likely not applicable anymore.
                if ((delta.getFlags() & IModelDelta.REVEAL) != 0 && delta.getIndex() == request.getModelIndex()) {
                    delta.setFlags(delta.getFlags() & ~IModelDelta.REVEAL);
                }
            }
        }
        checkIfRestoreComplete();
    }


    void addStateUpdateListener(IStateUpdateListener listener) {
        fStateUpdateListeners.add(listener);
    }

    void removeStateUpdateListener(IStateUpdateListener listener) {
        fStateUpdateListeners.remove(listener);
    }

    void notifyStateUpdate(final Object input, final int type, final IViewerUpdate update) {
        if (!fStateUpdateListeners.isEmpty()) {
			for (IStateUpdateListener iStateUpdateListener : fStateUpdateListeners) {
				final IStateUpdateListener listener = iStateUpdateListener;
                SafeRunner.run(new ISafeRunnable() {
                    @Override
					public void run() throws Exception {
                        switch (type) {
							case STATE_SAVE_SEQUENCE_BEGINS:
								listener.stateSaveUpdatesBegin(input);
								break;
							case STATE_SAVE_SEQUENCE_COMPLETE:
								listener.stateSaveUpdatesComplete(input);
								break;
							case STATE_RESTORE_SEQUENCE_BEGINS:
								listener.stateRestoreUpdatesBegin(input);
								break;
							case STATE_RESTORE_SEQUENCE_COMPLETE:
								listener.stateRestoreUpdatesComplete(input);
								break;
							case TreeModelContentProvider.UPDATE_BEGINS:
								listener.stateUpdateStarted(input, update);
								break;
							case TreeModelContentProvider.UPDATE_COMPLETE:
								listener.stateUpdateComplete(input, update);
								break;
							default:
								break;
                        }
                    }

                    @Override
					public void handleException(Throwable exception) {
                        DebugUIPlugin.log(exception);
                    }
                });
            }
        }
    }
}
