/*******************************************************************************
 * Copyright (c) 2006, 2013 Cognos Incorporated, IBM Corporation 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
 * 
 *******************************************************************************/
package org.eclipse.osgi.framework.internal.core;

import java.io.File;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLDecoder;
import java.security.CodeSource;
import java.util.*;
import org.eclipse.core.runtime.internal.adaptor.EclipseAdaptorMsg;
import org.eclipse.osgi.util.NLS;

/*
 * This class should be used in ALL places in the framework implementation to get "system" properties.
 * The static methods on this class should be used instead of the System#getProperty, System#setProperty etc methods.
 */
public class FrameworkProperties {

	/**@GuardedBy FrameworkProperties.class*/
	private static Properties properties;

	// A flag of some sort will have to be supported. 
	// Many existing plugins get framework propeties directly from System instead of BundleContext. 
	// Note that the OSGi TCK is one example where this property MUST be set to false because many TCK bundles set and read system properties.
	private static final String USING_SYSTEM_PROPERTIES_KEY = "osgi.framework.useSystemProperties"; //$NON-NLS-1$
	private static final String PROP_FRAMEWORK = "osgi.framework"; //$NON-NLS-1$
	private static final String PROP_INSTALL_AREA = "osgi.install.area"; //$NON-NLS-1$

	public static Properties getProperties() {
		SecurityManager sm = System.getSecurityManager();
		if (sm != null)
			sm.checkPropertiesAccess();
		return internalGetProperties(null);
	}

	public static String getProperty(String key) {
		return getProperty(key, null);
	}

	public static String getProperty(String key, String defaultValue) {
		SecurityManager sm = System.getSecurityManager();
		if (sm != null)
			sm.checkPropertyAccess(key);
		return internalGetProperties(null).getProperty(key, defaultValue);
	}

	public static String setProperty(String key, String value) {
		SecurityManager sm = System.getSecurityManager();
		if (sm != null)
			sm.checkPermission(new PropertyPermission(key, "write")); //$NON-NLS-1$
		return (String) internalGetProperties(null).put(key, value);
	}

	public static String clearProperty(String key) {
		SecurityManager sm = System.getSecurityManager();
		if (sm != null)
			sm.checkPermission(new PropertyPermission(key, "write")); //$NON-NLS-1$
		return (String) internalGetProperties(null).remove(key);
	}

	private static synchronized Properties internalGetProperties(String usingSystemProperties) {
		if (properties == null) {
			Properties systemProperties = System.getProperties();
			if (usingSystemProperties == null)
				usingSystemProperties = systemProperties.getProperty(USING_SYSTEM_PROPERTIES_KEY);
			if (usingSystemProperties == null || usingSystemProperties.equalsIgnoreCase(Boolean.TRUE.toString())) {
				properties = systemProperties;
			} else {
				// use systemProperties for a snapshot
				// also see requirements in Bundlecontext.getProperty(...))
				properties = new Properties();
				// snapshot of System properties for uses of getProperties who expect to see framework properties set as System properties
				// we need to do this for all system properties because the properties object is used to back
				// BundleContext#getProperty method which expects all system properties to be available
				synchronized (systemProperties) {
					// bug 360198 - must synchronize on systemProperties to avoid concurrent modification exception
					properties.putAll(systemProperties);
				}
			}
		}
		return properties;
	}

	public static synchronized void setProperties(Map<String, String> input) {
		if (input == null) {
			// just use internal props;  note that this will reuse a previous set of properties if they were set
			internalGetProperties("false"); //$NON-NLS-1$
			return;
		}
		properties = null;
		Properties toSet = internalGetProperties("false"); //$NON-NLS-1$
		for (Iterator<String> keys = input.keySet().iterator(); keys.hasNext();) {
			String key = keys.next();
			Object value = input.get(key);
			if (value instanceof String) {
				toSet.setProperty(key, (String) value);
				continue;
			}
			value = input.get(key);
			if (value != null)
				toSet.put(key, value);
			else
				toSet.remove(key);
		}
	}

	public static synchronized boolean inUse() {
		return properties != null;
	}

	public static void initializeProperties() {
		// initialize some framework properties that must always be set
		if (getProperty(PROP_FRAMEWORK) == null || getProperty(PROP_INSTALL_AREA) == null) {
			CodeSource cs = FrameworkProperties.class.getProtectionDomain().getCodeSource();
			if (cs == null)
				throw new IllegalArgumentException(NLS.bind(EclipseAdaptorMsg.ECLIPSE_STARTUP_PROPS_NOT_SET, PROP_FRAMEWORK + ", " + PROP_INSTALL_AREA)); //$NON-NLS-1$
			URL url = cs.getLocation();
			// allow props to be preset
			if (getProperty(PROP_FRAMEWORK) == null) {
				String externalForm = getFrameworkPath(url.toExternalForm(), false);
				setProperty(PROP_FRAMEWORK, externalForm);
			}
			if (getProperty(PROP_INSTALL_AREA) == null) {
				String filePart = getFrameworkPath(url.getFile(), true);
				setProperty(PROP_INSTALL_AREA, filePart);
			}
		}
		// always decode these properties
		setProperty(PROP_FRAMEWORK, decode(getProperty(PROP_FRAMEWORK)));
		setProperty(PROP_INSTALL_AREA, decode(getProperty(PROP_INSTALL_AREA)));
	}

	private static String getFrameworkPath(String path, boolean parent) {
		if (File.separatorChar == '\\') {
			// in case on windows the \ is used
			path = path.replace('\\', '/');
		}
		if (parent) {
			int lastSlash = path.lastIndexOf('/');
			return lastSlash == -1 ? "/" : path.substring(0, lastSlash); //$NON-NLS-1$
		}
		return path;
	}

	public static String decode(String urlString) {
		//try to use Java 1.4 method if available
		try {
			Class<? extends URLDecoder> clazz = URLDecoder.class;
			Method method = clazz.getDeclaredMethod("decode", new Class[] {String.class, String.class}); //$NON-NLS-1$
			//first encode '+' characters, because URLDecoder incorrectly converts 
			//them to spaces on certain class library implementations.
			if (urlString.indexOf('+') >= 0) {
				int len = urlString.length();
				StringBuffer buf = new StringBuffer(len);
				for (int i = 0; i < len; i++) {
					char c = urlString.charAt(i);
					if (c == '+')
						buf.append("%2B"); //$NON-NLS-1$
					else
						buf.append(c);
				}
				urlString = buf.toString();
			}
			Object result = method.invoke(null, new Object[] {urlString, "UTF-8"}); //$NON-NLS-1$
			if (result != null)
				return (String) result;
		} catch (Exception e) {
			//JDK 1.4 method not found -- fall through and decode by hand
		}
		//decode URL by hand
		boolean replaced = false;
		byte[] encodedBytes = urlString.getBytes();
		int encodedLength = encodedBytes.length;
		byte[] decodedBytes = new byte[encodedLength];
		int decodedLength = 0;
		for (int i = 0; i < encodedLength; i++) {
			byte b = encodedBytes[i];
			if (b == '%') {
				byte enc1 = encodedBytes[++i];
				byte enc2 = encodedBytes[++i];
				b = (byte) ((hexToByte(enc1) << 4) + hexToByte(enc2));
				replaced = true;
			}
			decodedBytes[decodedLength++] = b;
		}
		if (!replaced)
			return urlString;
		try {
			return new String(decodedBytes, 0, decodedLength, "UTF-8"); //$NON-NLS-1$
		} catch (UnsupportedEncodingException e) {
			//use default encoding
			return new String(decodedBytes, 0, decodedLength);
		}
	}

	private static int hexToByte(byte b) {
		switch (b) {
			case '0' :
				return 0;
			case '1' :
				return 1;
			case '2' :
				return 2;
			case '3' :
				return 3;
			case '4' :
				return 4;
			case '5' :
				return 5;
			case '6' :
				return 6;
			case '7' :
				return 7;
			case '8' :
				return 8;
			case '9' :
				return 9;
			case 'A' :
			case 'a' :
				return 10;
			case 'B' :
			case 'b' :
				return 11;
			case 'C' :
			case 'c' :
				return 12;
			case 'D' :
			case 'd' :
				return 13;
			case 'E' :
			case 'e' :
				return 14;
			case 'F' :
			case 'f' :
				return 15;
			default :
				throw new IllegalArgumentException("Switch error decoding URL"); //$NON-NLS-1$
		}
	}
}
