/*******************************************************************************
 * Copyright (c) 2007, 2008 Symbian Software Limited 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:
 *     Bala Torati (Symbian) - Initial API and implementation
 *     Mark Espiritu (VaST Systems) - bug 215960
 *******************************************************************************/
package org.eclipse.cdt.core.templateengine;

import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.eclipse.cdt.core.CCorePlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.FileLocator;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IExtension;
import org.eclipse.core.runtime.Platform;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;


/**
 * TemplateEngine is implemented as a Singleton. TemplateEngine is responsible for 
 * creating SharedDefaults and initialising the SharedDefaults. Template instances
 * are obtained from TemplateEngine.
 * 
 * @since 4.0
 */
public class TemplateEngine {

	public static String TEMPLATES_EXTENSION_ID = CCorePlugin.PLUGIN_ID + ".templates"; //$NON-NLS-1$
	public static String TEMPLATE_ASSOCIATIONS_EXTENSION_ID = CCorePlugin.PLUGIN_ID + ".templateAssociations"; //$NON-NLS-1$
	
	/**
	 * static reference to the Singleton TemplateEngine instance.
	 */
	private static TemplateEngine TEMPLATE_ENGINE;
	
	/**
	 * This is a Map <WizardID, TemplateInfo>.
	 */
	private Map<String, List<TemplateInfo>> templateInfoMap;

	/**
	 * TemplateEngine constructor, create and initialise SharedDefaults.
	 */
	private TemplateEngine() {
		templateInfoMap = new LinkedHashMap<String, List<TemplateInfo>>();
		initializeTemplateInfoMap();
	}

	/**
	 * Returns all the TemplateCore objects, no filtering is done.
	 */
	public TemplateCore[] getTemplates() {
		TemplateInfo[] templateInfoArray = getTemplateInfos();
		List<TemplateCore> tcores = new ArrayList<TemplateCore>();
		for (int i=0; i<templateInfoArray.length; i++) {
			TemplateInfo info = templateInfoArray[i];
			try {
				tcores.add(TemplateCore.getTemplate(info));
			} catch (TemplateInitializationException e) {
				CCorePlugin.log(CCorePlugin.createStatus(e.getMessage(), e));
			}
		}
		return tcores.toArray(new TemplateCore[tcores.size()]);
	}

	/**
	 * Returns the first template defined for the specified parameters
	 * @param projectType may not be null
	 * @param toolChain may be null to indicate no tool-chain filtering
	 * @param usageFilter a regex in java.util.regex.Pattern format, may be null to indicate no filtering
	 * @see java.util.regex.Pattern
	 * @return the TemplateCore for the first template defined for the specified parameters, or null
	 * if no such definition exists, or if there is an error initializing the template (the error will
	 * be logged).
	 */
	public TemplateCore getFirstTemplate(String projectType, String toolChain, String usageFilter) {
		TemplateInfo[] infos= getTemplateInfos(projectType, toolChain, usageFilter);
		if(infos.length>0) {
			try {
				return TemplateCore.getTemplate(infos[0]);
			} catch(TemplateInitializationException tie) {
				CCorePlugin.log(tie);
			}
		}
		return null;
	}
	
	/**
	 * Equivalent to calling the overloaded version of getFirstTemplate with null arguments for
	 * toolChain and usageFilter.
	 * @see TemplateEngine#getFirstTemplate(String, String, String)
	 * @return the first TemplateCore object registered, or null if this does not exist
	 */
	public TemplateCore getFirstTemplate(String projectType) {
		return getFirstTemplate(projectType, null, null);
	}

	/**
	 * This method will be called by Container UIs (Wizard, PropertyPage,
	 * PreferencePage). Create a Template instance, update the ValueStore, with
	 * SharedDefaults. This method calls the getTemplate(URL), after getting URL
	 * for the given String TemplateDescriptor.
	 */
	public TemplateCore[] getTemplates(String projectType, String toolChain, String usageFilter) {
		TemplateInfo[] templateInfoArray = getTemplateInfos(projectType, toolChain, usageFilter);
		List<TemplateCore> templatesList = new ArrayList<TemplateCore>();
		for (int i=0; i<templateInfoArray.length; i++) {
			TemplateInfo info = templateInfoArray[i];
			try {
				templatesList.add(TemplateCore.getTemplate(info));
			} catch (TemplateInitializationException tie) {
				CCorePlugin.log(tie);
			}
		}
		return templatesList.toArray(new TemplateCore[templatesList.size()]);
	}
	
	public TemplateCore[] getTemplates(String projectType, String toolChain) {
		return getTemplates(projectType, toolChain, null);
	}

	public TemplateCore[] getTemplates(String projectType) {
		return getTemplates(projectType, null);
	}
	
	public TemplateCore getTemplateById(String templateId) {
		TemplateCore[] templates = getTemplates();
		
		for(int i=0; i<templates.length; i++) {
			TemplateCore template = templates[i];
			if (template.getTemplateId().equalsIgnoreCase(templateId)) {
				return template;
			}
		}
		return null;
	}
	
	/**
	 * @return the SharedDefaults.
	 */
	public static Map<String, String> getSharedDefaults() {
		return SharedDefaults.getInstance().getSharedDefaultsMap();
	}

	/**
	 * update The SharedDefaults Map. This method will be called by Container
	 * UIs. After collecting data from UIPages, the IDs with Persist attribute
	 * as true, has to be persisted in SharedDefaults XML. For the same this
	 * method is called by passing the ValueStore(updated with user entered
	 * values). Get the PersistTrueIDs from TemplateDescriptor. Persist the
	 * values of IDs in ValueStore, which are also present in PersistTrueIDs
	 * vector.
	 * @param template
	 */
	public void updateSharedDefaults(TemplateCore template) {
		Map<String, String> tobePersisted = new HashMap<String, String>();
		Map<String, String> valueStore = template.getValueStore();
		for(String key : template.getPersistTrueIDs()) {
			tobePersisted.put(key, valueStore.get(key));
		}
		SharedDefaults.getInstance().updateShareDefaultsMap(tobePersisted);
	}

	/**
	 * create the singleton instance, check for null condition of
	 * TEMPLATE_ENGINE. If TEMPLATE_ENGINE is null create the TemplateEngine
	 * instance assign it to TEMPLATE_ENGINE. There is no need to have
	 * synchronized here(while creating TemplateEngine).
	 * 
	 * @return TEMPLATE_ENGINE, instance of TemplateEngine.
     * 
     * @since 4.0
	 */
	public static TemplateEngine getDefault() {
		if(TEMPLATE_ENGINE==null) {
			TEMPLATE_ENGINE = new TemplateEngine();
		}
		return TEMPLATE_ENGINE;
	}

	/**
	 * From the extension point take the class implementing the required
	 * functionality. Update the local HashMap of page-id and URL. This is for
	 * extension point "templates"
	 */
	private void initializeTemplateInfoMap() {
		String templateId = null;
		String location = null;
		String pluginId = null;
		String projectType = null;
		String filterPattern = null;
		boolean isCategory = false;
		
		IExtension[] extensions = Platform.getExtensionRegistry().getExtensionPoint(TEMPLATES_EXTENSION_ID).getExtensions();
		for(int i=0; i<extensions.length; i++) {
			IExtension extension = extensions[i];
			IConfigurationElement[] configElements = extension.getConfigurationElements();
			pluginId = extension.getNamespaceIdentifier(); // Plug-in id of the extending plug-in.
			for(int j=0; j<configElements.length; j++) {
				Object /*IPagesAfterTemplateSelectionProvider*/ extraPagesProvider = null;
				IConfigurationElement config = configElements[j];
				templateId = config.getAttribute(TemplateEngineHelper.ID);
				location = config.getAttribute(TemplateEngineHelper.LOCATION);
				projectType = config.getAttribute(TemplateEngineHelper.PROJECT_TYPE);
				filterPattern = config.getAttribute(TemplateEngineHelper.FILTER_PATTERN);
				isCategory = Boolean.valueOf(config.getAttribute(TemplateEngineHelper.IS_CATEGORY)).booleanValue();
				String providerAttribute = config.getAttribute(TemplateEngineHelper.EXTRA_PAGES_PROVIDER);
				if (providerAttribute != null) {
					try {
						extraPagesProvider = config.createExecutableExtension(TemplateEngineHelper.EXTRA_PAGES_PROVIDER);
					} catch (CoreException e) {
						CCorePlugin.log(CCorePlugin.createStatus("Unable to create extra pages for "+providerAttribute,e)); //$NON-NLS-1$
					}				
				}

				IConfigurationElement[] toolChainConfigs = config.getChildren(TemplateEngineHelper.TOOL_CHAIN);
				Set<String> toolChainIdSet = new LinkedHashSet<String>();
				for (int k=0; k < toolChainConfigs.length; k++) {
					toolChainIdSet.add(toolChainConfigs[k].getAttribute(TemplateEngineHelper.ID));
				}
				
				TemplateInfo templateInfo = new TemplateInfo(templateId, projectType, filterPattern, location, 
														pluginId, toolChainIdSet,
														extraPagesProvider, isCategory);
				if (!templateInfoMap.containsKey(projectType)) {
					templateInfoMap.put(projectType, new ArrayList<TemplateInfo>());
				}
				(templateInfoMap.get(projectType)).add(templateInfo);
			}
		}
		// Check for tool Chains added to the templates outside template info definition
		addToolChainsToTemplates();
	}

	private void addToolChainsToTemplates() {
		String templateId = null;
		TemplateCore[] templates = getTemplates();

		IExtension[] extensions = Platform.getExtensionRegistry().getExtensionPoint(TEMPLATE_ASSOCIATIONS_EXTENSION_ID).getExtensions();
		for(int i=0; i<extensions.length; i++) {
			IExtension extension = extensions[i];
			IConfigurationElement[] configElements = extension.getConfigurationElements();
			for(int j=0; j<configElements.length; j++) {
				IConfigurationElement config = configElements[j];
				templateId = config.getAttribute(TemplateEngineHelper.ID);
				
				IConfigurationElement[] toolChainConfigs = config.getChildren(TemplateEngineHelper.TOOL_CHAIN);
				Set<String> toolChainIdSet = new LinkedHashSet<String>();
				for (int k=0; k < toolChainConfigs.length; k++) {
					toolChainIdSet.add(toolChainConfigs[k].getAttribute(TemplateEngineHelper.ID));
				}
				
				for (int k=0; k < templates.length; k++) {
					String id = templates[k].getTemplateInfo().getTemplateId();
					if (id == null) {
						id = templates[k].getTemplateId();
					}
					if (id != null && id.equals(templateId)) {
						toolChainIdSet.addAll(Arrays.asList(templates[k].getTemplateInfo().getToolChainIds()));
						templates[k].getTemplateInfo().setToolChainSet(toolChainIdSet);
					}
				}
			}
		}
	}
	
	/**
	 * Gets an array of template info objects matching the criteria passed as parameters.
	 * @param projectType may not be null
	 * @param toolChain may be null to indicate no tool-chain
	 * @param usageFilter a usage string which is matched against the filter from the template, may be null
	 * to indicate no usage filtering
	 * @return an array of TemplateInfo objects (never null)
	 */
	public TemplateInfo[] getTemplateInfos(String projectType, String toolChain, String usageFilter) {
		List<TemplateInfo> templateInfoList = templateInfoMap.get(projectType.trim());
		List<TemplateInfo> matchedTemplateInfoList = new ArrayList<TemplateInfo>();
		
		if (templateInfoList != null) {
			for(TemplateInfo templateInfo : templateInfoList) {
				String filterPattern = templateInfo.getFilterPattern();
				String[] toolChains = templateInfo.getToolChainIds();

				if (toolChain != null) {
					for (int j=0; j < toolChains.length; j++) {
						if (toolChains[j].equals(toolChain)) {
							if (usageFilter != null && filterPattern.matches(usageFilter)) {
								matchedTemplateInfoList.add(templateInfo);
							} else if (usageFilter == null) {
								matchedTemplateInfoList.add(templateInfo);
							}
							continue;
						}
					}
				} else {
					if (usageFilter != null && filterPattern.matches(usageFilter)) {
						matchedTemplateInfoList.add(templateInfo);
					} else if (usageFilter == null) {
						matchedTemplateInfoList.add(templateInfo);
					}
				}
			}
		}
		return matchedTemplateInfoList.toArray(new TemplateInfo[matchedTemplateInfoList.size()]);
	}

	public TemplateInfo[] getTemplateInfos(String projectType, String toolChain) {
		return getTemplateInfos(projectType, toolChain, null);
	}

	public TemplateInfo[] getTemplateInfos(String projectType) {
		return getTemplateInfos(projectType, null, null);
	}

	/**
	 * @return all TemplateInfo objects known to the TemplateEngine
	 */
	public TemplateInfo[] getTemplateInfos() {
		List<TemplateInfo> infoList = new ArrayList<TemplateInfo>();
		for(List<TemplateInfo> infos : templateInfoMap.values()) {
			infoList.addAll(infos);
		}
		return infoList.toArray(new TemplateInfo[infoList.size()]);
	}


	/**
	 * @return the map from project-type ID's to all associated TemplateInfo instances
	 */
	public Map<String, List<TemplateInfo>> getTemplateInfoMap() {
		return templateInfoMap;
	}

	/**
	 * Returns the Template Schema URL
	 * 
	 * @return URL of the Template Schema.
	 * @throws IOException
	 */
	public URL getTemplateSchemaURL() throws IOException {
		return FileLocator.toFileURL(Platform.getBundle(CCorePlugin.PLUGIN_ID).getEntry("schema/TemplateDescriptorSchema.xsd")); //$NON-NLS-1$
	}
	
	/**
	 * Returns the Children of the Element.
	 * @param element
	 * @return List of the child elements
     * 
     * @since 4.0
	 */
	public static List<Element> getChildrenOfElement(Element element) {
		List<Element> list = new ArrayList<Element>();
		NodeList children = element.getChildNodes();
		for (int i = 0, l = children.getLength(); i < l; i++) {
			Node child = children.item(i);
			if (child.getNodeType() == Node.ELEMENT_NODE) {
				list.add((Element) child);
			}
		}
		return list;
	}
	
	/**
	 * Returns the child elements by Tag
	 * 
	 * @param element
	 * @param tag
	 * @return List of child elements
     * 
     * @since 4.0
	 */
	public static List<Element> getChildrenOfElementByTag(Element element, String tag) {
		List<Element> list = new ArrayList<Element>();
		NodeList children = element.getChildNodes();
		for (int i = 0, l = children.getLength(); i < l; i++) {
			Node child = children.item(i);
			if (child.getNodeType() == Node.ELEMENT_NODE && child.getNodeName().equals(tag)) {
				list.add((Element) child);
			}
		}
		return list;
	}
}
