/*******************************************************************************
 * Copyright (c) 2005, 2010 Texas Instruments Incorporated 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:
 *     Texas Instruments - initial API and implementation
 *     Intel Corporation - adaptation to new project model
 *     IBM Corporation
 *******************************************************************************/
package org.eclipse.cdt.managedbuilder.ui.wizards;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
import java.util.TreeMap;

import org.eclipse.cdt.managedbuilder.core.BuildException;
import org.eclipse.cdt.managedbuilder.core.IToolChain;
import org.eclipse.cdt.managedbuilder.core.ManagedBuildManager;
import org.eclipse.cdt.managedbuilder.internal.ui.Messages;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IExtension;
import org.eclipse.core.runtime.IExtensionPoint;
import org.eclipse.core.runtime.Platform;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.jface.wizard.IWizardPage;

/**
 *  This class is responsible for managing the use of custom pages in the Managed Build System's
 *  New Project wizards.
 *
 *  This class is a singleton.
 *
 * @noinstantiate This class is not intended to be instantiated by clients.
 */
public final class MBSCustomPageManager {
	public static final String PAGE_ID = "org.eclipse.cdt.managedbuilder.ui.wizard.platformPage"; //$NON-NLS-1$

	/**
	 * ID attribute of nature element
	 */
	public static final String NATURE_ID = "natureID"; //$NON-NLS-1$

	/**
	 *  versions supported attribute of toolchain element
	 */
	public static final String VERSIONS_SUPPORTED = "versionsSupported"; //$NON-NLS-1$

	/**
	 * ID attribute of toolchain element
	 */
	public static final String TOOLCHAIN_ID = "toolchainID"; //$NON-NLS-1$

	/**
	 *  ID attribute of projectType element
	 */
	public static final String PROJECT_TYPE_ID = "projectTypeID"; //$NON-NLS-1$

	/**
	 *  nature element
	 */
	public static final String NATURE = "nature"; //$NON-NLS-1$

	/**
	 *  toolchain element
	 */
	public static final String TOOLCHAIN = "toolchain"; //$NON-NLS-1$

	/**
	 *  project type element
	 */
	public static final String PROJECT_TYPE = "projectType"; //$NON-NLS-1$

	/**
	 *   attribute for the class associated witha wizardPage element
	 */
	public static final String PAGE_CLASS = "pageClass"; //$NON-NLS-1$

	/**
	 *  attribute for the operation that is run for a wizardPage during the wizard's DoRunEpilogue() method
	 */
	public static final String OPERATION_CLASS = "operationClass"; //$NON-NLS-1$

	/**
	 *   ID attribute for wizardPage
	 */
	public static final String ID = "ID"; //$NON-NLS-1$

	/**
	 *   element for a custom wizard page
	 */
	public static final String WIZARD_PAGE = "wizardPage"; //$NON-NLS-1$

	/**
	 * Maps String IDs to IWizardPages
	 */
	private static Map idToPageDataMap = null;

	/**
	 *   The set of pages that this manager knows about.
	 */
	private static Set pageSet = null;

	/**
	 * Maps page IDs to the properties that page has set.
	 */
	private static java.util.Map pageIDtoPagePropertiesMap = null;

	private static final String EXTENSION_POINT_ID = "org.eclipse.cdt.managedbuilder.ui.newWizardPages"; //$NON-NLS-1$

	private static List hiddenList;

	/**
	 *
	 * Looks for contributions to the extension point org.eclipse.cdt.managedbuilder.ui.newWizardPages and adds all pages to the manager.
	 * @since 3.0
	 *
	 */
	public static void loadExtensions() throws BuildException {
		loadExtensionsSynchronized();
	}

	private synchronized static void loadExtensionsSynchronized() throws BuildException {
		// Get the extensions
		IExtensionPoint extensionPoint = Platform.getExtensionRegistry().getExtensionPoint(EXTENSION_POINT_ID);
		if (extensionPoint != null) {
			IExtension[] extensions = extensionPoint.getExtensions();
			if (extensions != null) {

				for (int i = 0; i < extensions.length; ++i) {
					IExtension extension = extensions[i];

					// Get the "configuraton elements" defined in the plugin.xml file.
					// Note that these "configuration elements" are not related to the
					// managed build system "configurations".
					// From the PDE Guide:
					//  A configuration element, with its attributes and children, directly
					//  reflects the content and structure of the extension section within the
					//  declaring plug-in's manifest (plugin.xml) file.
					IConfigurationElement[] elements = extension.getConfigurationElements();

					// process the top level elements for this extension
					for (int k = 0; k < elements.length; k++) {
						IConfigurationElement element = elements[k];

						if (element.getName().equals(WIZARD_PAGE)) {
							// load the data associated with the wizard page
							loadWizardPage(element);
						}

						else {
							// there are currently no other supported element types
							// so throw an exception
							throw new BuildException(Messages.MBSCustomPageManager_error0 + element.getName()
									+ Messages.MBSCustomPageManager_error1 + EXTENSION_POINT_ID);
						}
					}

				}

			}
		}
	}

	private static void loadWizardPage(IConfigurationElement element) throws BuildException {
		// get the ID and wizard page
		String id = element.getAttribute(ID);

		//		String operationClassName = element.getAttribute(OPERATION_CLASS); // optional element so may be null

		IWizardPage wizardPage = null;
		Object operation = null;
		// instantiate the classes specified in the manifest

		// first try to create the page class, which is required
		try {
			wizardPage = (IWizardPage) element.createExecutableExtension(PAGE_CLASS);

			// the operation is an optional element so it might not be present
			if (element.getAttribute(OPERATION_CLASS) != null)
				operation = element.createExecutableExtension(OPERATION_CLASS);
		} catch (CoreException e) {
			// convert to a build exception
			throw new BuildException(e.getMessage());
		}

		// create the page data and add it to ourselves
		MBSCustomPageData currentPageData;

		// Custom pages prior to CDT 4.0 were required to provide Runnables as operations.
		// Post 4.0, IRunnableWithProgress are accepted as well.
		if (operation instanceof Runnable) {
			currentPageData = new MBSCustomPageData(id, wizardPage, (Runnable) operation, false);
		} else if (operation instanceof IRunnableWithProgress || operation == null) {
			currentPageData = new MBSCustomPageData(id, wizardPage, (IRunnableWithProgress) operation, false);
		} else {
			throw new BuildException(element.getName());
		}

		idToPageDataMap.put(id, currentPageData);
		pageSet.add(currentPageData);

		// load any child elements
		IConfigurationElement[] children = element.getChildren();

		for (int k = 0; k < children.length; k++) {
			IConfigurationElement childElement = children[k];

			if (childElement.getName().equals(PROJECT_TYPE)) {
				loadProjectType(childElement, currentPageData);
			}

			else {
				if (childElement.getName().equals(TOOLCHAIN)) {
					loadToolchain(childElement, currentPageData);
				}

				else {
					if (childElement.getName().equals(NATURE)) {
						loadNature(childElement, currentPageData);
					}

					else {
						// no other types supported... throw an exception
						//						 there are currently no other supported element types
						// so throw an exception
						throw new BuildException(Messages.MBSCustomPageManager_error2 + element.getName()
								+ Messages.MBSCustomPageManager_error3 + EXTENSION_POINT_ID);
					}
				}
			}
		}
	}

	private static void loadProjectType(IConfigurationElement element, MBSCustomPageData currentPageData)
			throws BuildException {
		String projectType = element.getAttribute(PROJECT_TYPE_ID);

		if (projectType != null)
			currentPageData.addProjectType(projectType);
		else
			throw new BuildException(Messages.MBSCustomPageManager_error4);
	}

	private static void loadToolchain(IConfigurationElement element, MBSCustomPageData currentPageData)
			throws BuildException {
		String toolchainID = element.getAttribute(TOOLCHAIN_ID);

		if (toolchainID != null) {
			// get the supported versions
			String unparsedVersions = element.getAttribute(VERSIONS_SUPPORTED);
			String[] versionsSupported = null;

			if (unparsedVersions != null) {
				// parse out the individual versions - they are comma separated
				versionsSupported = unparsedVersions.split(","); //$NON-NLS-1$
			}

			// add toolchain data for the page
			currentPageData.addToolchain(toolchainID, versionsSupported);
		} else
			throw new BuildException(Messages.MBSCustomPageManager_error5);
	}

	private static void loadNature(IConfigurationElement element, MBSCustomPageData currentPageData)
			throws BuildException {
		String nature = element.getAttribute(NATURE_ID);

		if (nature != null)
			currentPageData.addNature(nature);
		else
			throw new BuildException(Messages.MBSCustomPageManager_error6);
	}

	/**
	 *
	 * @param pageID - The unique ID of the page to search for.
	 *
	 * @return - The MBSCustomPageData corresponding to the page, or null
	 * if not found.
	 *
	 * @since 3.0
	 */
	public static MBSCustomPageData getPageData(String pageID) {
		return (MBSCustomPageData) idToPageDataMap.get(pageID);
	}

	/**
	 * @param pageID - The unique ID of the page to be tested.
	 * @return true if the page is visible given the currently selected project type, nature, and toolchain.  false otherwise.
	 * @since 3.0
	 */
	public static boolean isPageVisible(String pageID) {
		MBSCustomPageData page = getPageData(pageID);

		if (page == null)
			return false;

		if (getPageHideStatus(pageID))
			return false;

		//	first, find out what project type, toolchain, and nature have been set
		Map pagePropertiesMap = (Map) pageIDtoPagePropertiesMap.get(PAGE_ID);

		Object projectType = pagePropertiesMap.get(PROJECT_TYPE);
		List toolchainList = (List) pagePropertiesMap.get(TOOLCHAIN);
		Object nature = pagePropertiesMap.get(NATURE);

		//		 does the page follow nature and project type constraints?
		if (page.shouldBeVisibleForNature(nature) && page.shouldBeVisibleForProjectType(projectType)) {
			MBSCustomPageData.ToolchainData[] toolchainData = page.getToolchains();
			// if no toolchains are specified then we're done
			if (toolchainData == null || toolchainList == null)
				return true;

			// otherwise, iterate through the toolchains to see if one matches
			for (int k = 0; k < toolchainData.length; k++) {
				// check all toolchains, see if there is one for which we should be present
				Iterator toolchainIterator = toolchainList.iterator();
				while (toolchainIterator.hasNext()) {
					IToolChain toolchain = (IToolChain) toolchainIterator.next();
					if (toolchain != null) {
						String id = ManagedBuildManager.getIdFromIdAndVersion(toolchain.getId());
						String version = ManagedBuildManager.getVersionFromIdAndVersion(toolchain.getId());
						// check the ID and version
						if (page.shouldBeVisibleForToolchain(id, version))
							return true;
					}
				}
			}
		}
		return false;
	}

	/**
	 *
	 * Publishes a piece of data associated with a wizard page.  Clients (e.g. other wizard pages) can retrieve
	 * the values of these pieces of data later given the proper page ID and key.
	 *
	 * @param pageID - The unique ID of the page for which the data is being added.
	 *
	 * @param key - A unique name by which the data is referred to.
	 *
	 * @param data - The data to be stored.  No assumptions are made about the type of data stored.  It is up to the
	 * 				 contributor of a given page to establish their own contract as to what type of data is stored.
	 *
	 * @since 3.0
	 *
	 * @see org.eclipse.cdt.managedbuilder.ui.wizards.MBSCustomPageManager#getPageProperty(String, String)
	 */
	public static void addPageProperty(String pageID, String key, Object data) {
		Map propertiesMap = (Map) pageIDtoPagePropertiesMap.get(pageID);

		if (propertiesMap == null) {
			propertiesMap = new TreeMap();
			pageIDtoPagePropertiesMap.put(pageID, propertiesMap);
		}

		propertiesMap.put(key, data);
	}

	/**
	 * Retrieves a previously published piece of data associated with a wizard page.
	 *
	 *
	 * @param pageID - The unique ID of the page for which the
	 * data should be retrieved.
	 *
	 * @param key - The unique name of the data to be retrieved.
	 *
	 * @return  The data that was stored for the given key.  No assumptions are made about the type of data stored.  It is up to the
	 * 			contributor of a given page to establish their own contract as to what type of data is stored.
	 *
	 * 			There are certain well known pieces of data published by the stock wizard pages provided by the Managed Build System.
	 * 			See org.eclipse.cdt.maangedbuilder.ui.wizards.CProjectPlatformPage.
	 *
	 * @see org.eclipse.cdt.maangedbuilder.ui.wizards.CProjectPlatformPage
	 * @see org.eclipse.cdt.managedbuilder.ui.wizards.MBSCustomPageManager#addPageProperty(String, String, Object)
	 *
	 * @since 3.0
	 */
	public static Object getPageProperty(String pageID, String key) {
		Map propertiesMap = (Map) pageIDtoPagePropertiesMap.get(pageID);

		if (propertiesMap != null) {
			return propertiesMap.get(key);
		}

		else
			return null;
	}

	/**
	 * Gets the next page that should appear in the wizard.  This takes into account the selected
	 * project type, project nature, and toolchains.
	 *
	 * @param currentPageID - The unique ID of the page the wizard is currently displaying.
	 * @return The next page that should be displayed in the wizard, or null if at the end of the wizard.
	 * @since 3.0
	 */
	public static IWizardPage getNextPage(String currentPageID) {
		// find the current page in the set of pages
		MBSCustomPageData pageData = getPageData(currentPageID);
		Iterator iterator = pageSet.iterator();
		while (iterator.hasNext()) {
			if (pageData.equals(iterator.next())) {
				IWizardPage nextPage = null;
				while (iterator.hasNext() && nextPage == null) {
					MBSCustomPageData potentialPage = (MBSCustomPageData) iterator.next();
					if (isPageVisible(potentialPage.getID()))
						nextPage = potentialPage.getWizardPage();
				}
				return nextPage;
			}
		}
		return null;
	}

	/**
	 * Adds an entry for a stock page into the manager.  This is used for pages provided by the Managed Build System that are not loaded via the
	 * extension point mechanism.
	 *
	 * @param page - The IWizardPage to add.
	 * @param pageID - A unique ID to associate with this page.  This ID will be used to refer to the page by the rest of the system.
	 * @since 3.0
	 */
	public static void addStockPage(IWizardPage page, String pageID) {
		MBSCustomPageData pageData = new MBSCustomPageData(pageID, page, (IRunnableWithProgress) null, true);
		idToPageDataMap.put(pageID, pageData);
		pageSet.add(pageData);
	}

	/**
	 * Gets the previous page that should appear in the wizard.  This takes into account the selected
	 * project type, project nature, and toolchains.  Stock pages can be returned by this method as well as
	 * custom pages.
	 *
	 * @param currentPageID - The unique ID of the page currently being displayed in the wizard.
	 * @return - The IWizardPage that corresponds to the previous page to be displayed in the wizard, or null if at the start of the wizard.
	 * @since 3.0
	 */
	public static IWizardPage getPreviousPage(String currentPageID) {
		// find the current page in the set of pages
		MBSCustomPageData pageData = getPageData(currentPageID);
		MBSCustomPageData currentData = null;

		Iterator iterator = pageSet.iterator();

		// since Java has no concept of a reverse-iterator yet, we must keep a stack of the
		// pages we have visited, so that we can get them in reverse chronological order
		Stack pageDataStack = new Stack();

		while (iterator.hasNext()) {
			currentData = (MBSCustomPageData) iterator.next();

			if (currentData == pageData) {

				// we found the page we're looking for so stop looking
				break;
			}

			else {
				pageDataStack.push(currentData);
			}
		}

		if (currentData == pageData) {
			// we found our page
			// look for the previous page that satisfies all project type, toolchain, and nature criteria

			// first, find out what project type, toolchain, and nature have been set
			//			Map pagePropertiesMap = (Map) pageIDtoPagePropertiesMap.get(PAGE_ID);

			//			String projectType = pagePropertiesMap.get(PROJECT_TYPE).toString();

			//			Set toolchainSet = (Set) pagePropertiesMap.get(TOOLCHAIN);

			//			String nature = pagePropertiesMap.get(NATURE).toString();

			IWizardPage prevPage = null;

			boolean pageFound = false;

			// use the stack to visit the previous pages
			while (pageDataStack.size() != 0 && !pageFound) {
				MBSCustomPageData potentialPage = (MBSCustomPageData) pageDataStack.pop();

				if (isPageVisible(potentialPage.getID())) {
					pageFound = true;
					prevPage = potentialPage.getWizardPage();
				}

			}

			if (pageFound)
				return prevPage;

		}

		return null;
	}

	/**
	 * Gets the pages that the page manager knows about.
	 *
	 * @return An array of IWizardPage objects corresponding to all pages the manager knows about.
	 * Pages are returned in the order they appear in the wizard, and include both stock and custom pages.
	 *
	 * @since 3.0
	 *
	 * @see getCustomPages()
	 */
	public static IWizardPage[] getPages() {
		IWizardPage[] pages = new IWizardPage[pageSet.size()];

		Iterator iterator = pageSet.iterator();

		int k = 0;

		while (iterator.hasNext()) {
			MBSCustomPageData page = (MBSCustomPageData) iterator.next();

			pages[k++] = page.getWizardPage();
		}

		return pages;
	}

	/**
	 * Gets all custom pages that the page manager knows about.
	 *
	 * @return An array of IWizardPage objects corresponding to all custom pages the manager knows about.
	 * Pages are returned in the order they appear in the wizard.  Stock pages are not included.
	 *
	 * @since 3.0
	 *
	 * @see getPages()
	 */
	public static IWizardPage[] getCustomPages() {
		Set customPageSet = new LinkedHashSet();

		Iterator pageIterator = pageSet.iterator();

		while (pageIterator.hasNext()) {
			MBSCustomPageData page = (MBSCustomPageData) pageIterator.next();

			if (!page.isStockPage()) {
				customPageSet.add(page.getWizardPage());
			}

		}

		Iterator iterator = customPageSet.iterator();

		IWizardPage[] pages = new IWizardPage[customPageSet.size()];

		int k = 0;
		while (iterator.hasNext()) {
			pages[k++] = (IWizardPage) iterator.next();
		}

		return pages;
	}

	/**
	 * Gets all operations that should be run during the wizard's DoRunEpilogue() method.  Only operations for visible pages are returned.
	 *
	 * @return array of type Runnable[] corresponding to the operations.
	 *
	 * @since 3.0
	 */
	public static IRunnableWithProgress[] getOperations() {
		Set operationSet = new LinkedHashSet();

		Iterator pageIterator = pageSet.iterator();

		while (pageIterator.hasNext()) {
			MBSCustomPageData page = (MBSCustomPageData) pageIterator.next();

			if (!page.isStockPage() && isPageVisible(page.getID())) {
				if (page.getOperation() != null) {
					operationSet.add(page.getOperation());
				}
			}

		}

		if (operationSet.size() == 0)
			return null;

		Iterator iterator = operationSet.iterator();

		IRunnableWithProgress[] operations = new IRunnableWithProgress[operationSet.size()];

		int k = 0;
		while (iterator.hasNext()) {
			operations[k++] = (IRunnableWithProgress) iterator.next();
		}

		return operations;

	}

	/**
	 *  Initializes the manager.
	 *
	 *  This method should be called before any other operations are performed using this class, and should
	 *  be called every time pages are added to the wizard.
	 *
	 *  @since 3.0
	 */
	public static void init() {
		idToPageDataMap = new TreeMap();
		pageIDtoPagePropertiesMap = new TreeMap();
		pageSet = new LinkedHashSet();
		hiddenList = new ArrayList();
	}

	// singleton class - do not use
	private MBSCustomPageManager() {

	}

	/**
	 * Ability to hide pages explicitly,
	 * not depending of nature/projecttype filter
	 *
	 * @param p - page to hide
	 * @param status - true means hidden
	 */
	public static void setPageHideStatus(String Id, boolean status) {
		if (hiddenList.contains(Id)) {
			if (!status)
				hiddenList.remove(Id);
		} else {
			if (status)
				hiddenList.add(Id);
		}
	}

	public static boolean getPageHideStatus(String Id) {
		return hiddenList.contains(Id);
	}
}
