/*******************************************************************************
 * Copyright (c) 2000, 2015 IBM Corporation 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:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/

package org.eclipse.ui.internal;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Map;
import org.eclipse.core.runtime.Assert;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IMemento;
import org.eclipse.ui.INavigationHistory;
import org.eclipse.ui.INavigationLocation;
import org.eclipse.ui.INavigationLocationProvider;
import org.eclipse.ui.IPartListener2;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchPartReference;
import org.eclipse.ui.IWorkbenchPartSite;
import org.eclipse.ui.internal.StartupThreading.StartupRunnable;

/**
 * Implementation of the back and forward actions.
 */
public class NavigationHistory implements INavigationHistory {

	private static final boolean DEBUG = false;

	private static final int CAPACITY = 50;

	private NavigationHistoryAction backwardAction;

	private NavigationHistoryAction forwardAction;

	private int ignoreEntries;

	private ArrayList<NavigationHistoryEntry> history = new ArrayList<>(CAPACITY);

	Map<Object, PerTabHistory> perTabHistoryMap = new HashMap<>();

	private ArrayList<NavigationHistoryEditorInfo> editors = new ArrayList<>(CAPACITY);

	private IWorkbenchPage page;

	private int activeEntry = 0;

	/**
	 * Creates a new NavigationHistory to keep the NavigationLocation entries of the
	 * specified page.
	 *
	 * @param page the workbench page
	 */
	public NavigationHistory(final IWorkbenchPage page) {
		this.page = page;
		page.addPartListener(new IPartListener2() {
			@Override
			public void partActivated(IWorkbenchPartReference partRef) {
			}

			@Override
			public void partBroughtToTop(IWorkbenchPartReference partRef) {
			}

			@Override
			public void partDeactivated(IWorkbenchPartReference partRef) {
			}

			@Override
			public void partOpened(IWorkbenchPartReference partRef) {
			}

			@Override
			public void partHidden(IWorkbenchPartReference partRef) {
			}

			@Override
			public void partVisible(IWorkbenchPartReference partRef) {
			}

			@Override
			public void partClosed(IWorkbenchPartReference partRef) {
				if (isPerTabHistoryEnabled() && partRef instanceof EditorReference) {
					if (!((EditorReference) partRef).isDisposed()) {
						Object editorTabCookie = ((EditorReference) partRef).getModel().getWidget();
						disposeHistoryForTab(editorTabCookie);
						updateActions();
					}
				}
				updateNavigationHistory(partRef, true);
			}

			@Override
			public void partInputChanged(IWorkbenchPartReference partRef) {
				updateNavigationHistory(partRef, false);
			}

			private void updateNavigationHistory(IWorkbenchPartReference partRef, boolean partClosed) {
				if (partRef != null && partRef.getPart(false) instanceof IEditorPart) {
					IEditorPart editor = (IEditorPart) partRef.getPart(false);
					IEditorInput input = editor.getEditorInput();
					String id = editor.getSite().getId();
					Iterator<NavigationHistoryEditorInfo> e = editors.iterator();
					NavigationHistoryEditorInfo info = null;
					NavigationHistoryEditorInfo currentInfo = null;
					NavigationHistoryEntry current = getEntry(activeEntry);
					if (current != null) {
						currentInfo = current.editorInfo;
					}
					while (e.hasNext()) {
						info = e.next();
						if (id.equals(info.editorID) && input.equals(info.editorInput)) {
							if (partClosed && info != currentInfo) {
								info.handlePartClosed();
							}
							break;
						}
						info = null;
					}
					if (info == null) {
						return;
					}
					boolean isEntryDisposed = false;
					Iterator<NavigationHistoryEntry> it = history.iterator();
					int i = 0;
					while (it.hasNext()) {
						NavigationHistoryEntry entry = it.next();
						if (entry.editorInfo == info) {
							if (!entry.handlePartClosed()) {
								// update the active entry since we are removing an item
								if (i < activeEntry) {
									activeEntry--;
								} else if (i == activeEntry) {
									if (i != 0) {
										activeEntry--;
									}
								} else {
									// activeEntry is before item we deleted
									i++;
								}
								isEntryDisposed = true;
								it.remove();
								disposeEntry(entry);
							} else {
								i++;
							}
						}
					}

					/*
					 * Promote the entry of the last closed editor to be the active one, see:
					 * https://bugs.eclipse.org/bugs/show_bug.cgi?id=154431
					 */
					if (!isEntryDisposed && page.getActiveEditor() == null && activeEntry < history.size())
						activeEntry++;

					updateActions();
				}
			}
		});
	}

	private Display getDisplay() {
		return Workbench.getInstance().getDisplay();
	}

	private boolean isPerTabHistoryEnabled() {
		return false;
	}

	/*
	 * Adds an editor to the editor history without getting its location.
	 */
	public void markEditor(final IEditorPart part) {
		if (ignoreEntries > 0 || part == null) {
			return;
		}
		/*
		 * Ignore all entries until the async exec runs. Workaround to avoid extra entry
		 * when using Open Declaration (F3) that opens another editor.
		 */
		ignoreEntries++;
		getDisplay().asyncExec(() -> {
			if (--ignoreEntries == 0) {
				if (part.getEditorSite() instanceof EditorSite) {
					EditorSite site = (EditorSite) part.getEditorSite();
					Control c = (Control) site.getModel().getWidget();
					if (c == null || c.isDisposed()) {
						return;
					}
					NavigationHistoryEntry e = getEntry(activeEntry);
					if (e != null && part.getEditorInput() != e.editorInfo.editorInput) {
						updateEntry(e);
					}
					addEntry(part);
				}
			}
		});
	}

	@Override
	public void markLocation(IEditorPart part) {
		addEntry(part);
	}

	/*
	 * Return the backward history entries. Return in restore order (i.e., the first
	 * entry is the entry that would become active if the "Backward" action was
	 * executed). <p> (Called by NavigationHistoryAction) </p>
	 */
	NavigationHistoryEntry[] getBackwardEntries() {
		if (isPerTabHistoryEnabled()) {
			return getEntriesForTab(false);
		}
		int length = activeEntry;
		NavigationHistoryEntry[] entries = new NavigationHistoryEntry[length];
		for (int i = 0; i < activeEntry; i++) {
			entries[activeEntry - 1 - i] = getEntry(i);
		}
		return entries;
	}

	/*
	 * Return the forward history entries. Return in restore order (i.e., the first
	 * entry is the entry that would become active if the "Forward" action was
	 * executed). <p> (Called by NavigationHistoryAction) </p>
	 */
	NavigationHistoryEntry[] getForwardEntries() {
		if (isPerTabHistoryEnabled()) {
			return getEntriesForTab(true);
		}
		int length = history.size() - activeEntry - 1;
		length = Math.max(0, length);
		NavigationHistoryEntry[] entries = new NavigationHistoryEntry[length];
		for (int i = activeEntry + 1; i < history.size(); i++) {
			entries[i - activeEntry - 1] = getEntry(i);
		}
		return entries;
	}

	@Override
	public INavigationLocation[] getLocations() {
		INavigationLocation result[] = new INavigationLocation[history.size()];
		for (int i = 0; i < result.length; i++) {
			NavigationHistoryEntry e = history.get(i);
			result[i] = e.location;
		}
		return result;
	}

	@Override
	public INavigationLocation getCurrentLocation() {
		NavigationHistoryEntry entry = getEntry(activeEntry);
		return entry == null ? null : entry.location;
	}

	/**
	 * Disposes this NavigationHistory and all entries.
	 */
	public void dispose() {
		disposeHistoryForTabs();
		Iterator<NavigationHistoryEntry> e = history.iterator();
		while (e.hasNext()) {
			NavigationHistoryEntry entry = e.next();
			disposeEntry(entry);
		}
	}

	/**
	 * Keeps a reference to the forward action to update its state whenever needed.
	 * <p>
	 * (Called by NavigationHistoryAction)
	 * </p>
	 */
	void setForwardAction(NavigationHistoryAction action) {
		forwardAction = action;
		updateActions();
	}

	/**
	 * Keeps a reference to the backward action to update its state whenever needed.
	 * <p>
	 * (Called by NavigationHistoryAction)
	 * </p>
	 */
	void setBackwardAction(NavigationHistoryAction action) {
		backwardAction = action;
		updateActions();
	}

	/*
	 * Returns the history entry indexed by <code>index</code>
	 */
	private NavigationHistoryEntry getEntry(int index) {
		if (0 <= index && index < history.size()) {
			return history.get(index);
		}
		return null;
	}

	/*
	 * Adds the specified entry to the history.
	 */
	private void add(NavigationHistoryEntry entry) {
		removeForwardEntries();
		if (history.size() == CAPACITY) {
			NavigationHistoryEntry e = history.remove(0);
			disposeEntry(e);
		}
		history.add(entry);
		activeEntry = history.size() - 1;
	}

	/*
	 * Remove all entries after the active entry.
	 */
	private void removeForwardEntries() {
		int length = history.size();
		for (int i = activeEntry + 1; i < length; i++) {
			NavigationHistoryEntry e = history.remove(activeEntry + 1);
			disposeEntry(e);
		}
	}

	/*
	 * Adds a location to the history.
	 */
	private void addEntry(IEditorPart part) {
		if (ignoreEntries > 0 || part == null) {
			return;
		}

		if (isPerTabHistoryEnabled()) {
			markLocationForTab(part);
		}
		INavigationLocation location = null;
		if (part instanceof INavigationLocationProvider) {
			location = ((INavigationLocationProvider) part).createNavigationLocation();
		}

		NavigationHistoryEntry current = getEntry(activeEntry);
		if (current != null && current.editorInfo.memento != null) {
			current.editorInfo.restoreEditor();
			checkDuplicates(current.editorInfo);
		}
		NavigationHistoryEntry e = createEntry(page, part, location);
		if (current == null) {
			add(e);
		} else if (e.mergeInto(current)) {
			disposeEntry(e);
			removeForwardEntries();
		} else {
			add(e);
		}
		printEntries("added entry"); //$NON-NLS-1$
		updateActions();
	}

	/*
	 * Prints all the entries in the console. For debug only.
	 */
	private void printEntries(String label) {
		if (DEBUG) {
			System.out.println("+++++ " + label + "+++++ "); //$NON-NLS-1$ //$NON-NLS-2$
			int size = history.size();
			for (int i = 0; i < size; i++) {
				String append = activeEntry == i ? ">>" : ""; //$NON-NLS-1$ //$NON-NLS-2$
				System.out.println(append + "Index: " + i + " " + history.get(i)); //$NON-NLS-1$ //$NON-NLS-2$
			}
		}
	}

	/*
	 * Returns true if the forward action can be performed otherwise returns false.
	 * <p> (Called by NavigationHistoryAction) </p>
	 */
	/* package */boolean canForward() {
		if (isPerTabHistoryEnabled()) {
			return hasEntriesForTab(true);
		}
		return (0 <= activeEntry + 1) && (activeEntry + 1 < history.size());
	}

	/*
	 * Returns true if the backward action can be performed otherwise returns false.
	 * <p> (Called by NavigationHistoryAction) </p>
	 */
	/* package */boolean canBackward() {
		if (isPerTabHistoryEnabled()) {
			return hasEntriesForTab(false);
		}
		return (0 <= activeEntry - 1) && (activeEntry - 1 < history.size());
	}

	/*
	 * Update the actions enable/disable and tooltip state.
	 */
	private void updateActions() {
		if (backwardAction != null) {
			backwardAction.update();
		}
		if (forwardAction != null) {
			forwardAction.update();
		}
	}

	/*
	 * Restore the specified entry
	 */
	private void gotoEntry(NavigationHistoryEntry entry) {
		if (entry == null) {
			return;
		}
		try {
			ignoreEntries++;
			if (entry.editorInfo.memento != null) {
				entry.editorInfo.restoreEditor();
				checkDuplicates(entry.editorInfo);
			}
			entry.restoreLocation();
			updateActions();
			printEntries("goto entry"); //$NON-NLS-1$
		} finally {
			ignoreEntries--;
		}
	}

	/*
	 * update the active entry
	 */
	private void updateEntry(NavigationHistoryEntry activeEntry) {
		if (activeEntry == null || activeEntry.location == null) {
			return;
		}
		activeEntry.location.update();
		printEntries("updateEntry"); //$NON-NLS-1$
	}

	/*
	 * Perform the forward action by getting the next location and restoring its
	 * context. <p> (Called by NavigationHistoryAction) </p>
	 */
	void forward() {
		if (isPerTabHistoryEnabled()) {
			forwardForTab();
			return;
		}
		if (canForward()) {
			shiftEntry(true);
		}
	}

	/*
	 * Perform the backward action by getting the previous location and restoring
	 * its context. <p> (Called by NavigationHistoryAction) </p>
	 */
	void backward() {
		if (isPerTabHistoryEnabled()) {
			backwardForTab();
			return;
		}
		if (canBackward()) {
			shiftEntry(false);
		}
	}

	/*
	 * Shift the history back or forward
	 */
	private void shiftEntry(boolean forward) {
		updateEntry(getEntry(activeEntry));
		if (forward) {
			activeEntry++;
		} else {
			activeEntry--;
		}
		NavigationHistoryEntry entry = getEntry(activeEntry);
		if (entry != null) {
			gotoEntry(entry);
		}
	}

	/*
	 * Shift the history to the given entry. <p> (Called by NavigationHistoryAction)
	 * </p>
	 */
	void shiftCurrentEntry(NavigationHistoryEntry entry, boolean forward) {
		if (isPerTabHistoryEnabled()) {
			gotoEntryForTab(entry, forward);
			return;
		}
		updateEntry(getEntry(activeEntry));
		activeEntry = history.indexOf(entry);
		gotoEntry(entry);
	}

	/**
	 * Save the state of this history into the memento.
	 */
	void saveState(IMemento memento) {
		NavigationHistoryEntry cEntry = getEntry(activeEntry);
		if (cEntry == null || !cEntry.editorInfo.isPersistable()) {
			return;
		}

		ArrayList<NavigationHistoryEditorInfo> editors = new ArrayList<>(this.editors);
		for (Iterator<NavigationHistoryEditorInfo> iter = editors.iterator(); iter.hasNext();) {
			NavigationHistoryEditorInfo info = iter.next();
			if (!info.isPersistable()) {
				iter.remove();
			}
		}
		IMemento editorsMem = memento.createChild(IWorkbenchConstants.TAG_EDITORS);
		for (NavigationHistoryEditorInfo info : editors) {
			info.saveState(editorsMem.createChild(IWorkbenchConstants.TAG_EDITOR));
		}

		ArrayList<NavigationHistoryEntry> list = new ArrayList<>(history.size());
		int size = history.size();
		for (int i = 0; i < size; i++) {
			NavigationHistoryEntry entry = history.get(i);
			if (entry.editorInfo.isPersistable()) {
				list.add(entry);
			}
		}
		size = list.size();
		for (int i = 0; i < size; i++) {
			NavigationHistoryEntry entry = list.get(i);
			IMemento childMem = memento.createChild(IWorkbenchConstants.TAG_ITEM);
			if (entry == cEntry) {
				childMem.putString(IWorkbenchConstants.TAG_ACTIVE, "true"); //$NON-NLS-1$
			}
			entry.saveState(childMem, list);
			childMem.putInteger(IWorkbenchConstants.TAG_INDEX, editors.indexOf(entry.editorInfo));
		}
	}

	/**
	 * Restore the state of this history from the memento.
	 */
	void restoreState(IMemento memento) {
		IMemento editorsMem = memento.getChild(IWorkbenchConstants.TAG_EDITORS);
		IMemento items[] = memento.getChildren(IWorkbenchConstants.TAG_ITEM);
		if (items.length == 0 || editorsMem == null) {
			if (page.getActiveEditor() != null) {
				markLocation(page.getActiveEditor());
			}
			return;
		}

		IMemento children[] = editorsMem.getChildren(IWorkbenchConstants.TAG_EDITOR);
		NavigationHistoryEditorInfo editorsInfo[] = new NavigationHistoryEditorInfo[children.length];
		for (int i = 0; i < editorsInfo.length; i++) {
			editorsInfo[i] = new NavigationHistoryEditorInfo(children[i]);
			editors.add(editorsInfo[i]);
		}

		for (int i = 0; i < items.length; i++) {
			IMemento item = items[i];
			int index = item.getInteger(IWorkbenchConstants.TAG_INDEX).intValue();
			NavigationHistoryEditorInfo info = editorsInfo[index];
			info.refCount++;
			NavigationHistoryEntry entry = new NavigationHistoryEntry(info, page, null, null);
			history.add(entry);
			entry.restoreState(item);
			if (item.getString(IWorkbenchConstants.TAG_ACTIVE) != null) {
				activeEntry = i;
			}
		}

		final NavigationHistoryEntry entry = getEntry(activeEntry);
		if (entry != null && entry.editorInfo.editorInput != null) {
			if (page.getActiveEditor() == page.findEditor(entry.editorInfo.editorInput)) {
				StartupThreading.runWithoutExceptions(new StartupRunnable() {

					@Override
					public void runWithException() throws Throwable {
						gotoEntry(entry);
					}
				});
			}
		}
	}

	public NavigationHistoryEntry createEntry(IWorkbenchPage page, IEditorPart part, INavigationLocation location) {
		String editorID = part.getSite().getId();
		IEditorInput editorInput = part.getEditorInput();
		NavigationHistoryEditorInfo info = null;
		for (Iterator<NavigationHistoryEditorInfo> iter = editors.iterator(); iter.hasNext();) {
			info = iter.next();
			if (editorID.equals(info.editorID) && editorInput.equals(info.editorInput)) {
				info.refCount++;
				break;
			}
			info = null;
		}
		if (info == null) {
			info = new NavigationHistoryEditorInfo(part);
			info.refCount++;
			editors.add(info);
		}
		return new NavigationHistoryEntry(info, page, part, location);
	}

	public void disposeEntry(NavigationHistoryEntry entry) {
		if (entry.editorInfo == null) {
			return;
		}
		entry.editorInfo.refCount--;
		if (entry.editorInfo.refCount == 0) {
			editors.remove(entry.editorInfo);
		}
		entry.dispose();
	}

	void checkDuplicates(NavigationHistoryEditorInfo info) {
		NavigationHistoryEditorInfo dup = null;
		if (info.editorInput == null) {
			return;
		}
		for (Iterator<NavigationHistoryEditorInfo> iter = editors.iterator(); iter.hasNext();) {
			dup = iter.next();
			if (info != dup && info.editorID.equals(dup.editorID) && info.editorInput.equals(dup.editorInput)) {
				break;
			}
			dup = null;
		}
		if (dup == null) {
			return;
		}
		for (NavigationHistoryEntry entry : history) {
			if (entry.editorInfo == dup) {
				entry.editorInfo = info;
				info.refCount++;
			}
		}
		editors.remove(dup);
	}

	/*********************************************************/
	/*** new per-tab history code ***/
	/*********************************************************/

	private static class PerTabHistory {
		LinkedList<NavigationHistoryEntry> backwardEntries = new LinkedList<>();
		NavigationHistoryEntry currentEntry = null;
		LinkedList<NavigationHistoryEntry> forwardEntries = new LinkedList<>();
	}

	private void setNewCurrentEntryForTab(PerTabHistory perTabHistory, NavigationHistoryEntry entry) {
		if (perTabHistory.currentEntry != null) {
			perTabHistory.backwardEntries.addFirst(perTabHistory.currentEntry);
		}
		perTabHistory.currentEntry = entry;
		removeEntriesForTab(perTabHistory.forwardEntries);
	}

	private Object getCookieForTab(IEditorPart part) {
		if (part != null) {
			IWorkbenchPartSite site = part.getSite();
			if (site instanceof PartSite) {
				PartSite partSite = (PartSite) site;
				WorkbenchPartReference ref = (WorkbenchPartReference) partSite.getPartReference();
				if (!ref.isDisposed()) {
					return partSite.getModel().getWidget();
				}
			}
		}
		return null;
	}

	private void markLocationForTab(IEditorPart part) {
		if (part instanceof ErrorEditorPart) {
			updateActions();
			return;
		}
		Object tabCookie = getCookieForTab(part);
		if (tabCookie != null) {
			INavigationLocation location = null;
			if (part instanceof INavigationLocationProvider) {
				location = ((INavigationLocationProvider) part).createNavigationLocation();
			}
			PerTabHistory perTabHistory = perTabHistoryMap.get(tabCookie);
			if (perTabHistory == null) {
				perTabHistory = new PerTabHistory();
				perTabHistoryMap.put(tabCookie, perTabHistory);
			}
			NavigationHistoryEntry current = perTabHistory.currentEntry;
			if (current != null && current.editorInfo.memento != null) {
				current.editorInfo.restoreEditor();
				checkDuplicates(current.editorInfo);
			}
			NavigationHistoryEntry entry = createEntry(page, part, location);
			if (current != null && entry.mergeInto(current)) {
				disposeEntry(entry);
				removeEntriesForTab(perTabHistory.forwardEntries);
			} else {
				setNewCurrentEntryForTab(perTabHistory, entry);
			}
		}
		updateActions();
	}

	public void updateCookieForTab(Object oldCookie, Object newCookie) {
		if (newCookie.equals(oldCookie)) {
			return;
		}
		PerTabHistory perTabHistory = perTabHistoryMap.remove(oldCookie);
		if (perTabHistory != null) {
			perTabHistoryMap.put(newCookie, perTabHistory);
		}
	}

	private void gotoEntryForTab(NavigationHistoryEntry target, boolean forward) {
		Object editorTabCookie = getCookieForTab(page.getActiveEditor());
		if (editorTabCookie != null) {
			PerTabHistory perTabHistory = perTabHistoryMap.get(editorTabCookie);
			if (perTabHistory != null) {
				LinkedList<NavigationHistoryEntry> source = forward ? perTabHistory.forwardEntries
						: perTabHistory.backwardEntries;
				LinkedList<NavigationHistoryEntry> destination = forward ? perTabHistory.backwardEntries
						: perTabHistory.forwardEntries;
				if (perTabHistory.currentEntry != null) {
					if (perTabHistory.currentEntry.location != null) {
						perTabHistory.currentEntry.location.update();
					}
					destination.addFirst(perTabHistory.currentEntry);
				}
				NavigationHistoryEntry newCurrent = null;
				while (!source.isEmpty() && newCurrent == null) {
					NavigationHistoryEntry entry = source.removeFirst();
					if (entry.equals(target)) {
						newCurrent = entry;
					} else {
						destination.addFirst(entry);
					}
				}
				Assert.isTrue(newCurrent != null);
				perTabHistory.currentEntry = newCurrent;
				try {
					ignoreEntries++;
					if (newCurrent.editorInfo.memento != null) {
						newCurrent.editorInfo.restoreEditor();
						checkDuplicates(newCurrent.editorInfo);
					}
					newCurrent.restoreLocation();
					updateActions();
				} finally {
					ignoreEntries--;
				}
			}
		}
	}

	private void forwardForTab() {
		Object editorTabCookie = getCookieForTab(page.getActiveEditor());
		if (editorTabCookie != null) {
			PerTabHistory perTabHistory = perTabHistoryMap.get(editorTabCookie);
			if (perTabHistory != null && !perTabHistory.forwardEntries.isEmpty()) {
				NavigationHistoryEntry newCurrent = perTabHistory.forwardEntries.removeFirst();
				if (perTabHistory.currentEntry != null) {
					final INavigationLocation location = perTabHistory.currentEntry.location;
					if (location != null) {
						location.update();
					}
					perTabHistory.backwardEntries.addFirst(perTabHistory.currentEntry);
				}
				perTabHistory.currentEntry = newCurrent;
				try {
					ignoreEntries++;
					if (newCurrent.editorInfo.memento != null) {
						newCurrent.editorInfo.restoreEditor();
						checkDuplicates(newCurrent.editorInfo);
					}
					newCurrent.restoreLocation();
					updateActions();
				} finally {
					ignoreEntries--;
				}
			}
		}
	}

	private void backwardForTab() {
		Object editorTabCookie = getCookieForTab(page.getActiveEditor());
		if (editorTabCookie != null) {
			PerTabHistory perTabHistory = perTabHistoryMap.get(editorTabCookie);
			if (perTabHistory != null && !perTabHistory.backwardEntries.isEmpty()) {
				NavigationHistoryEntry newCurrent = perTabHistory.backwardEntries
						.removeFirst();
				if (perTabHistory.currentEntry != null) {
					perTabHistory.currentEntry.location.update();
					perTabHistory.forwardEntries.addFirst(perTabHistory.currentEntry);
				}
				perTabHistory.currentEntry = newCurrent;
				try {
					ignoreEntries++;
					if (newCurrent.editorInfo.memento != null) {
						newCurrent.editorInfo.restoreEditor();
						checkDuplicates(newCurrent.editorInfo);
					}
					newCurrent.restoreLocation();
					updateActions();
				} finally {
					ignoreEntries--;
				}
			}
		}
	}

	private boolean hasEntriesForTab(boolean forward) {
		Object editorTabCookie = getCookieForTab(page.getActiveEditor());
		if (editorTabCookie != null) {
			PerTabHistory perTabHistory = perTabHistoryMap.get(editorTabCookie);
			if (perTabHistory != null) {
				LinkedList<NavigationHistoryEntry> entries = forward ? perTabHistory.forwardEntries
						: perTabHistory.backwardEntries;
				return !entries.isEmpty();
			}
		}
		return false;
	}

	/**
	 * Returns entries in restore order.
	 *
	 * @param forward <code>true</code> for forward and <code>false</code> for
	 *                backward history
	 * @return the navigation history entries
	 */
	private NavigationHistoryEntry[] getEntriesForTab(boolean forward) {
		Object editorTabCookie = getCookieForTab(page.getActiveEditor());
		if (editorTabCookie != null) {
			PerTabHistory perTabHistory = perTabHistoryMap.get(editorTabCookie);
			if (perTabHistory != null) {
				LinkedList<NavigationHistoryEntry> entries = forward ? perTabHistory.forwardEntries
						: perTabHistory.backwardEntries;
				return entries.toArray(new NavigationHistoryEntry[entries.size()]);
			}
		}
		return new NavigationHistoryEntry[0];
	}

	private void disposeHistoryForTabs() {
		Object[] keys = perTabHistoryMap.keySet().toArray();
		for (Object key : keys) {
			disposeHistoryForTab(key);
		}
	}

	void disposeHistoryForTab(Object editorTabCookie) {
		PerTabHistory perTabHistory = perTabHistoryMap.remove(editorTabCookie);
		if (perTabHistory != null) {
			if (perTabHistory.currentEntry != null) {
				disposeEntry(perTabHistory.currentEntry);
				perTabHistory.currentEntry = null;
			}
			removeEntriesForTab(perTabHistory.backwardEntries);
			removeEntriesForTab(perTabHistory.forwardEntries);
		}
	}

	private void removeEntriesForTab(LinkedList<NavigationHistoryEntry> entries) {
		for (Iterator<NavigationHistoryEntry> it = entries.iterator(); it.hasNext();) {
			NavigationHistoryEntry entry = it.next();
			disposeEntry(entry);
			it.remove();
		}
	}

	public boolean updateActive(IEditorPart editor) {
		NavigationHistoryEntry e = getEntry(activeEntry);
		if (e == null)
			return false;
		// 1) check if editor ID matches
		IWorkbenchPartSite site = editor.getSite();
		if (site == null) // might happen if site has not being initialized yet
			return false;
		String editorID = site.getId();
		if (editorID == null) // should not happen for an editor
			return false;
		if (!editorID.equals(e.editorInfo.editorID))
			return false;
		// 2) check that input matches
		IEditorInput input = editor.getEditorInput();
		if (input == null)
			return false;
		if (!input.equals(e.editorInfo.editorInput))
			return false;
		updateEntry(e);
		return true;
	}
}
