/*******************************************************************************
 * Copyright (c) 2001, 2020 IBM Corporation and others.
 * All rights reserved. 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
 *     Jens Lukowski/Innoopract - initial renaming/restructuring
 *     
 *******************************************************************************/
package org.eclipse.wst.sse.core.internal.encoding;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.charset.IllegalCharsetNameException;
import java.nio.charset.UnsupportedCharsetException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.Properties;
import java.util.PropertyResourceBundle;

import org.eclipse.core.runtime.FileLocator;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Platform;
import org.eclipse.wst.sse.core.internal.encoding.util.Logger;
import org.osgi.framework.Bundle;

/**
 * CommonCharsets is a utility class to provide a central place to map some
 * IANA charset name to a Java charset name. In 1.4 JRE's this mostly is a
 * thin wrapper of existing Charset functionality. It does, however, allow
 * some "overriding" of the built in JRE mappings in the event they are
 * necessary. See CodedIO.checkMappingOverrides.
 * <p>
 * This class also provides some convenient human readable description for the
 * charset names which can be used in user interfaces. The description is NL
 * aware based on locale. The data is populated via the charset.properties
 * file only once, based on static initialization of the hashtables.
 * <p>
 * The IANA tags are based on reference information found at the
 * http://www.iana.org site. Specifically see
 * http://www.iana.org/assignments/character-sets
 */
public final class CommonCharsetNames {

	private static Properties defaultIANAmappings = null;

	private static List<String> encodings = null;

	private static Map<String, String> supportedEncodingDisplayNames = null;

	private static Map<String, String> prefNames = null;

	/**
	 * Returns list of commonly available encoding names. Suitable for
	 * populating a UI dialog or drop down. This list would be a subset of all
	 * possible charsets the VM supports (which can get into the hundreds).
	 * For the VM supported charsets, use
	 * <code>Charset.availableCharsets()</code>
	 * 
	 * @return String[]
	 */
	public static String[] getCommonCharsetNames() {
		String[] enc = new String[getEncodings().size()];
		getEncodings().toArray(enc);
		return enc;
	}

	/**
	 * @return Returns the defaultIANAmappings.
	 */
	private static Properties getDefaultIANAMappings() {
		if (defaultIANAmappings == null) {
			defaultIANAmappings = new Properties();
			Bundle keyBundle = Platform.getBundle(ICodedResourcePlugin.ID);
			IPath keyPath = new Path("config/defaultIANA.properties"); //$NON-NLS-1$
			URL location = Platform.find(keyBundle, keyPath);
			InputStream propertiesInputStream = null;
			try {
				propertiesInputStream = location.openStream();
				defaultIANAmappings.load(propertiesInputStream);
			}
			catch (IOException e) {
				// if can't read, just assume there's no
				// default IANA mappings
				// and repeated attempts will not occur,
				// since they
				// will be represented by an empty
				// Properties object
			}
		}
		return defaultIANAmappings;
	}

	/**
	 * Returns display (translated) string for encoding name. If there is no
	 * "custom" translated version available, it defers to ther VM's Charset
	 * support. It will return null if no display name is available.
	 * 
	 * @param String
	 *            charset name
	 * @return Human friendly display name
	 */
	public static String getDisplayString(String charsetName) {
		if (charsetName == null)
			return null;
		String result = getSupportedEncodingDisplayNames().get(charsetName);
		if (result == null) {
			// if we don't have a special one, just return
			// what's provided by Charset

			try {
				Charset charset = Charset.forName(charsetName);
				result = charset.displayName();
			}
			catch (UnsupportedCharsetException e) {
				// if not supported, the display name is
				// the least of clients concerns :)
			}
		}
		return result;
	}

	public static String getPreferenceName(String charsetName) {
		if (charsetName == null)
			return null;

		String result = getPreferenceNames().get(charsetName);
		if (result == null) {
			return charsetName;
		}

		return result;
	}
	/**
	 * @return Returns the javaEncodings.
	 */
	private static List<String> getEncodings() {
		if (encodings == null) {
			initHashTables();
		}
		return encodings;
	}

	public static String getIanaPreferredCharsetName(String charsetName) {
		String preferredName = charsetName;

		try {
			Charset charset = Charset.forName(charsetName);
			if (charset.name() != null) {
				preferredName = charset.name();
			}
		}
		catch (IllegalCharsetNameException e) {
			// just return input if illegal
		}
		catch (UnsupportedCharsetException e) {
			// just return input if illegal
		}
		return preferredName;
	}

	/**
	 * Returns a default IANA name that is listed in CommonCharsetNames. Here
	 * is how it checks: 1. check to see if charsetName is in the
	 * CommonCharsetNames list and if so, just return it. 2. check to see if
	 * charsetName is listed in defaultIANAmappings which contains a mapping
	 * of more common encodings and the default IANA name they should map to.
	 * 3. return defaultIanaName if all else fails
	 */
	public static String getPreferredDefaultIanaName(String charsetName, String defaultIanaName) {
		String preferredName = defaultIanaName;
		String guessedName = charsetName;
		try {
			guessedName = CodedIO.getAppropriateJavaCharset(charsetName);
		}
		catch (IllegalCharsetNameException e) {
			// just ignore if illegal
		}
		catch (UnsupportedCharsetException e) {
			// just ignore if illegal
		}
		if (getEncodings().contains(guessedName))
			preferredName = guessedName;
		else {
			preferredName = getDefaultIANAMappings().getProperty(guessedName, preferredName);
		}

		return preferredName;
	}

	/**
	 * @return
	 */
	private static Map<String, String> getSupportedEncodingDisplayNames() {
		if (supportedEncodingDisplayNames == null) {
			initHashTables();
		}
		return supportedEncodingDisplayNames;
	}

	private static Map<String,String> getPreferenceNames() {
		if (prefNames == null) {
			initHashTables();
		}
		return prefNames;
	}

	private static void initHashTables() {
		if (supportedEncodingDisplayNames == null) {
			// Initialize hash table for encoding table
			supportedEncodingDisplayNames = new HashMap<String,String>();
			encodings = new ArrayList<>();
			prefNames = new HashMap<String,String>();

			FallbackPropertyResourceBundle bundle = null;
			InputStream bundleStream = null;
			try {
				bundleStream = FileLocator.openStream(Platform.getBundle(ICodedResourcePlugin.ID), Path.fromOSString("$nl$/config/charset.properties"), true);
				bundle = new FallbackPropertyResourceBundle(bundleStream, Path.fromOSString("/config/charset.properties"));

				String totalNumString = bundle.getProperty("totalnumber");//$NON-NLS-1$
				int totalNum = 0;
				if (totalNumString.length() != 0) {
					try {
						totalNum = Integer.valueOf(totalNumString).intValue();
					}
					catch (NumberFormatException e) {
						totalNum = 0;
					}
				}

				for (int i = 0; i < totalNum; i++) {
					String iana = bundle.getProperty("codeset." + i + ".iana");//$NON-NLS-2$//$NON-NLS-1$
					String displayName = bundle.getProperty("codeset." + i + ".label");//$NON-NLS-2$//$NON-NLS-1$
					String prefName = null;
					try {
						prefName = bundle.getProperty("codeset." + i + ".prefName"); //$NON-NLS-1$ //$NON-NLS-2$
					}
					catch (MissingResourceException e) {
					}

					encodings.add(iana);
					supportedEncodingDisplayNames.put(iana, displayName);
					if (prefName != null)
						prefNames.put(prefName, iana);
				}
			}
			catch (IOException e) {
				Logger.logException("invalid install or configuration", e); //$NON-NLS-1$
			}
			finally {
				try {
					if (bundleStream != null)
						bundleStream.close();
				}
				catch (IOException x) {
				}
			}
		}
	}

	/**
	 * 
	 * A property resource bundle that can fall back to a different resource bundle
	 * in the case where there isn't a string for a key.
	 * 
	 */
	static class FallbackPropertyResourceBundle extends PropertyResourceBundle {

		private PropertyResourceBundle fallback = null;
		private IPath fallbackPath;

		public FallbackPropertyResourceBundle(InputStream stream, IPath fallbackPath) throws IOException {
			super(stream);
			this.fallbackPath = fallbackPath;
		}

		/**
		 * Returns the property value for a specified key. Returns null if a property
		 * doesn't exist
		 * 
		 * @param key the key to look up
		 * @return the string property or null if it doesn't exist
		 */
		public String getProperty(String key) {
			String property = null;
			try {
				property = getString(key);
			}
			catch (MissingResourceException e) {
				if (fallback == null && fallbackPath != null) {
					final Bundle bundle = Platform.getBundle(ICodedResourcePlugin.ID);
					if (bundle != null) {
						InputStream stream = null;
						try {
							stream = FileLocator.openStream(bundle, fallbackPath, true);
							if (stream != null) {
								fallback = new PropertyResourceBundle(stream);
							}
						}
						catch (IOException ioe) {
						}
						finally {
							if (stream != null) {
								try {
									stream.close();
								}
								catch (IOException ioe) {
								}
							}
						}
					}
				}
				if (fallback != null) {
					try {
						property = fallback.getString(key);
					}
					catch (MissingResourceException mre) {
					}
				}
			}
			return property;
		}
	}

	public static void main(String[] args) {
		// unit test only
		String test = "Cp1252"; //$NON-NLS-1$
		String result = CommonCharsetNames.getIanaPreferredCharsetName(test);
		System.out.println(test + " --> " + result); //$NON-NLS-1$

		test = "MS932"; //$NON-NLS-1$
		result = CommonCharsetNames.getIanaPreferredCharsetName(test);
		System.out.println(test + " --> " + result); //$NON-NLS-1$

	}

	public CommonCharsetNames() {
		super();
		initHashTables();
	}
}
