package org.jivesoftware.openfire.group;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.AbstractSet;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.jivesoftware.database.DbConnectionManager;
import org.jivesoftware.openfire.event.GroupEventDispatcher;
import org.jivesoftware.util.PersistableMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Default implementation of a writable {@link Map} to manage group properties.
 * Updates made to the elements in this map will also be applied to the database.
 * Note this implementation assumes group property changes will be relatively
 * infrequent and therefore does not try to optimize database I/O for performance.
 * Each call to a {@link Map} mutator method (direct or indirect via {@link Iterator})
 * will result in a corresponding synchronous update to the database.
 * 
 * @param <K> Property key
 * @param <V> Property value
 */

public class DefaultGroupPropertyMap<K,V> extends PersistableMap<K,V> {

	private static final long serialVersionUID = 3128889631577167040L;
    private static final Logger logger = LoggerFactory.getLogger(DefaultGroupPropertyMap.class);

	// moved from {@link Group} as these are specific to the default provider
    private static final String DELETE_PROPERTY =
            "DELETE FROM ofGroupProp WHERE groupName=? AND name=?";
    private static final String DELETE_ALL_PROPERTIES =
            "DELETE FROM ofGroupProp WHERE groupName=?";
    private static final String UPDATE_PROPERTY =
        "UPDATE ofGroupProp SET propValue=? WHERE name=? AND groupName=?";
    private static final String INSERT_PROPERTY =
        "INSERT INTO ofGroupProp (groupName, name, propValue) VALUES (?, ?, ?)";

    private Group group;
	
    /**
     * Group properties map constructor; requires associated {@link Group} instance
     * @param group The group that owns these properties
     */
	public DefaultGroupPropertyMap(Group group) {
		this.group = group;
	}
	
	/**
	 * Custom method to put properties into the map, optionally without
	 * triggering persistence. This is used when the map is being 
	 * initially loaded from the database.
	 * 
	 * @param key The property name
	 * @param value The property value
	 * @param persist True if the changes should be persisted to the database
	 * @return The original value or null if the property did not exist
	 */
	@Override
	public V put(K key, V value, boolean persist) {
		V originalValue = super.put(key, value);
		// we only support persistence for <String, String>
		if (persist && key instanceof String && value instanceof String) {
			if (logger.isDebugEnabled())
				logger.debug("Persisting group property [" + key + "]: " + value);
			if (originalValue instanceof String) { // existing property		
				updateProperty((String)key, (String)value, (String)originalValue);
			} else {
				insertProperty((String)key, (String)value);
			}
		}
		return originalValue;
	}

	@Override
	public V put(K key, V value) {
		if (value == null) { // treat null value as "remove"
			return remove(key);
		} else {
			return put(key, value, true);
		}
	}
	
	@Override
	public V remove(Object key) {
		V result = super.remove(key);
		if (key instanceof String) {
			deleteProperty((String)key);
		}
		return result;
	}

	@Override
	public void clear() {
		super.clear();
		deleteAllProperties();
	}

	@Override
	public Set<K> keySet() {
		// custom class needed here to handle key.remove()
		return new PersistenceAwareKeySet<K>(super.keySet());
	}

	@Override
	public Collection<V> values() {
		// custom class needed here to suppress value.remove()
		return Collections.unmodifiableCollection(super.values());
	}

	@Override
	public Set<Entry<K, V>> entrySet() {
		// custom class needed here to handle entrySet mutators
		return new PersistenceAwareEntrySet<Entry<K,V>>(super.entrySet());
	}

	/**
	 * Persistence-aware {@link Set} for group property keys. This class returns
	 * a custom iterator that can handle property removal.
	 */
	private class PersistenceAwareKeySet<E> extends AbstractSet<K> {

		private Set<K> delegate;
		
		/**
		 * Sole constructor; requires wrapped {@link Set} for delegation
		 * @param delegate A collection of keys from the map
		 */
		public PersistenceAwareKeySet(Set<K> delegate) {
			this.delegate = delegate;
		}

		@Override
		public Iterator<K> iterator() {
			return new KeyIterator<E>(delegate.iterator());
		}

		@Override
		public int size() {
			return delegate.size();
		}
	}

	/**
	 * This iterator updates the database when a property key is removed.
	 */
	private class KeyIterator<E> implements Iterator<K> {

		private Iterator<K> delegate;
		private K current;
		
		/**
		 * Sole constructor; requires wrapped {@link Iterator} for delegation
		 * @param delegate An iterator for all the keys from the map
		 */
		public KeyIterator(Iterator<K> delegate) {
			this.delegate = delegate;
		}
		
		/**
		 * Delegated to corresponding method in the backing {@link Iterator}
		 */
		@Override
		public boolean hasNext() {
			return delegate.hasNext();
		}

		/**
		 * Delegated to corresponding method in the backing {@link Iterator}
		 */
		@Override
		public K next() {
			current = delegate.next();
			return current;
		}

		/**
		 * Removes the property corresponding to the current key from
		 * the underlying map. Also applies update to the database.
		 */
		@Override
		public void remove() {
			delegate.remove();
			if (current instanceof String) {
				deleteProperty((String)current);
			}
			current = null;
		}
	}
	
	/**
	 * Persistence-aware {@link Set} for group properties (as {@link Map.Entry})
	 */
	private class PersistenceAwareEntrySet<E> implements Set<Entry<K, V>> {

		private Set<Entry<K, V>> delegate;
		
		/**
		 * Sole constructor; requires wrapped {@link Set} for delegation
		 * @param delegate A collection of entries ({@link Map.Entry}) from the map
		 */
		public PersistenceAwareEntrySet(Set<Entry<K, V>> delegate) {
			this.delegate = delegate;
		}

		/**
		 * Returns a custom iterator for the entries in the backing map
		 */
		@Override
		public Iterator<Entry<K, V>> iterator() {
			return new EntryIterator<Entry<K,V>>(delegate.iterator());
		}

		/**
		 * Removes the given key from the backing map, and applies the
		 * corresponding update to the database.
		 * 
		 * @param o A {@link Map.Entry} within this set
		 * @return True if the set contained the given key
		 */
		@Override
		public boolean remove(Object o) {
			boolean propertyExists = delegate.remove(o);
			if (propertyExists) {
				deleteProperty((String)((Entry<K,V>)o).getKey());
			}
			return propertyExists;
		}

		/**
		 * Removes all the elements in the set, and applies the
		 * corresponding update to the database.
		 */
		@Override
		public void clear() {
			delegate.clear();
			deleteAllProperties();
		}

		// these methods are problematic (and not really necessary),
		// so they are not implemented
		
		/**
		 * @throws UnsupportedOperationException
		 */
		@Override
		public boolean removeAll(Collection<?> c) {
			throw new UnsupportedOperationException();
		}

		/**
		 * @throws UnsupportedOperationException
		 */
		@Override
		public boolean retainAll(Collection<?> c) {
			throw new UnsupportedOperationException();
		}
		
		// per docs for {@link Map.entrySet}, these methods are not supported

		/**
		 * @throws UnsupportedOperationException
		 */
		@Override
		public boolean add(Entry<K, V> o) {
			return delegate.add(o);
		}

		/**
		 * @throws UnsupportedOperationException
		 */
		@Override
		public boolean addAll(Collection<? extends Entry<K, V>> c) {
			return delegate.addAll(c);
		}

		// remaining {@link Set} methods can be delegated safely
		
		/**
		 * Delegated to corresponding method in the backing {@link Set}
		 */
		@Override
		public int size() {
			return delegate.size();
		}

		/**
		 * Delegated to corresponding method in the backing {@link Set}
		 */
		@Override
		public boolean isEmpty() {
			return delegate.isEmpty();
		}

		/**
		 * Delegated to corresponding method in the backing {@link Set}
		 */
		@Override
		public boolean contains(Object o) {
			return delegate.contains(o);
		}

		/**
		 * Delegated to corresponding method in the backing {@link Set}
		 */
		@Override
		public Object[] toArray() {
			return delegate.toArray();
		}

		/**
		 * Delegated to corresponding method in the backing {@link Set}
		 */
		@Override
		public <T> T[] toArray(T[] a) {
			return delegate.toArray(a);
		}

		/**
		 * Delegated to corresponding method in the backing {@link Set}
		 */
		@Override
		public boolean containsAll(Collection<?> c) {
			return delegate.containsAll(c);
		}

		/**
		 * Delegated to corresponding method in the backing {@link Set}
		 */
		public boolean equals(Object o) {
			return delegate.equals(o);
		}

		/**
		 * Delegated to corresponding method in the backing {@link Set}
		 */
		public int hashCode() {
			return delegate.hashCode();
		}
	}

	/**
	 * Remove group property from the database when the {@link Iterator.remove}
	 * method is invoked via the {@link Map.entrySet} set
	 */
	private class EntryIterator<E> implements Iterator<Entry<K, V>> {

		private Iterator<Entry<K,V>> delegate;
		private EntryWrapper<E> current;
		
		/**
		 * Sole constructor; requires wrapped {@link Iterator} for delegation
		 * @param delegate An iterator for all the keys from the map
		 */
		public EntryIterator(Iterator<Entry<K,V>> delegate) {
			this.delegate = delegate;
		}
		/**
		 * Delegated to corresponding method in the backing {@link Iterator}
		 */
		@Override
		public boolean hasNext() {
			return delegate.hasNext();
		}

		/**
		 * Delegated to corresponding method in the backing {@link Iterator}
		 */
		@Override
		public Entry<K,V> next() {
			current = new EntryWrapper<>(delegate.next());
			return current;
		}

		/**
		 * Removes the property corresponding to the current key from
		 * the underlying map. Also applies update to the database.
		 */
		@Override
		public void remove() {
			delegate.remove();
			K key = current.getKey();
			if (key instanceof String) {
				deleteProperty((String)key);
			}
			current = null;
		}
	}
	
	/**
	 * Update the database when a group property is updated via {@link Map.Entry.setValue}
	 */
	private class EntryWrapper<E> implements Entry<K,V> {
		private Entry<K,V> delegate;

		/**
		 * Sole constructor; requires wrapped {@link Map.Entry} for delegation
		 * @param delegate The corresponding entry from the map
		 */
		public EntryWrapper(Entry<K,V> delegate) {
			this.delegate = delegate;
		}
		
		/**
		 * Delegated to corresponding method in the backing {@link Map.Entry}
		 */
		@Override
		public K getKey() {
			return delegate.getKey();
		}
		
		/**
		 * Delegated to corresponding method in the backing {@link Map.Entry}
		 */
		@Override
		public V getValue() {
			return delegate.getValue();
		}
		
		/**
		 * Set the value of the property corresponding to this entry. This
		 * method also updates the database as needed depending on the new
		 * property value. A null value will cause the property to be deleted
		 * from the database.
		 * 
		 * @param value The new property value
		 * @return The old value of the corresponding property
		 */
		@Override
		public V setValue(V value) {
			V oldValue = delegate.setValue(value);
			K key = delegate.getKey();
			if (key instanceof String) {
				if (value instanceof String) {
					if (oldValue == null) {
						insertProperty((String) key, (String) value);
					} else if (!value.equals(oldValue)) {
						updateProperty((String)key,(String)value, (String)oldValue);
					}
				} else {
					deleteProperty((String)key);
				}
			}
			return oldValue;
		}
	}

	/**
	 * Persist a new group property to the database for the current group
	 * 
	 * @param key Property name
	 * @param value Property value
	 */
	private synchronized void insertProperty(String key, String value) {
        Connection con = null;
        PreparedStatement pstmt = null;
        try {
            con = DbConnectionManager.getConnection();
            pstmt = con.prepareStatement(INSERT_PROPERTY);
            pstmt.setString(1, group.getName());
            pstmt.setString(2, key);
            pstmt.setString(3, value);
            pstmt.executeUpdate();
        }
        catch (SQLException e) {
            logger.error(e.getMessage(), e);
        }
        finally {
            DbConnectionManager.closeConnection(pstmt, con);
        }
        Map<String, Object> event = new HashMap<>();
        event.put("propertyKey", key);
        event.put("type", "propertyAdded");
        GroupEventDispatcher.dispatchEvent(group,
                GroupEventDispatcher.EventType.group_modified, event);
    }

	/**
	 * Update the value of an existing group property for the current group
	 * 
	 * @param key Property name
	 * @param value Property value
	 * @param originalValue Original property value
	 */
    private synchronized void updateProperty(String key, String value, String originalValue) {
        Connection con = null;
        PreparedStatement pstmt = null;
        try {
            con = DbConnectionManager.getConnection();
            pstmt = con.prepareStatement(UPDATE_PROPERTY);
            pstmt.setString(1, value);
            pstmt.setString(2, key);
            pstmt.setString(3, group.getName());
            pstmt.executeUpdate();
        }
        catch (SQLException e) {
            logger.error(e.getMessage(), e);
        }
        finally {
            DbConnectionManager.closeConnection(pstmt, con);
        }
        Map<String, Object> event = new HashMap<>();
        event.put("propertyKey", key);
        event.put("type", "propertyModified");
        event.put("originalValue", originalValue);
        GroupEventDispatcher.dispatchEvent(group,
                GroupEventDispatcher.EventType.group_modified, event);
    }

    /**
     * Delete a group property from the database for the current group
     * 
     * @param key Property name
     */
    private synchronized void deleteProperty(String key) {
        Connection con = null;
        PreparedStatement pstmt = null;
        try {
            con = DbConnectionManager.getConnection();
            pstmt = con.prepareStatement(DELETE_PROPERTY);
            pstmt.setString(1, group.getName());
            pstmt.setString(2, key);
            pstmt.executeUpdate();
        }
        catch (SQLException e) {
            logger.error(e.getMessage(), e);
        }
        finally {
            DbConnectionManager.closeConnection(pstmt, con);
        }
        Map<String, Object> event = new HashMap<>();
        event.put("type", "propertyDeleted");
        event.put("propertyKey", key);
        GroupEventDispatcher.dispatchEvent(group,
            GroupEventDispatcher.EventType.group_modified, event);
    }

    /**
     * Delete all properties from the database for the current group
     */
    private synchronized void deleteAllProperties() {
        Connection con = null;
        PreparedStatement pstmt = null;
        try {
            con = DbConnectionManager.getConnection();
            pstmt = con.prepareStatement(DELETE_ALL_PROPERTIES);
            pstmt.setString(1, group.getName());
            pstmt.executeUpdate();
        }
        catch (SQLException e) {
            logger.error(e.getMessage(), e);
        }
        finally {
            DbConnectionManager.closeConnection(pstmt, con);
        }
        Map<String, Object> event = new HashMap<>();
        event.put("type", "propertyDeleted");
        event.put("propertyKey", "*");
        GroupEventDispatcher.dispatchEvent(group,
            GroupEventDispatcher.EventType.group_modified, event);
    }
}
