/*
 * ModeShape (http://www.modeshape.org)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.modeshape.jcr.spi.index.provider;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.modeshape.common.annotation.Immutable;
import org.modeshape.jcr.ExecutionContext;
import org.modeshape.jcr.JcrLexicon;
import org.modeshape.jcr.ModeShapeLexicon;
import org.modeshape.jcr.cache.CachedNode.Properties;
import org.modeshape.jcr.cache.NodeKey;
import org.modeshape.jcr.cache.change.AbstractPropertyChange;
import org.modeshape.jcr.cache.change.ChangeSetAdapter.NodeTypePredicate;
import org.modeshape.jcr.cache.change.PropertyAdded;
import org.modeshape.jcr.cache.change.PropertyChanged;
import org.modeshape.jcr.cache.change.PropertyRemoved;
import org.modeshape.jcr.value.BinaryValue;
import org.modeshape.jcr.value.Name;
import org.modeshape.jcr.value.Path;
import org.modeshape.jcr.value.PathFactory;
import org.modeshape.jcr.value.Property;
import org.modeshape.jcr.value.ValueFactory;
import org.modeshape.jcr.value.binary.BinaryStoreException;

/**
 * Utility for creating generic {@link IndexChangeAdapter} instances, which support both single and multi-valued properties.
 *
 * @author Randall Hauch (rhauch@redhat.com)
 * @author Horia Chiorean (hchiorea@redhat.com)
 */
@Immutable
public class IndexChangeAdapters {

    /**
     * Creates a composite change adapter which handles the case when an index has multiple columns.
     *
     * @param context the execution context; may not be null
     * @param matcher the node type matcher used to determine which nodes should be included in the index; may not be null
     * @param workspaceName the name of the workspace; may not be null
     * @param index the index that should be used; may not be null
     * @param adapters an {@link Iterable} of existing "discrete" adapters.
     * @return the new {@link IndexChangeAdapter}; never null
     */
    public static IndexChangeAdapter forMultipleColumns( ExecutionContext context,
                                                         NodeTypePredicate matcher,
                                                         String workspaceName,
                                                         ProvidedIndex<?> index,
                                                         Iterable<IndexChangeAdapter> adapters ) {
        return new MultiColumnChangeAdapter(context ,workspaceName, matcher, index, adapters);        
    } 

    /**
     * Create an {@link IndexChangeAdapter} implementation that handles the "mode:nodeDepth" property.
     *
     * @param context the execution context; may not be null
     * @param matcher the node type matcher used to determine which nodes should be included in the index; may not be null
     * @param workspaceName the name of the workspace; may not be null
     * @param index the index that should be used; may not be null
     * @return the new {@link IndexChangeAdapter}; never null
     */
    public static IndexChangeAdapter forNodeDepth( ExecutionContext context,
                                                   NodeTypePredicate matcher,
                                                   String workspaceName,
                                                   ProvidedIndex<?> index ) {
        return new NodeDepthChangeAdapter(context, matcher, workspaceName, index);
    }

    /**
     * Create an {@link IndexChangeAdapter} implementation that handles the "jcr:name" property.
     *
     * @param context the execution context; may not be null
     * @param matcher the node type matcher used to determine which nodes should be included in the index; may not be null
     * @param workspaceName the name of the workspace; may not be null
     * @param index the local index that should be used; may not be null
     * @return the new {@link IndexChangeAdapter}; never null
     */
    public static IndexChangeAdapter forNodeName( ExecutionContext context,
                                                  NodeTypePredicate matcher,
                                                  String workspaceName,
                                                  ProvidedIndex<?> index ) {
        return new NodeNameChangeAdapter(context, matcher, workspaceName, index);
    }

    /**
     * Create an {@link IndexChangeAdapter} implementation that handles the "mode:localName" property.
     *
     * @param context the execution context; may not be null
     * @param matcher the node type matcher used to determine which nodes should be included in the index; may not be null
     * @param workspaceName the name of the workspace; may not be null
     * @param index the local index that should be used; may not be null
     * @return the new {@link IndexChangeAdapter}; never null
     */
    public static IndexChangeAdapter forNodeLocalName( ExecutionContext context,
                                                       NodeTypePredicate matcher,
                                                       String workspaceName,
                                                       ProvidedIndex<?> index ) {
        return new NodeLocalNameChangeAdapter(context, matcher, workspaceName, index);
    }

    /**
     * Create an {@link IndexChangeAdapter} implementation that handles the "jcr:path" property.
     *
     * @param context the execution context; may not be null
     * @param matcher the node type matcher used to determine which nodes should be included in the index; may not be null
     * @param workspaceName the name of the workspace; may not be null
     * @param index the local index that should be used; may not be null
     * @return the new {@link IndexChangeAdapter}; never null
     */
    public static IndexChangeAdapter forNodePath( ExecutionContext context,
                                                  NodeTypePredicate matcher,
                                                  String workspaceName,
                                                  ProvidedIndex<?>  index ) {
        return new NodePathChangeAdapter(context, matcher, workspaceName, index);
    }

    /**
     * Create an {@link IndexChangeAdapter} implementation that handles the "jcr:primaryType" property.
     *
     * @param context the execution context; may not be null
     * @param matcher the node type matcher used to determine which nodes should be included in the index; may not be null
     * @param workspaceName the name of the workspace; may not be null
     * @param index the local index that should be used; may not be null
     * @return the new {@link IndexChangeAdapter}; never null
     */
    public static IndexChangeAdapter forPrimaryType( ExecutionContext context,
                                                     NodeTypePredicate matcher,
                                                     String workspaceName,
                                                     ProvidedIndex<?> index ) {
        return new PrimaryTypeChangeAdapter(context, matcher, workspaceName, index);
    }

    /**
     * Create an {@link IndexChangeAdapter} implementation that handles the "jcr:mixinTypes" property.
     *
     * @param context the execution context; may not be null
     * @param matcher the node type matcher used to determine which nodes should be included in the index; may not be null
     * @param workspaceName the name of the workspace; may not be null
     * @param index the local index that should be used; may not be null
     * @return the new {@link IndexChangeAdapter}; never null
     */
    public static IndexChangeAdapter forMixinTypes( ExecutionContext context,
                                                    NodeTypePredicate matcher,
                                                    String workspaceName,
                                                    ProvidedIndex<?> index ) {
        return new MixinTypesChangeAdapter(context, matcher, workspaceName, index);
    }

    /**
     * Create an {@link IndexChangeAdapter} implementation that handles a node property, either single or multi-valued.
     *
     * @param context the execution context; may not be null
     * @param matcher the node type matcher used to determine which nodes should be included in the index; may not be null
     * @param workspaceName the name of the workspace; may not be null
     * @param propertyName the name of the property; may not be null
     * @param factory the value factory for the property's value type; may not be null
     * @param index the local index that should be used; may not be null
     * @return the new {@link IndexChangeAdapter}; never null
     */
    public static <T> IndexChangeAdapter forProperty( ExecutionContext context,
                                                      NodeTypePredicate matcher,
                                                      String workspaceName,
                                                      Name propertyName,
                                                      ValueFactory<T> factory,
                                                      ProvidedIndex<?> index ) {
        return new PropertyChangeAdapter<>(context, matcher, workspaceName, propertyName, factory, index);
    }
    
    /**
     * Create an {@link IndexChangeAdapter} implementation that handles a unique-valued property, where every property value is
     * unique across all nodes.
     *
     * @param context the execution context; may not be null
     * @param matcher the node type matcher used to determine which nodes should be included in the index; may not be null
     * @param workspaceName the name of the workspace; may not be null
     * @param propertyName the name of the property; may not be null
     * @param factory the value factory for the property's value type; may not be null
     * @param index the local index that should be used; may not be null
     * @return the new {@link IndexChangeAdapter}; never null
     */
    public static <T> IndexChangeAdapter forUniqueValuedProperty( ExecutionContext context,
                                                                  NodeTypePredicate matcher,
                                                                  String workspaceName,
                                                                  Name propertyName,
                                                                  ValueFactory<T> factory,
                                                                  ProvidedIndex<?> index ) {
        return new UniquePropertyChangeAdapter<>(context, matcher, workspaceName, propertyName, factory, index);
    }

    /**
     * Create an {@link IndexChangeAdapter} implementation that handles a enumerated properties, either single or multi-valued.
     *
     * @param context the execution context; may not be null
     * @param matcher the node type matcher used to determine which nodes should be included in the index; may not be null
     * @param workspaceName the name of the workspace; may not be null
     * @param propertyName the name of the property; may not be null
     * @param index the local index that should be used; may not be null
     * @return the new {@link IndexChangeAdapter}; never null
     */
    public static IndexChangeAdapter forEnumeratedProperty( ExecutionContext context,
                                                            NodeTypePredicate matcher,
                                                            String workspaceName,
                                                            Name propertyName,
                                                            ProvidedIndex<?> index ) {
        return new EnumeratedPropertyChangeAdapter(context, matcher, workspaceName, propertyName, index);
    }
    
    /**
     * Create an {@link IndexChangeAdapter} implementation that handles node type information.
     *
     * @param propertyName a symbolic name of the property that will be sent to the {@link ProvidedIndex} when the adapter
     * notices that there are either primary type of mixin type changes.
     * @param context the execution context; may not be null
     * @param matcher the node type matcher used to determine which nodes should be included in the index; may not be null
     * @param workspaceName the name of the workspace; may not be null
     * @param index the local index that should be used; may not be null
     * @return the new {@link IndexChangeAdapter}; never null
     */
    public static IndexChangeAdapter forNodeTypes( String propertyName, 
                                                   ExecutionContext context,
                                                   NodeTypePredicate matcher,
                                                   String workspaceName,
                                                   ProvidedIndex<?> index ) {
        return new NodeTypesChangeAdapter(propertyName, context, matcher, workspaceName, index);
    }

    /**
     * Create an {@link IndexChangeAdapter} implementation that handles full text information.
     *
     * @param context the execution context; may not be null
     * @param matcher the node type matcher used to determine which nodes should be included in the index; may not be null
     * @param workspaceName the name of the workspace; may not be null
     * @param propertyName the name of the property; may not be null
     * @param factory the value factory for the property's value type; may not be null
     * @param index the local index that should be used; may not be null
     * @return the new {@link IndexChangeAdapter}; never null
     */
    public static IndexChangeAdapter forTextProperty( ExecutionContext context,
                                                      NodeTypePredicate matcher,
                                                      String workspaceName,
                                                      Name propertyName,
                                                      ValueFactory<String> factory,
                                                      ProvidedIndex<?> index ) {
        return new TextPropertyChangeAdapter(context, matcher, workspaceName, propertyName, factory,index);
    }

    private IndexChangeAdapters() {
    }
    
    protected static class MultiColumnChangeAdapter extends IndexChangeAdapter {
        private List<PathBasedChangeAdapter<?>> pathAdapters;
        private List<AbstractPropertyChangeAdapter<?>> propertyAdapters;

        protected MultiColumnChangeAdapter( ExecutionContext context,
                                            String workspaceName,
                                            NodeTypePredicate predicate,
                                            ProvidedIndex<?> index,
                                            Iterable<IndexChangeAdapter> adapters ) {
            super(context, workspaceName, predicate, index);

            pathAdapters = new ArrayList<>();
            propertyAdapters = new ArrayList<>();

            for (IndexChangeAdapter adapter : adapters) {
                if (adapter instanceof PathBasedChangeAdapter) {
                    pathAdapters.add((PathBasedChangeAdapter<?>) adapter);
                } else if (adapter instanceof  AbstractPropertyChangeAdapter) {
                    propertyAdapters.add((AbstractPropertyChangeAdapter<?>) adapter);
                }
            }
            
            assert !pathAdapters.isEmpty() || !propertyAdapters.isEmpty();
        }

        @Override
        protected void modifyProperties( NodeKey key, Name primaryType, Set<Name> mixinTypes,
                                         Map<Name, AbstractPropertyChange> propChanges ) {
            // only the property adapters should be interested in this
            if (propertyAdapters.isEmpty()) {
                return;
            }
            for (AbstractPropertyChangeAdapter<?> propertyChangeAdapter : propertyAdapters) {
                propertyChangeAdapter.modifyProperties(key, primaryType, mixinTypes, propChanges);                
            }
        }

        @Override
        protected void addNode( String workspaceName, NodeKey key, Path path, Name primaryType, Set<Name> mixinTypes,
                                Properties properties ) {
            // only the path based adapters should be interested in this since properties are handled via modify properties
            if (pathAdapters.isEmpty()) {
                return;
            }
            for (PathBasedChangeAdapter<?> pathAdapter : pathAdapters) {
                pathAdapter.addNode(workspaceName, key, path, primaryType, mixinTypes, properties);
            }
        }

        @Override
        protected void reindexNode( String workspaceName, NodeKey key, Path path, Name primaryType, Set<Name> mixinTypes,
                                    Properties properties, boolean queryable ) {
            
            //first remove all data for the given key...
            String nodeKey = nodeKey(key);
            index().remove(nodeKey);
            
            //then based on each of the adapters types, add information back to the index...
            if (!pathAdapters.isEmpty()) {
                for (PathBasedChangeAdapter<?> pathAdapter : pathAdapters) {
                    index().add(nodeKey, pathAdapter.propertyName, pathAdapter.valueOf(path));
                }   
            }
            if (!propertyAdapters.isEmpty()) {
                for (AbstractPropertyChangeAdapter<?> propertyAdapter : propertyAdapters) {
                    Property property = properties.getProperty(propertyAdapter.propertyName);
                    if (property != null) {
                        propertyAdapter.addValues(key, property);
                    }
                }
            }
        }

        @Override
        protected void removeNode( String workspaceName, NodeKey key, NodeKey parentKey, Path path, Name primaryType,
                                   Set<Name> mixinTypes ) {
            // just remove everything for that node from the index...
            index().remove(nodeKey(key));
        }


        @Override
        protected void moveNode( String workspaceName, NodeKey key, Name primaryType, Set<Name> mixinTypes, NodeKey oldParent,
                                 NodeKey newParent, Path newPath, Path oldPath ) {
            // only the path based adapters should be interested in this since properties are handled via modify properties
            if (pathAdapters.isEmpty()) {
                return;
            }
            for (PathBasedChangeAdapter<?> pathAdapter : pathAdapters) {
                pathAdapter.moveNode(workspaceName, key, primaryType, mixinTypes, oldParent, newParent, newPath, oldPath);
            }
        }

        @Override
        protected void renameNode( String workspaceName, NodeKey key, Path newPath, Path.Segment oldSegment, Name primaryType,
                                   Set<Name> mixinTypes ) {
            // only the path based adapters should be interested in this since properties are handled via modify properties
            if (pathAdapters.isEmpty()) {
                return;
            }
            for (PathBasedChangeAdapter<?> pathAdapter : pathAdapters) {
                pathAdapter.renameNode(workspaceName, key, newPath, oldSegment, primaryType, mixinTypes);
            }
        }

        @Override
        protected void reorderNode( String workspaceName, NodeKey key, Name primaryType, Set<Name> mixinTypes, NodeKey parent,
                                    Path newPath, Path oldPath, Path reorderedBeforePath,
                                    Map<NodeKey, Map<Path, Path>> snsPathChangesByNodeKey ) {
            // only the path based adapters should be interested in this since properties are handled via modify properties
            if (pathAdapters.isEmpty()) {
                return;
            }
            for (PathBasedChangeAdapter<?> pathAdapter : pathAdapters) {
                pathAdapter.reorderNode(workspaceName, key, primaryType, mixinTypes, parent, newPath, oldPath, reorderedBeforePath, 
                                        snsPathChangesByNodeKey);
            }
        }
    }

    protected static abstract class PathBasedChangeAdapter<T> extends IndexChangeAdapter {
        protected final String propertyName;
        
        protected PathBasedChangeAdapter( ExecutionContext context,
                                          NodeTypePredicate matcher,
                                          String workspaceName,
                                          ProvidedIndex<?> index,
                                          Name propertyName) {
            super(context, workspaceName, matcher, index);
            assert propertyName != null;
            this.propertyName = propertyName.getString(context.getNamespaceRegistry());
        }
        
        protected T valueOf(Path path) {
            return path.isRoot() ? convertRoot(path) : convert(path); 
        }

        protected abstract T convertRoot( Path path );

        protected abstract T convert( Path path );
        
        @Override
        protected void addNode( String workspaceName,
                                NodeKey key,
                                Path path,
                                Name primaryType,
                                Set<Name> mixinTypes,
                                Properties properties ) {
            index().add(nodeKey(key), propertyName, valueOf(path));
        }

        @Override
        protected void reindexNode( String workspaceName,
                                    NodeKey key,
                                    Path path,
                                    Name primaryType,
                                    Set<Name> mixinTypes,
                                    Properties properties,
                                    boolean queryable ) {
            String nodeKey = nodeKey(key);
            index().remove(nodeKey, propertyName, valueOf(path));
            if (queryable) {
                index().add(nodeKey(key), propertyName, valueOf(path));
            }            
        }

        @Override
        protected void moveNode( String workspaceName,
                                 NodeKey key,
                                 Name primaryType,
                                 Set<Name> mixinTypes,
                                 NodeKey oldParent,
                                 NodeKey newParent,
                                 Path newPath,
                                 Path oldPath ) {
            String nodeKey = nodeKey(key);
            index().remove(nodeKey, propertyName, valueOf(oldPath));
            index().add(nodeKey, propertyName, valueOf(newPath));
        }

        @Override
        protected void removeNode( String workspaceName,
                                   NodeKey key,
                                   NodeKey parentKey,
                                   Path path,
                                   Name primaryType,
                                   Set<Name> mixinTypes ) {
            index().remove(nodeKey(key));
        }

        @Override
        protected void renameNode( String workspaceName, 
                                   NodeKey key, 
                                   Path newPath, 
                                   Path.Segment oldSegment, 
                                   Name primaryType,
                                   Set<Name> mixinTypes ) {
            PathFactory pathFactory = context.getValueFactories().getPathFactory();
            Path oldPath = pathFactory.create(newPath.subpath(0, newPath.size()), oldSegment);
            String nodeKey = nodeKey(key);
            index().remove(nodeKey, propertyName, valueOf(oldPath));
            index().add(nodeKey, propertyName, valueOf(newPath));
        }

        @Override
        protected void reorderNode( String workspaceName,
                                    NodeKey key,
                                    Name primaryType,
                                    Set<Name> mixinTypes,
                                    NodeKey parent,
                                    Path newPath,
                                    Path oldPath,
                                    Path reorderedBeforePath, 
                                    Map<NodeKey, Map<Path, Path>> snsPathChangesByNodeKey ) {
            if (newPath.getLastSegment().hasIndex() || oldPath.getLastSegment().hasIndex()) {
                // SNS reorderings can cause a path to change (see https://issues.jboss.org/browse/MODE-2510)
                String nodeKey = nodeKey(key);
                index().remove(nodeKey, propertyName, valueOf(oldPath));
                index().add(nodeKey, propertyName, valueOf(newPath));
                // now look if there are additional SNSs which changed their path and if so, update the indexes for each
                snsPathChangesByNodeKey.forEach(( snsKey, pathMap ) -> {
                    String snsKeyString = nodeKey(snsKey);
                    Map.Entry<Path, Path> pathChangeEntry = pathMap.entrySet().iterator().next();
                    index().remove(snsKeyString, propertyName, valueOf(pathChangeEntry.getKey()));
                    index().add(snsKeyString, propertyName, valueOf(pathChangeEntry.getValue()));
                });
            }
            //otherwise no SNS are involved so the nodekey and path information doesn't need to change
        }
        @Override
        public String toString() {
            return  getClass().getSimpleName() + "(\"" + index.getName() + "\" : \"" + propertyName + "\")";
        }
    }

    protected static final class NodeDepthChangeAdapter extends PathBasedChangeAdapter<Long> {
        public NodeDepthChangeAdapter( ExecutionContext context,
                                       NodeTypePredicate matcher,
                                       String workspaceName,
                                       ProvidedIndex<?> index ) {
            super(context, matcher, workspaceName, index, ModeShapeLexicon.DEPTH);
        }

        @Override
        protected Long convert( Path path ) {
            return (long)path.size();
        }

        @Override
        protected Long convertRoot( Path path ) {
            return convert(path);
        }
    }

    protected static final class NodeNameChangeAdapter extends PathBasedChangeAdapter<Name> {
        public NodeNameChangeAdapter( ExecutionContext context,
                                      NodeTypePredicate matcher,
                                      String workspaceName,
                                      ProvidedIndex<?> index) {
            super(context, matcher, workspaceName, index, JcrLexicon.NAME);
        }

        @Override
        protected Name convert( Path path ) {
            return path.getLastSegment().getName();
        }

        @Override
        protected Name convertRoot( Path path ) {
            return Path.ROOT_NAME;
        }

        @Override
        protected void reorderNode( String workspaceName, NodeKey key, Name primaryType, Set<Name> mixinTypes, NodeKey parent,
                                    Path newPath, Path oldPath, Path reorderedBeforePath,
                                    Map<NodeKey, Map<Path, Path>> snsPathChangesByNodeKey ) {
            // reordering should not really change the name...
        }
    }

    protected static final class NodeLocalNameChangeAdapter extends PathBasedChangeAdapter<String> {
        public NodeLocalNameChangeAdapter( ExecutionContext context,
                                           NodeTypePredicate matcher,
                                           String workspaceName,
                                           ProvidedIndex<?> index ) {
            super(context, matcher, workspaceName, index, ModeShapeLexicon.LOCALNAME);
        }

        @Override
        protected String convert( Path path ) {
            return path.getLastSegment().getName().getLocalName();
        }

        @Override
        protected String convertRoot( Path path ) {
            return "";
        }

        @Override
        protected void reorderNode( String workspaceName, NodeKey key, Name primaryType, Set<Name> mixinTypes, NodeKey parent,
                                    Path newPath, Path oldPath, Path reorderedBeforePath,
                                    Map<NodeKey, Map<Path, Path>> snsPathChangesByNodeKey ) {
            // reordering should not really change the name...
        }
    }

    protected static final class NodePathChangeAdapter extends PathBasedChangeAdapter<Path> {
        public NodePathChangeAdapter( ExecutionContext context,
                                      NodeTypePredicate matcher,
                                      String workspaceName,
                                      ProvidedIndex<?> index ) {
            super(context, matcher, workspaceName, index, JcrLexicon.PATH);
        }

        @Override
        protected Path convert( Path path ) {
            return path;
        }

        @Override
        protected Path convertRoot( Path path ) {
            return path;
        }
    }

    protected static abstract class AbstractPropertyChangeAdapter<T> extends IndexChangeAdapter {
        protected final Name propertyName;
        protected final ValueFactory<T> valueFactory;

        public AbstractPropertyChangeAdapter( ExecutionContext context,
                                              NodeTypePredicate matcher,
                                              String workspaceName,
                                              Name propertyName,
                                              ValueFactory<T> valueFactory,
                                              ProvidedIndex<?> index) {
            super(context, workspaceName, matcher, index);
            this.propertyName = propertyName;
            this.valueFactory = valueFactory;
        }

        protected final T convert( Object value ) {
            return valueFactory.create(value);
        }

        protected abstract void addValues( NodeKey key,
                                           Property property );

        protected abstract void addValue( NodeKey key,
                                          Object value );
        
        protected abstract void removeValues(NodeKey key, Property property);
        
        protected final String propertyName() {
            return propertyName.getString(context.getNamespaceRegistry());
        }

        @Override
        protected void addNode( String workspaceName,
                                NodeKey key,
                                Path path,
                                Name primaryType,
                                Set<Name> mixinTypes,
                                Properties properties ) {
            // Properties on new nodes are always represented as 'PropertyAdded' events, and handled via 'modifyProperties' ...
        }

        @Override
        protected void reindexNode( String workspaceName,
                                    NodeKey key,
                                    Path path,
                                    Name primaryType,
                                    Set<Name> mixinTypes,
                                    Properties properties,
                                    boolean queryable ) {
            if (properties != null) {
                assert propertyName != null;
                Property prop = properties.getProperty(propertyName);
                if (prop != null) {
                    removeValues(key, prop);
                    if (queryable) {
                        addValues(key, prop);
                    }
                }
            }
        }

        @Override
        protected void modifyProperties( NodeKey key,
                                         Name primaryType, 
                                         Set<Name> mixinTypes, 
                                         Map<Name, AbstractPropertyChange> propChanges ) {
            AbstractPropertyChange propChange = propChanges.get(propertyName);
            if (propChange instanceof PropertyChanged) {
                PropertyChanged change = (PropertyChanged)propChange;
                removeValues(key, change.getOldProperty());
                addValues(key, change.getNewProperty());
            } else if (propChange instanceof PropertyAdded) {
                PropertyAdded added = (PropertyAdded)propChange;
                addValues(key, added.getProperty());
            } else if (propChange instanceof PropertyRemoved) {
                removeValues(key, propChange.getProperty());
            }
        }

        @Override
        protected void removeNode( String workspaceName,
                                   NodeKey key,
                                   NodeKey parentKey,
                                   Path path,
                                   Name primaryType,
                                   Set<Name> mixinTypes ) {
            index().remove(nodeKey(key));
        }

        @Override
        public String toString() {
            return  getClass().getSimpleName() + "(\"" + index.getName() + "\" : \"" + propertyName() + "\")"; 
        }
    }

    protected static class PropertyChangeAdapter<T> extends AbstractPropertyChangeAdapter<T> {
        public PropertyChangeAdapter( ExecutionContext context,
                                      NodeTypePredicate matcher,
                                      String workspaceName,
                                      Name propertyName,
                                      ValueFactory<T> valueFactory,
                                      ProvidedIndex<?> index ) {
            super(context, matcher, workspaceName, propertyName, valueFactory, index);
        }

        @Override
        protected void addValues( NodeKey key,
                                  Property property ) {
            if (property.isEmpty()) {
                return;
            }
            String nodeKey = nodeKey(key);
            String propertyName = propertyName();
            if (property.isMultiple()) {
                index().add(nodeKey, propertyName, property.getValuesAsArray(valueFactory));
            } else {
                index().add(nodeKey, propertyName,  convert(property.getFirstValue()));
            }
        }

        @Override
        protected void addValue( NodeKey key,
                                 Object value ) {
            if (value == null) {
                return;
            }
            index().add(nodeKey(key), propertyName(), convert(value));
        }

        @Override
        protected void removeValues( NodeKey key, Property property ) {
            if (property.isEmpty()) {
                return;
            }
            String nodeKey = nodeKey(key);
            String propertyName = propertyName();
            if (property.isMultiple()) {
                index().remove(nodeKey, propertyName, property.getValuesAsArray(valueFactory));
            } else {
                index().remove(nodeKey, propertyName, convert(property.getFirstValue())); 
            }
        }
    }
    
    protected static final class PrimaryTypeChangeAdapter extends PropertyChangeAdapter<Name> {
        public PrimaryTypeChangeAdapter( ExecutionContext context,
                                         NodeTypePredicate matcher,
                                         String workspaceName,
                                         ProvidedIndex<?> index ) {
            super(context, matcher, workspaceName, JcrLexicon.PRIMARY_TYPE, context.getValueFactories().getNameFactory(), index);
        }
    }

    protected static final class MixinTypesChangeAdapter extends PropertyChangeAdapter<Name> {
        public MixinTypesChangeAdapter( ExecutionContext context,
                                        NodeTypePredicate matcher,
                                        String workspaceName,
                                        ProvidedIndex<?> index ) {
            super(context, matcher, workspaceName, JcrLexicon.MIXIN_TYPES, context.getValueFactories().getNameFactory(), index);
        }
    }

    protected static final class UniquePropertyChangeAdapter<T> extends AbstractPropertyChangeAdapter<T> {
        public UniquePropertyChangeAdapter( ExecutionContext context,
                                            NodeTypePredicate matcher,
                                            String workspaceName,
                                            Name propertyName,
                                            ValueFactory<T> valueFactory,
                                            ProvidedIndex<?> index ) {
            super(context, matcher, workspaceName, propertyName, valueFactory, index);
        }

        @Override
        protected void addValues( NodeKey key,
                                  Property property ) {
            index().add(nodeKey(key), propertyName(), convert(property.getFirstValue()));
        }

        @Override
        protected final void addValue( NodeKey key,
                                       Object value ) {
            index().add(nodeKey(key), propertyName(), convert(value));
        }

        @Override
        protected void removeValues( NodeKey key, Property property ) {
            index().remove(nodeKey(key), propertyName(), convert(property.getFirstValue()));
        }
    }

    protected static class EnumeratedPropertyChangeAdapter extends PropertyChangeAdapter<String> {
        public EnumeratedPropertyChangeAdapter( ExecutionContext context,
                                                NodeTypePredicate matcher,
                                                String workspaceName,
                                                Name propertyName,
                                                ProvidedIndex<?> index ) {
            super(context, matcher, workspaceName, propertyName, context.getValueFactories().getStringFactory(), index);
        }
    }

    protected static final class NodeTypesChangeAdapter extends EnumeratedPropertyChangeAdapter {
        private final String property;
        public NodeTypesChangeAdapter( String property, 
                                       ExecutionContext context,
                                       NodeTypePredicate matcher,
                                       String workspaceName,
                                       ProvidedIndex<?> index ) {
            // note that this doesn't care about the property, for which it will use the name of the index
            super(context, matcher, workspaceName, null, index);
            this.property = property;
            assert this.property != null;
        }
        
        @Override
        protected void modifyProperties( NodeKey key,
                                         Name primaryType, 
                                         Set<Name> mixinTypes, 
                                         Map<Name, AbstractPropertyChange> propChanges ) {
            List<Object> newValues = new ArrayList<>();
            List<Object> oldValues = new ArrayList<>();
            
            AbstractPropertyChange primaryTypeChange = propChanges.get(JcrLexicon.PRIMARY_TYPE);
            if (primaryTypeChange instanceof PropertyChanged) {
                PropertyChanged change = (PropertyChanged)primaryTypeChange;
                oldValues.add(change.getOldProperty().getFirstValue());
                newValues.add(change.getNewProperty().getFirstValue());
            } else if (primaryTypeChange instanceof PropertyAdded) {
                newValues.add(primaryTypeChange.getProperty().getFirstValue());
            } else if (primaryTypeChange instanceof PropertyRemoved) {
                oldValues.add(primaryTypeChange.getProperty().getFirstValue());
            }

            AbstractPropertyChange mixinsTypeChange = propChanges.get(JcrLexicon.MIXIN_TYPES);
            if (mixinsTypeChange instanceof PropertyChanged) {
                PropertyChanged change = (PropertyChanged)mixinsTypeChange;
                Property oldProperty = change.getOldProperty();
                if (!oldProperty.isEmpty()) {
                    oldValues.addAll(Arrays.asList(oldProperty.getValuesAsArray()));
                }
                Property newProperty = change.getNewProperty();
                if (!newProperty.isEmpty()) {
                    newValues.addAll(Arrays.asList(newProperty.getValuesAsArray()));
                }
            } else if (mixinsTypeChange instanceof PropertyAdded) {
                if (!mixinsTypeChange.getProperty().isEmpty()) {
                    newValues.addAll(Arrays.asList(mixinsTypeChange.getProperty().getValuesAsArray()));
                }
            } else if (mixinsTypeChange instanceof PropertyRemoved) {
                if (!mixinsTypeChange.getProperty().isEmpty()) {
                    oldValues.addAll(Arrays.asList(mixinsTypeChange.getProperty().getValuesAsArray()));
                }
            }

            if (primaryTypeChange == null && mixinsTypeChange == null) {
                // neither the primary nor the mixins have changed, so nothing to do...
                return;
            }
            if (primaryTypeChange == null) {
                // only the mixins have changed so to have the complete information we need to add the primary type
                newValues.add(primaryType);
            } else if (mixinsTypeChange == null && mixinTypes != null) {
                // only the primary type has changed, to have the complete information we need to add the 
                newValues.addAll(mixinTypes);
            }
            
            String nodeKey = nodeKey(key);
            if (!oldValues.isEmpty()) {
                // there are values which require removal, so remove all of them because we'll submit a complete set of values 
                // below
                index().remove(nodeKey);
            }
            assert !newValues.isEmpty();
            index().add(nodeKey, property, valueFactory.create(newValues.toArray()));
        }

        @Override
        protected void removeNode( String workspaceName,
                                   NodeKey key,
                                   NodeKey parentKey,
                                   Path path,
                                   Name primaryType,
                                   Set<Name> mixinTypes ) {
            index().remove(nodeKey(key));
        }

        @Override
        protected void reindexNode( String workspaceName, NodeKey key, Path path, Name primaryType, Set<Name> mixinTypes,
                                    Properties properties, boolean queryable ) {
            String nodeKey = nodeKey(key);
            index().remove(nodeKey);
            if (!queryable) {
                return;
            }
            addTypeInformation(key, primaryType, mixinTypes);
        }


        private void addTypeInformation( NodeKey key, Name primaryType, Set<Name> mixinTypes ) {
            List<Name> values = new ArrayList<>();
            values.add(primaryType);
            if (!mixinTypes.isEmpty()) {
                values.addAll(mixinTypes);
            }
            index().add(nodeKey(key), property, valueFactory.create(values.toArray()));
        }
    }
    
    protected static final class TextPropertyChangeAdapter extends PropertyChangeAdapter<String> {
        public TextPropertyChangeAdapter( ExecutionContext context,
                                          NodeTypePredicate matcher, String workspaceName, Name propertyName,
                                          ValueFactory<String> valueFactory, ProvidedIndex<?> index ) {
            super(context, matcher, workspaceName, propertyName, valueFactory, index);
        }

        @Override
        protected void addValues( NodeKey key, Property property ) {
            StringBuilder builder = textFrom(property);
            if (builder.length() > 0) {
                index().add(nodeKey(key), propertyName(), builder.toString());    
            }
        }

        @Override
        protected void removeValues( NodeKey key, Property property ) {
            StringBuilder builder = textFrom(property);
            if (builder.length() > 0) {
                index().remove(nodeKey(key),propertyName(), builder.toString());
            }
        }
        
        protected StringBuilder textFrom(Property property) {
            StringBuilder builder = new StringBuilder();
            if (property.isEmpty()) {
                return builder;
            }
            if (!property.isBinary()) {
                String[] values = property.getValuesAsArray(valueFactory);  
                for (int i = 0; i < values.length; i++) {
                    builder.append(values[i]);
                    if (i < values.length - 1) {
                        builder.append(" ");
                    }
                }
            } else {
                for (Iterator<Object> valuesIterator = property.iterator(); valuesIterator.hasNext();) {
                    Object value = valuesIterator.next();
                    assert value instanceof BinaryValue;
                    BinaryValue binaryValue = (BinaryValue) value;
                    try {
                        String extractedText = context.getBinaryStore().getText(binaryValue);
                        builder.append(extractedText);
                        if (valuesIterator.hasNext()) {
                            builder.append(" ");
                        }
                    } catch (BinaryStoreException e) {
                        logger.debug(e, "Error trying to get extracted text for {0}", binaryValue);
                    }
                }
            }
            return builder;
        }
    }

}
