/*
 * 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.value.binary;

import java.io.IOException;
import java.io.InputStream;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import javax.jcr.RepositoryException;
import org.modeshape.common.collection.Collections;
import org.modeshape.common.logging.Logger;
import org.modeshape.common.util.CheckArg;
import org.modeshape.jcr.JcrI18n;
import org.modeshape.jcr.TextExtractors;
import org.modeshape.jcr.mimetype.MimeTypeDetector;
import org.modeshape.jcr.mimetype.NullMimeTypeDetector;
import org.modeshape.jcr.text.TextExtractorContext;
import org.modeshape.jcr.value.BinaryKey;
import org.modeshape.jcr.value.BinaryValue;

/**
 * A {@link BinaryStore} implementation that stores files in other BinaryStores. This store is initialized with a map of number of
 * BinaryStores. On retrieval, the CompositeBinaryStore will look in all the other BinaryStores for the value. When storing a
 * value, the CompositeBinaryStore may receive a StorageHint that MAY be used when determining which named BinaryStore to write
 * to. If a storage hint is not provided (or doesn't match a store), the value will be stored in the default store.
 */
public class CompositeBinaryStore implements BinaryStore {

    private static final String DEFAULT_STRATEGY_HINT = "default";
    private volatile TextExtractors extractors;
    private volatile MimeTypeDetector detector = NullMimeTypeDetector.INSTANCE;

    protected Logger logger = Logger.getLogger(getClass());

    private Map<String, BinaryStore> namedStores;
    private BinaryStore defaultBinaryStore;

    /**
     * Initialize a new CompositeBinaryStore using a Map of other BinaryKeys that are keyed by an implementer-provided key. The
     * named stores must include a default BinaryStore that will be used in the absence of storage hints.
     * 
     * @param namedStores a {@code Map} of inner stores, grouped by the hint.
     */
    public CompositeBinaryStore( Map<String, BinaryStore> namedStores ) {
        this.namedStores = namedStores;
        this.defaultBinaryStore = null;
    }

    /**
     * Initialize the store, and initialize all the named stores.
     */
    @Override
    public void start() {
        Iterator<Map.Entry<String, BinaryStore>> it = getNamedStoreIterator();

        while (it.hasNext()) {
            BinaryStore bs = it.next().getValue();
            bs.start();
        }

    }

    /**
     * Shut down all the named stores
     */
    @Override
    public void shutdown() {
        Iterator<Map.Entry<String, BinaryStore>> it = getNamedStoreIterator();

        while (it.hasNext()) {
            BinaryStore bs = it.next().getValue();
            bs.shutdown();
        }
    }

    @Override
    public long getMinimumBinarySizeInBytes() {
        long minimumBinarySize = Long.MAX_VALUE;

        Iterator<Map.Entry<String, BinaryStore>> it = getNamedStoreIterator();

        while (it.hasNext()) {
            BinaryStore bs = it.next().getValue();
            if (minimumBinarySize > bs.getMinimumBinarySizeInBytes()) {
                minimumBinarySize = bs.getMinimumBinarySizeInBytes();
            }
        }

        return minimumBinarySize;
    }

    @Override
    public void setMinimumBinarySizeInBytes( long minSizeInBytes ) {

        Iterator<Map.Entry<String, BinaryStore>> it = getNamedStoreIterator();

        while (it.hasNext()) {
            BinaryStore bs = it.next().getValue();
            bs.setMinimumBinarySizeInBytes(minSizeInBytes);
        }
    }

    @Override
    public void setTextExtractors( TextExtractors textExtractors ) {
        CheckArg.isNotNull(textExtractors, "textExtractors");
        this.extractors = textExtractors;

        Iterator<Map.Entry<String, BinaryStore>> it = getNamedStoreIterator();
        while (it.hasNext()) {
            BinaryStore bs = it.next().getValue();
            bs.setTextExtractors(textExtractors);
        }
    }

    @Override
    public void setMimeTypeDetector( MimeTypeDetector mimeTypeDetector ) {
        CheckArg.isNotNull(mimeTypeDetector, "mimeTypeDetector");
        this.detector = mimeTypeDetector;

        Iterator<Map.Entry<String, BinaryStore>> it = getNamedStoreIterator();

        while (it.hasNext()) {
            BinaryStore bs = it.next().getValue();
            bs.setMimeTypeDetector(mimeTypeDetector);
        }
    }

    @Override
    public BinaryValue storeValue( InputStream stream, boolean markAsUnused ) throws BinaryStoreException {
        return storeValue(stream, DEFAULT_STRATEGY_HINT, markAsUnused);
    }

    @Override
    public BinaryValue storeValue( InputStream stream, String hint, boolean markAsUnused ) throws BinaryStoreException {
        BinaryStore binaryStore = selectBinaryStore(hint);
        BinaryValue bv = binaryStore.storeValue(stream, markAsUnused);
        logger.debug("Stored binary " + bv.getKey() + " into binary store " + binaryStore + " used=" + markAsUnused);
        return bv;
    }

    /**
     * Move a value from one named store to another store
     * 
     * @param key Binary key to transfer from the source store to the destination store
     * @param source a hint for discovering the source repository; may be null
     * @param destination a hint for discovering the destination repository
     * @return the {@link BinaryKey} value of the moved binary, never {@code null}
     * @throws BinaryStoreException if a source store cannot be found or the source store does not contain the binary key
     */
    public BinaryKey moveValue( BinaryKey key,
                                String source,
                                String destination ) throws BinaryStoreException {
        final BinaryStore sourceStore;

        if (source == null) {
            sourceStore = findBinaryStoreContainingKey(key);
        } else {
            sourceStore = selectBinaryStore(source);
        }

        // could not find source store, or
        if (sourceStore == null || !sourceStore.hasBinary(key)) {
            throw new BinaryStoreException(JcrI18n.unableToFindBinaryValue.text(key, sourceStore));
        }

        BinaryStore destinationStore = selectBinaryStore(destination);

        // key is already in the destination store
        if (sourceStore.equals(destinationStore)) {
            return key;
        }

        final BinaryValue binaryValue = storeValue(sourceStore.getInputStream(key), destination, false);
        sourceStore.markAsUnused(java.util.Collections.singleton(key));

        return binaryValue.getKey();
    }

    /**
     * Move a BinaryKey to a named store
     * 
     * @param key Binary key to transfer from the source store to the destination store
     * @param destination a hint for discovering the destination repository
     * @throws BinaryStoreException if anything unexpected fails
     */
    public void moveValue( BinaryKey key,
                           String destination ) throws BinaryStoreException {
        moveValue(key, null, destination);
    }

    @Override
    public InputStream getInputStream( BinaryKey key ) throws BinaryStoreException {
        Iterator<Map.Entry<String, BinaryStore>> it = getNamedStoreIterator();

        while (it.hasNext()) {
            final Map.Entry<String, BinaryStore> entry = it.next();

            final String binaryStoreKey = entry.getKey();

            BinaryStore binaryStore = entry.getValue();
            logger.trace("Checking binary store " + binaryStoreKey + " for key " + key);
            try {
                return binaryStore.getInputStream(key);
            } catch (BinaryStoreException e) {
                // this exception is "normal", and is thrown
                logger.trace(e, "The named store " + binaryStoreKey + " raised exception");
            }
        }

        throw new BinaryStoreException(JcrI18n.unableToFindBinaryValue.text(key, this.toString()));
    }

    @Override
    public boolean hasBinary( BinaryKey key ) {
        Iterator<Map.Entry<String, BinaryStore>> it = getNamedStoreIterator();

        while (it.hasNext()) {
            BinaryStore bs = it.next().getValue();
            if (bs.hasBinary(key)) {
                return true;
            }
        }

        return false;
    }

    @SuppressWarnings( "unused" )
    @Override
    public void markAsUnused( Iterable<BinaryKey> keys ) throws BinaryStoreException {
        Iterator<Map.Entry<String, BinaryStore>> it = getNamedStoreIterator();

        while (it.hasNext()) {
            Map.Entry<String, BinaryStore> entry = it.next();

            final String binaryStoreKey = entry.getKey();
            BinaryStore bs = entry.getValue();

            try {
                bs.markAsUnused(keys);
            } catch (BinaryStoreException e) {
                logger.debug(e, "The named store " + binaryStoreKey + " raised exception");
            }
        }
    }

    @Override
    public void markAsUsed( Iterable<BinaryKey> keys ) {
        Iterator<Map.Entry<String, BinaryStore>> it = getNamedStoreIterator();

        while (it.hasNext()) {
            Map.Entry<String, BinaryStore> entry = it.next();

            final String binaryStoreKey = entry.getKey();
            BinaryStore bs = entry.getValue();

            try {
                bs.markAsUsed(keys);
            } catch (BinaryStoreException e) {
                logger.debug(e, "The named store " + binaryStoreKey + " raised exception");
            }
        }
    }

    @SuppressWarnings( "unused" )
    @Override
    public void removeValuesUnusedLongerThan( long minimumAge,
                                              TimeUnit unit ) throws BinaryStoreException {
        Iterator<Map.Entry<String, BinaryStore>> it = getNamedStoreIterator();

        while (it.hasNext()) {
            Map.Entry<String, BinaryStore> entry = it.next();

            final String binaryStoreKey = entry.getKey();
            BinaryStore bs = entry.getValue();

            try {
                bs.removeValuesUnusedLongerThan(minimumAge, unit);
            } catch (BinaryStoreException e) {
                logger.debug(e, "The named store " + binaryStoreKey + " raised exception");
            }
        }
    }

    @Override
    public String getText( BinaryValue binary ) throws BinaryStoreException {

        if (binary instanceof InMemoryBinaryValue) {
            if (extractors == null || !extractors.extractionEnabled()) {
                return null;
            }

            // The extracted text will never be stored, so try directly using the text extractors ...
            return extractors.extract((InMemoryBinaryValue)binary, new TextExtractorContext(detector));
        }

        Iterator<Map.Entry<String, BinaryStore>> it = getNamedStoreIterator();

        while (it.hasNext()) {
            Map.Entry<String, BinaryStore> entry = it.next();

            final String binaryStoreKey = entry.getKey();
            BinaryStore bs = entry.getValue();
            try {
                if (bs.hasBinary(binary.getKey())) {
                    return bs.getText(binary);
                }
            } catch (BinaryStoreException e) {
                logger.debug(e, "The named store " + binaryStoreKey + " raised exception");
                if (!it.hasNext()) {
                    throw e;
                }
            }
        }

        throw new BinaryStoreException(JcrI18n.unableToFindBinaryValue.text(binary.getKey(), this));
    }

    @Override
    public String getMimeType( BinaryValue binary,
                               String name ) throws IOException, RepositoryException {
        if (detector == null) {
            return null;
        }

        String detectedMimeType = detector.mimeTypeOf(name, binary);
        if (binary instanceof InMemoryBinaryValue) {
            return detectedMimeType;
        }

        Iterator<Map.Entry<String, BinaryStore>> it = getNamedStoreIterator();

        while (it.hasNext()) {
            Map.Entry<String, BinaryStore> entry = it.next();

            final String binaryStoreKey = entry.getKey();
            BinaryStore bs = entry.getValue();

            try {
                if (bs.hasBinary(binary.getKey())) {
                    return bs.getMimeType(binary, name);
                }
            } catch (BinaryStoreException e) {
                logger.debug(e, "The named store " + binaryStoreKey + " raised exception");
                if (!it.hasNext()) {
                    throw e;
                }
            }
        }

        throw new BinaryStoreException(JcrI18n.unableToFindBinaryValue.text(binary.getKey(), this));
    }

    @Override
    public Iterable<BinaryKey> getAllBinaryKeys() throws BinaryStoreException {

        Iterable<BinaryKey> generatedIterable = new HashSet<BinaryKey>();
        Iterator<Map.Entry<String, BinaryStore>> binaryStoreIterator = getNamedStoreIterator();

        while (binaryStoreIterator.hasNext()) {
            BinaryStore bs = binaryStoreIterator.next().getValue();

            generatedIterable = Collections.concat(generatedIterable, bs.getAllBinaryKeys());
        }

        return generatedIterable;
    }

    /**
     * Get an iterator over all the named stores
     * 
     * @return an iterator over the map of binary stores and their given names
     */
    public Iterator<Map.Entry<String, BinaryStore>> getNamedStoreIterator() {
        return namedStores.entrySet().iterator();
    }

    /**
     * Get the named binary store that contains the key
     * 
     * @param key the key to the binary content; never null
     * @return the BinaryStore that contains the given key
     */
    public BinaryStore findBinaryStoreContainingKey( BinaryKey key ) {
        Iterator<Map.Entry<String, BinaryStore>> binaryStoreIterator = getNamedStoreIterator();

        while (binaryStoreIterator.hasNext()) {
            BinaryStore bs = binaryStoreIterator.next().getValue();
            if (bs.hasBinary(key)) {
                return bs;
            }
        }

        return null;
    }

    /**
     * Select a named binary store for the given hint
     * 
     * @param hint a hint to a binary store; possibly null
     * @return a named BinaryStore from the hint, or the default store
     */
    private BinaryStore selectBinaryStore( String hint ) {

        BinaryStore namedBinaryStore = null;

        if (hint != null) {
            logger.trace("Selecting named binary store for hint: " + hint);
            namedBinaryStore = namedStores.get(hint);
        }

        if (namedBinaryStore == null) {
            namedBinaryStore = getDefaultBinaryStore();
        }

        logger.trace("Selected binary store: " + namedBinaryStore.toString());

        return namedBinaryStore;
    }

    private BinaryStore getDefaultBinaryStore() {
        if (defaultBinaryStore == null) {
            if (namedStores.containsKey(DEFAULT_STRATEGY_HINT)) {
                defaultBinaryStore = namedStores.get(DEFAULT_STRATEGY_HINT);
            } else {
                logger.trace("Did not find a named binary store with the key 'default', picking the first binary store in the list");
                final Iterator<BinaryStore> iterator = namedStores.values().iterator();

                if (iterator.hasNext()) {
                    defaultBinaryStore = iterator.next();
                }
            }
        }

        return defaultBinaryStore;
    }

}
