/******************************************************************************
 * Copyright (c) 2006, 2010 VMware Inc.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * and Apache License v2.0 which accompanies this distribution. 
 * The Eclipse Public License is available at 
 * http://www.eclipse.org/legal/epl-v10.html and the Apache License v2.0
 * is available at http://www.opensource.org/licenses/apache2.0.php.
 * You may elect to redistribute this code under either of these licenses. 
 * 
 * Contributors:
 *   VMware Inc.
 *****************************************************************************/

package org.eclipse.gemini.blueprint.test.internal.util;

import java.io.IOException;
import java.util.Collection;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.Map.Entry;

import org.springframework.core.io.Resource;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

/**
 * Loads a property file performing key expansion.
 * 
 * Provides simple property substitution, without support for inner or nested placeholders. Also, the algorithm does
 * only one parsing so derivative placeholders are not supported.
 * 
 * 
 * @author Costin Leau
 * 
 */
public abstract class PropertiesUtil {

	private static final String DELIM_START = "${";

	private static final String DELIM_STOP = "}";

	private static final Properties EMPTY_PROPERTIES = new Properties();

	private static final class OrderedProperties extends Properties implements Cloneable {

		private final Map<Object, Object> map = new LinkedHashMap<Object, Object>();

		/**
		 * 
		 * @see java.util.Map#clear()
		 */
		public void clear() {
			map.clear();
		}

		/**
		 * @param key
		 * @return
		 * @see java.util.Map#containsKey(java.lang.Object)
		 */
		public boolean containsKey(Object key) {
			return map.containsKey(key);
		}

		/**
		 * @param value
		 * @return
		 * @see java.util.Map#containsValue(java.lang.Object)
		 */
		public boolean containsValue(Object value) {
			return map.containsValue(value);
		}

		/**
		 * @return
		 * @see java.util.Map#entrySet()
		 */
		public Set<Entry<Object, Object>> entrySet() {
			return map.entrySet();
		}

		/**
		 * @param o
		 * @return
		 * @see java.util.Map#equals(java.lang.Object)
		 */
		public boolean equals(Object o) {
			return map.equals(o);
		}

		/**
		 * @param key
		 * @return
		 * @see java.util.Map#get(java.lang.Object)
		 */
		public Object get(Object key) {
			return map.get(key);
		}

		@Override
		public String getProperty(String key) {
			Object oval = map.get(key);
			String sval = (oval instanceof String) ? (String) oval : null;
			return ((sval == null) && (defaults != null)) ? defaults.getProperty(key) : sval;
		}

		/**
		 * @return
		 * @see java.util.Map#hashCode()
		 */
		public int hashCode() {
			return map.hashCode();
		}

		/**
		 * @return
		 * @see java.util.Map#isEmpty()
		 */
		public boolean isEmpty() {
			return map.isEmpty();
		}

		/**
		 * @return
		 * @see java.util.Map#keySet()
		 */
		public Set<Object> keySet() {
			return map.keySet();
		}

		/**
		 * @param key
		 * @param value
		 * @return
		 * @see java.util.Map#put(java.lang.Object, java.lang.Object)
		 */
		public Object put(Object key, Object value) {
			return map.put(key, value);
		}

		/**
		 * @param m
		 * @see java.util.Map#putAll(java.util.Map)
		 */
		public void putAll(Map<? extends Object, ? extends Object> m) {
			map.putAll(m);
		}

		/**
		 * @param key
		 * @return
		 * @see java.util.Map#remove(java.lang.Object)
		 */
		public Object remove(Object key) {
			return map.remove(key);
		}

		/**
		 * @return
		 * @see java.util.Map#size()
		 */
		public int size() {
			return map.size();
		}

		/**
		 * @return
		 * @see java.util.Map#values()
		 */
		public Collection<Object> values() {
			return map.values();
		}

		@Override
		public synchronized boolean contains(Object value) {
			return map.containsValue(value);
		}

		@Override
		public synchronized Enumeration<Object> elements() {
			return new IteratorBackedEnumeration(map.values().iterator());
		}

		@Override
		public synchronized Enumeration<Object> keys() {
			return new IteratorBackedEnumeration(map.keySet().iterator());
		}

		@Override
		public synchronized String toString() {
			return map.toString();
		}

		private final class IteratorBackedEnumeration<K> implements Enumeration<K> {

			private final Iterator<? extends K> iterator;

			public IteratorBackedEnumeration(Iterator<? extends K> iterator) {
				this.iterator = iterator;
			}

			public boolean hasMoreElements() {
				return iterator.hasNext();
			}

			public K nextElement() {
				return iterator.next();
			}
		}
	}

	/**
	 * Shortcut method - loads a property object from the given input stream and applies property expansion. The
	 * returned properties object preserves order at the expense of speed.
	 * 
	 * @param resource
	 * @return
	 */
	public static Properties loadAndExpand(Resource resource) {
		Properties props = new OrderedProperties();

		if (resource == null)
			return props;

		try {
			props.load(resource.getInputStream());
		} catch (IOException ex) {
			return null;
		}
		return expandProperties(props);
	}

	/**
	 * Filter/Eliminate keys that start with the given prefix.
	 * 
	 * @param properties
	 * @param prefix
	 * @return
	 */
	public static Properties filterKeysStartingWith(Properties properties, String prefix) {
		if (!StringUtils.hasText(prefix))
			return EMPTY_PROPERTIES;

		Assert.notNull(properties);

		Properties excluded = (properties instanceof OrderedProperties ? new OrderedProperties() : new Properties());

		// filter ignore keys out
		for (Enumeration enm = properties.keys(); enm.hasMoreElements();) {
			String key = (String) enm.nextElement();
			if (key.startsWith(prefix)) {
				excluded.put(key, properties.get(key));
			}
		}
		
		for (Enumeration enm = excluded.keys(); enm.hasMoreElements();) {
			properties.remove(enm.nextElement());
		}
		return excluded;
	}

	/**
	 * Filter/Eliminate keys that have a value that starts with the given prefix.
	 * 
	 * @param properties
	 * @param prefix
	 * @return
	 */
	public static Properties filterValuesStartingWith(Properties properties, String prefix) {
		if (!StringUtils.hasText(prefix))
			return EMPTY_PROPERTIES;

		Assert.notNull(properties);
		Properties excluded = (properties instanceof OrderedProperties ? new OrderedProperties() : new Properties());

		for (Enumeration enm = properties.keys(); enm.hasMoreElements();) {
			String key = (String) enm.nextElement();
			String value = properties.getProperty(key);
			if (value.startsWith(prefix)) {
				excluded.put(key, value);
			}
		}
		
		for (Enumeration enm = excluded.keys(); enm.hasMoreElements();) {
			properties.remove(enm.nextElement());
		}
		return excluded;
	}

	/**
	 * Apply placeholder expansion to the given properties object.
	 * 
	 * Will return a new properties object, containing the expanded entries. Note that both keys and values will be
	 * expanded.
	 * 
	 * @param props
	 * @return
	 */
	public static Properties expandProperties(Properties props) {
		Assert.notNull(props);

		Set entrySet = props.entrySet();

		Properties newProps = (props instanceof OrderedProperties ? new OrderedProperties() : new Properties());

		for (Iterator iter = entrySet.iterator(); iter.hasNext();) {
			// first expand the keys
			Map.Entry entry = (Map.Entry) iter.next();
			String key = (String) entry.getKey();
			String value = (String) entry.getValue();

			String resultKey = expandProperty(key, props);
			String resultValue = expandProperty(value, props);

			// replace old entry

			newProps.put(resultKey, resultValue);
		}

		return newProps;
	}

	private static String expandProperty(String prop, Properties properties) throws IllegalArgumentException {

		boolean hasPlaceholder = false;
		String copy = prop;

		StringBuilder result = new StringBuilder();

		int index = 0;
		// dig out the placeholders
		do {
			index = copy.indexOf(DELIM_START);
			if (index >= 0) {
				hasPlaceholder = true;

				// add stuff before the delimiter
				result.append(copy.substring(0, index));
				// remove the delimiter
				copy = copy.substring(index + DELIM_START.length());
				// find ending delim
				int stopIndex = copy.indexOf(DELIM_STOP);
				String token = null;

				if (stopIndex >= 0) {
					// discover token
					token = copy.substring(0, stopIndex);
					// remove ending delimiter
					copy = copy.substring(stopIndex + 1);
					// append the replacement for the token
					result.append(properties.getProperty(token));
				}

				else
					throw new IllegalArgumentException("cannot interpret property " + prop + " due of token [" + copy
							+ "]");

			} else {
				hasPlaceholder = false;
				// make sure to append the remaining string
				result.append(copy);
			}

		} while (hasPlaceholder);

		return result.toString();
	}
}
