/*******************************************************************************
 * Copyright (c) 2000, 2017 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.update.internal.configurator.branding;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.text.MessageFormat; // Can't use ICU, possible launch problem?
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.MissingResourceException;
import java.util.Properties;
import java.util.PropertyResourceBundle;
import java.util.StringTokenizer;

import org.eclipse.core.runtime.FileLocator;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.osgi.util.NLS;
import org.eclipse.update.internal.configurator.Messages;
import org.eclipse.update.internal.configurator.Utils;
import org.osgi.framework.Bundle;
import org.osgi.framework.Constants;

/**
 * Reads the information found in an "INI" file. This file must be in a
 * standard Java properties format. A properties file may also be provided
 * to NL values in the INI file - values must start with the % prefix. A
 * mapping file may also be provided that contains "fill-ins" for the
 * properties file - format being "n = some text", where n is a number.
 */
public class IniFileReader {
	private static final String PID = "org.eclipse.update.configurator"; //$NON-NLS-1$
	private static final Status OK_STATUS = new Status(IStatus.OK, PID, 0, "", null); //$NON-NLS-1$
	private static final String KEY_PREFIX = "%"; //$NON-NLS-1$
	private static final String KEY_DOUBLE_PREFIX = "%%"; //$NON-NLS-1$
	private static final String NLS_TAG = "$nl$"; //$NON-NLS-1$

	private final String featureId;
	private final String pluginId;
	private final String iniFilename;
	private final String propertiesFilename;
	private final String mappingsFilename;
	private Properties ini = null;
	private PropertyResourceBundle properties = null;
	private Object[] mappings = null;
	private Bundle bundle;

	/**
	 * Creates an INI file reader that can parse the contents into key,value pairs.
	 * 
	 * @param featureId the unique identifier of the feature, must not be <code>null</code>
	 * @param pluginId the unique identifier of the feature plug-in, must not be <code>null</code>
	 * @param iniFilename the INI file name, must not be <code>null</code>
	 * @param propertiesFilename the properties filename, can be <code>null</code> if not required
	 * @param mappingsFilename the mappings filename, can be <code>null</code> if not required
	 */
	public IniFileReader(String featureId, String pluginId, String iniFilename, String propertiesFilename, String mappingsFilename) {
		super();
		
		if (featureId == null || pluginId == null || iniFilename == null) {
			throw new IllegalArgumentException();
		}
			
		this.featureId = featureId;
		this.pluginId = pluginId;
		this.iniFilename = iniFilename;
		this.propertiesFilename = propertiesFilename;
		this.mappingsFilename = mappingsFilename;
	}

	/**
	 * Read the contents of the INI, properties, and mappings files.
	 * Does nothing if the content has already been read and parsed.
	 * 
	 * @return an <code>IStatus</code> indicating the success or failure
	 * 	of reading and parsing the INI file content
	 */
	public IStatus load() {
		if (ini != null)
			return OK_STATUS;
			
		// attempt to locate the corresponding plugin
		bundle = Utils.getBundle(pluginId);
		if (bundle == null || bundle.getState() == Bundle.UNINSTALLED || bundle.getState() == Bundle.INSTALLED) {
			bundle = null; // make it null for other test down the road
			String message = NLS.bind(Messages.IniFileReader_MissingDesc, (new String[] { featureId }));
			return new Status(IStatus.ERROR, PID, 0, message, null);
		}

		// Determine the ini file location
		URL iniURL = null;
		IOException ioe = null;
		iniURL = FileLocator.find(bundle, IPath.fromOSString(NLS_TAG).append(iniFilename), null);
		if (iniURL == null) {
			String message = NLS.bind(Messages.IniFileReader_OpenINIError, (new String[] { iniFilename }));
			return new Status(IStatus.ERROR, PID, 0, message, ioe);
		}
		
		// Determine the properties file location
		URL propertiesURL = null;
		if (propertiesFilename != null && !propertiesFilename.isEmpty()) {
			propertiesURL = FileLocator.find(bundle, IPath.fromOSString(NLS_TAG).append(propertiesFilename), null);
		}

		// Determine the mappings file location
		URL mappingsURL = null;
		if (mappingsFilename != null && mappingsFilename.length() > 0) {
			mappingsURL = FileLocator.find(bundle, IPath.fromOSString(NLS_TAG).append(mappingsFilename), null);
		}

		// OK to pass null properties and/or mapping file
		return load(iniURL, propertiesURL, mappingsURL);
	}
		
	/**
	 * Returns the string value for the given key, or <code>null</code>.
	 * The string value is NLS if requested.
	 * 
	 * @return the string value for the given key, or <code>null</code>
	 */
	public String getString(String key, boolean doNls, Hashtable<String, String> runtimeMappings) {
		if (ini == null)
			return null;
		String value = ini.getProperty(key);
		if (value != null && doNls)
			return getResourceString(value, runtimeMappings);
		return value;
	}


	/**
	 * Returns a URL for the given key, or <code>null</code>.
	 * 
	 * @return a URL for the given key, or <code>null</code>
	 */
	public URL getURL(String key) {
		if (ini == null)
			return null;

		URL url = null;
		String fileName = ini.getProperty(key);
		if (fileName != null) {
			if (bundle == null)
				return null;
			url = FileLocator.find(bundle, IPath.fromOSString(fileName), null);
		}
		return url;
	}
	
	/**
	 * Returns a array of URL for the given key, or <code>null</code>. The
	 * property value should be a comma separated list of urls, tokens for
	 * which bundle cannot build an url will have a null entry.
	 * 
	 * @param key name of the property containing the requested urls
	 * @return a URL for the given key, or <code>null</code>
	 * @since 3.0
	 */
	public URL[] getURLs(String key) {
		if (ini == null || bundle == null)
			return null;

		String value = ini.getProperty(key);
		if (value == null)
			return null;

		StringTokenizer tokens = new StringTokenizer(value, ","); //$NON-NLS-1$
		ArrayList<URL> array = new ArrayList<>(10);
		while (tokens.hasMoreTokens()) {
			String str = tokens.nextToken().trim();
			array.add(FileLocator.find(bundle, IPath.fromOSString(str), null));
		}

		URL[] urls = new URL[array.size()];
		array.toArray(urls);
		return urls;
	}

	/**
	 * Returns the feature plugin label, or <code>null</code>.
	 * 
	 * @return the feature plugin lable, or <code>null</code> if none.
	 */
	public String getFeaturePluginLabel() {
		if (bundle == null)
			return null;
		return bundle.getHeaders().get(Constants.BUNDLE_NAME);
	}
	
	/**
	 * Returns the provider name for this feature, or <code>null</code>.
	 * 
	 * @return the provider name for this feature, or <code>null</code>
	 */
	public String getProviderName() {
		if (bundle == null)
			return null;
		return bundle.getHeaders().get(Constants.BUNDLE_VENDOR);
	}
	
	/*
	 * Returns a resource string corresponding to the given argument 
	 * value and bundle.
	 * If the argument value specifies a resource key, the string
	 * is looked up in the given resource bundle. If the argument does not
	 * specify a valid key, the argument itself is returned as the
	 * resource string. The key lookup is performed against the
	 * specified resource bundle. If a resource string 
	 * corresponding to the key is not found in the resource bundle
	 * the key value, or any default text following the key in the
	 * argument value is returned as the resource string.
	 * A key is identified as a string begining with the "%" character.
	 * Note that the "%" character is stripped off prior to lookup
	 * in the resource bundle.
	 * <p>
	 * For example, assume resource bundle plugin.properties contains
	 * name = Project Name
	 * <pre>
	 *     <li>getResourceString("Hello World") returns "Hello World"</li>
	 *     <li>getResourceString("%name") returns "Project Name"</li>
	 *     <li>getResourceString("%name Hello World") returns "Project Name"</li>
	 *     <li>getResourceString("%abcd Hello World") returns "Hello World"</li>
	 *     <li>getResourceString("%abcd") returns "%abcd"</li>
	 *     <li>getResourceString("%%name") returns "%name"</li>
	 *     <li>getResourceString(<code>null</code>) returns <code>null</code></li>
	 * </pre>
	 * </p>
	 *
	 * @param value the value or <code>null</code>
	 * @param runtimeMappings runtime mappings or <code>null</code>
	 * @return the resource string
	 */
	public String getResourceString(String value, Hashtable<String, String> runtimeMappings) {
		
		if (value == null)
			return null;
		String s = value.trim();

		if (!s.startsWith(KEY_PREFIX))
			return s;

		if (s.startsWith(KEY_DOUBLE_PREFIX))
			return s.substring(1);

		int ix = s.indexOf(" "); //$NON-NLS-1$
		String key = ix == -1 ? s : s.substring(0, ix);
		String dflt = ix == -1 ? s : s.substring(ix + 1);

		if (properties == null)
			return dflt;

		String result = null;
		try {
			result = properties.getString(key.substring(1));
		} catch (MissingResourceException e) {
			return dflt;
		}
		if (runtimeMappings != null) {
			for (Enumeration<String> e = runtimeMappings.keys(); e.hasMoreElements();) {
				String keyValue = e.nextElement();
				int i = result.indexOf(keyValue);
				if (i != -1) {
					String s1 = result.substring(0,i);
					String s2 = runtimeMappings.get(keyValue);
					String s3 = result.substring(i+keyValue.length());
					result = s1 + s2 + s3;
				}
			}
		}
	
		if (result.indexOf('{') != -1) {
			// We test for the curly braces since due to NL issues we do not
			// want to use MessageFormat unless we have to.
			try {
				result = MessageFormat.format(result, mappings);
			} catch (IllegalArgumentException e) {
				//ignore and return string without bound parameters
			}
		}
		
		return result;	
	}

	/*
	 * Read the contents of the ini, properties, and mappings files.
	 */
	private IStatus load(URL iniURL, URL propertiesURL, URL mappingsURL) {

		InputStream is = null;
		try {
			is = iniURL.openStream();
			ini = new Properties();
			ini.load(is);
		} catch (IOException e) {
			ini = null;
			String message = NLS.bind(Messages.IniFileReader_ReadIniError, (new String[] { iniURL.toExternalForm() }));
			return new Status(IStatus.ERROR, PID, 0, message, e);
		} finally {
			try {
				if (is != null)
					is.close();
			} catch (IOException e) {
			}
		}

		if (propertiesURL != null) {
			is = null;
			try {
				is = propertiesURL.openStream();
				properties = new PropertyResourceBundle(is);
			} catch (IOException e) {
				properties = null;
				String message = NLS.bind(Messages.IniFileReader_ReadPropError, (new String[] { propertiesURL.toExternalForm() }));
				return new Status(IStatus.ERROR, PID, 0, message, e);
			} finally {
				try {
					if (is != null)
						is.close();
				} catch (IOException e) {
				}
			}
		}

		PropertyResourceBundle bundle = null;
		if (mappingsURL != null) {
			is = null;
			try {
				is = mappingsURL.openStream();
				bundle = new PropertyResourceBundle(is);
			} catch (IOException e) {
				bundle = null;
				String message = NLS.bind(Messages.IniFileReader_ReadMapError, (new String[] { mappingsURL.toExternalForm() }));
				return new Status(IStatus.ERROR, PID, 0, message, e);
			} finally {
				try {
					if (is != null)
						is.close();
				} catch (IOException e) {
				}
			}
		}

		ArrayList<String> mappingsList = new ArrayList<>();
		if (bundle != null) {
			boolean found = true;
			int i = 0;
			while (found) {
				try {
					mappingsList.add(bundle.getString(Integer.toString(i)));
				} catch (MissingResourceException e) {
					found = false;
				}
				i++;
			}
		}
		mappings = mappingsList.toArray(new String[mappingsList.size()]);
		
		return OK_STATUS;
	}
}
