/*
 * This file is part of muCommander, http://www.mucommander.com
 *
 * muCommander is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * muCommander is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.mucommander.io.backup;

import com.mucommander.commons.file.AbstractFile;
import com.mucommander.commons.file.FileFactory;

import java.io.File;
import java.io.IOException;
import java.io.OutputStream;

/**
 * Saves file in as crash-safe a manner as possible.
 * <p>
 * In order to prevent system or muCommander failures to corrupt configuration files,
 * the BackupOutputStream implements the following algorithm:
 * <ul>
 *   <li>Write its content to a backup file instead of the requested file</li>
 *   <li>When close is called, copy the content of the backup file over the original file</li>
 * </ul>
 * This way, if a crash was to happen while configuration files are being saved, either of the
 * following will happen:
 * <ul>
 *   <li>
 *     The backup file is not properly saved, but the original configuration is left untouched.
 *     We have lost <i>some</i> information (modifications since last save) but not <i>all</i>.
 *   </li>
 *   <li>
 *     The original file is not properly saved, but the backup file is correct. This is easy to check,
 *     as the backup and original file should always have the same size. If they don't, then the backup
 *     file should be used rather than the original one.
 *   </li>
 * </ul>
 * </p>
 * <p>
 * Files that have been saved by this class should be read with {@link BackupInputStream}
 * in order to make sure that an uncorrupt version of them is loaded.
 * </p>
 * <p>
 * The <code>BackupOutputStream</code> monitors all of its own I/O operations. If an error occurs, then the backup
 * operation will not be performed when {@link #close()} is called. It's possible to force the backup operation by
 * using the {@link #close(boolean)} method.
 * </p>
 * @see    BackupInputStream
 * @author Nicolas Rinaudo
 */
public class BackupOutputStream extends OutputStream implements BackupConstants {
    // - Instance fields --------------------------------------------------------
    // --------------------------------------------------------------------------
    /** The underlying OutputStream */
    private OutputStream out;
    /** Path of the original file. */
    private AbstractFile     target;
    /** Path to the backup file. */
    private AbstractFile     backup;
    /** Whether or not an error occured while writing to the backup file. */
    private boolean          error;



    // - Initialisation ---------------------------------------------------------
    // --------------------------------------------------------------------------
    /**
     * Opens a backup output stream on the specified file.
     * @param     file        file on which to open a backup output stream.
     * @exception IOException thrown if any IO error occurs.
     */
    public BackupOutputStream(File file) throws IOException {this(FileFactory.getFile(file.getAbsolutePath()));}

    /**
     * Opens a backup output stream on the specified file.
     * @param     file        file on which to open a backup output stream.
     * @exception IOException thrown if any IO error occurs.
     */
    public BackupOutputStream(String file) throws IOException {this(FileFactory.getFile((new File(file)).getAbsolutePath()));}

    /**
     * Opens a backup output stream on the specified file.
     * @param     file        file on which to open a backup output stream.
     * @exception IOException thrown if any IO error occurs.
     */
    public BackupOutputStream(AbstractFile file) throws IOException {this(file, FileFactory.getFile(file.getAbsolutePath() + BACKUP_SUFFIX));}

    /**
     * Opens an output stream on the specified file using the specified backup file.
     * @param     file        file on which to open the backup output stream.
     * @param     save        file that will be used for backup.
     * @exception IOException thrown if any IO error occurs.
     */
    private BackupOutputStream(AbstractFile file, AbstractFile save) throws IOException {
        out = save.getOutputStream();
        target = file;
        backup = save;
    }



    // - Error catching ---------------------------------------------------------
    // --------------------------------------------------------------------------
    /**
     * Flushes this output stream and forces any buffered output bytes to be written out to the stream.
     * <p>
     * This method calls the <code>flush()</code> method of its underlying output stream.
     * </p>
     * <p>
     * If an error occurs at this point, the {@link #close()} method will not overwrite the target file. This can be
     * forced through the {@link #close(boolean)} method.
     * </p>
     * @throws IOException if an I/O error occurs.
     */
    @Override
    public void flush() throws IOException {
        if(error)
            out.flush();
        else {
            try {out.flush();}
            catch(IOException e) {
                error = true;
                throw e;
            }
        }
    }

    /**
     * Writes b.length bytes to this output stream.
     * <p>
     * This method calls the <code>write(byte[] b)</code> method of its underlying output stream.
     * </p>
     * <p>
     * If an error occurs at this point, the {@link #close()} method will not overwrite the target file. This can be
     * forced through the {@link #close(boolean)} method.
     * </p>
     * @param  b           the data to be written.
     * @throws IOException if an I/O error occurs.
     */
    @Override
    public void write(byte[] b) throws IOException {
        if(error)
            out.write(b);
        else {
            try {out.write(b);}
            catch(IOException e) {
                error = true;
                throw e;
            }
        }
    }

    /**
     * Writes len bytes from the specified byte array starting at offset off to this output stream.
     * <p>
     * This method calls the <code>write(byte[] b, int off, int len)</code> method of its underlying output stream.
     * </p>
     * <p>
     * If an error occurs at this point, the {@link #close()} method will not overwrite the target file. This can be
     * forced through the {@link #close(boolean)} method.
     * </p>
     * @param  b           the data to be written.
     * @param  off         the start offset in the data.
     * @param  len         the number of bytes to write.
     * @throws IOException if an I/O error occurs.
     */
    @Override
    public void write(byte[] b, int off, int len) throws IOException {
        if(error)
            out.write(b, off, len);
        else {
            try {out.write(b, off, len);}
            catch(IOException e) {
                error = true;
                throw e;
            }
        }
    }

    /**
     * Writes the specified byte to this output stream.
     * <p>
     * This method calls the <code>write(byte b)</code> method of its underlying output stream.
     * </p>
     * <p>
     * If an error occurs at this point, the {@link #close()} method will not overwrite the target file. This can be
     * forced through the {@link #close(boolean)} method.
     * </p>
     * @param  b           the data to be written.
     * @throws IOException if an I/O error occurs.
     */
    @Override
    public void write(int b) throws IOException {
        if(error)
            out.write(b);
        else {
            try {out.write(b);}
            catch(IOException e) {
                error = true;
                throw e;
            }
        }
    }



    // - Backup -----------------------------------------------------------------
    // --------------------------------------------------------------------------
    /**
     * Overwrites the target file with the backup one.
     * @exception IOException thrown if any IO related error occurs.
     */
    private void backup() throws IOException {
        // Deletes the destination file (AbstractFile.copyTo now fails when the destination exists).
        if(target.exists())
            target.delete();

        // We're not using backup.moveTo(target) because we want to make absolutely sure
        // that if an error occurs in the middle of the operation, at least one of the two files
        // is complete.
        backup.copyTo(target);
        backup.delete();
    }

    /**
     * Finishes the backup operation.
     * @exception IOException thrown if any IO related error occurs.
     */
    @Override
    public void close() throws IOException {close(!error);}

    /**
     * Closes the output stream.
     * <p>
     * The <code>backup</code> parameter is meant for those cases when an error happened
     * while writing to the stream: if it did, we don't want to propagate to the target
     * file, and thus should prevent the backup operation from being performed.
     * </p>
     * @param     backup      whether or not to overwrite the target file by the backup one.
     * @exception IOException thrown if any IO related error occurs.
     */
    public void close(boolean backup) throws IOException {
        // Closes the underlying output stream.
        out.flush();
        out.close();

        if(backup)
            backup();
    }
}
