/*
 * Copyright 2010-2015 Institut Pasteur.
 * 
 * This file is part of Icy.
 * 
 * Icy 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.
 * 
 * Icy 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 Icy. If not, see <http://www.gnu.org/licenses/>.
 */
package icy.file;

import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.text.DecimalFormat;

import icy.gui.frame.progress.FailedAnnounceFrame;
import icy.gui.frame.progress.FileFrame;
import icy.gui.menu.ApplicationMenu;
import icy.image.IcyBufferedImage;
import icy.image.IcyBufferedImageUtil;
import icy.image.colormodel.IcyColorModel;
import icy.image.lut.LUT;
import icy.main.Icy;
import icy.painter.Overlay;
import icy.preferences.GeneralPreferences;
import icy.roi.ROI;
import icy.sequence.MetaDataUtil;
import icy.sequence.Sequence;
import icy.system.IcyExceptionHandler;
import icy.type.DataType;
import icy.util.OMEUtil;
import icy.util.StringUtil;
import loci.common.services.ServiceException;
import loci.formats.FormatException;
import loci.formats.IFormatWriter;
import loci.formats.UnknownFormatException;
import loci.formats.meta.MetadataRetrieve;
import loci.formats.out.APNGWriter;
import loci.formats.out.AVIWriter;
import loci.formats.out.JPEG2000Writer;
import loci.formats.out.JPEGWriter;
import loci.formats.out.OMETiffWriter;
import loci.formats.out.TiffWriter;
import ome.xml.meta.OMEXMLMetadata;

/**
 * Sequence / Image saver class.<br>
 * <br>
 * Supported save format are the following : TIFF (preferred), PNG, JPG and AVI.
 * When sequence is saved as multiple file the following naming convention is used :<br>
 * <code>filename-tttt-zzzz</code>
 * 
 * @author Stephane & Fab
 */
public class Saver
{
    /**
     * @deprecated use {@link OMEUtil#generateMetaData(int, int, int, int, int, DataType, boolean)} instead
     */
    @Deprecated
    public static loci.formats.ome.OMEXMLMetadata generateMetaData(int sizeX, int sizeY, int sizeC, int sizeZ, int sizeT,
            DataType dataType) throws ServiceException
    {
        return (loci.formats.ome.OMEXMLMetadata) OMEUtil.generateMetaData(sizeX, sizeY, sizeC, sizeZ, sizeT, dataType, false);
    }

    /**
     * @deprecated use {@link OMEUtil#generateMetaData(int, int, int, int, int, DataType, boolean)} instead
     */
    @Deprecated
    public static loci.formats.ome.OMEXMLMetadata generateMetaData(int sizeX, int sizeY, int sizeC, int sizeZ, int sizeT, int dataType,
            boolean signedDataType) throws ServiceException
    {
        return (loci.formats.ome.OMEXMLMetadata) OMEUtil.generateMetaData(sizeX, sizeY, sizeC, sizeZ, sizeT,
                DataType.getDataType(dataType, signedDataType), false);
    }

    /**
     * @deprecated use {@link OMEUtil#generateMetaData(int, int, int, DataType, boolean)} instead
     */
    @Deprecated
    public static loci.formats.ome.OMEXMLMetadata generateMetaData(int sizeX, int sizeY, int sizeC, DataType dataType)
            throws ServiceException
    {
        return (loci.formats.ome.OMEXMLMetadata) OMEUtil.generateMetaData(sizeX, sizeY, sizeC, 1, 1, dataType, false);
    }

    /**
     * @deprecated use {@link OMEUtil#generateMetaData(int, int, int, DataType, boolean)} instead
     */
    @Deprecated
    public static loci.formats.ome.OMEXMLMetadata generateMetaData(int sizeX, int sizeY, int sizeC, int dataType, boolean signedDataType)
            throws ServiceException
    {
        return (loci.formats.ome.OMEXMLMetadata) OMEUtil.generateMetaData(sizeX, sizeY, sizeC,
                DataType.getDataType(dataType, signedDataType), false);
    }

    /**
     * Returns the {@link ImageFileFormat} corresponding to specified {@link IFormatWriter}.<br>
     * <code>defaultValue</code> is returned if no matching format is found.
     */
    public static ImageFileFormat getImageFileFormat(IFormatWriter writer, ImageFileFormat defaultValue)
    {
        if (writer instanceof TiffWriter)
            return ImageFileFormat.TIFF;
        if (writer instanceof APNGWriter)
            return ImageFileFormat.PNG;
        if (writer instanceof JPEGWriter)
            return ImageFileFormat.JPG;
        if (writer instanceof JPEG2000Writer)
            return ImageFileFormat.JPG;
        if (writer instanceof AVIWriter)
            return ImageFileFormat.AVI;

        return defaultValue;
    }

    /**
     * @deprecated Use {@link #getImageFileFormat(IFormatWriter, ImageFileFormat)} instead.
     */
    @Deprecated
    public static FileFormat getFileFormat(IFormatWriter writer, FileFormat defaultValue)
    {
        return getImageFileFormat(writer, ImageFileFormat.getFormat(defaultValue)).toFileFormat();
    }

    /**
     * Return the writer to use for the specified ImageFileFormat.<br>
     * <br>
     * The following writer are currently supported :<br>
     * <code>OMETiffWriter</code> : TIFF image file (default)<br>
     * <code>APNGWriter</code> : PNG image file<br>
     * <code>JPEGWriter</code> : JPG image file<br>
     * <code>AVIWriter</code> : AVI video file<br>
     * 
     * @param format
     *        {@link ImageFileFormat} we want to retrieve the saver.<br>
     *        Accepted values:<br>
     *        {@link ImageFileFormat#TIFF}<br>
     *        {@link ImageFileFormat#PNG}<br>
     *        {@link ImageFileFormat#JPG}<br>
     *        {@link ImageFileFormat#AVI}<br>
     *        null
     */
    public static IFormatWriter getWriter(ImageFileFormat format)
    {
        final IFormatWriter result;

        switch (format)
        {
            case PNG:
                result = new APNGWriter();
                break;

            case JPG:
                result = new JPEGWriter();
                break;

            case AVI:
                result = new AVIWriter();
                break;

            default:
                result = new OMETiffWriter();
                // this way we are sure the TIF saver is always compressing
                try
                {
                    result.setCompression("LZW");
                }
                catch (FormatException e)
                {
                    // no compression
                }
                break;
        }

        return result;
    }

    /**
     * @deprecated Use {@link #getWriter(ImageFileFormat)} instead.
     */
    @Deprecated
    public static IFormatWriter getWriter(FileFormat fileFormat)
    {
        return getWriter(ImageFileFormat.getFormat(fileFormat));
    }

    /**
     * Return the writer to use for the specified filename extension.<br>
     * <br>
     * The following writer are currently supported :<br>
     * <code>OMETiffWriter</code> : TIFF image file (default)<br>
     * <code>APNGWriter</code> : PNG image file<br>
     * <code>JPEGWriter</code> : JPG image file<br>
     * <code>AVIWriter</code> : AVI video file<br>
     * 
     * @param ext
     *        Extension we want to retrieve the corresponding image writer.
     * @param defaultFormat
     *        default {@link ImageFileFormat} to use if <code>ext</code> is not recognized.<br>
     *        Accepted values:<br>
     *        {@link ImageFileFormat#TIFF}<br>
     *        {@link ImageFileFormat#PNG}<br>
     *        {@link ImageFileFormat#JPG}<br>
     *        {@link ImageFileFormat#AVI}<br>
     *        null
     */
    public static IFormatWriter getWriter(String ext, ImageFileFormat defaultFormat)
    {
        return getWriter(ImageFileFormat.getWriteFormat(ext, defaultFormat));
    }

    /**
     * @deprecated Use {@link #getWriter(String, ImageFileFormat)} instead.
     */
    @Deprecated
    public static IFormatWriter getWriter(String ext, FileFormat defaultFormat)
    {
        return getWriter(ext, ImageFileFormat.getFormat(defaultFormat));
    }

    /**
     * @deprecated Use {@link #getWriter(String, FileFormat)} instead.
     */
    @Deprecated
    public static IFormatWriter getWriter(String ext)
    {
        return getWriter(ext, ImageFileFormat.TIFF);
    }

    /**
     * Return the writer to use for the specified file.<br>
     * <br>
     * The following writer are currently supported :<br>
     * <code>OMETiffWriter</code> : TIFF image file (default)<br>
     * <code>APNGWriter</code> : PNG image file<br>
     * <code>JPEGWriter</code> : JPG image file<br>
     * <code>AVIWriter</code> : AVI video file<br>
     * 
     * @param file
     *        File we want to retrieve the corresponding image writer.
     * @param defaultFormat
     *        default {@link ImageFileFormat} to use if <code>file</code> is not recognized.<br>
     *        Accepted values:<br>
     *        {@link ImageFileFormat#TIFF}<br>
     *        {@link ImageFileFormat#PNG}<br>
     *        {@link ImageFileFormat#JPG}<br>
     *        {@link ImageFileFormat#AVI}<br>
     *        null
     */
    public static IFormatWriter getWriter(File file, ImageFileFormat defaultFormat)
    {
        return getWriter(FileUtil.getFileExtension(file.getName(), false), defaultFormat);
    }

    /**
     * @deprecated Use {@link #getWriter(File, ImageFileFormat)} instead.
     */
    @Deprecated
    public static IFormatWriter getWriter(File file, FileFormat defaultFormat)
    {
        return getWriter(file, ImageFileFormat.getFormat(defaultFormat));
    }

    /**
     * @deprecated Use {@link #getWriter(File, FileFormat)} instead.
     */
    @Deprecated
    public static IFormatWriter getWriter(File file)
    {
        return getWriter(file, ImageFileFormat.TIFF);
    }

    /**
     * Return the closest compatible {@link IcyColorModel} supported by the specified ImageFileFormat
     * from the specified image description.<br>
     * That means this file format is able to save the data described by the returned {@link IcyColorModel} without any
     * loss
     * or conversion.<br>
     * 
     * @param imageFileFormat
     *        Image file format we want to test compatibility
     * @param numChannel
     *        number of channel of the image
     * @param dataType
     *        image data type
     */
    public static IcyColorModel getCompatibleColorModel(ImageFileFormat imageFileFormat, int numChannel,
            DataType dataType)
    {
        final DataType outDataType;
        final int outNumChannel;

        switch (imageFileFormat)
        {
            default:
            case TIFF:
                // TIFF supports all formats
                outDataType = dataType;
                outNumChannel = numChannel;
                break;

            case PNG:
                // PNG only supports byte data type (short is not really valid)
                if (dataType.getSize() > 1)
                    outDataType = DataType.UBYTE;
                else
                    outDataType = dataType;

                // PNG supports a maximum of 4 channels
                outNumChannel = Math.min(numChannel, 4);
                break;

            case AVI:
            case JPG:
                // JPG, AVI, default only supports byte data type
                if (dataType.getSize() > 1)
                    outDataType = DataType.UBYTE;
                else
                    outDataType = dataType;

                // 3 channels at max
                if (numChannel > 3)
                    outNumChannel = 3;
                else
                {
                    // special case of 2 channels
                    if (numChannel == 2)
                        // convert to RGB
                        outNumChannel = 3;
                    else
                        outNumChannel = numChannel;
                }
                break;
        }

        return IcyColorModel.createInstance(outNumChannel, outDataType);
    }

    /**
     * Return the closest compatible {@link IcyColorModel} supported by the specified image file format
     * from the specified {@link IcyColorModel}.<br>
     * That means this image file format supports saving data described by the returned {@link IcyColorModel} without
     * any loss or conversion.
     * 
     * @param imageFileFormat
     *        Image file format we want to test compatibility
     * @param colorModel
     *        the colorModel describing data / image format
     */
    public static IcyColorModel getCompatibleColorModel(ImageFileFormat imageFileFormat, IcyColorModel colorModel)
    {
        return getCompatibleColorModel(imageFileFormat, colorModel.getNumComponents(), colorModel.getDataType_());
    }

    /**
     * Return true if the specified image file format is compatible with the image description.<br>
     * That means this image file format supports saving data without any loss or conversion.
     * 
     * @param imageFileFormat
     *        Image file format we want to test compatibility
     * @param numChannel
     *        number of channel of the image
     * @param alpha
     *        true if the image has an alpha channel
     * @param dataType
     *        image data type
     */
    public static boolean isCompatible(ImageFileFormat imageFileFormat, int numChannel, boolean alpha,
            DataType dataType)
    {
        return isCompatible(imageFileFormat, IcyColorModel.createInstance(numChannel, dataType));
    }

    /**
     * Return true if the specified image file format is compatible with the given {@link IcyColorModel}. <br>
     * That means this image file format supports saving data described by the returned {@link IcyColorModel} without
     * any loss or conversion.<br>
     * The color map data are never preserved, they are always restored to their default.<br>
     */
    public static boolean isCompatible(ImageFileFormat imageFileFormat, IcyColorModel colorModel)
    {
        return colorModel.isCompatible(getCompatibleColorModel(imageFileFormat, colorModel));
    }

    /**
     * Return true if the specified image file format is compatible to save the given Sequence.<br>
     * That means this image file format supports saving all original data (3D/4D/5D) without any loss or conversion.
     */
    public static boolean isCompatible(ImageFileFormat imageFileFormat, Sequence sequence)
    {
        final boolean multiZ = sequence.getSizeZ() > 1;
        final boolean multiT = sequence.getSizeT() > 1;

        switch (imageFileFormat)
        {
            case JPG:
            case PNG:
                // JPG and PNG: no support for time sequence or 3D image
                if ((multiZ) || (multiT))
                    return false;
                break;

            case AVI:
                // AVI: not support for 3D image
                if (multiZ)
                    return false;
                break;
        }

        return isCompatible(imageFileFormat, sequence.getColorModel());
    }

    /**
     * Return the separate channel flag from specified image file format and color space
     */
    private static boolean getSeparateChannelFlag(ImageFileFormat imageFileFormat, int numChannel, DataType dataType)
    {
        // only if we have more than 1 channel
        if (numChannel > 1)
        {
            // only TIFF writer support it: better to not separate channel for RGB images
            if (imageFileFormat.equals(ImageFileFormat.TIFF))
                return (numChannel != 3) || (dataType.getSize() > 1);
        }

        // others writers does not support separated channel
        return false;
    }

    /**
     * Return the separate channel flag from specified image file format and color space
     */
    private static boolean getSeparateChannelFlag(ImageFileFormat imageFileFormat, IcyColorModel colorModel)
    {
        return getSeparateChannelFlag(imageFileFormat, colorModel.getNumComponents(), colorModel.getDataType_());
    }

    /**
     * Return the closest compatible {@link IcyColorModel} supported by writer
     * from the specified image description.<br>
     * That means the writer is able to save the data described by the returned {@link IcyColorModel} without any loss
     * or conversion.<br>
     * 
     * @param writer
     *        IFormatWriter we want to test compatibility
     * @param numChannel
     *        number of channel of the image
     * @param dataType
     *        image data type
     */
    public static IcyColorModel getCompatibleColorModel(IFormatWriter writer, int numChannel, DataType dataType)
    {
        return getCompatibleColorModel(getImageFileFormat(writer, ImageFileFormat.TIFF), numChannel, dataType);
    }

    /**
     * Return the closest compatible {@link IcyColorModel} supported by writer
     * from the specified {@link IcyColorModel}.<br>
     * That means the writer is able to save the data described by the returned {@link IcyColorModel} without any loss
     * or conversion.<br>
     * 
     * @param writer
     *        IFormatWriter we want to test compatibility
     * @param colorModel
     *        the colorModel describing data / image format
     */
    public static IcyColorModel getCompatibleColorModel(IFormatWriter writer, IcyColorModel colorModel)
    {
        return getCompatibleColorModel(writer, colorModel.getNumComponents(), colorModel.getDataType_());
    }

    /**
     * Return true if the specified writer is compatible with the image description.<br>
     * That means the writer is able to save the data without any loss or conversion.<br>
     * 
     * @param numChannel
     *        number of channel of the image
     * @param alpha
     *        true if the image has an alpha channel
     * @param dataType
     *        image data type
     */
    public static boolean isCompatible(IFormatWriter writer, int numChannel, boolean alpha, DataType dataType)
    {
        return isCompatible(writer, IcyColorModel.createInstance(numChannel, dataType));
    }

    /**
     * Return true if the specified writer is compatible with the given {@link IcyColorModel}. <br>
     * That means the writer is able to save the data described by the colorModel without any loss
     * or conversion.<br>
     * The color map data are never preserved, they are always restored to their default.<br>
     */
    public static boolean isCompatible(IFormatWriter writer, IcyColorModel colorModel)
    {
        return colorModel.isCompatible(getCompatibleColorModel(writer, colorModel));
    }

    /**
     * Return true if the specified writer is compatible to save the given Sequence.<br>
     * That means the writer is able to save all original data (3D/4D/5D) without any loss or conversion.
     */
    public static boolean isCompatible(IFormatWriter writer, Sequence sequence)
    {
        return isCompatible(getImageFileFormat(writer, ImageFileFormat.TIFF), sequence);
    }

    /**
     * Return the separate channel flag from specified writer and color space
     */
    private static boolean getSeparateChannelFlag(IFormatWriter writer, int numChannel, DataType dataType)
    {
        return getSeparateChannelFlag(getImageFileFormat(writer, ImageFileFormat.TIFF), numChannel, dataType);
    }

    /**
     * Return the separate channel flag from specified writer and color space
     */
    private static boolean getSeparateChannelFlag(IFormatWriter writer, IcyColorModel colorModel)
    {
        return getSeparateChannelFlag(writer, colorModel.getNumComponents(), colorModel.getDataType_());
    }

    /**
     * Save the specified sequence in the specified file.<br>
     * If sequence contains severals images then file is used as a directory<br>
     * to store all single images.
     * 
     * @param sequence
     *        sequence to save
     * @param file
     *        file where we want to save sequence
     */
    public static void save(Sequence sequence, File file)
    {
        save(sequence, file, 15, (sequence.getSizeZ() * sequence.getSizeT()) > 1, true);
    }

    /**
     * @deprecated Use {@link #save(Sequence, File, boolean, boolean)} instead.
     */
    @Deprecated
    public static void save(Sequence sequence, File file, boolean multipleFiles)
    {
        save(sequence, file, 0, sequence.getSizeZ() - 1, 0, sequence.getSizeT() - 1, 15, multipleFiles, true);
    }

    /**
     * Save the specified sequence in the specified file.<br>
     * When the sequence contains severals image the multiFile flag is used to indicate<br>
     * if images are saved in severals files (file then specify a directory) or in a single file.
     * 
     * @param sequence
     *        sequence to save
     * @param file
     *        file where we want to save sequence
     * @param multipleFiles
     *        flag to indicate if images are saved in separate file
     * @param showProgress
     *        show progress bar
     */
    public static void save(Sequence sequence, File file, boolean multipleFiles, boolean showProgress)
    {
        save(sequence, file, 15, multipleFiles, showProgress);
    }

    /**
     * @deprecated Use {@link #save(Sequence, File, int, boolean, boolean)} instead.
     */
    @Deprecated
    public static void save(Sequence sequence, File file, int zMin, int zMax, int tMin, int tMax, int fps,
            boolean multipleFiles)
    {
        save(sequence, file, zMin, zMax, tMin, tMax, fps, multipleFiles, true);
    }

    /**
     * @deprecated Use {@link #save(Sequence, File, int, boolean, boolean)} instead.
     */
    @Deprecated
    public static void save(Sequence sequence, File file, int zMin, int zMax, int tMin, int tMax, int fps,
            boolean multipleFile, boolean showProgress)
    {
        save(null, sequence, file, fps, multipleFile, showProgress, true);
    }

    /**
     * Save the specified sequence in the specified file.<br>
     * When the sequence contains severals image the multipleFile flag is used to indicate<br>
     * if images are saved as separate files (file then specify a directory) or not.<br>
     * zMin - zMax and tMin - tMax define the Z and T images range to save.<br>
     * 
     * @param sequence
     *        sequence to save
     * @param file
     *        file where we want to save sequence
     * @param fps
     *        frame rate for AVI sequence save
     * @param multipleFile
     *        flag to indicate if images are saved in separate file
     * @param showProgress
     *        show progress bar
     */
    public static void save(Sequence sequence, File file, int fps, boolean multipleFile, boolean showProgress)
    {
        save(null, sequence, file, fps, multipleFile, showProgress, true);
    }

    /**
     * @deprecated Use {@link #save(IFormatWriter, Sequence, File, int, boolean, boolean, boolean)} instead.
     */
    @Deprecated
    public static void save(IFormatWriter formatWriter, Sequence sequence, File file, int zMin, int zMax, int tMin,
            int tMax, int fps, boolean multipleFile, boolean showProgress)
    {
        save(formatWriter, sequence, file, fps, multipleFile, showProgress, true);
    }

    /**
     * @deprecated Use {@link #save(IFormatWriter, Sequence, File, int, boolean, boolean, boolean)} instead.
     */
    @Deprecated
    public static void save(IFormatWriter formatWriter, Sequence sequence, File file, int zMin, int zMax, int tMin,
            int tMax, int fps, boolean multipleFile, boolean showProgress, boolean addToRecent)
    {
        save(formatWriter, sequence, file, fps, multipleFile, showProgress, addToRecent);
    }

    /**
     * Save the specified sequence in the specified file.<br>
     * When the sequence contains severals image the multipleFile flag is used to indicate
     * if images are saved as separate files (file then specify a directory) or not.<br>
     * <code>zMin</code> - <code>zMax</code> and <code>tMin</code> - <code>tMax</code> define the Z
     * and T images range to save.<br>
     * 
     * @param formatWriter
     *        writer used to save sequence (define the image format).<br>
     *        If set to <code>null</code> then writer is determined from the file extension.<br>
     *        If destination file does not have a valid extension (for folder for instance) then you
     *        have to specify a valid Writer to write the image file (see {@link #getWriter(ImageFileFormat)})
     * @param sequence
     *        sequence to save
     * @param file
     *        file where we want to save sequence.<br>
     *        Depending the <code>formatWriter</code> the file extension may be modified.<br>
     *        That is preferred as saving an image with a wrong extension may result in error on
     *        future read (wrong reader detection).<br>
     * @param fps
     *        frame rate for AVI sequence save
     * @param multipleFile
     *        flag to indicate if images are saved in separate file.<br>
     *        When multiple file is enabled the <code>file</code> parameter is considerer as a folder if it doens't have
     *        any extension
     * @param showProgress
     *        show progress bar
     * @param addToRecent
     *        add the saved sequence to recent opened sequence list
     */
    public static void save(IFormatWriter formatWriter, Sequence sequence, File file, int fps, boolean multipleFile,
            boolean showProgress, boolean addToRecent)
    {
        final String filePath = FileUtil.cleanPath(FileUtil.getGenericPath(file.getAbsolutePath()));
        final int sizeT = sequence.getSizeT();
        final int sizeZ = sequence.getSizeZ();
        final int numImages = sizeT * sizeZ;
        final FileFrame saveFrame;
        final ApplicationMenu mainMenu;

        if (addToRecent)
            mainMenu = Icy.getMainInterface().getApplicationMenu();
        else
            mainMenu = null;
        if (showProgress && !Icy.getMainInterface().isHeadLess())
            saveFrame = new FileFrame("Saving", filePath);
        else
            saveFrame = null;
        try
        {
            if (saveFrame != null)
            {
                saveFrame.setLength(numImages);
                saveFrame.setPosition(0);
            }

            final IFormatWriter writer;
            final Sequence savedSequence;

            // get the writer
            if (formatWriter == null)
                writer = getWriter(file, ImageFileFormat.TIFF);
            else
                writer = formatWriter;

            if (writer == null)
                throw new UnknownFormatException("Can't find a valid image writer for the specified file: " + file);

            // need multiple files ?
            if ((numImages > 1) && multipleFile)
            {
                // save as severals images
                final DecimalFormat decimalFormat = new DecimalFormat("0000");
                final String fileName = FileUtil.getFileName(filePath, false);
                String fileExt = FileUtil.getFileExtension(filePath, true);

                String fileBaseDirectory = FileUtil.getDirectory(filePath);
                if (fileBaseDirectory.endsWith("/"))
                    fileBaseDirectory = fileBaseDirectory.substring(0, fileBaseDirectory.length() - 1);

                // no extension (directory) ?
                if (StringUtil.isEmpty(fileExt))
                {
                    // filename is part of directory
                    fileBaseDirectory += FileUtil.separator + fileName;
                    // use the default file extension for the specified writer
                    fileExt = "." + getImageFileFormat(writer, ImageFileFormat.TIFF).getExtensions()[0];
                }

                final String filePathWithoutExt = fileBaseDirectory + FileUtil.separator + fileName;

                // create output directory
                FileUtil.createDir(fileBaseDirectory);

                // default name used --> use filename
                if (sequence.isDefaultName())
                    sequence.setName(fileName);
                sequence.setFilename(fileBaseDirectory);

                // assume that is the saved sequence (used for metadata)
                savedSequence = sequence;

                for (int t = 0; t < sizeT; t++)
                {
                    for (int z = 0; z < sizeZ; z++)
                    {
                        String filename = filePathWithoutExt;

                        if (sizeT > 1)
                            filename += "_t" + decimalFormat.format(t);
                        if (sizeZ > 1)
                            filename += "_z" + decimalFormat.format(z);
                        filename += fileExt;

                        // save as single image file
                        save(writer, sequence, filename, t, z, fps, saveFrame);
                    }
                }

                // add as one item to recent file list
                if (mainMenu != null)
                    mainMenu.addRecentFile(fileBaseDirectory);
            }
            else
            {
                final ImageFileFormat iff = getImageFileFormat(writer, ImageFileFormat.TIFF);
                final String fileExt = FileUtil.getFileExtension(filePath, false);
                // force to set correct file extension
                final String fixedFilePath;

                if (iff.matches(fileExt))
                    fixedFilePath = filePath;
                else
                    fixedFilePath = filePath + "." + iff.getExtensions()[0];

                // default name used --> use filename
                if (sequence.isDefaultName())
                    sequence.setName(FileUtil.getFileName(filePath, false));

                // save whole sequence into a single file
                savedSequence = save(writer, sequence, fixedFilePath, -1, -1, fps, saveFrame);

                // we set filename on actual saved Sequence
                savedSequence.setFilename(filePath);

                // add as one item to recent file list
                if (mainMenu != null)
                    mainMenu.addRecentFile(fixedFilePath);
            }

            // Sequence persistence enabled --> save XML
            if (GeneralPreferences.getSequencePersistence())
                savedSequence.saveXMLData();
        }
        catch (Exception e)
        {
            IcyExceptionHandler.showErrorMessage(e, true);
            if (showProgress && !Icy.getMainInterface().isHeadLess())
                new FailedAnnounceFrame("Failed to save image(s) (see output console for details)", 15);
            return;
        }
        finally
        {
            if (saveFrame != null)
                saveFrame.close();
        }
    }

    /**
     * Save a single image from bytes buffer to the specified file.
     */
    private static void saveImage(IFormatWriter formatWriter, byte[] data, int width, int height, int numChannel,
            boolean separateChannel, DataType dataType, File file, boolean force) throws FormatException, IOException
    {
        final String filePath = FileUtil.cleanPath(FileUtil.getGenericPath(file.getAbsolutePath()));

        if (FileUtil.exists(filePath))
        {
            // forced ? first delete the file else LOCI won't save it
            if (force)
                FileUtil.delete(filePath, true);
            else
                throw new IOException("File already exists");
        }
        // ensure parent directory exist
        FileUtil.ensureParentDirExist(filePath);

        final IFormatWriter writer;
        final boolean separateCh;

        if (formatWriter == null)
        {
            // get the writer
            writer = getWriter(FileUtil.getFileExtension(filePath, false), ImageFileFormat.TIFF);

            // prepare the metadata
            try
            {
                separateCh = getSeparateChannelFlag(writer, numChannel, dataType);
                writer.setMetadataRetrieve((MetadataRetrieve) MetaDataUtil.generateMetaData(width, height, numChannel,
                        dataType, separateCh));
            }
            catch (ServiceException e)
            {
                System.err.println("Saver.saveImage(...) error :");
                IcyExceptionHandler.showErrorMessage(e, true);
            }
        }
        else
        {
            // ready to use writer (metadata already prepared)
            writer = formatWriter;
            separateCh = separateChannel;
        }

        // we never interleaved data even if some image viewer need it to correctly read image (win XP viewer)
        writer.setInterleaved(false);
        writer.setId(filePath);
        writer.setSeries(0);
        // usually give better save performance
        writer.setWriteSequentially(true);

        try
        {
            // separated channel data
            if (separateChannel)
            {
                final int pitch = width * height * dataType.getSize();
                final byte[] dataChannel = new byte[pitch];
                int offset = 0;

                for (int c = 0; c < numChannel; c++)
                {
                    System.arraycopy(data, offset, dataChannel, 0, pitch);
                    writer.saveBytes(c, dataChannel);
                    offset += pitch;
                }
            }
            else
                // save all data at once
                writer.saveBytes(0, data);
        }
        catch (Exception e)
        {
            System.err.println("Saver.saveImage(...) error :");
            IcyExceptionHandler.showErrorMessage(e, true);
        }

        writer.close();
    }

    /**
     * Save a single image from bytes buffer to the specified file.
     */
    public static void saveImage(byte[] data, int width, int height, int numChannel, DataType dataType, File file,
            boolean force) throws FormatException, IOException
    {
        saveImage(null, data, width, height, numChannel, false, dataType, file, force);
    }

    /**
     * @deprecated Use {@link #saveImage(byte[], int, int, int, DataType, File, boolean)} instead
     */
    @Deprecated
    public static void saveImage(byte[] data, int width, int height, int numChannel, int dataType,
            boolean signedDataType, File file, boolean force) throws FormatException, IOException
    {
        saveImage(data, width, height, numChannel, DataType.getDataType(dataType, signedDataType), file, force);
    }

    /**
     * Save a single image to the specified file
     * 
     * @param image
     * @throws IOException
     * @throws FormatException
     */
    public static void saveImage(IcyBufferedImage image, File file, boolean force) throws FormatException, IOException
    {
        final IFormatWriter writer = getWriter(file, ImageFileFormat.TIFF);

        if (writer == null)
            throw new UnknownFormatException("Can't find a valid image writer for the specified file: " + file);

        final boolean separateChannel = getSeparateChannelFlag(writer, image.getIcyColorModel());

        try
        {
            writer.setMetadataRetrieve((MetadataRetrieve) MetaDataUtil.generateMetaData(image, separateChannel));
        }
        catch (ServiceException e)
        {
            System.err.println("Saver.saveImage(...) error :");
            IcyExceptionHandler.showErrorMessage(e, true);
        }

        // get byte order
        final boolean littleEndian = !writer.getMetadataRetrieve().getPixelsBinDataBigEndian(0, 0).booleanValue();
        // then save the image
        saveImage(writer, image.getRawData(littleEndian), image.getSizeX(), image.getSizeY(), image.getSizeC(),
                separateChannel, image.getDataType_(), file, force);
    }

    /**
     * Save the specified sequence in the specified file using the given writer.<br>
     * If posT or/and posZ are defined then only a sub part of the original Sequence is saved.
     * 
     * @param writer
     *        writer used to save sequence (define the image format, cannot be <code>null</code> at this point)
     * @param sequence
     *        sequence to save
     * @param filePath
     *        file name where we want to save sequence
     * @param posT
     *        frame index to save (-1 to save all frame from input sequence)
     * @param posZ
     *        slice index to save (-1 to save all slice from input sequence)
     * @param fps
     *        frame rate for AVI writer
     * @param saveFrame
     *        progress frame for save operation (can be null)
     * @return Actual saved Sequence (can be different from input one if conversion was needed)
     * @throws ServiceException
     * @throws IOException
     * @throws FormatException
     */
    private static Sequence save(IFormatWriter writer, Sequence sequence, String filePath, int posT, int posZ, int fps,
            FileFrame saveFrame) throws ServiceException, FormatException, IOException
    {
        // TODO: temporary fix for the "incorrect close operation" bug in Bio-Formats
        // with OME TIF writer, remove it when fixed.
        // {
        // try
        // {
        // writer = formatWriter.getClass().newInstance();
        // }
        // catch (Exception e)
        // {
        // throw new ServiceException("Can't create new writer instance: " + e);
        // }
        // }

        final File file = new File(filePath);

        // first delete the file else LOCI won't save it correctly
        if (file.exists())
            file.delete();
        // ensure parent directory exist
        FileUtil.ensureParentDirExist(file);

        final ImageFileFormat saveFormat = getImageFileFormat(writer, ImageFileFormat.TIFF);
        final int sizeT = sequence.getSizeT();
        final int sizeZ = sequence.getSizeZ();
        final int adjZ, adjT;
        final int tMin, tMax;
        final int zMin, zMax;

        // adjust posT and posZ depending the writer support
        switch (saveFormat)
        {
            default:
            case TIFF:
                // no restriction for TIFF
                adjZ = posZ;
                adjT = posT;
                break;

            case AVI:
                // AVI: always save single slice
                adjZ = (posZ < 0) ? sizeZ / 2 : posZ;
                adjT = posT;
                break;

            case JPG:
            case PNG:
                // JPG or PNG: always save single image
                adjZ = (posZ < 0) ? sizeZ / 2 : posZ;
                adjT = (posT < 0) ? sizeT / 2 : posT;
                break;
        }

        // convert Sequence in good format for specified writer
        final Sequence compatibleSequence = getCompatibleSequenceForWriter(writer, sequence, adjT, adjZ);
        // get channel separation flag
        final boolean separateChannel = getSeparateChannelFlag(saveFormat, compatibleSequence.getColorModel());
        // prepare metadata
        final OMEXMLMetadata metadata = MetaDataUtil.generateMetaData(compatibleSequence, separateChannel);

        // clean unwanted planes
        MetaDataUtil.keepPlanes(metadata, 0, adjT, adjZ, -1);
        if (adjT < 0)
        {
            // all frame
            tMin = 0;
            tMax = sizeT - 1;
        }
        else
        {
            // single frame
            tMin = tMax = adjT;
            MetaDataUtil.setSizeT(metadata, 0, 1);
        }
        if (adjZ < 0)
        {
            // all slice
            zMin = 0;
            zMax = sizeZ - 1;
        }
        else
        {
            // single slice
            zMin = zMax = adjZ;
            MetaDataUtil.setSizeZ(metadata, 0, 1);
        }

        // specific to TIFF writer
        if (writer instanceof TiffWriter)
        {
            // > 2GB --> use big tiff (important to do it before setId(..) call)
            if (MetaDataUtil.getDataSize(metadata, 0, 0) > 2000000000L)
                ((TiffWriter) writer).setBigTiff(true);
        }

        // set settings
        writer.setFramesPerSecond(fps);
        // generate metadata
        writer.setMetadataRetrieve((MetadataRetrieve) metadata);
        // no interleave (XP default viewer want interleaved channel to correctly read image)
        writer.setInterleaved(false);
        // set id
        writer.setId(filePath);
        // init
        writer.setSeries(0);
        // usually give better save performance
        writer.setWriteSequentially(true);

        final int sizeC = compatibleSequence.getSizeC();
        // get endianess
        final boolean littleEndian = !writer.getMetadataRetrieve().getPixelsBinDataBigEndian(0, 0).booleanValue();
        byte[] data = null;

        try
        {
            int imageIndex = 0;
            // XYCZT order is important here (see metadata)
            for (int t = tMin; t <= tMax; t++)
            {
                for (int z = zMin; z <= zMax; z++)
                {
                    // interrupt process (partial save)
                    if ((saveFrame != null) && saveFrame.isCancelRequested())
                        return compatibleSequence;

                    final IcyBufferedImage image = compatibleSequence.getImage(t, z);

                    // separated channel data
                    if (separateChannel)
                    {
                        for (int c = 0; c < sizeC; c++)
                        {
                            if (image != null)
                            {
                                // avoid multiple allocation
                                data = image.getRawData(c, data, 0, littleEndian);
                                writer.saveBytes(imageIndex, data);
                            }

                            imageIndex++;
                        }
                    }
                    else
                    {
                        if (image != null)
                        {
                            // avoid multiple allocation
                            data = image.getRawData(data, 0, littleEndian);
                            writer.saveBytes(imageIndex, data);
                        }

                        imageIndex++;
                    }

                    if (saveFrame != null)
                        saveFrame.incPosition();
                }
            }
        }
        finally
        {
            // always close writer after a file has been saved
            writer.close();
        }

        return compatibleSequence;
    }

    /**
     * Returns a compatible Sequence representing the input sequence so it can be saved with the specified writer.<br>
     * If the writer support the input sequence then the input sequence is directly returned.
     * 
     * @param writer
     *        writer used to save sequence (define the image format, cannot be <code>null</code>)
     * @param sequence
     *        sequence to save
     * @param posT
     *        frame index to keep (-1 for all frame)
     * @param posZ
     *        slice index to keep (-1 for all slice)
     * @return the compatible sequence for given Writer
     */
    public static Sequence getCompatibleSequenceForWriter(IFormatWriter writer, Sequence sequence, int posT, int posZ)
    {
        final int sizeC = sequence.getSizeC();
        final DataType dataType = sequence.getDataType_();
        final boolean needConvert;
        final ImageFileFormat imageFormat = getImageFileFormat(writer, ImageFileFormat.TIFF);

        // adjust posT and posZ depending the writer support
        switch (imageFormat)
        {
            default:
                // assume TIFF
                needConvert = false;
                break;

            case AVI:
            case JPG:
                // JPG, AVI: only supports byte data type and Gray/RGB images
                needConvert = (dataType.getSize() > 1) || (sizeC == 2) || (sizeC > 3);
                break;

            case PNG:
                // PNG: support byte data type with a maximum of 4 channels
                needConvert = (dataType.getSize() > 1) || (sizeC > 4);
                break;
        }

        // no conversion needed
        if (!needConvert)
            return sequence;

        final int sizeT = sequence.getSizeT();
        final int sizeZ = sequence.getSizeZ();
        final int tMin, tMax;
        final int zMin, zMax;

        if (posT < 0)
        {
            // all frame
            tMin = 0;
            tMax = sizeT - 1;
        }
        else
            // single frame
            tMin = tMax = posT;
        if (posZ < 0)
        {
            // all slice
            zMin = 0;
            zMax = sizeZ - 1;
        }
        else
            // single slice
            zMin = zMax = posZ;

        // wanted image type
        final int imageType = (sizeC > 1) ? BufferedImage.TYPE_INT_RGB : BufferedImage.TYPE_BYTE_GRAY;
        // image receiver
        final BufferedImage imgOut = new BufferedImage(sequence.getSizeX(), sequence.getSizeY(), imageType);
        // conversion LUT (use default sequence one)
        final LUT lut = sequence.getDefaultLUT();

        // create compatible sequence
        final Sequence result = new Sequence(OMEUtil.createOMEMetadata(sequence.getMetadata()));

        result.beginUpdate();
        try
        {
            for (int t = tMin; t <= tMax; t++)
                for (int z = zMin; z <= zMax; z++)
                    result.setImage(t, z, IcyBufferedImageUtil.toBufferedImage(sequence.getImage(t, z), imgOut, lut));

            // preserve ROI and overlays (for XML metadata preservation)
            for (ROI roi : sequence.getROIs())
                result.addROI(roi);
            for (Overlay overlay : sequence.getOverlays())
                result.addOverlay(overlay);

            // rename channels and set final name
            switch (imageType)
            {
                default:
                case BufferedImage.TYPE_INT_RGB:
                    result.setChannelName(0, "red");
                    result.setChannelName(1, "green");
                    result.setChannelName(2, "blue");
                    break;

                case BufferedImage.TYPE_BYTE_GRAY:
                    result.setChannelName(0, "gray");
                    break;
            }
            result.setName(sequence.getName() + " (" + imageFormat + ")");
        }
        finally
        {
            result.endUpdate();
        }

        return result;
    }
}