/*
 * 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.api.sequencer;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import javax.jcr.NamespaceException;
import javax.jcr.NamespaceRegistry;
import javax.jcr.Node;
import javax.jcr.Property;
import javax.jcr.RepositoryException;
import org.modeshape.jcr.api.Logger;
import org.modeshape.jcr.api.nodetype.NodeTypeManager;

/**
 * A component that reads recently-changed content (often uploaded files) and extracts additional information from the content.
 * <p>
 * Each ModeShape repository can be configured with zero or more sequencers. Each sequencer is configured with a set of match
 * conditions that define the acceptable patterns for the paths of changed nodes, as well as a path specification that defines
 * where the derived (or generated) output should be placed. Then when clients change nodes with paths that satisfy the match
 * conditions, the repository will create a new Session and invoke the sequencer, which is then expected to process the changed
 * content and generate the derived information under the supplied parent node. The session will be saved automatically or, if an
 * exception is thrown, discard the changes and close the session.
 * </p>
 */
public abstract class Sequencer {

    private final UUID uuid = UUID.randomUUID();

    /**
     * The logger instance, set via reflection
     */
    private Logger logger;

    /**
     * The name of this sequencer, set via reflection
     */
    private String name;

    /**
     * The name of the repository that owns this sequencer, set via reflection
     */
    private String repositoryName;

    /**
     * The multiple path expressions for this sequencer, set via reflection
     */
    private Object[] pathExpressions;

    /**
     * The singular path expression of this sequencer, set via reflection
     */
    private String pathExpression;

    /**
     * The set of MIME types that this sequencer will process. Subclasses should set call
     * {@link #registerDefaultMimeTypes(String...)} in the no-arg constructor to set the default MIME types for the sequencer, but
     * the field may be overwritten in the sequencer's configuration by setting the "acceptedMimeTypes" field to an array of
     * string values.
     */
    private String[] acceptedMimeTypes = {};

    private Set<String> acceptedMimeTypesSet = null;

    private boolean initialized = false;

    /**
     * Return the unique identifier for this sequencer.
     * 
     * @return the unique identifier; never null
     */
    public final UUID getUniqueId() {
        return uuid;
    }

    /**
     * Get the name for this sequencer.
     * 
     * @return the name, or null if there is no description
     */
    public final String getName() {
        return name;
    }

    /**
     * Get the name of the repository.
     * 
     * @return the repository name; never null
     */
    public final String getRepositoryName() {
        return repositoryName;
    }

    /**
     * Obtain the path expressions as configured on the sequencer. This method always returns a copy to prevent modification of
     * the values.
     * 
     * @return the path expressions; never null but possibly empty
     */
    public final String[] getPathExpressions() {
        String pathExpression = this.pathExpression;
        Object[] pathExpressions = this.pathExpressions;
        if (pathExpression == null && (pathExpressions == null || pathExpressions.length == 0)) {
            // there's none ...
            return new String[] {};
        }
        if (pathExpression != null && (pathExpressions == null || pathExpressions.length == 0)) {
            // There's just one ...
            return new String[] {pathExpression};
        }
        List<String> expressions = new ArrayList<String>(pathExpressions.length + 1);
        addExpression(expressions, pathExpression);
        for (Object value : pathExpressions) {
            addExpression(expressions, value);
        }
        return expressions.toArray(new String[expressions.size()]);
    }

    private void addExpression( List<String> values,
                                Object value ) {
        assert !initialized : "No expressions can be added after the sequencer has been initialized";
        if (value instanceof String) {
            String str = (String)value;
            str = str.trim();
            if (str.length() != 0) {
                values.add(str);
            }
        }
    }

    /**
     * Initialize the sequencer. This is called automatically by ModeShape once for each Sequencer instance, and should not be
     * called by the sequencer.
     * <p>
     * By default this method does nothing, so it should be overridden by implementations to do a one-time initialization of any
     * internal components. For example, sequencers can use the supplied <code>registry</code> and <code>nodeTypeManager</code>
     * objects to register custom namesapces and node types required by the generated content.
     * </p>
     * 
     * @param registry the namespace registry that can be used to register custom namespaces; never null
     * @param nodeTypeManager the node type manager that can be used to register custom node types; never null
     * @throws RepositoryException if operations on the {@link NamespaceRegistry} or {@link NodeTypeManager} fail
     * @throws IOException if any stream based operations fail (like importing cnd files)
     */
    public void initialize( NamespaceRegistry registry,
                            NodeTypeManager nodeTypeManager ) throws RepositoryException, IOException {
        // Subclasses may not necessarily call 'super.initialize(...)', but if they do then we can make this assertion ...
        assert !initialized : "The Sequencer.initialize(...) method should not be called by subclasses; ModeShape has already (and automatically) initialized the Sequencer";
    }

    /**
     * Method called by the code calling {@link #initialize(NamespaceRegistry, NodeTypeManager)} (typically via reflection) to
     * signal that the initialize method is completed. See Sequencers.initialize() for details, and no this method is indeed used.
     */
    @SuppressWarnings( "unused" )
    private void postInitialize() {
        if (!initialized) {
            initialized = true;

            // ------------------------------------------------------------------------------------------------------------
            // Add any code here that needs to run after #initialize(...), which will be overwritten by subclasses
            // ------------------------------------------------------------------------------------------------------------

            // Make immutable the Set<String> of accepts MIME types ...
            acceptedMimeTypesSet = Collections.unmodifiableSet(getAcceptedMimeTypes());
        }
    }

    /**
     * Execute the sequencing operation on the specified property, which has recently been created or changed.
     * <p>
     * Each sequencer is expected to process the value of the property, extract information from the value, and write a structured
     * representation (in the form of a node or a subgraph of nodes) using the supplied output node. Note that the output node
     * will either be:
     * <ol>
     * <li>the selected node, in which case the sequencer was configured to generate the output information directly under the
     * selected input node; or</li>
     * <li>a newly created node in a different location than node being sequenced (in this case, the primary type of the new node
     * will be 'nt:unstructured', but the sequencer can easily change that using {@link Node#setPrimaryType(String)})</li>
     * </ol>
     * </p>
     * <p>
     * The implementation is expected to always clean up all resources that it acquired, even in the case of exceptions.
     * </p>
     * <p>
     * Note: This method <em>must</em> be threadsafe: ModeShape will likely invoke this method concurrently in separate threads,
     * and the method should never modify the state or fields of the Sequencer implementation class. All initialization should be
     * performed in {@link #initialize(NamespaceRegistry, NodeTypeManager)}.
     * </p>
     * 
     * @param inputProperty the property that was changed and that should be used as the input; never null
     * @param outputNode the node that represents the output for the derived information; never null, and will either be
     *        {@link Node#isNew() new} if the output is being placed outside of the selected node, or will not be new when the
     *        output is to be placed on the selected input node
     * @param context the context in which this sequencer is executing, and which may contain additional parameters useful when
     *        generating the output structure; never null
     * @return true if the sequencer's output should be saved, or false otherwise
     * @throws Exception if there was a problem with the sequencer that could not be handled. All exceptions will be logged
     *         automatically as errors by ModeShape.
     */
    public abstract boolean execute( Property inputProperty,
                                     Node outputNode,
                                     Context context ) throws Exception;

    @Override
    public String toString() {
        return repositoryName + " -> " + getClass().getName() + " uuid=" + uuid + (name != null ? (" : " + name) : "");
    }

    /**
     * Registers a namespace using the given {@link NamespaceRegistry}, if the namespace has not been previously registered.
     * 
     * @param namespacePrefix a non-null {@code String}
     * @param namespaceUri a non-null {@code String}
     * @param namespaceRegistry a {@code NamespaceRegistry} instance.
     * @return true if the namespace has been registered, or false if it was already registered
     * @throws RepositoryException if anything fails during the registration process
     */
    protected boolean registerNamespace( String namespacePrefix,
                                         String namespaceUri,
                                         NamespaceRegistry namespaceRegistry ) throws RepositoryException {
        if (namespacePrefix == null || namespaceUri == null) {
            throw new IllegalArgumentException("Neither the namespace prefix, nor the uri should be null");
        }
        try {
            // if the call succeeds, means it was previously registered
            namespaceRegistry.getPrefix(namespaceUri);
            return false;
        } catch (NamespaceException e) {
            // namespace not registered yet
            namespaceRegistry.registerNamespace(namespacePrefix, namespaceUri);
            return true;
        }
    }

    /**
     * Registers node types from a CND file, using the given {@link NodeTypeManager}. Any namespaces defined in the CND file will
     * be automatically registered as well.
     * 
     * @param cndFile the relative path to the cnd file, which is loaded using via {@link Class#getResourceAsStream(String)}
     * @param nodeTypeManager the node type manager with which the cnd will be registered
     * @param allowUpdate a boolean which indicates whether updates on existing node types are allowed or no. See
     *        {@link NodeTypeManager#registerNodeType(javax.jcr.nodetype.NodeTypeDefinition, boolean)}
     * @throws RepositoryException if anything fails
     * @throws IOException if any stream related operations fail
     */
    protected void registerNodeTypes( String cndFile,
                                      NodeTypeManager nodeTypeManager,
                                      boolean allowUpdate ) throws RepositoryException, IOException {
        InputStream cndStream = getClass().getResourceAsStream(cndFile);
        registerNodeTypes(cndStream, nodeTypeManager, allowUpdate);
    }

    /**
     * See {@link Sequencer#registerNodeTypes(String, org.modeshape.jcr.api.nodetype.NodeTypeManager, boolean)}
     * 
     * @param cndStream the input stream containing the CND file; may not be null
     * @param nodeTypeManager the node type manager with which the node types in the CND file should be registered; may not be
     *        null
     * @param allowUpdate a boolean which indicates whether updates on existing node types are allowed or no. See
     *        {@link NodeTypeManager#registerNodeType(javax.jcr.nodetype.NodeTypeDefinition, boolean)}
     * @throws RepositoryException if anything fails
     * @throws IOException if any stream related operations fail
     */
    protected void registerNodeTypes( InputStream cndStream,
                                      NodeTypeManager nodeTypeManager,
                                      boolean allowUpdate ) throws RepositoryException, IOException {
        if (cndStream == null) {
            throw new IllegalArgumentException("The stream to the given cnd file is null");
        }
        nodeTypeManager.registerNodeTypes(cndStream, allowUpdate);
    }

    protected final Logger getLogger() {
        return logger;
    }

    /**
     * Set the MIME types that are accepted by default, if there are any. This method should be called from the
     * {@link #initialize(NamespaceRegistry, NodeTypeManager)} method in the subclass.
     * <p>
     * This method can be called more than once to add additional mime types.
     * </p>
     * 
     * @param mimeTypes the array of MIME types that are accepted by this sequencer
     * @see #isAccepted(String)
     */
    protected final void registerDefaultMimeTypes( String... mimeTypes ) {
        assert !initialized : "No default MIME types can be registered after the sequencer has been initialized";
        if (mimeTypes != null && mimeTypes.length != 0 && acceptedMimeTypes.length == 0) {
            // There are no overridden mime types, so we can register the default MIME types ...
            if (acceptedMimeTypesSet == null) acceptedMimeTypesSet = new HashSet<String>();
            for (String mimeType : mimeTypes) {
                if (mimeType == null) continue;
                mimeType = mimeType.trim();
                if (mimeType.length() == 0) continue;
                acceptedMimeTypesSet.add(mimeType);
            }
        }
    }

    /**
     * Utility method to obtain the set of accepted MIME types. The resulting set will either be those set by default in the
     * subclass' overridden {@link #initialize(NamespaceRegistry, NodeTypeManager)} method or the MIME types explicitly set in the
     * sequencers configuration.
     * 
     * @return the set of MIME types that are accepted by this Sequencer instance; never null but possibly empty if this Sequencer
     *         instance accepts all MIME types
     */
    protected final Set<String> getAcceptedMimeTypes() {
        if (acceptedMimeTypesSet == null) {
            // No defaults are registered, so use those non-defaults ...
            acceptedMimeTypesSet = new HashSet<String>();
            for (String mimeType : acceptedMimeTypes) {
                if (mimeType == null) continue;
                mimeType = mimeType.trim();
                if (mimeType.length() == 0) continue;
                acceptedMimeTypesSet.add(mimeType);
            }
        }
        return acceptedMimeTypesSet;
    }

    /**
     * Determine if this sequencer requires the content to have a specific MIME type
     * 
     * @return true if this sequencer can only process certain MIME types, or false if there are no restrictions
     */
    public final boolean hasAcceptedMimeTypes() {
        return !getAcceptedMimeTypes().isEmpty();
    }

    /**
     * Determine if this sequencer has been configured to accept and process content with the supplied MIME type.
     * 
     * @param mimeType the MIME type
     * @return true if content with the supplied the MIME type is to be processed (or when <code>mimeType</code> is null and
     *         therefore not known), or false otherwise
     * @see #hasAcceptedMimeTypes()
     */
    public final boolean isAccepted( String mimeType ) {
        if (mimeType != null && hasAcceptedMimeTypes()) {
            return getAcceptedMimeTypes().contains(mimeType.trim());
        }
        return true; // accept all mime types
    }

    /**
     * The sequencer context represents the complete context of a sequencer invocation. Currently, this information includes the
     * current time of sequencer execution.
     */
    public interface Context {

        /**
         * Get the timestamp of the sequencing. This is always the timestamp of the change event that is being processed.
         * 
         * @return timestamp the "current" timestamp; never null
         */
        Calendar getTimestamp();

        /**
         * Returns a {@link org.modeshape.jcr.api.ValueFactory} instance which can be used to perform additional type conversions,
         * from what {@link javax.jcr.ValueFactory} offers
         * 
         * @return a non-null value factory, using the output node's session as context
         */
        org.modeshape.jcr.api.ValueFactory valueFactory();
    }

}
