/*******************************************************************************
 * 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.navigator.framelist;

import java.util.ArrayList;
import java.util.List;

import org.eclipse.core.commands.common.EventManager;
import org.eclipse.core.runtime.Assert;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;

/**
 * Supports a web-browser style of navigation by maintaining a list
 * of frames.  Each frame holds a snapshot of a view at some point
 * in time.
 * <p>
 * The frame list obtains a snapshot of the current frame from a frame source
 * on creation, and whenever switching to a different frame.
 * </p>
 * <p>
 * A property change notification is sent whenever the current page changes.
 * </p>
 * @since 3.4
 */
public class FrameList extends EventManager {

	/** Property name constant for the current frame. */
	public static final String P_CURRENT_FRAME = "currentFrame"; //$NON-NLS-1$

	/** Property name constant for the reset. */
	public static final String P_RESET = "reset"; //$NON-NLS-1$

	private IFrameSource source;

	private List<Frame> frames;

	private int current;

	/**
	 * Creates a new frame list with the given source.
	 *
	 * @param source the frame source
	 */
	public FrameList(IFrameSource source) {
		this.source = source;
		init();
	}

	/**
	 * Adds a property change listener.
	 * Has no effect if an identical listener is already registered.
	 *
	 * @param listener a property change listener
	 */
	public void addPropertyChangeListener(IPropertyChangeListener listener) {
		addListenerObject(listener);
	}

	/**
	 * Moves the frame pointer back by one.
	 * Has no effect if there is no frame before the current one.
	 * Fires a <code>P_CURRENT_FRAME</code> property change event.
	 */
	public void back() {
		if (current > 0) {
			setCurrent(current - 1);
		}
	}

	/**
	 * Notifies any property change listeners that a property has changed.
	 * Only listeners registered at the time this method is called are notified.
	 *
	 * @param event the property change event
	 *
	 * @see IPropertyChangeListener#propertyChange
	 */
	protected void firePropertyChange(PropertyChangeEvent event) {
		Object[] listeners = getListeners();
		for (Object listener : listeners) {
			((IPropertyChangeListener) listener).propertyChange(event);
		}
	}

	/**
	 * Moves the frame pointer forward by one.
	 * Has no effect if there is no frame after the current one.
	 * Fires a <code>P_CURRENT_FRAME</code> property change event.
	 */
	public void forward() {
		if (current < frames.size() - 1) {
			setCurrent(current + 1);
		}
	}

	/**
	 * Returns the current frame.
	 * Returns <code>null</code> if there is no current frame.
	 *
	 * @return the current frame, or <code>null</code>
	 */
	public Frame getCurrentFrame() {
		return getFrame(current);
	}

	/**
	 * Returns the index of the current frame.
	 *
	 * @return the index of the current frame
	 */
	public int getCurrentIndex() {
		return current;
	}

	/**
	 * Returns the frame at the given index, or <code>null</code>
	 * if the index is &le; 0 or &ge; <code>size()</code>.
	 *
	 * @param index the index of the requested frame
	 * @return the frame at the given index or <code>null</code>
	 */
	public Frame getFrame(int index) {
		if (index < 0 || index >= frames.size()) {
			return null;
		}
		return frames.get(index);
	}

	/**
	 * Returns the frame source.
	 * @return an IFrameSource
	 */
	public IFrameSource getSource() {
		return source;
	}

	/**
	 * Adds the given frame after the current frame,
	 * and advances the pointer to the new frame.
	 * Before doing so, updates the current frame, and removes any frames following the current frame.
	 * Fires a <code>P_CURRENT_FRAME</code> property change event.
	 *
	 * @param frame the frame to add
	 */
	public void gotoFrame(Frame frame) {
		for (int i = frames.size(); --i > current;) {
			frames.remove(i);
		}
		frame.setParent(this);
		int index = frames.size();
		frame.setIndex(index);
		frames.add(frame);
		setCurrent(index);
	}

	private void init() {
		Frame frame = source.getFrame(IFrameSource.CURRENT_FRAME, 0);
		frame.setParent(this);
		frame.setIndex(0);
		frames = new ArrayList<>();
		frames.add(frame);
		current = 0;
	}
	/**
	 * Removes a property change listener.
	 * Has no effect if an identical listener is not registered.
	 *
	 * @param listener a property change listener
	 */
	public void removePropertyChangeListener(IPropertyChangeListener listener) {
		removeListenerObject(listener);
	}

	/**
	 * Reset this frame list to its initial state.
	 */
	public void reset() {
		init();
		firePropertyChange(new PropertyChangeEvent(this, P_RESET,
				null, getFrame(current)));
	}

	/**
	 * Sets the current frame to the one with the given index.
	 * Updates the old current frame, and fires a <code>P_CURRENT_FRAME</code> property change event
	 * if the current frame changes.
	 *
	 * @param newCurrent the index of the frame
	 */
	void setCurrent(int newCurrent) {
		Assert.isTrue(newCurrent >= 0 && newCurrent < frames.size());
		int oldCurrent = this.current;
		if (oldCurrent != newCurrent) {
			updateCurrentFrame();
			this.current = newCurrent;
			firePropertyChange(new PropertyChangeEvent(this, P_CURRENT_FRAME,
					getFrame(oldCurrent), getFrame(newCurrent)));
		}
	}

	/**
	 * Sets the current frame to the frame with the given index.
	 * Fires a <code>P_CURRENT_FRAME</code> property change event
	 * if the current frame changes.
	 * @param index
	 */
	public void setCurrentIndex(int index) {
		if (index != -1 && index != current) {
			setCurrent(index);
		}
	}

	/**
	 * Returns the number of frames in the frame list.
	 * @return the number of frames.
	 */
	public int size() {
		return frames.size();
	}

	/**
	 * Replaces the current frame in this list with the current frame
	 * from the frame source.  No event is fired.
	 */
	public void updateCurrentFrame() {
		Assert.isTrue(current >= 0);
		Frame frame = source.getFrame(IFrameSource.CURRENT_FRAME,
				IFrameSource.FULL_CONTEXT);
		frame.setParent(this);
		frame.setIndex(current);
		frames.set(current, frame);
	}

}
