/**
 * Copyright (c) 2005-2006 Aptana, Inc.
 *
 * 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. If redistributing this code,
 * this entire header must remain intact.
 */
package org.eclipse.eclipsemonkey.ui.scriptableView;

import java.util.List;
import java.util.Vector;

import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.IToolBarManager;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.swt.SWT;
import org.eclipse.swt.browser.Browser;
import org.eclipse.swt.browser.LocationEvent;
import org.eclipse.swt.browser.LocationListener;
import org.eclipse.swt.browser.ProgressEvent;
import org.eclipse.swt.browser.ProgressListener;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.ui.IActionBars;
import org.eclipse.ui.IMemento;
import org.eclipse.ui.IViewSite;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.actions.ActionFactory;
import org.eclipse.ui.part.ViewPart;
import org.eclipse.ui.plugin.AbstractUIPlugin;

/**
 * @author Paul Colton (Aptana, Inc.)
 *
 */
public abstract class BrowserView extends ViewPart {
	private String partId; //$NON-NLS-1$
	private Browser browser;
	private String url;

	// TODO: Add to plugin strings for localization

	/**
	 * Browser_homeButton_tooltip
	 */
	public static String Browser_homeButton_tooltip = Messages.BrowserView_Home;

	/**
	 * Browser_forwardButton_tooltip
	 */
	public static String Browser_forwardButton_tooltip = Messages.BrowserView_NavigateToNextTopic;

	/**
	 * Browser_backwardButton_tooltip
	 */
	public static String Browser_backwardButton_tooltip = Messages.BrowserView_NavigateToPreviousTopic;

	/**
	 * Browser_invalidConfig
	 */
	public static String Browser_invalidConfig = Messages.BrowserView_InvalidConfiguration;

	/**
	 * urlListener
	 */
	protected BrowserIntroPartLocationListener urlListener = new BrowserIntroPartLocationListener(this);

	/**
	 * history
	 */
	protected History history = new History();

	// Global actions

	/**
	 * backAction
	 */
	protected Action backAction = new Action()
	{

		{
			setToolTipText(BrowserView.Browser_backwardButton_tooltip);
			setImageDescriptor(BrowserView.getImageDescriptor("icons/backward_nav_on.gif")); //$NON-NLS-1$
			setDisabledImageDescriptor(BrowserView.getImageDescriptor("icons/backward_nav_off.gif")); //$NON-NLS-1$
		}

		public void run()
		{
			navigateBackward();
		}
	};

	/**
	 * forwardAction
	 */
	protected Action forwardAction = new Action()
	{
		{
			setToolTipText(BrowserView.Browser_forwardButton_tooltip);
			setImageDescriptor(BrowserView.getImageDescriptor("icons/forward_nav_on.gif")); //$NON-NLS-1$
			setDisabledImageDescriptor(BrowserView.getImageDescriptor("icons/forward_nav_off.gif")); //$NON-NLS-1$
		}

		public void run()
		{
			navigateForward();
		}
	};

	/**
	 * Returns the startup URL (to display when this control initially loads
	 * 
	 * @return String
	 */
	public abstract String getStartUrl();

	/**
	 * homeAction
	 */
	protected Action homeAction = new Action()
	{
		{
			setToolTipText(BrowserView.Browser_homeButton_tooltip); //$NON-NLS-1$
			setImageDescriptor(BrowserView.getImageDescriptor("icons/home_nav_on.gif")); //$NON-NLS-1$
			setDisabledImageDescriptor(BrowserView.getImageDescriptor("icons/home_nav_off.gif")); //$NON-NLS-1$
		}

		public void run()
		{
			navigateHome();
		}
	};

	/**
	 * The constructor.
	 */
	public BrowserView()
	{
		url = getStartUrl();
		partId = getPartId();
	}

	/**
	 * Returns the "part ID" which is used in the contexts.xml file
	 * 
	 * @return String
	 */
	public abstract String getPartId();

	/**
	 * @see org.eclipse.ui.IViewPart#init(org.eclipse.ui.IViewSite, org.eclipse.ui.IMemento)
	 */
	public void init(IViewSite site, IMemento memento) throws PartInitException
	{
		super.init(site, memento);

		if (memento != null)
		{
			url = memento.getString(partId);
			sendMessage(url);
		}
	}

	/**
	 * @see org.eclipse.ui.IPersistable#saveState(org.eclipse.ui.IMemento)
	 */
	public void saveState(IMemento memento)
	{
		if (url != null)
		{
			memento.putString(partId, url);
		}
	}

	/**
	 * This is a callback that will allow us to create the viewer and initialize it.
	 * 
	 * @param parent
	 */
	public void createPartControl(Composite parent)
	{
		browser = new Browser(parent, SWT.NONE);
		if (this.url != null)
		{
			browser.setUrl(this.url);
		}

		PlatformUI.getWorkbench().getHelpSystem().setHelp(browser, "com.aptana.ide.js.ui." + partId); //$NON-NLS-1$

		// add a location listener on the browser so we can intercept
		// LocationEvents. Responsible for intercepting URLs and updating UI
		// with history.
		browser.addLocationListener(urlListener);

		// add a location listener that will clear a flag at the end of any
		// navigation to a page. This is used in conjunction with the location
		// listener to filter out redundant navigations due to frames.
		browser.addProgressListener(new ProgressListener()
		{

			public void changed(ProgressEvent event)
			{
				// no-op
			}

			public void completed(ProgressEvent event)
			{
				urlListener.flagEndOfNavigation();
				urlListener.flagEndOfFrameNavigation();
				urlListener.flagRemovedTempUrl();
				updateNavigationActionsState();
			}
		});

		addToolBarActions();
	}

	/*******************************************************************************************************************
	 * Pulled from BrowserIntroPartImplementation
	 ******************************************************************************************************************/

	protected void addToolBarActions()
	{

		IActionBars actionBars = getViewSite().getActionBars();

		IToolBarManager toolBarManager = actionBars.getToolBarManager();
		actionBars.setGlobalActionHandler(ActionFactory.FORWARD.getId(), forwardAction);
		actionBars.setGlobalActionHandler(ActionFactory.BACK.getId(), backAction);
		toolBarManager.add(homeAction);
		toolBarManager.add(backAction);
		toolBarManager.add(forwardAction);
		toolBarManager.update(true);
		actionBars.updateActionBars();
		updateNavigationActionsState();
	}

	/**
	 * Return a reference to the browser
	 * 
	 * @return Browser
	 */
	public Browser getBrowser()
	{
		return browser;
	}

	/**
	 * Passing the focus request to the viewer's control.
	 */
	public void setFocus()
	{
		browser.setFocus();
	}

	/**
	 * @param url 
	 */
	public void sendMessage(String url)
	{
		if (url != null)
		{
			this.url = url;
			if (browser != null)
			{
				browser.setUrl(url);
			}
		}
	}

	/**
	 * updateNavigationActionsState
	 */
	protected void updateNavigationActionsState()
	{
		// in static html intro, use browser history.
		forwardAction.setEnabled(browser.isForwardEnabled());
		backAction.setEnabled(browser.isBackEnabled());
	}

	/**
	 * navigateBackward
	 * 
	 * @return boolean
	 */
	public boolean navigateBackward()
	{
		return browser.back();
	}

	/**
	 * navigateForward
	 * 
	 * @return boolean
	 */
	public boolean navigateForward()
	{
		return browser.forward();
	}

	/**
	 * navigateHome
	 * 
	 * @return boolean
	 */
	public boolean navigateHome()
	{
		String location = url;
		boolean success = browser.setUrl(location);
		updateHistory(location);

		return success;
	}

	/**
	 * Updates the UI navigation history with either a real URL.
	 * 
	 * @param location
	 */
	public void updateHistory(String location)
	{
		history.updateHistory(location);
		updateNavigationActionsState();
	}
	
	
	/**
	 * Retrieves the image descriptor associated with resource from the image
	 * descriptor registry. If the image descriptor cannot be retrieved, attempt
	 * to find and load the image descriptor at the location specified in
	 * resource.
	 * 
	 * @param imageFilePath
	 *            the image descriptor to retrieve
	 * @return The image descriptor assocated with resource or the default
	 *         "missing" image descriptor if one could not be found
	 */
	private static ImageDescriptor getImageDescriptor(String imageFilePath) {
		
		ImageDescriptor imageDescriptor = AbstractUIPlugin.imageDescriptorFromPlugin(
				"org.eclipse.eclipsemonkey.ui", imageFilePath);

		if (imageDescriptor == null) {
			imageDescriptor = ImageDescriptor.getMissingImageDescriptor();
		}

		return imageDescriptor;
	}

	/**
	 * A Location Listener that knows how to intercept OOBE action URLs. It also knows how to update UI navigation
	 * history.
	 */
	private class BrowserIntroPartLocationListener implements LocationListener
	{
		private BrowserView implementation;

		/**
		 * Takes the implementation as an input.
		 * 
		 * @param implementation
		 */
		public BrowserIntroPartLocationListener(BrowserView implementation)
		{
			this.implementation = implementation;
		}

		/**
		 * @see org.eclipse.swt.browser.LocationListener#changed(org.eclipse.swt.browser.LocationEvent)
		 */
		public void changed(LocationEvent event)
		{
			String url = event.location;
			if (url == null)
			{
				return;
			}

			// guard against unnecessary History updates.
			Browser browser = (Browser) event.getSource();
			if (browser.getData("navigation") != null //$NON-NLS-1$
					&& browser.getData("navigation").equals("true")) //$NON-NLS-1$ //$NON-NLS-2$
			{
				return;
			}
		}

		/**
		 * Intercept any LocationEvents on the browser. If the event location is a valid IntroURL, cancel the event and
		 * execute the intro action that is embedded in the URL
		 * 
		 * @param event
		 */
		public void changing(LocationEvent event)
		{
			String url = event.location;
			if (url == null)
			{
				return;
			}
		}

		/**
		 * flagStartOfFrameNavigation
		 */
		public void flagStartOfFrameNavigation()
		{
			if (implementation.getBrowser().getData("frameNavigation") == null) //$NON-NLS-1$
			{
				implementation.getBrowser().setData("frameNavigation", "true"); //$NON-NLS-1$ //$NON-NLS-2$
			}
		}

		/**
		 * flagEndOfFrameNavigation
		 */
		public void flagEndOfFrameNavigation()
		{
			implementation.getBrowser().setData("frameNavigation", null); //$NON-NLS-1$
		}

		/**
		 * flagStartOfNavigation
		 */
		public void flagStartOfNavigation()
		{
			if (implementation.getBrowser().getData("navigation") == null) //$NON-NLS-1$
			{
				implementation.getBrowser().setData("navigation", "true"); //$NON-NLS-1$ //$NON-NLS-2$
			}
		}

		/**
		 * flagEndOfNavigation
		 */
		public void flagEndOfNavigation()
		{
			implementation.getBrowser().setData("navigation", null); //$NON-NLS-1$
		}

		/**
		 * flagStoredTempUrl
		 */
		public void flagStoredTempUrl()
		{
			if (implementation.getBrowser().getData("tempUrl") == null) //$NON-NLS-1$
			{
				implementation.getBrowser().setData("tempUrl", "true"); //$NON-NLS-1$ //$NON-NLS-2$
			}
		}

		/**
		 * flagRemovedTempUrl
		 */
		public void flagRemovedTempUrl()
		{
			implementation.getBrowser().setData("tempUrl", null); //$NON-NLS-1$
		}
	}

	/**
	 * All browser history
	 * 
	 * @author Ingo Muschenetz
	 */
	private class History
	{
		// History of Intro Pages and real URL visited by Intro Browser. All
		// elements are all of type HistoryObject.
		private Vector history = new Vector();

		private int navigationLocation = 0;

		/**
		 * Model class for history objects. A history object may be a URL or an Intro page. A URL is a regular URL
		 * navigated to from a fully qualified link. An intro page may be an IFrame page. IFrame pages are not created
		 * for every Help topic navigated in an embedded IFrame. Instead the same IFrame is stored in history as a
		 * different object with the IFrameURL set. This way the model actually creates one page for every embedded Help
		 * Topic target but the navigation history updates the IFrame accordingly.
		 */
		class HistoryObject
		{
			String iframeUrl;

			String url;

			HistoryObject(Object location)
			{
				if (location instanceof String)
				{
					this.url = (String) location;
				}
			}

			String getIFrameUrl()
			{
				return iframeUrl;
			}

			String getUrl()
			{
				return url;
			}

			boolean isURL()
			{
				return (url != null) ? true : false;
			}

			boolean isIFramePage()
			{
				return (iframeUrl != null) ? true : false;
			}

		}

		/**
		 * Updates the UI navigation history with either a real URL, or a page ID.
		 * 
		 * @param location
		 */
		public void updateHistory(String location)
		{
			// quick exit.
			if (!history.isEmpty() && isSameLocation(location))
			{
				// resetting the same location is useless.
				return;
			}
			doUpdateHistory(location);
		}

		private void doUpdateHistory(Object location)
		{
			// we got here due to an intro URL listener or an SWT Form hyperlink
			// listener. location may be a URL or an IntroPage.
			if (navigationLocation == getHistoryEndPosition())
			{
				// we are at the end of the vector, just push.
				pushToHistory(location);
			}
			else
			{
				// we already navigated. add item at current location, and clear
				// rest of history. (Same as browser behavior.)
				trimHistory(location);
			}
		}

		private boolean isSameLocation(Object location)
		{
			HistoryObject currentLocation = getCurrentLocation();
			if (location instanceof String && currentLocation.isURL())
			{
				return currentLocation.getUrl().equals(location);
			}

			return false;
		}

		private void pushToHistory(Object location)
		{
			history.add(new HistoryObject(location));
			// point the nav location to the end of the vector.
			navigationLocation = getHistoryEndPosition();
		}

		/**
		 * removeLastHistory
		 */
		public void removeLastHistory()
		{
			history.remove(getHistoryEndPosition());
			// point the nav location to the end of the vector.
			navigationLocation = getHistoryEndPosition();
		}

		private void trimHistory(Object location)
		{
			List newHistory = history.subList(0, navigationLocation + 1);
			history = new Vector(newHistory);
			history.add(new HistoryObject(location));
			// point the nav location to the end of the vector.
			navigationLocation = getHistoryEndPosition();
		}

		/**
		 * Return the position of the last element in the navigation history. If vector is empty, return 0.
		 * 
		 * @param vector
		 * @return int
		 */
		private int getHistoryEndPosition()
		{
			if (history.isEmpty())
			{
				return 0;
			}
			return history.size() - 1;
		}

		/**
		 * navigateHistoryBackward
		 */
		public void navigateHistoryBackward()
		{
			if (badNavigationLocation(navigationLocation - 1))
			{
				// do nothing. We are at the beginning.
				return;
			}
			--navigationLocation;
		}

		/**
		 * Navigate forward in the history.
		 */
		public void navigateHistoryForward()
		{
			if (badNavigationLocation(navigationLocation + 1))
			{
				// do nothing. We are at the beginning.
				return;
			}
			++navigationLocation;
		}

		private boolean badNavigationLocation(int navigationLocation)
		{
			if (navigationLocation < 0 || navigationLocation >= history.size())
			{
				// bad nav location.
				return true;
			}
			return false;
		}

		/**
		 * Returns true if the current location in the navigation history represents a URL. False if the location is an
		 * Intro Page id.
		 * 
		 * @return Returns the locationIsURL.
		 */
		private HistoryObject getCurrentLocation()
		{
			return (HistoryObject) history.elementAt(navigationLocation);
		}

		/**
		 * canNavigateForward
		 * 
		 * @return boolean
		 */
		public boolean canNavigateForward()
		{
			return navigationLocation != getHistoryEndPosition() ? true : false;
		}

		/**
		 * canNavigateBackward
		 * 
		 * @return boolean
		 */
		public boolean canNavigateBackward()
		{
			return navigationLocation == 0 ? false : true;
		}

		/**
		 * currentLocationIsUrl
		 * 
		 * @return boolean
		 */
		public boolean currentLocationIsUrl()
		{
			return getCurrentLocation().isURL();
		}

		/**
		 * getCurrentLocationAsUrl
		 * 
		 * @return boolean
		 */
		public String getCurrentLocationAsUrl()
		{
			return getCurrentLocation().getUrl();
		}

		/**
		 * clear
		 */
		public void clear()
		{
			history.clear();
			navigationLocation = 0;
		}

	}
}
