package eu.fbk.knowledgestore.filestore;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.zip.Deflater;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

import javax.annotation.Nullable;

import com.google.common.base.Function;
import com.google.common.base.MoreObjects;
import com.google.common.base.Preconditions;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import eu.fbk.knowledgestore.data.Data;
import eu.fbk.knowledgestore.data.Stream;

/**
 * A {@code FileStore} decorator that GZIPs all the compressible files written to it.
 * <p>
 * A {@code GzippedFileStore} intercepts reads and writes to an underlying {@code FileStore},
 * respectively applying GZIP compression and decompression to stored files in case they are
 * compressible. Compression level and size of buffer used for compression / decompression can be
 * configured by the user. Whether a file can be compressed is detected starting from its
 * extension and the matching Internet MIME type using the facilities of {@link Data}. In case
 * compression is applied, the name of the stored file is changed adding a {@code .gz} suffix.
 * </p>
 */
public final class GzippedFileStore extends ForwardingFileStore {

    private static final Logger LOGGER = LoggerFactory.getLogger(GzippedFileStore.class);

    private static final int DEFAULT_COMPRESSION_LEVEL = Deflater.DEFAULT_COMPRESSION;

    private static final int DEFAULT_BUFFER_SIZE = 512;

    private static final boolean DEFAULT_FORCE_COMPRESSION = true;

    private final FileStore delegate;

    private final int compressionLevel;

    private final int bufferSize;

    private final boolean forceCompression;

    /**
     * Creates a new instance wrapping the {@code FileStore} supplied and using the default
     * compression level and buffer size.
     *
     * @param delegate
     *            the wrapped {@code FileStore}
     * @see #GzippedFileStore(FileStore, Integer, Integer)
     */
    public GzippedFileStore(final FileStore delegate) {
        this(delegate, null, null, null);
    }

    /**
     * Creates a new instance wrapping the {@code FileStore} supplied and using the specified
     * compression level.
     *
     * @param delegate
     *            the wrapped {@code FileStore}, not null
     * @param compressionLevel
     *            the desired compression level for compressible files, on a 0-9 scale (from no
     *            compression to best compression) or -1 for default compression; if null defaults
     *            to -1
     * @param bufferSize
     *            the size of the buffer used by the GZIP inflaters / deflaters; if null defaults
     *            to 512
     * @param forceCompression
     *            if true, supplied files are always compressed independently of their (detected)
     *            MIME type; if null defaults to false
     */
    public GzippedFileStore(final FileStore delegate, @Nullable final Integer compressionLevel,
            @Nullable final Integer bufferSize, @Nullable final Boolean forceCompression) {

        Preconditions.checkNotNull(delegate);
        Preconditions.checkArgument(compressionLevel == null
                || compressionLevel == Deflater.DEFAULT_COMPRESSION //
                || compressionLevel >= Deflater.BEST_SPEED
                && compressionLevel <= Deflater.BEST_COMPRESSION);
        Preconditions.checkArgument(bufferSize > 0);

        this.delegate = Preconditions.checkNotNull(delegate);
        this.compressionLevel = MoreObjects.firstNonNull(compressionLevel,
                DEFAULT_COMPRESSION_LEVEL);
        this.bufferSize = MoreObjects.firstNonNull(bufferSize, DEFAULT_BUFFER_SIZE);
        this.forceCompression = MoreObjects.firstNonNull(forceCompression,
                DEFAULT_FORCE_COMPRESSION);

        LOGGER.info("GZippedFileStore configured, compression={}, buffer={}", compressionLevel,
                bufferSize);
    }

    @Override
    protected FileStore delegate() {
        return this.delegate;
    }

    @Override
    public InputStream read(final String filename) throws FileMissingException, IOException {
        final String internalFilename = toInternalFilename(filename);
        if (internalFilename.equals(filename)) {
            return super.read(filename);
        } else {
            return new GZIPInputStream(super.read(internalFilename), this.bufferSize);
        }
    }

    @Override
    public OutputStream write(final String filename) throws FileExistsException, IOException {
        final String internalFilename = toInternalFilename(filename);
        if (internalFilename.equals(filename)) {
            return super.write(filename);
        } else {
            return new GZIPOutputStream(super.write(internalFilename), this.bufferSize) {

                {
                    this.def.setLevel(GzippedFileStore.this.compressionLevel);
                }

            };
        }
    }

    @Override
    public void delete(final String filename) throws FileMissingException, IOException {
        super.delete(toInternalFilename(filename));
    }

    @Override
    public Stream<String> list() throws IOException {
        return super.list().transform(new Function<String, String>() {

            @Override
            public String apply(final String filename) {
                return toExternalFilename(filename);
            }

        }, 0);
    }

    private String toInternalFilename(final String filename) {
        return this.forceCompression || Data.isMimeTypeCompressible( //
                Data.extensionToMimeType(filename)) ? filename + ".gz" : filename;
    }

    private String toExternalFilename(final String filename) {
        return filename.endsWith(".gz") ? filename.substring(0, filename.length() - 3) : filename;
    }

}
