/*******************************************************************************
 * Copyright (c) 2006, 2012 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.pde.internal.core;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.FactoryConfigurationError;
import javax.xml.parsers.ParserConfigurationException;

import org.eclipse.osgi.service.resolver.BundleDescription;
import org.eclipse.osgi.util.ManifestElement;
import org.eclipse.pde.core.plugin.IPlugin;
import org.eclipse.pde.core.plugin.IPluginBase;
import org.eclipse.pde.core.plugin.IPluginLibrary;
import org.eclipse.pde.core.plugin.IPluginModelBase;
import org.eclipse.pde.internal.core.ibundle.IBundleModel;
import org.eclipse.pde.internal.core.ibundle.IBundlePluginModelBase;
import org.osgi.framework.BundleException;
import org.osgi.framework.Constants;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.helpers.DefaultHandler;

/**
 * Stores additional information from the manifest files of plugins and stores
 * this information in separate xml file.  Accessed through PDEState.
 */
public class PDEAuxiliaryState {

	private static String CACHE_EXTENSION = ".pluginInfo"; //$NON-NLS-1$

	private static String ATTR_BUNDLE_ID = "bundleID"; //$NON-NLS-1$
	private static String ATTR_BUNDLE_STRUCTURE = "isBundle"; //$NON-NLS-1$
	private static String ATTR_CLASS = "class"; //$NON-NLS-1$
	private static String ATTR_EXPORTED = "exported"; //$NON-NLS-1$
	private static String ATTR_EXTENSIBLE_API = "hasExtensibleAPI"; //$NON-NLS-1$
	private static String ATTR_LOCALIZATION = "localization"; //$NON-NLS-1$
	private static String ATTR_NAME = "name"; //$NON-NLS-1$
	private static String ATTR_PATCH = "patch"; //$NON-NLS-1$
	private static String ATTR_PROJECT = "project"; //$NON-NLS-1$
	private static String ATTR_PROVIDER = "provider"; //$NON-NLS-1$
	private static String ATTR_BUNDLE_SOURCE = "bundleSource"; //$NON-NLS-1$

	private static String ELEMENT_BUNDLE = "bundle"; //$NON-NLS-1$
	private static String ELEMENT_LIB = "library"; //$NON-NLS-1$
	private static String ELEMENT_ROOT = "map"; //$NON-NLS-1$

	protected Map<String, PluginInfo> fPluginInfos;

	/**
	 * Constructor
	 */
	protected PDEAuxiliaryState() {
		fPluginInfos = new HashMap<>();
	}

	/**
	 * Constructor, gets the plugin info objects stored in the passed state
	 * and adds them to this state.
	 * @param state state containing plugin infos to initialize this state with
	 */
	protected PDEAuxiliaryState(PDEAuxiliaryState state) {
		fPluginInfos = new HashMap<>(state.fPluginInfos);
	}

	/**
	 * Provides a simple way of storing auxiliary data for a plugin
	 */
	static class PluginInfo {
		String name;
		String providerName;
		String className;
		boolean hasExtensibleAPI;
		boolean isPatchFragment;
		boolean hasBundleStructure;
		String[] libraries;
		String project;
		String localization;
		String bundleSourceEntry;
		boolean exportsExternalAnnotations;
	}

	/**
	 * Helper method to create a plugin info object for the given
	 * element.  The plugin info object is added to the map.
	 */
	private void createPluginInfo(Element element) {
		PluginInfo info = new PluginInfo();
		if (element.hasAttribute(ATTR_NAME)) {
			info.name = element.getAttribute(ATTR_NAME);
		}
		if (element.hasAttribute(ATTR_PROVIDER)) {
			info.providerName = element.getAttribute(ATTR_PROVIDER);
		}
		if (element.hasAttribute(ATTR_CLASS)) {
			info.className = element.getAttribute(ATTR_CLASS);
		}
		info.hasExtensibleAPI = "true".equals(element.getAttribute(ATTR_EXTENSIBLE_API)); //$NON-NLS-1$
		info.isPatchFragment = "true".equals(element.getAttribute(ATTR_PATCH)); //$NON-NLS-1$
		info.hasBundleStructure = !"false".equals(element.getAttribute(ATTR_BUNDLE_STRUCTURE)); //$NON-NLS-1$
		if (element.hasAttribute(ATTR_PROJECT)) {
			info.project = element.getAttribute(ATTR_PROJECT);
		}
		if (element.hasAttribute(ATTR_LOCALIZATION)) {
			info.localization = element.getAttribute(ATTR_LOCALIZATION);
		}
		if (element.hasAttribute(ATTR_BUNDLE_SOURCE)) {
			info.bundleSourceEntry = element.getAttribute(ATTR_BUNDLE_SOURCE);
		}

		NodeList libs = element.getChildNodes();
		ArrayList<String> list = new ArrayList<>(libs.getLength());
		for (int i = 0; i < libs.getLength(); i++) {
			if (libs.item(i).getNodeType() == Node.ELEMENT_NODE) {
				Element lib = (Element) libs.item(i);
				list.add(lib.getAttribute(ATTR_NAME));
			}
		}
		info.libraries = list.toArray(new String[list.size()]);
		fPluginInfos.put(element.getAttribute(ATTR_BUNDLE_ID), info);
	}

	public String getClassName(long bundleID) {
		PluginInfo info = fPluginInfos.get(Long.toString(bundleID));
		return info == null ? null : info.className;
	}

	public boolean hasExtensibleAPI(long bundleID) {
		PluginInfo info = fPluginInfos.get(Long.toString(bundleID));
		return info == null ? false : info.hasExtensibleAPI;
	}

	public boolean isPatchFragment(long bundleID) {
		PluginInfo info = fPluginInfos.get(Long.toString(bundleID));
		return info == null ? false : info.isPatchFragment;
	}

	public boolean hasBundleStructure(long bundleID) {
		PluginInfo info = fPluginInfos.get(Long.toString(bundleID));
		return info == null ? false : info.hasBundleStructure;
	}

	public String getPluginName(long bundleID) {
		PluginInfo info = fPluginInfos.get(Long.toString(bundleID));
		return info == null ? null : info.name;
	}

	public String getProviderName(long bundleID) {
		PluginInfo info = fPluginInfos.get(Long.toString(bundleID));
		return info == null ? null : info.providerName;
	}

	public String[] getLibraryNames(long bundleID) {
		PluginInfo info = fPluginInfos.get(Long.toString(bundleID));
		return info == null ? new String[0] : info.libraries;
	}

	public String getBundleLocalization(long bundleID) {
		PluginInfo info = fPluginInfos.get(Long.toString(bundleID));
		return info == null ? null : info.localization;
	}

	public String getProject(long bundleID) {
		PluginInfo info = fPluginInfos.get(Long.toString(bundleID));
		return info == null ? null : info.project;
	}

	public String getBundleSourceEntry(long bundleID) {
		PluginInfo info = fPluginInfos.get(Long.toString(bundleID));
		return info == null ? null : info.bundleSourceEntry;
	}

	public boolean exportsExternalAnnotations(long bundleID) {
		PluginInfo info = fPluginInfos.get(Long.toString(bundleID));
		return info == null ? false : info.exportsExternalAnnotations;
	}

	/**
	 * Builds an xml document storing the auxiliary plugin info.
	 * @param dir directory location to create the file
	 */
	protected void savePluginInfo(File dir) {
		try {
			@SuppressWarnings("restriction")
			Document doc = org.eclipse.core.internal.runtime.XmlProcessorFactory.newDocumentWithErrorOnDOCTYPE();
			Element root = doc.createElement(ELEMENT_ROOT);

			Iterator<String> iter = fPluginInfos.keySet().iterator();
			while (iter.hasNext()) {
				String key = iter.next();
				Element element = doc.createElement(ELEMENT_BUNDLE);
				element.setAttribute(ATTR_BUNDLE_ID, key);
				PluginInfo info = fPluginInfos.get(key);
				if (info.className != null) {
					element.setAttribute(ATTR_CLASS, info.className);
				}
				if (info.providerName != null) {
					element.setAttribute(ATTR_PROVIDER, info.providerName);
				}
				if (info.name != null) {
					element.setAttribute(ATTR_NAME, info.name);
				}
				if (info.hasExtensibleAPI) {
					element.setAttribute(ATTR_EXTENSIBLE_API, "true"); //$NON-NLS-1$
				}
				if (info.isPatchFragment) {
					element.setAttribute(ATTR_PATCH, "true"); //$NON-NLS-1$
				}
				if (!info.hasBundleStructure) {
					element.setAttribute(ATTR_BUNDLE_STRUCTURE, "false"); //$NON-NLS-1$
				}
				if (info.localization != null) {
					element.setAttribute(ATTR_LOCALIZATION, info.localization);
				}
				if (info.bundleSourceEntry != null) {
					element.setAttribute(ATTR_BUNDLE_SOURCE, info.bundleSourceEntry);
				}
				if (info.libraries != null) {
					for (String library : info.libraries) {
						Element lib = doc.createElement(ELEMENT_LIB);
						lib.setAttribute(ATTR_NAME, library);
						element.appendChild(lib);
					}
				}
				root.appendChild(element);
			}
			doc.appendChild(root);
			XMLPrintHandler.writeFile(doc, new File(dir, CACHE_EXTENSION));
		} catch (Exception e) {
			PDECore.log(e);
		}
	}

	/**
	 * Loads plugin info objects from the pluginInfo xml file stored in the
	 * given directory.
	 * @param dir location to look for the pluginInfo file
	 * @return true if the file was read successfully, false otherwise
	 */
	protected boolean readPluginInfoCache(File dir) {
		File file = new File(dir, CACHE_EXTENSION);
		if (file.exists() && file.isFile()) {
			try {
				@SuppressWarnings("restriction")
				DocumentBuilder documentBuilder = org.eclipse.core.internal.runtime.XmlProcessorFactory
						.createDocumentBuilderWithErrorOnDOCTYPE();
				documentBuilder.setErrorHandler(new DefaultHandler());
				Document doc = documentBuilder.parse(file);
				Element root = doc.getDocumentElement();
				if (root != null) {
					NodeList list = root.getChildNodes();
					for (int i = 0; i < list.getLength(); i++) {
						if (list.item(i).getNodeType() == Node.ELEMENT_NODE) {
							createPluginInfo((Element) list.item(i));
						}
					}
				}
				return true;
			} catch (org.xml.sax.SAXException | IOException | ParserConfigurationException e) {
				PDECore.log(e);
			}
		}
		return false;
	}

	/**
	 * Returns whether the auxiliary state exists in the given directory.
	 *
	 * @param dir parent directory
	 * @return whether the state file exist
	 */
	protected boolean exists(File dir) {
		File file = new File(dir, CACHE_EXTENSION);
		return file.exists() && file.isFile();
	}

	/**
	 * Writes out auxiliary information from the given models to an xml file
	 * in the given destination directory.
	 * @param models models to collect information from
	 * @param destination directory to create the xml file in
	 */
	public static void writePluginInfo(IPluginModelBase[] models, File destination) {
		try {
			@SuppressWarnings("restriction")
			Document doc = org.eclipse.core.internal.runtime.XmlProcessorFactory
					.newDocumentWithErrorOnDOCTYPE();
			Element root = doc.createElement(ELEMENT_ROOT);
			doc.appendChild(root);
			for (int i = 0; i < models.length; i++) {
				IPluginBase plugin = models[i].getPluginBase();
				BundleDescription desc = models[i].getBundleDescription();
				if (desc == null) {
					continue;
				}
				Element element = doc.createElement(ELEMENT_BUNDLE);
				element.setAttribute(ATTR_BUNDLE_ID, Long.toString(desc.getBundleId()));
				element.setAttribute(ATTR_PROJECT, models[i].getUnderlyingResource().getProject().getName());
				if (plugin instanceof IPlugin && ((IPlugin) plugin).getClassName() != null) {
					element.setAttribute(ATTR_CLASS, ((IPlugin) plugin).getClassName());
				}
				if (plugin.getProviderName() != null) {
					element.setAttribute(ATTR_PROVIDER, plugin.getProviderName());
				}
				if (plugin.getName() != null) {
					element.setAttribute(ATTR_NAME, plugin.getName());
				}
				if (ClasspathUtilCore.hasExtensibleAPI(models[i])) {
					element.setAttribute(ATTR_EXTENSIBLE_API, "true"); //$NON-NLS-1$
				} else if (ClasspathUtilCore.isPatchFragment(models[i])) {
					element.setAttribute(ATTR_PATCH, "true"); //$NON-NLS-1$
				}
				if (!(models[i] instanceof IBundlePluginModelBase)) {
					element.setAttribute(ATTR_BUNDLE_STRUCTURE, "false"); //$NON-NLS-1$
				}
				if (models[i] instanceof IBundlePluginModelBase) {
					String localization = ((IBundlePluginModelBase) models[i]).getBundleLocalization();
					if (localization != null) {
						element.setAttribute(ATTR_LOCALIZATION, localization);
					}
				}
				if (models[i] instanceof IBundlePluginModelBase) {
					IBundleModel bundleModel = ((IBundlePluginModelBase) models[i]).getBundleModel();
					if (bundleModel != null) {
						String bundleSourceEntry = bundleModel.getBundle().getHeader(ICoreConstants.ECLIPSE_SOURCE_BUNDLE);
						if (bundleSourceEntry != null) {
							element.setAttribute(ATTR_BUNDLE_SOURCE, bundleSourceEntry);
						}
					}
				}
				IPluginLibrary[] libraries = plugin.getLibraries();
				for (int j = 0; j < libraries.length; j++) {
					Element lib = doc.createElement(ELEMENT_LIB);
					lib.setAttribute(ATTR_NAME, libraries[j].getName());
					if (!libraries[j].isExported()) {
						lib.setAttribute(ATTR_EXPORTED, "false"); //$NON-NLS-1$
					}
					element.appendChild(lib);
				}
				root.appendChild(element);
			}
			XMLPrintHandler.writeFile(doc, new File(destination, CACHE_EXTENSION));
		} catch (ParserConfigurationException | FactoryConfigurationError | IOException e) {
		}
	}

	/**
	 * Collects auxiliary information from the manifest and stores it in this state.
	 * @param desc bundle description for the given manifest
	 * @param manifest dictionary of headers in the bundle's manifest file
	 * @param hasBundleStructure whether the plugin has bundle structure
	 */
	protected void addAuxiliaryData(BundleDescription desc, Map<String, String> manifest, boolean hasBundleStructure) {
		PluginInfo info = new PluginInfo();
		info.name = manifest.get(Constants.BUNDLE_NAME);
		info.providerName = manifest.get(Constants.BUNDLE_VENDOR);

		String className = manifest.get(ICoreConstants.PLUGIN_CLASS);
		info.className = className != null ? className : (String) manifest.get(Constants.BUNDLE_ACTIVATOR);
		info.libraries = getClasspath(manifest);
		info.hasExtensibleAPI = "true".equals(manifest.get(ICoreConstants.EXTENSIBLE_API)); //$NON-NLS-1$
		info.isPatchFragment = "true".equals(manifest.get(ICoreConstants.PATCH_FRAGMENT)); //$NON-NLS-1$
		info.localization = manifest.get(Constants.BUNDLE_LOCALIZATION);
		info.hasBundleStructure = hasBundleStructure;
		info.bundleSourceEntry = manifest.get(ICoreConstants.ECLIPSE_SOURCE_BUNDLE);
		info.exportsExternalAnnotations = "true" //$NON-NLS-1$
				.equals(manifest.get(ICoreConstants.ECLIPSE_EXPORT_EXTERNAL_ANNOTATIONS));
		fPluginInfos.put(Long.toString(desc.getBundleId()), info);
	}

	/**
	 * Retrieves the classpath entries from the manifest dictionary
	 * @param manifest dictionary containing manifest headers
	 * @return string array of classpath entries
	 */
	protected String[] getClasspath(Map<String, String> manifest) {
		String fullClasspath = manifest.get(Constants.BUNDLE_CLASSPATH);
		String[] result = new String[0];
		try {
			if (fullClasspath != null) {
				ManifestElement[] classpathEntries = ManifestElement.parseHeader(Constants.BUNDLE_CLASSPATH, fullClasspath);
				result = new String[classpathEntries.length];
				for (int i = 0; i < classpathEntries.length; i++) {
					result[i] = classpathEntries[i].getValue();
				}
			}
		} catch (BundleException e) {
		}
		return result;
	}

	/**
	 * Clears the plugin info object map.
	 */
	protected void clear() {
		fPluginInfos.clear();
	}

}
