/*******************************************************************************
 * Copyright (c) 2000, 2005 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/

package org.eclipse.ui.internal;

import java.util.ArrayList;
import java.util.Iterator;

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;

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

    private static final int CAPACITY = 50;

    private NavigationHistoryAction backwardAction;

    private NavigationHistoryAction forwardAction;

    private int ignoreEntries;

    private ArrayList history = new ArrayList(CAPACITY);

    private ArrayList editors = new ArrayList(CAPACITY);

    private WorkbenchPage page;

    private int activeEntry = 0;

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

            public void partBroughtToTop(IWorkbenchPartReference partRef) {
            }

            public void partDeactivated(IWorkbenchPartReference partRef) {
            }

            public void partOpened(IWorkbenchPartReference partRef) {
            }
			
            public void partHidden(IWorkbenchPartReference partRef) {
            }
			
            public void partVisible(IWorkbenchPartReference partRef) {
            }

            public void partClosed(IWorkbenchPartReference partRef) {
				updateNavigationHistory(partRef, true);
            }
			
			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 e = editors.iterator();
                    NavigationHistoryEditorInfo info = null;
                    NavigationHistoryEditorInfo currentInfo = null;
                    NavigationHistoryEntry current = getEntry(activeEntry);
                    if (current != null)
                        currentInfo = current.editorInfo;
                    while (e.hasNext()) {
                        info = (NavigationHistoryEditorInfo) e.next();
                        if (id.equals(info.editorID)
                                && input.equals(info.editorInput)) {
                            if (partClosed && info != currentInfo)
                                info.handlePartClosed();
                            break;
                        } else {
                            info = null;
                        }
                    }
                    if (info == null)
                        return;
                    e = history.iterator();
                    int i = 0;
                    while (e.hasNext()) {
                        NavigationHistoryEntry entry = (NavigationHistoryEntry) e
                                .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++;
                                }
                                e.remove();
                                entry.dispose();
                            } else {
                                i++;
                            }
                    }
                    updateActions();
                }
            }
        });
    }

    private Display getDisplay() {
        return page.getWorkbenchWindow().getShell().getDisplay();
    }

    /*
     * 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(new Runnable() {
            public void run() {
                ignoreEntries--;
                EditorSite site = (EditorSite) (part.getEditorSite());
                Control c = site.getPane().getControl();
                if (c == null || c.isDisposed())
                    return;
                NavigationHistoryEntry e = getEntry(activeEntry);
                if (e != null
                        && part.getEditorInput() != e.editorInfo.editorInput)
                    updateEntry(e);
                addEntry(part, true);
            }
        });
    }

    /*
     * (non-Javadoc)
     * Method declared on INavigationHistory.
     */
    public void markLocation(IEditorPart part) {
        addEntry(part, true);
    }

    /*
     * 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).
     */
    NavigationHistoryEntry[] getBackwardEntries() {
        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).
     */
    NavigationHistoryEntry[] getForwardEntries() {
        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;
    }

    /*
     * (non-Javadoc)
     * Method declared on INavigationHistory.
     */
    public INavigationLocation[] getLocations() {
        INavigationLocation result[] = new INavigationLocation[history.size()];
        for (int i = 0; i < result.length; i++) {
            NavigationHistoryEntry e = (NavigationHistoryEntry) history.get(i);
            result[i] = e.location;
        }
        return result;
    }

    /*
     * (non-Javadoc)
     * Method declared on INavigationHistory.
     */
    public INavigationLocation getCurrentLocation() {
        NavigationHistoryEntry entry = getEntry(activeEntry);
        return entry == null ? null : entry.location;
    }

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

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

    /**
     * Keeps a reference to the backward action to update its state
     * whenever needed.
     */
    public 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 (NavigationHistoryEntry) history.get(index);
        return null;
    }

    /*
     * Adds the specified entry to the history.
     */
    private void add(NavigationHistoryEntry entry) {
        removeForwardEntries();
        if (history.size() == CAPACITY) {
            NavigationHistoryEntry e = (NavigationHistoryEntry) 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 = (NavigationHistoryEntry) history
                    .remove(activeEntry + 1);
            disposeEntry(e);
        }
    }

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

        INavigationLocation location = null;
        if (markLocation && 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 (false) {
            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.
     */
    /* package */boolean canForward() {
        return (0 <= activeEntry + 1) && (activeEntry + 1 < history.size());
    }

    /*
     * Returns true if the backward action can be performed otherwise returns false.
     */
    /* package */boolean canBackward() {
        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.
     */
    public void forward() {
        if (canForward())
            shiftEntry(true);
    }

    /*
     * Perform the backward action by getting the previous location and restoring
     * its context.
     */
    public void backward() {
        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.
     */
    protected void shiftCurrentEntry(NavigationHistoryEntry entry) {
        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 editors = (ArrayList) this.editors.clone();
        for (Iterator iter = editors.iterator(); iter.hasNext();) {
            NavigationHistoryEditorInfo info = (NavigationHistoryEditorInfo) iter
                    .next();
            if (!info.isPersistable())
                iter.remove();
        }
        IMemento editorsMem = memento
                .createChild(IWorkbenchConstants.TAG_EDITORS);
        for (Iterator iter = editors.iterator(); iter.hasNext();) {
            NavigationHistoryEditorInfo info = (NavigationHistoryEditorInfo) iter
                    .next();
            info.saveState(editorsMem
                    .createChild(IWorkbenchConstants.TAG_EDITOR));
        }

        ArrayList list = new ArrayList(history.size());
        int size = history.size();
        for (int i = 0; i < size; i++) {
            NavigationHistoryEntry entry = (NavigationHistoryEntry) history
                    .get(i);
            if (entry.editorInfo.isPersistable())
                list.add(entry);
        }
        size = list.size();
        for (int i = 0; i < size; i++) {
            NavigationHistoryEntry entry = (NavigationHistoryEntry) 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;
        }

        NavigationHistoryEntry entry = getEntry(activeEntry);
        if (entry != null && entry.editorInfo.editorInput != null) {
            if (page.getActiveEditor() == page
                    .findEditor(entry.editorInfo.editorInput))
                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 iter = editors.iterator(); iter.hasNext();) {
            info = (NavigationHistoryEditorInfo) iter.next();
            if (editorID.equals(info.editorID)
                    && editorInput.equals(info.editorInput)) {
                info.refCount++;
                break;
            } else {
                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) //failed to restore
            return;
        for (Iterator iter = editors.iterator(); iter.hasNext();) {
            dup = (NavigationHistoryEditorInfo) iter.next();
            if (info != dup && info.editorID.equals(dup.editorID)
                    && info.editorInput.equals(dup.editorInput))
                break;
            else
                dup = null;
        }
        if (dup == null)
            return;
        for (Iterator iter = history.iterator(); iter.hasNext();) {
            NavigationHistoryEntry entry = (NavigationHistoryEntry) iter.next();
            if (entry.editorInfo == dup) {
                entry.editorInfo = info;
                info.refCount++;
            }
        }
        editors.remove(dup);
    }
}
