/*
 * Copyright (c) 2008-2014 MongoDB, Inc.
 * 
 * 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 me.lightspeed7.mongofs.gridfs;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Date;

import me.lightspeed7.mongofs.BufferedChunksOutputStream;
import me.lightspeed7.mongofs.FileChunksOutputStreamSink;
import me.lightspeed7.mongofs.InputFile;
import me.lightspeed7.mongofs.MongoFileConstants;
import me.lightspeed7.mongofs.util.BytesCopier;

import org.bson.types.ObjectId;
import org.mongodb.Document;
import org.mongodb.MongoCollection;

import com.mongodb.DBCollection;
import com.mongodb.MongoException;

/**
 * This class represents a GridFS file to be written to the database Operations include: - writing data obtained from an InputStream -
 * getting an OutputStream to stream the data out
 * 
 * @author David Buschman
 * @author Eliot Horowitz and Guy K. Kloss
 */
public class GridFSInputFile extends GridFSFile implements InputFile {

    private final InputStream inputStream;
    private final boolean closeStreamOnPersist;
    private boolean savedChunks = false;
    private OutputStream outputStream = null;

    /**
     * Default constructor setting the GridFS file name and providing an input stream containing data to be written to the file.
     * 
     * @param fs
     *            The GridFS connection handle.
     * @param in
     *            Stream used for reading data from.
     * @param filename
     *            Name of the file to be created.
     * @param closeStreamOnPersist
     *            indicate the passed in input stream should be closed once the data chunk persisted
     */
    protected GridFSInputFile(final GridFS fs, final InputStream in, final String filename, final boolean closeStreamOnPersist) {

        this.fs = fs;
        this.inputStream = in;
        this.filename = filename;
        this.closeStreamOnPersist = closeStreamOnPersist;

        this.id = new ObjectId();
        this.chunkSize = GridFS.DEFAULT_CHUNKSIZE;
        this.uploadDate = new Date();
    }

    private OutputStream generateOutputStream(final DBCollection collection) {

        GridFSInputFileAdapter adapter = new GridFSInputFileAdapter(this);

        FileChunksOutputStreamSink streamSink = new FileChunksOutputStreamSink(new MongoCollection<Document>(collection), this.id, adapter,
                null);

        BufferedChunksOutputStream stream = new BufferedChunksOutputStream(streamSink, this.chunkSize);
        return stream;
    }

    /**
     * Default constructor setting the GridFS file name and providing an input stream containing data to be written to the file.
     * 
     * @param fs
     *            The GridFS connection handle.
     * @param in
     *            Stream used for reading data from.
     * @param filename
     *            Name of the file to be created.
     */
    protected GridFSInputFile(final GridFS fs, final InputStream in, final String filename) {

        this(fs, in, filename, false);
    }

    /**
     * Constructor that only provides a file name, but does not rely on the presence of an {@link java.io.InputStream}. An
     * {@link java.io.OutputStream} can later be obtained for writing using the {@link #getOutputStream()} method.
     * 
     * @param fs
     *            The GridFS connection handle.
     * @param filename
     *            Name of the file to be created.
     */
    protected GridFSInputFile(final GridFS fs, final String filename) {

        this(fs, null, filename);
    }

    /**
     * Minimal constructor that does not rely on the presence of an {@link java.io.InputStream}. An {@link java.io.OutputStream} can later
     * be obtained for writing using the {@link #getOutputStream()} method.
     * 
     * @param fs
     *            The GridFS connection handle.
     */
    protected GridFSInputFile(final GridFS fs) {

        this(fs, null, null);
    }

    public void setId(final Object id) {

        this.id = id;
    }

    /**
     * Sets the file name on the GridFS entry.
     * 
     * @param filename
     *            File name.
     */
    public void setFilename(final String filename) {

        this.filename = filename;
    }

    /**
     * Sets the content type (MIME type) on the GridFS entry.
     * 
     * @param contentType
     *            Content type.
     */
    public void setContentType(final String contentType) {

        this.contentType = contentType;
    }

    /**
     * Set the chunk size. This must be called before saving any data.
     * 
     * @param chunkSize
     *            The size in bytes.
     */
    public void setChunkSize(final int chunkSize) {

        if (outputStream != null || savedChunks) {
            return;
        }
        this.chunkSize = chunkSize;
    }

    /**
     * calls save with the existing chunk size.
     * 
     * @throws MongoException
     */
    @Override
    public void save() {

        save(chunkSize);
    }

    /**
     * Internal use only!!
     */
    final void superSave() {

        super.save();
    }

    /**
     * This method first calls saveChunks(long) if the file data has not been saved yet. Then it persists the file entry to GridFS.
     * 
     * @param chunkSize
     *            Size of chunks for file in bytes.
     * @throws MongoException
     */
    public void save(final int chunkSize) {

        if (outputStream != null) {
            throw new MongoException("cannot mix OutputStream and regular save()");
        }

        // note that chunkSize only changes chunkSize in case we actually save chunks
        // otherwise there is a risk file and chunks are not compatible
        if (!savedChunks) {
            try {
                saveChunks(chunkSize);
            } catch (IOException ioe) {
                throw new MongoException("couldn't save chunks", ioe);
            }
        }

        super.save();
    }

    /**
     * Saves all data into chunks from configured {@link java.io.InputStream} input stream to GridFS.
     * 
     * @return Number of the next chunk.
     * @throws IOException
     *             on problems reading the new entry's {@link java.io.InputStream}.
     * @throws MongoException
     */
    public int saveChunks() throws IOException {

        return saveChunks(chunkSize);
    }

    /**
     * Saves all data into chunks from configured {@link java.io.InputStream} input stream to GridFS. A non-default chunk size can be
     * specified. This method does NOT save the file object itself, one must call save() to do so.
     * 
     * @param chunkSize
     *            Size of chunks for file in bytes.
     * @return Number of the next chunk.
     * @throws IOException
     *             on problems reading the new entry's {@link java.io.InputStream}.
     * @throws MongoException
     */
    public int saveChunks(final int chunkSize) throws IOException {

        if (outputStream != null) {
            throw new MongoException("Cannot mix OutputStream and regular save()");
        }
        if (savedChunks) {
            throw new MongoException("Chunks already saved!");
        }

        if (chunkSize <= 0) {
            throw new MongoException("chunkSize must be greater than zero");
        }

        if (this.chunkSize != chunkSize) {
            this.chunkSize = chunkSize;
        }

        // new stuff
        this.outputStream = generateOutputStream(fs.getChunksCollection());
        try {
            new BytesCopier(inputStream, this.outputStream, this.closeStreamOnPersist).transfer(false);
        } finally {
            this.outputStream.close();
        }

        // only write data, do not write file, in case one wants to change metadata
        return (int) this.getAsLong(MongoFileConstants.chunkCount.name());
    }

    public OutputStream getOutputStream() {

        if (outputStream == null) {
            outputStream = generateOutputStream(fs.getChunksCollection());
        }
        return outputStream;
    }

}
