/*
 * Hibernate, Relational Persistence for Idiomatic Java
 *
 * Copyright (c) 2010, Red Hat, Inc. and/or its affiliates or third-party contributors as
 * indicated by the @author tags or express copyright attribution
 * statements applied by the authors.  All third-party contributions are
 * distributed under license by Red Hat, Inc.
 *
 * This copyrighted material is made available to anyone wishing to use, modify,
 * copy, or redistribute it subject to the terms and conditions of the GNU
 * Lesser General Public License, as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License
 * for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this distribution; if not, write to:
 * Free Software Foundation, Inc.
 * 51 Franklin Street, Fifth Floor
 * Boston, MA  02110-1301  USA
 */
package org.hibernate.search.util.configuration.impl;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.TreeSet;

import org.hibernate.search.util.logging.impl.Log;
import org.hibernate.search.util.logging.impl.LoggerFactory;

/**
 * A wrapper to {@link Properties}, to restrict the availability of values to only those which have a key
 * beginning with a given masking string.
 *
 * Supported methods to enumerate the list of properties are:
 * <ul>
 * <li>propertyNames()</li>
 * <li>keySet()</li>
 * <li>keys()</li>
 * </ul>
 * Other methods including methods returning Entries and values are not supported
 *
 * @author Sanne Grinovero
 * @author Emmanuel Bernard
 */
public class MaskedProperty extends Properties implements Serializable {

	private static final long serialVersionUID = -593307257383085113L;
	private static final Log log = LoggerFactory.make();

	private final Properties masked;
	private final Properties fallBack;
	private final String radix;
	private transient Set<Object> propertyNames;

	/**
	 * Provides a view to the provided Properties hiding
	 * all keys not starting with some [mask.].
	 *
	 * @param propsToMask the Properties containing the values.
	 * @param mask a {@link java.lang.String} object.
	 */
	public MaskedProperty(Properties propsToMask, String mask) {
		this( propsToMask, mask, null );
	}

	/**
	 * Provides a view to the provided Properties hiding
	 * all keys not starting with some [mask.].
	 * If no value is found then a value is returned from propsFallBack,
	 * without masking.
	 *
	 * @param propsToMask
	 * @param mask
	 * @param propsFallBack
	 */
	public MaskedProperty(Properties propsToMask, String mask, Properties propsFallBack) {
		if ( propsToMask == null || mask == null ) {
			throw new java.lang.IllegalArgumentException();
		}
		this.masked = propsToMask;
		this.radix = mask + ".";
		this.fallBack = propsFallBack;
	}

	@Override
	public String getProperty(String key) {
		String compositeKey = radix + key;
		String value = masked.getProperty( compositeKey );
		if ( value != null ) {
			log.tracef( "found a match for key: [%s] value: %s", compositeKey, value );
			return value;
		}
		else if ( fallBack != null ) {
			return fallBack.getProperty( key );
		}
		else {
			return null;
		}
	}

	/**
	 * @throws IllegalArgumentException if the key is not a String instance
	 */
	@Override
	public synchronized boolean containsKey(Object key) {
		if ( !( key instanceof String ) ) {
			throw new IllegalArgumentException( "key must be a String" );
		}
		return getProperty( key.toString() ) != null;
	}

	@Override
	public String getProperty(String key, String defaultValue) {
		String val = getProperty( key );
		return ( val == null ) ? defaultValue : val;
	}

	/**
	 * @throws UnsupportedOperationException
	 */
	@Override
	public void list(PrintStream out) {
		throw new UnsupportedOperationException();
	}

	/**
	 * @throws UnsupportedOperationException
	 */
	@Override
	public void list(PrintWriter out) {
		throw new UnsupportedOperationException();
	}

	/**
	 * @throws UnsupportedOperationException
	 */
	@Override
	public void load(InputStream inStream) throws IOException {
		throw new UnsupportedOperationException();
	}

	/**
	 * @throws UnsupportedOperationException
	 */
	@Override
	public void loadFromXML(InputStream in) throws IOException {
		throw new UnsupportedOperationException();
	}

	@Override
	public synchronized Enumeration<?> propertyNames() {
		initPropertyNames();
		return Collections.enumeration( propertyNames );
	}

	private synchronized void initPropertyNames() {
		if ( propertyNames != null ) {
			return;
		}
		Set<Object> maskedProperties = new TreeSet<Object>();
		//we use keys to be safe and avoid CCE for non String key entries
		Enumeration<?> maskedNames = masked.propertyNames();
		while ( maskedNames.hasMoreElements() ) {
			Object key = maskedNames.nextElement();
			if ( String.class.isInstance( key ) ) {
				String maskedProperty = (String) key;
				if ( maskedProperty.startsWith( radix ) ) {
					maskedProperties.add( maskedProperty.substring( radix.length(), maskedProperty.length() ) );
				}
			}
		}
		if ( fallBack != null ) {
			Enumeration<?> fallBackNames = fallBack.propertyNames();
			while ( fallBackNames.hasMoreElements() ) {
				Object key = fallBackNames.nextElement();
				if ( String.class.isInstance( key ) ) {
					maskedProperties.add( key );
				}
			}
		}
		propertyNames = Collections.unmodifiableSet( maskedProperties );
	}

	/**
	 * @throws UnsupportedOperationException
	 */
	@Override
	public void save(OutputStream out, String comments) {
		throw new UnsupportedOperationException();
	}

	/**
	 * @throws UnsupportedOperationException
	 */
	@Override
	public Object setProperty(String key, String value) {
		throw new UnsupportedOperationException();
	}

	/**
	 * @throws UnsupportedOperationException
	 */
	@Override
	public void store(OutputStream out, String comments)
			throws IOException {
		throw new UnsupportedOperationException();
	}

	/**
	 * @throws UnsupportedOperationException
	 */
	@Override
	public void storeToXML(OutputStream os, String comment,
			String encoding) throws IOException {
		throw new UnsupportedOperationException();
	}

	/**
	 * @throws UnsupportedOperationException
	 */
	@Override
	public void storeToXML(OutputStream os, String comment)
			throws IOException {
		throw new UnsupportedOperationException();
	}

	/**
	 * @throws UnsupportedOperationException
	 */
	@Override
	public void clear() {
		throw new UnsupportedOperationException();
	}

	/**
	 * @throws UnsupportedOperationException
	 */
	@Override
	public Object clone() {
		throw new UnsupportedOperationException();
	}

	@Override
	public synchronized boolean contains(Object value) {
		initPropertyNames();
		return propertyNames.contains( value );
	}

	/**
	 * @throws UnsupportedOperationException
	 */
	@Override
	public boolean containsValue(Object value) {
		throw new UnsupportedOperationException();
	}

	/**
	 * @throws UnsupportedOperationException
	 */
	@Override
	public Enumeration<Object> elements() {
		//TODO
		throw new UnsupportedOperationException();
	}

	/**
	 * @throws UnsupportedOperationException
	 */
	@Override
	public Set<java.util.Map.Entry<Object, Object>> entrySet() {
		throw new UnsupportedOperationException();
	}

	/**
	 * @throws UnsupportedOperationException
	 */
	@Override
	public Object get(Object key) {
		throw new UnsupportedOperationException();
	}

	@Override
	public synchronized boolean isEmpty() {
		initPropertyNames();
		return propertyNames.isEmpty();
	}

	/**
	 * @throws UnsupportedOperationException
	 */
	@Override
	public synchronized Enumeration<Object> keys() {
		initPropertyNames();
		return Collections.enumeration( propertyNames );
	}

	@Override
	public synchronized Set<Object> keySet() {
		initPropertyNames();
		return propertyNames;
	}

	/**
	 * @throws UnsupportedOperationException
	 */
	@Override
	public Object put(Object key, Object value) {
		throw new UnsupportedOperationException();
	}

	/**
	 * @throws UnsupportedOperationException
	 */
	@Override
	public void putAll(Map<? extends Object, ? extends Object> t) {
		throw new UnsupportedOperationException();
	}

	/**
	 * @throws UnsupportedOperationException
	 */
	@Override
	protected void rehash() {
		throw new UnsupportedOperationException();
	}

	/**
	 * @throws UnsupportedOperationException
	 */
	@Override
	public Object remove(Object key) {
		throw new UnsupportedOperationException();
	}

	/**
	 * @throws UnsupportedOperationException
	 */
	@Override
	public synchronized int size() {
		initPropertyNames();
		return propertyNames.size();
	}

	@Override
	public synchronized String toString() {
		HashMap fake = new HashMap();
		Enumeration<?> names = propertyNames();
		while ( names.hasMoreElements() ) {
			Object nextElement = names.nextElement();
			fake.put( nextElement, this.getProperty( nextElement.toString() ) );
		}
		return fake.toString();
	}

	/**
	 * @throws UnsupportedOperationException
	 */
	@Override
	public Collection<Object> values() {
		throw new UnsupportedOperationException();
	}

	@Override
	public synchronized int hashCode() {
		final int prime = 31;
		int result = ( ( fallBack == null ) ? 0 : fallBack.hashCode() );
		result = prime * result + masked.hashCode();
		result = prime * result + radix.hashCode();
		return result;
	}

	@Override
	public synchronized boolean equals(Object obj) {
		if ( this == obj ) {
			return true;
		}
		if ( obj == null ) {
			return false;
		}
		if ( getClass() != obj.getClass() ) {
			return false;
		}
		final MaskedProperty other = (MaskedProperty) obj;
		if ( fallBack == null ) {
			if ( other.fallBack != null ) {
				return false;
			}
		}
		else if ( !fallBack.equals( other.fallBack ) ) {
			return false;
		}
		if ( !masked.equals( other.masked ) ) {
			return false;
		}
		if ( !radix.equals( other.radix ) ) {
			return false;
		}
		return true;
	}

}
