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);
    }
}
