/*
 * 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.cache.change;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import org.modeshape.jcr.ExecutionContext;
import org.modeshape.jcr.cache.CachedNode.Properties;
import org.modeshape.jcr.cache.NodeKey;
import org.modeshape.jcr.value.BinaryKey;
import org.modeshape.jcr.value.Name;
import org.modeshape.jcr.value.Path;
import org.modeshape.jcr.value.Path.Segment;
import org.modeshape.jcr.value.Property;

/**
 * An adapter class that processes {@link ChangeSet} instances and for each {@link Change} calls the appropriate protected method
 * that, by default, do nothing. Implementations simply override at least one of the relevant methods for the kind of changes they
 * expect.
 *
 * @author Randall Hauch (rhauch@redhat.com)
 */
public abstract class ChangeSetAdapter implements ChangeSetListener {

    public static interface NodeTypePredicate {
        /**
         * Determine if the given primary type and mixin types satisfies the node type requirements of an event.
         *
         * @param primaryType the primary type of the event
         * @param mixinTypes the set of nodes mixins; never null but possibly empty
         * @return true if the event should be processed, or false otherwise
         */
        boolean matchesType( Name primaryType,
                             Set<Name> mixinTypes );

        /**
         * Determine if the given primary type and mixin types are queryable according to this predicate, in the JSR-283 sense.
         *
         * @param primaryType the primary type of a node
         * @param mixinTypes the set of nodes mixins; never null but possibly empty
         * @return false if there's at least one type which is marked as 'noquery', true otherwise.
         */
        boolean isQueryable( Name primaryType, Set<Name> mixinTypes );
    }

    protected final ExecutionContext context;
    protected final NodeTypePredicate predicate;

    /**
     * @param context the execution context in which this adapter is supposed to run; may not be null
     */
    public ChangeSetAdapter( ExecutionContext context ) {
        this(context, null);
    }

    public ChangeSetAdapter( ExecutionContext context,
                             NodeTypePredicate predicate ) {
        assert context != null;
        this.context = context;
        this.predicate = predicate;
    }

    @Override
    public void notify( ChangeSet changeSet ) {
        final String workspaceName = changeSet.getWorkspaceName();
        if (workspaceName != null) {
            if (includesWorkspace(workspaceName)) {
                try {
                    beginWorkspaceChanges();
                    final Map<Name, AbstractPropertyChange> propChanges = new HashMap<>();
                    NodeKey lastKey = null;
                    Name lastPrimaryType = null;
                    Set<Name> lastMixinTypes = null;
                    for (Change change : changeSet) {
                        if (change instanceof AbstractNodeChange) {
                            AbstractNodeChange anc = (AbstractNodeChange) change;
                            Name primaryType = anc.getPrimaryType();
                            Set<Name> mixinTypes = anc.getMixinTypes();
                            if (!matchesType(primaryType, mixinTypes)) {
                                continue;
                            }
                            if (!isQueryable(primaryType, mixinTypes)) {
                                continue;
                            }

                            if (change instanceof NodeAdded) {
                                firePropertyChanges(lastKey, lastPrimaryType, lastMixinTypes, propChanges);
                                NodeAdded added = (NodeAdded) change;
                                addNode(workspaceName, added.getKey(), added.getPath(), added.getPrimaryType(),
                                        added.getMixinTypes(), props(added.getProperties()));
                            } else if (change instanceof NodeRemoved) {
                                firePropertyChanges(lastKey, lastPrimaryType, lastMixinTypes, propChanges);
                                NodeRemoved removed = (NodeRemoved) change;
                                removeNode(workspaceName, removed.getKey(), removed.getParentKey(), removed.getPath(),
                                           removed.getPrimaryType(), removed.getMixinTypes());
                            } else if (change instanceof AbstractPropertyChange) {
                                AbstractPropertyChange propChange = (AbstractPropertyChange) change;
                                if (!propChange.getKey().equals(lastKey)) {
                                    firePropertyChanges(lastKey, lastPrimaryType, lastMixinTypes,
                                                        propChanges);
                                }
                                propChanges.put(propChange.getProperty().getName(), propChange);
                            } else if (change instanceof NodeChanged) {
                                firePropertyChanges(lastKey, lastPrimaryType, lastMixinTypes, propChanges);
                                NodeChanged nodeChanged = (NodeChanged) change;
                                changeNode(workspaceName, nodeChanged.getKey(), nodeChanged.getPath(),
                                           nodeChanged.getPrimaryType(), nodeChanged.getMixinTypes());
                            } else if (change instanceof NodeMoved) {
                                firePropertyChanges(lastKey, lastPrimaryType, lastMixinTypes, propChanges);
                                NodeMoved moved = (NodeMoved) change;
                                moveNode(workspaceName, moved.getKey(), moved.getPrimaryType(), moved.getMixinTypes(),
                                         moved.getNewParent(), moved.getOldParent(), moved.getNewPath(), moved.getOldPath()
                                        );
                            } else if (change instanceof NodeRenamed) {
                                firePropertyChanges(lastKey, lastPrimaryType, lastMixinTypes, propChanges);
                                NodeRenamed renamed = (NodeRenamed) change;
                                renameNode(workspaceName, renamed.getKey(), renamed.getPath(), renamed.getOldSegment(),
                                           renamed.getPrimaryType(), renamed.getMixinTypes());
                            } else if (change instanceof NodeReordered) {
                                firePropertyChanges(lastKey, lastPrimaryType, lastMixinTypes, propChanges);
                                NodeReordered reordered = (NodeReordered) change;
                                reorderNode(workspaceName, reordered.getKey(), reordered.getPrimaryType(),
                                            reordered.getMixinTypes(), reordered.getParent(), reordered.getPath(),
                                            reordered.getOldPath(), reordered.getReorderedBeforePath(), reordered.getSnsPathChangesByNodeKey());
                            } else if (change instanceof NodeSequenced) {
                                firePropertyChanges(lastKey, lastPrimaryType, lastMixinTypes, propChanges);
                                NodeSequenced s = (NodeSequenced) change;
                                sequenced(workspaceName, s.getKey(), s.getPath(), s.getPrimaryType(), s.getMixinTypes(),
                                          s.getOutputNodeKey(), s.getOutputNodePath(), s.getOutputPath(), s.getUserId(),
                                          s.getSelectedPath(), s.getSequencerName());
                            } else if (change instanceof NodeSequencingFailure) {
                                firePropertyChanges(lastKey, lastPrimaryType, lastMixinTypes, propChanges);
                                NodeSequencingFailure f = (NodeSequencingFailure) change;
                                sequenceFailure(workspaceName, f.getKey(), f.getPath(), f.getPrimaryType(), f.getMixinTypes(),
                                                f.getOutputPath(), f.getUserId(), f.getSelectedPath(), f.getSequencerName(),
                                                f.getCause());
                            }
                            lastKey = anc.getKey();
                            lastPrimaryType = anc.getPrimaryType();
                            lastMixinTypes = anc.getMixinTypes();
                        }
                    }
                    if (lastKey != null) {
                        firePropertyChanges(lastKey, lastPrimaryType, lastMixinTypes, propChanges);
                    }
                } finally {
                    completeWorkspaceChanges();
                }
            }
        } else {
            beginChanges();
            for (Change change : changeSet) {
                if (change instanceof WorkspaceAdded) {
                    WorkspaceAdded added = (WorkspaceAdded) change;
                    addWorkspace(added.getWorkspaceName());
                } else if (change instanceof WorkspaceRemoved) {
                    WorkspaceRemoved removed = (WorkspaceRemoved) change;
                    removeWorkspace(removed.getWorkspaceName());
                } else if (change instanceof RepositoryMetadataChanged) {
                    repositoryMetadataChanged();
                } else if (change instanceof BinaryValueUnused) {
                    BinaryValueUnused bvu = (BinaryValueUnused) change;
                    binaryValueUnused(bvu.getKey());
                }
            }
            completeChanges();
        }
    }

    private boolean matchesType( Name primaryType,
                                 Set<Name> mixinTypes ) {
        return predicate == null || predicate.matchesType(primaryType, mixinTypes);
    }

    private boolean isQueryable( Name primaryType,
                                 Set<Name> mixinTypes ) {
        return predicate == null || predicate.isQueryable(primaryType, mixinTypes);
    }

    /**
     * Signals the beginning of processing for the workspace-related changes in a transaction described by a single
     * {@link ChangeSet}.
     *
     * @see #completeWorkspaceChanges()
     */
    protected void beginWorkspaceChanges() {
    }

    /**
     * Signals the end of processing for the workspace-related changes in a transaction described by a single {@link ChangeSet}.
     *
     * @see #beginWorkspaceChanges()
     */
    protected void completeWorkspaceChanges() {
    }

    /**
     * Signals the beginning of processing for the changes in a transaction described by a single {@link ChangeSet}.
     *
     * @see #completeChanges()
     */
    protected void beginChanges() {
    }

    /**
     * Signals the end of processing for the changes in a transaction described by a single {@link ChangeSet}.
     *
     * @see #beginChanges()
     */
    protected void completeChanges() {
    }

    private void firePropertyChanges( NodeKey key,
                                      Name primaryType,
                                      Set<Name> mixinTypes,
                                      Map<Name, AbstractPropertyChange> propChanges ) {
        if (!propChanges.isEmpty()) {
            modifyProperties(key, primaryType, mixinTypes, propChanges);
            propChanges.clear();
        }
    }

    /**
     * Handle the addition, change, and removal of one or more properties of a single node. This method is called once for each
     * existing node whose properties are modified.
     *
     * @param key the unique key for the node; may not be null
     * @param primaryType the primary type of the node for which the modifications occurred; never null
     * @param mixinTypes the mixin types of the node for which the modifications occurred; never null but possibly empty
     * @param propChanges the map of property modification events, keyed by the names of the properties; never null and never
     */
    protected void modifyProperties( NodeKey key,
                                     Name primaryType,
                                     Set<Name> mixinTypes,
                                     Map<Name, AbstractPropertyChange> propChanges ) {
    }

    private final Properties props( final Map<Name, Property> properties ) {
        return new Properties() {
            @Override
            public org.modeshape.jcr.value.Property getProperty( Name name ) {
                return properties.get(name);
            }

            @Override
            public Iterator<Property> iterator() {
                return properties.values().iterator();
            }
        };
    }

    /**
     * Determine whether changes in the named workspace should be processed. By default this method returns {@code true}, which
     * means changes to content in all workspaces are handled by this adapter.
     *
     * @param workspaceName the workspace that has changes in content
     * @return true if the changes should be processed, or false otherwise
     */
    protected boolean includesWorkspace( String workspaceName ) {
        return true;
    }

    /**
     * Handle the addition of a node.
     *
     * @param workspaceName the workspace in which the node information should be available; may not be null
     * @param key the unique key for the node; may not be null
     * @param path the path of the node; may not be null
     * @param primaryType the primary type of the node; may not be null
     * @param mixinTypes the mixin types for the node; may not be null but may be empty
     * @param properties the properties of the node; may not be null but may be empty
     */
    protected void addNode( String workspaceName,
                            NodeKey key,
                            Path path,
                            Name primaryType,
                            Set<Name> mixinTypes,
                            Properties properties ) {
    }

    /**
     * Handle the reindexing of a node.
     *
     * @param workspaceName the workspace in which the node information should be available; may not be null
     * @param key the unique key for the node; may not be null
     * @param path the path of the node; may not be null
     * @param primaryType the primary type of the node; may not be null
     * @param mixinTypes the mixin types for the node; may not be null but may be empty
     * @param properties the properties of the node; may not be null but may be empty
     * @param queryable true if the node is queryable, false otherwise
     */
    protected void reindexNode( String workspaceName,
                                NodeKey key,
                                Path path,
                                Name primaryType,
                                Set<Name> mixinTypes,
                                Properties properties,
                                boolean queryable ) {
    }

    /**
     * Handle the removal of a node.
     *
     * @param workspaceName the workspace in which the node information should be available; may not be null
     * @param key the unique key for the node; may not be null
     * @param parentKey the unique key for the parent of the node; may not be null
     * @param path the path of the node; may not be null
     * @param primaryType the primary type of the node; may not be null
     * @param mixinTypes the mixin types for the node; may not be null but may be empty
     */
    protected void removeNode( String workspaceName,
                               NodeKey key,
                               NodeKey parentKey,
                               Path path,
                               Name primaryType,
                               Set<Name> mixinTypes ) {

    }

    /**
     * Handle the change of a node.
     *
     * @param workspaceName the workspace in which the node information should be available; may not be null
     * @param key the unique key for the node; may not be null
     * @param path the path of the node; may not be null
     * @param primaryType the primary type of the node; may not be null
     * @param mixinTypes the mixin types for the node; may not be null but may be empty
     */
    protected void changeNode( String workspaceName,
                               NodeKey key,
                               Path path,
                               Name primaryType,
                               Set<Name> mixinTypes ) {
    }

    /**
     * Handle the move of a node.
     *
     * @param workspaceName the workspace in which the node information should be available; may not be null
     * @param key the unique key for the node; may not be null
     * @param primaryType the primary type of the node; may not be null
     * @param mixinTypes the mixin types for the node; may not be null but may be empty
     * @param oldParent the key of the node's parent before it was moved; may not be null
     * @param newParent the key of the node's parent after it was moved; may not be null
     * @param newPath the new path of the node after it was moved; may not be null
     * @param oldPath the old path of the node before it was moved; may not be null
     */
    protected void moveNode( String workspaceName,
                             NodeKey key,
                             Name primaryType,
                             Set<Name> mixinTypes,
                             NodeKey oldParent,
                             NodeKey newParent,
                             Path newPath,
                             Path oldPath ) {

    }

    /**
     * Handle the renaming of a node.
     *
     * @param workspaceName the workspace in which the node information should be available; may not be null
     * @param key the unique key for the node; may not be null
     * @param newPath the new path of the node after it was moved; may not be null
     * @param oldSegment the old segment of the node before it was moved; may not be null
     * @param primaryType the primary type of the node; may not be null
     * @param mixinTypes the mixin types for the node; may not be null but may be empty
     */
    protected void renameNode( String workspaceName,
                               NodeKey key,
                               Path newPath,
                               Segment oldSegment,
                               Name primaryType,
                               Set<Name> mixinTypes ) {

    }

    /**
     * Handle the reordering of a node.
     *
     * @param workspaceName the workspace in which the node information should be available; may not be null
     * @param key the unique key for the node; may not be null
     * @param primaryType the primary type of the node; may not be null
     * @param mixinTypes the mixin types for the node; may not be null but may be empty
     * @param parent the key of the node's parent; may not be null
     * @param newPath the new path of the node after it was moved; may not be null
     * @param oldPath the old path of the node before it was moved; may not be null
     * @param reorderedBeforePath the path of the node before which the node was placed; null if it was moved to the end
     * @param snsPathChangesByNodeKey a map which may contain additional path changes if the reordering was performed on a SNS
     */
    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 ) {

    }

    protected void sequenced( String workspaceName,
                              NodeKey sequencedNodeKey,
                              Path sequencedNodePath,
                              Name sequencedNodePrimaryType,
                              Set<Name> sequencedNodeMixinTypes,
                              NodeKey outputNodeKey,
                              Path outputNodePath,
                              String outputPath,
                              String userId,
                              String selectedPath,
                              String sequencerName ) {

    }

    protected void sequenceFailure( String workspaceName,
                                    NodeKey sequencedNodeKey,
                                    Path sequencedNodePath,
                                    Name sequencedNodePrimaryType,
                                    Set<Name> sequencedNodeMixinTypes,
                                    String outputPath,
                                    String userId,
                                    String selectedPath,
                                    String sequencerName,
                                    Throwable cause ) {

    }

    /**
     * Handle the changing of the repository metadata information.
     */
    protected void repositoryMetadataChanged() {
    }

    /**
     * Handle the marking of a binary value as being unused.
     *
     * @param key the binary key of the binary value that is no longer used; may not be null
     */
    protected void binaryValueUnused( BinaryKey key ) {
    }

    /**
     * Handle the addition of a new workspace in the repository.
     *
     * @param workspaceName the name of the workspace that was added; may not be null
     */
    protected void addWorkspace( String workspaceName ) {
    }

    /**
     * Handle the removal of a workspace in the repository.
     *
     * @param workspaceName the name of the workspace that was removed; may not be null
     */
    protected void removeWorkspace( String workspaceName ) {
    }

}
