/*
 * 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.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import javax.jcr.RepositoryException;
import org.modeshape.common.annotation.ThreadSafe;
import org.modeshape.common.logging.Logger;
import org.modeshape.common.util.CheckArg;
import org.modeshape.common.util.StringUtil;
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;

/**
 * An abstract class for a {@link BinaryStore}, with common functionality needed by implementation classes.
 */
@ThreadSafe
public abstract class AbstractBinaryStore implements BinaryStore {

    /**
     * The default minimum size (in bytes) of binary values that are persisted in the binary store is 4096 bytes, or 4KB.
     */
    static final long DEFAULT_MINIMUM_BINARY_SIZE_IN_BYTES = 1024 * 4;

    private static final long DEFAULT_LATCH_WAIT_IN_SECONDS = 10L;

    private static final long LARGE_SIZE = 1 << 25; // 32MB
    private static final long MEDIUM_FILE_SIZE = 1 << 20; // 1MB
    private static final long SMALL_FILE_SIZE = 1 << 15; // 32K
    private static final long TINY_FILE_SIZE = 1 << 10; // 1K

    private static final int LARGE_BUFFER_SIZE = 1 << 20; // 1MB
    protected static final int MEDIUM_BUFFER_SIZE = 1 << 16; // 64K
    private static final int SMALL_BUFFER_SIZE = 1 << 12; // 4K
    private static final int TINY_BUFFER_SIZE = 1 << 11; // 2K

    protected final Logger logger = Logger.getLogger(getClass());
    private final AtomicLong minBinarySizeInBytes = new AtomicLong(DEFAULT_MINIMUM_BINARY_SIZE_IN_BYTES);

    private volatile TextExtractors extractors;
    private volatile MimeTypeDetector detector = NullMimeTypeDetector.INSTANCE;

    /**
     * Given a number of bytes representing the length of a file, returns the optimum size for a buffer that should be used
     * when reading/working with that file
     *
     * @param fileSize the size of a file, in bytes
     * @return the size of a read/write buffer
     */
    public static int bestBufferSize( long fileSize ) {
        assert fileSize >= 0;
        if (fileSize < TINY_FILE_SIZE) {
            return (int)fileSize + 2;
        }
        if (fileSize < SMALL_FILE_SIZE) {
            return TINY_BUFFER_SIZE;
        }
        if (fileSize < MEDIUM_FILE_SIZE) {
            return SMALL_BUFFER_SIZE;
        }
        if (fileSize < LARGE_SIZE) {
            return MEDIUM_BUFFER_SIZE;
        }
        return LARGE_BUFFER_SIZE;
    }


    @Override
    public long getMinimumBinarySizeInBytes() {
        return minBinarySizeInBytes.get();
    }

    @Override
    public void setMinimumBinarySizeInBytes( long minSizeInBytes ) {
        CheckArg.isNonNegative(minSizeInBytes, "minSizeInBytes");
        minBinarySizeInBytes.set(minSizeInBytes);
    }

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

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

    @Override
    public final String getText( BinaryValue binary ) throws BinaryStoreException {
        // try and locate an already extracted text from the store
        if (binary instanceof StoredBinaryValue) {
            String extractedText = getExtractedText(binary);
            if (extractedText != null) {
                return extractedText;
            }
        }

        // there isn't extracted text stored, so try to extract text if extraction is enabled
        if (extractors == null || !extractors.extractionEnabled()) {
            return null;
        }

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

        // there isn't any text available, so wait for a job to finish and then return the result
        try {
            CountDownLatch latch = extractors.getWorkerLatch(binary.getKey(), false);
            if (latch == null) {
                // There is no latch, so just compute the text here ...
                latch = extractors.extract(this, binary, new TextExtractorContext(detector()));
            }
            // There was a latch, so wait till the work is done ...
            if (latch != null && latch.await(DEFAULT_LATCH_WAIT_IN_SECONDS, TimeUnit.SECONDS)) {
                return getExtractedText(binary);
            }
            // Stopped waiting ...
            return null;
        } catch (InterruptedException e) {
            throw new BinaryStoreException(e);
        }
    }

    @Override
    public String getMimeType( BinaryValue binary,
                               String name ) throws IOException, RepositoryException {
        
        if (detector == NullMimeTypeDetector.INSTANCE) {
            // no mime type detection is explicitly configured, so just return
            return null;
        }

        if (binary instanceof StoredBinaryValue) {
            String storedMimeType = getStoredMimeType(binary);
            if (!StringUtil.isBlank(storedMimeType)) {
                return storedMimeType;
            }
        }
        
        String detectedMimeType = detector().mimeTypeOf(name, binary);
        if (binary instanceof InMemoryBinaryValue) {
            return detectedMimeType;
        } else if (!StringUtil.isBlank(detectedMimeType)) {
            storeMimeType(binary, detectedMimeType);
        }
        return detectedMimeType;
    }

    @Override
    public boolean hasBinary( BinaryKey key ) {
        try {
            InputStream is = getInputStream(key);
            is.close();
        } catch (BinaryStoreException e) {
            return false;
        } catch (IOException e) {
            return false;
        }

        return true;
    }

    /**
     * Returns the stored mime-type of a binary value.
     *
     * @param binaryValue a {@code non-null} {@link BinaryValue}
     * @return either a non-empty {@code String} if a stored mimetype exists, or {@code null} if such a value doesn't exist yet.
     * @throws BinaryStoreException if there's a problem accessing the binary store or if the binary value cannot be found in the
     * store
     */
    protected abstract String getStoredMimeType( BinaryValue binaryValue ) throws BinaryStoreException;

    /**
     * Stores the given mime-type for a binary value.
     *
     * @param binaryValue a {@code non-null} {@link BinaryValue}
     * @param mimeType a non-empty {@code String}
     * @throws BinaryStoreException if there's a problem accessing the binary store
     */
    protected abstract void storeMimeType( BinaryValue binaryValue,
                                           String mimeType ) throws BinaryStoreException;

    /**
     * Stores the extracted text of a binary value into this store.
     *
     * @param source a {@code non-null} {@link BinaryValue} instance from which the text was extracted
     * @param extractedText a {@code non-null} and {@code non-blank} string representing the extracted text
     * @throws BinaryStoreException if the operation fails or if the extracted text cannot be stored for the given binary value
     * (regardless of the reason)
     */
    public abstract void storeExtractedText( BinaryValue source,
                                             String extractedText ) throws BinaryStoreException;

    /**
     * Returns the extracted text of a binary value, or {@code null} if such text hasn't been stored previously (but the binary
     * value can be found in the store)
     *
     * @param source a {@code non-null} {@link BinaryValue} instance from which the text was extracted
     * @return a {@code String} representing the extracted text, or {@code null} if such text hasn't been stored in this store
     *         previously.
     * @throws BinaryStoreException if the binary value cannot be found in the store.
     */
    public abstract String getExtractedText( BinaryValue source ) throws BinaryStoreException;

    /**
     * Get the text extractor that can be used to extract text by this store.
     *
     * @return the text extractor; never null
     */
    protected final TextExtractors extractors() {
        return this.extractors;
    }

    /**
     * Get the MIME type detector that can be used to find the MIME type for binary content
     *
     * @return the detector; never null
     */
    protected final MimeTypeDetector detector() {
        return detector;
    }

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

    @Override
    public void start() {
        //does nothing by default
    }

    @Override
    public void shutdown() {
        //does nothing by default
    }
}
