/*
 * @(#)Main.java  1.0  2010-12-26
 * 
 * Copyright © 2010 Werner Randelshofer, Immensee, Switzerland.
 * All rights reserved.
 * 
 * You may not use, copy or modify this file, except in compliance with the
 * license agreement you entered into with Werner Randelshofer.
 * For details see accompanying license terms.
 */
package ch.randelshofer.seqconverter;

import org.monte.media.Format;
import org.monte.media.image.BitmapImage;
import org.monte.media.anim.ANIMWriter;
import org.monte.media.avi.AVIOutputStream;
import org.monte.media.ilbm.ILBMEncoder;
import org.monte.media.quicktime.QuickTimeWriter;
import org.monte.media.seq.SEQDecoder;
import org.monte.media.seq.SEQFrame;
import org.monte.media.seq.SEQMovieTrack;
import org.monte.media.seq.SEQReader;
import ch.randelshofer.util.EnumIterable;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferByte;
import java.awt.image.DataBufferInt;
import java.awt.image.DataBufferUShort;
import java.awt.image.DirectColorModel;
import java.awt.image.IndexColorModel;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.prefs.Preferences;
import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.stream.FileImageInputStream;
import javax.imageio.stream.FileImageOutputStream;
import javax.imageio.stream.ImageInputStream;
import javax.swing.AbstractButton;
import javax.swing.JOptionPane;
import javax.swing.ProgressMonitor;
import javax.swing.SwingUtilities;
import javax.swing.TransferHandler;
import javax.swing.UIManager;
import org.jhotdraw.gui.Worker;
import static org.monte.media.VideoFormatKeys.*;
import static org.monte.media.AudioFormatKeys.*;

/**
 * A utility for converting Atari Cyberpaint Sequence animations (.SEQ) to
 * IFF ANIM or to AVI.
 *
 * @author Werner Randelshofer
 * @version 1.0 2010-12-26 Created.
 */
public class Main extends javax.swing.JFrame {

    /** Creates new form Main */
    public Main() {
        initComponents();

        Preferences prefs = Preferences.userNodeForPackage(Main.class);
        String outputFormat = prefs.get("outputFormat", "ANIM");
        for (AbstractButton btn : new EnumIterable<AbstractButton>(outputGroup.getElements())) {
            if (btn.getActionCommand().equals(outputFormat)) {
                btn.setSelected(true);
                break;
            }
        }
        variableFrameRateItem.setSelected(prefs.getBoolean("variableFrameRate", true));
        updateEnabled();
        updateTitle();

        setSize(260, 200);
        label.setTransferHandler(new TransferHandler() {

            @Override
            public boolean canImport(TransferSupport support) {
                return support.isDataFlavorSupported(DataFlavor.javaFileListFlavor);
            }

            @Override
            public boolean importData(TransferSupport support) {
                boolean success = false;
                List<File> fileList;
                try {
                    fileList = (List<File>) support.getTransferable().getTransferData(DataFlavor.javaFileListFlavor);
                    if (fileList != null) {
                        convert(fileList);
                        success = true;
                    }
                } catch (UnsupportedFlavorException ex) {
                    ex.printStackTrace();
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
                return success;
            }
        });
    }

    public void convert(final List<File> fileList) {
        label.setEnabled(false);
        final boolean variableFramerate = variableFrameRateItem.isSelected();
        new Worker<File>() {

            @Override
            protected File construct() throws Exception {
                File lastFile = null;
                for (final File f : fileList) {
                    SwingUtilities.invokeLater(new Runnable() {

                        @Override
                        public void run() {
                            label.setText("Converting " + f.getName() + " ...");
                        }
                    });

                    if (toIlbmItem.isSelected()) {
                        lastFile = convertToILBM(f);
                    } else if (toPNGItem.isSelected()) {
                        lastFile = convertToPNG(f);
                    } else if (toAnimItem.isSelected()) {
                        lastFile = convertToANIM(f, variableFramerate);
                    } else if (toAVIItem.isSelected()) {
                        lastFile = convertToAVI_RLE8(f);
                    } else if (toQuickTimeItem.isSelected()) {
                        lastFile = convertToQuickTime_RLE16(f, variableFramerate);
                    }
                }
                return lastFile;
            }

            @Override
            protected void failed(Throwable error) {
                label.setText("Error " + error.getMessage());
                error.printStackTrace();
            }

            @Override
            protected void done(File value) {
                if (value == null) {
                    label.setText("Done.");
                } else {
                    label.setText(value.getName() + " done.");
                }
            }

            @Override
            protected void finished() {
                label.setEnabled(true);
            }
        }.start();
    }

    private File convertToILBM(File inFile) throws IOException {
        File outDir = new File(inFile + ".ilbm");
        for (int i = 2; outDir.exists() && i < 1000; i++) {
            outDir = new File(inFile + ".ilbm " + i);
        }
        if (outDir.exists()) {
            throw new IOException(outDir + " exists.");
        }

        ImageInputStream in = null;
        ProgressMonitor pm = new ProgressMonitor(this, "Converting to " + outDir.getName(), "Reading " + inFile.getName() + "...", 0, 2);
        try {
            in = new FileImageInputStream(inFile);
            SEQDecoder decoder = new SEQDecoder(in);
            SEQMovieTrack track = new SEQMovieTrack();
            decoder.produce(track, false);
            pm.setMaximum(track.getFrameCount());

            ILBMEncoder encoder = new ILBMEncoder();
            if (track.getFrameCount() > 0) {
                int digits = (int) Math.ceil(Math.log10(track.getFrameCount()));
                StringBuilder zerosB = new StringBuilder();
                for (int i = 0; i < digits; i++) {
                    zerosB.append('0');
                }
                String zeros = zerosB.toString();

                outDir.mkdirs();
                BitmapImage img = new BitmapImage(track.getWidth(), track.getHeight(), track.getNbPlanes(), track.getFrame(0).getColorModel());

                for (int i = 0, n = track.getFrameCount(); i < n && !pm.isCanceled(); i++) {
                    String formatted = zeros + i;
                    formatted = formatted.substring(formatted.length() - zeros.length());
                    File outFile = new File(outDir, formatted + ".ilbm");
                    pm.setNote("Writing " + outFile.getName() + "...");
                    SEQFrame frame = track.getFrame(i);
                    frame.decode(img, track);
                    encoder.write(outFile, img, 0x11000);
                    pm.setProgress(i);
                }


            } else {
                throw new IOException("No frames in " + inFile.getName());
            }

        } finally {
            if (in != null) {
                in.close();
            }
            pm.close();
        }
        return outDir;
    }

    private File convertToPNG(File inFile) throws IOException {
        File outDir = new File(inFile + ".png");
        for (int i = 2; outDir.exists() && i < 1000; i++) {
            outDir = new File(inFile + ".png " + i);
        }
        if (outDir.exists()) {
            throw new IOException(outDir + " exists.");
        }

        ImageInputStream in = null;
        ProgressMonitor pm = new ProgressMonitor(this, "Converting to " + outDir.getName(), "Reading " + inFile.getName() + "...", 0, 2);
        try {
            in = new FileImageInputStream(inFile);
            SEQDecoder decoder = new SEQDecoder(in);
            decoder.setEnforce8BitColorModel(false);
            SEQMovieTrack track = new SEQMovieTrack();
            decoder.produce(track, false);
            decoder.setEnforce8BitColorModel(false);
            pm.setMaximum(track.getFrameCount());
            int width = track.getWidth();
            int height = track.getHeight();

            if (track.getFrameCount() > 0) {
                int digits = (int) Math.ceil(Math.log10(track.getFrameCount()));
                StringBuilder zerosB = new StringBuilder();
                for (int i = 0; i < digits; i++) {
                    zerosB.append('0');
                }
                String zeros = zerosB.toString();

                outDir.mkdirs();
                SEQFrame f0 = track.getFrame(0);
                BitmapImage bmp = new BitmapImage(width, height, track.getNbPlanes(), f0.getColorModel());
                bmp.setPreferredChunkyColorModel(f0.getColorModel());
                bmp.setEnforceDirectColors(false);

                BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_INDEXED, (IndexColorModel) f0.getColorModel());
                byte[] buf = ((DataBufferByte) img.getRaster().getDataBuffer()).getData();
                int[] imgRGBs = new int[16];
                int[] previousRGBs = new int[16];
                ((IndexColorModel) track.getFrame(0).getColorModel()).getRGBs(imgRGBs);
                ((IndexColorModel) track.getFrame(0).getColorModel()).getRGBs(previousRGBs);
                bmp.setBytePixels(buf);
                //

                for (int i = 0, n = track.getFrameCount(); i < n; i++) {
                    SEQFrame f = track.getFrame(i);
                    ((IndexColorModel) f.getColorModel()).getRGBs(imgRGBs);
                    if (!Arrays.equals(imgRGBs, previousRGBs)) {
                        img = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_INDEXED, (IndexColorModel) f.getColorModel());
                        buf = ((DataBufferByte) img.getRaster().getDataBuffer()).getData();
                        bmp.setBytePixels(buf);
                        System.arraycopy(imgRGBs, 0, previousRGBs, 0, imgRGBs.length);
                    }
                    f.decode(bmp, track);
                    bmp.setPlanarColorModel(f.getColorModel());
                    bmp.setPreferredChunkyColorModel(f.getColorModel());
                    bmp.convertToChunky();

                    String formatted = zeros + i;
                    formatted = formatted.substring(formatted.length() - zeros.length());
                    File outFile = new File(outDir, formatted + ".png");
                    pm.setNote("Writing " + outFile.getName() + "...");

                    // FIXME - Detect number of bits per pixel, ensure that correct value is written into video media header atom.
                    // FIXME - Maybe we should quietly enforce 24 bits per pixel
                    FileImageOutputStream imgOut = null;
                    try {
                        imgOut = new FileImageOutputStream(outFile);
                        ImageWriter iw = (ImageWriter) ImageIO.getImageWritersByMIMEType("image/png").next();
                        ImageWriteParam iwParam = iw.getDefaultWriteParam();
                        iw.setOutput(imgOut);
                        IIOImage iioImg = new IIOImage(img, null, null);
                        iw.write(null, iioImg, iwParam);
                        iw.dispose();
                    } finally {
                        if (imgOut != null) {
                            imgOut.close();
                        }
                    }
                    pm.setProgress(i);
                }


            } else {
                throw new IOException("No frames in " + inFile.getName());
            }

        } finally {
            if (in != null) {
                in.close();
            }
            pm.close();
        }
        return outDir;
    }

    public File convertToANIM(File inFile, boolean variableFramerate) throws IOException {
        File outFile = new File(inFile + ".anim");
        for (int i = 2; outFile.exists() && i < 1000; i++) {
            outFile = new File(inFile + " " + i + ".anim");
        }
        ProgressMonitor pm = null;
        SEQReader reader=null;
        try {

            pm = new ProgressMonitor(this, "Converting to " + outFile.getName(), "Reading " + inFile.getName() + "...", 0, 2);
             reader=new SEQReader(inFile, variableFramerate);
            pm.setMaximum(reader.getFrameCount());
            pm.setNote("Writing "+outFile.getName()+"...");

            ANIMWriter anim = new ANIMWriter(outFile);
            anim.setCAMG(0x11000);
            int jiffies=60;
            anim.setJiffies(jiffies);
            BitmapImage bmp = reader.createCompatibleBitmap();
            bmp.setEnforceDirectColors(true);
            for (int i = 0, n = reader.getFrameCount(); i < n && !pm.isCanceled(); i++) {
                reader.readFrame(i, bmp);
                anim.writeFrame(bmp, (int)(reader.getDuration(i)*jiffies/reader.getTimeBase()));
                pm.setNote("Writing frame " + i + "...");
                pm.setProgress(i + 1);
            }
            pm.setNote("Writing ANIM headers...");
            anim.close();
            pm.close();
            pm = null;
        } finally {
            if (reader != null) {
                reader.close();
            }
            if (pm != null) {
                pm.close();
            }
        }
        return outFile;
    }
    public File convertToANIM_OLD(File inFile, boolean variableFramerate) throws IOException {
        File outFile = new File(inFile + ".anim");
        for (int i = 2; outFile.exists() && i < 1000; i++) {
            outFile = new File(inFile + " " + i + ".anim");
        }
        ProgressMonitor pm = null;
        ImageInputStream in = null;
        try {

            pm = new ProgressMonitor(this, "Converting to " + outFile.getName(), "Reading " + inFile.getName() + "...", 0, 2);
            in = new FileImageInputStream(inFile);
            SEQDecoder decoder = new SEQDecoder(in);
            SEQMovieTrack track = new SEQMovieTrack();
            decoder.produce(track, false);
            if (variableFramerate) {
                pm.setNote("Removing duplicate frames...");
                int removed = removeDuplicateFrames(track);
               // System.out.println(removed + " frames removed");
            }
            pm.setMaximum(track.getFrameCount());
            pm.setNote("Writing "+outFile.getName()+"...");

            ANIMWriter anim = new ANIMWriter(outFile);
            anim.setCAMG(0x11000);
            int jiffies=60;
            anim.setJiffies(jiffies);
            int width = track.getWidth();
            int height = track.getHeight();
            BitmapImage bmp = new BitmapImage(width, height, track.getNbPlanes(), track.getFrame(0).getColorModel());
            bmp.setEnforceDirectColors(true);
            for (int i = 0, n = track.getFrameCount(); i < n && !pm.isCanceled(); i++) {
                SEQFrame frame=track.getFrame(i);
                frame.decode(bmp, track);
                bmp.setPlanarColorModel(frame.getColorModel());
                anim.writeFrame(bmp, (int)(frame.getRelTime()*jiffies/track.getJiffies()));
                pm.setNote("Writing frame " + i + "...");
                pm.setProgress(i + 1);
            }
            pm.setNote("Writing ANIM headers...");
            anim.close();
            pm.close();
            pm = null;
        } finally {
            if (in != null) {
                in.close();
            }
            if (pm != null) {
                pm.close();
            }
        }
        return outFile;
    }

    private File convertToAVI_Raw24(File inFile) throws IOException {
        File outFile = new File(inFile + ".avi");
        /*
        for (int i = 2; outFile.exists() && i < 1000; i++) {
            outFile = new File(inFile + " " + i + ".avi");
        }

        ImageInputStream in = null;
        AVIOutputStream avi = null;
        ProgressMonitor pm = new ProgressMonitor(this, "Converting to " + outFile.getName(), "Reading " + inFile.getName() + "...", 0, 2);
        try {
            in = new FileImageInputStream(inFile);
            pm.setProgress(1);
            SEQDecoder decoder = new SEQDecoder(in);
            SEQMovieTrack track = new SEQMovieTrack();
            decoder.produce(track, false);
            pm.setMaximum(track.getFrameCount() + 2);
            int width = track.getWidth();
            int height = track.getHeight();

            BitmapImage bmp = new BitmapImage(width, height, track.getNbPlanes(), track.getFrame(0).getColorModel());
            bmp.setEnforceDirectColors(true);
            BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
            int[] buf = ((DataBufferInt) img.getRaster().getDataBuffer()).getData();
            bmp.setIntPixels(buf);
            avi=null;// FIXME
            
            avi = new AVIOutputStream(outFile, VideoFormat.AVI_DIB,24);
            avi.setTimeScale(1);
            avi.setFrameRate(track.getJiffies() / (int) track.getFrameDuration(0));
//            avi.setFrameRate((int) track.getFrameDuration(0));
            for (int i = 0, n = track.getFrameCount(); i < n && !pm.isCanceled(); i++) {
                track.getFrame(i).decode(bmp, track);
                bmp.setPlanarColorModel(track.getFrame(i).getColorModel());
                bmp.convertToChunky();
                //img.setRGB(0, 0, width, height, bmp.getIntPixels(), 0, width);
                avi.writeFrame(img);
                pm.setNote("Writing frame " + i + "...");
                pm.setProgress(i + 1);
            }
            pm.setNote("Writing AVI headers...");
            avi.close();
            avi = null;
        } finally {
            if (in != null) {
                in.close();
            }
            if (avi != null) {
                avi.close();
            }
            if (pm != null) {
                pm.close();
            }
        }*/
        return outFile;
    }

    private File convertToAVI_Raw8(File inFile) throws IOException {
        File outFile = new File(inFile + ".avi");
        /*
        for (int i = 2; outFile.exists() && i < 1000; i++) {
            outFile = new File(inFile + " " + i + ".avi");
        }

        ImageInputStream in = null;
        AVIOutputStream avi = null;
        ProgressMonitor pm = new ProgressMonitor(this, "Converting to " + outFile.getName(), "Reading " + inFile.getName() + "...", 0, 2);
        try {
            in = new FileImageInputStream(inFile);
            pm.setProgress(1);
            SEQDecoder decoder = new SEQDecoder(in);
            SEQMovieTrack track = new SEQMovieTrack();
            decoder.produce(track, false);
            pm.setMaximum(track.getFrameCount() + 2);
            int width = track.getWidth();
            int height = track.getHeight();

            BitmapImage bmp = new BitmapImage(width, height, track.getNbPlanes(), track.getFrame(0).getColorModel());
            bmp.setEnforceDirectColors(false);
            bmp.setPreferredChunkyColorModel(track.getFrame(0).getColorModel());
            BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_INDEXED, (IndexColorModel) track.getFrame(0).getColorModel());
            byte[] buf = ((DataBufferByte) img.getRaster().getDataBuffer()).getData();
            bmp.setBytePixels(buf);
            avi = new AVIOutputStream(outFile, AVIOutputStream.AVIVideoFormat.RAW,8);
            avi.setTimeScale(1);
            avi.setPalette((IndexColorModel) track.getFrame(0).getColorModel());
            avi.setFrameRate(track.getJiffies() / (int) track.getFrameDuration(0));
//            avi.setFrameRate((int) track.getFrameDuration(0));

            int[] imgRGBs = new int[16];
            int[] previousRGBs = new int[16];
            ((IndexColorModel) track.getFrame(0).getColorModel()).getRGBs(imgRGBs);
            ((IndexColorModel) track.getFrame(0).getColorModel()).getRGBs(previousRGBs);


            for (int i = 0, n = track.getFrameCount(); i < n && !pm.isCanceled(); i++) {
                SEQFrame f = track.getFrame(i);
                ((IndexColorModel) f.getColorModel()).getRGBs(imgRGBs);
                if (!Arrays.equals(imgRGBs, previousRGBs)) {
                    img = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_INDEXED, (IndexColorModel) f.getColorModel());
                    buf = ((DataBufferByte) img.getRaster().getDataBuffer()).getData();
                    bmp.setBytePixels(buf);
                    System.arraycopy(imgRGBs, 0, previousRGBs, 0, imgRGBs.length);
                }

                f.decode(bmp, track);
                bmp.setPlanarColorModel(f.getColorModel());
                bmp.convertToChunky();
                buf[0] = (byte) i;
                avi.writeFrame(img);
                pm.setNote("Writing frame " + i + "...");
                pm.setProgress(i + 1);

            }
            pm.setNote("Writing AVI headers...");
            avi.close();
            avi = null;
        } finally {
            if (in != null) {
                in.close();
            }
            if (avi != null) {
                avi.close();
            }
            if (pm != null) {
                pm.close();
            }
        }*/
        return outFile;
    }

    /** This does not work, because Windows Media Player can not play back
     * 4-bit videos. */
    private File convertToAVI_Raw4(File inFile) throws IOException {
        File outFile = new File(inFile + ".avi");
        /*
        for (int i = 2; outFile.exists() && i < 1000; i++) {
            outFile = new File(inFile + " " + i + ".avi");
        }

        ImageInputStream in = null;
        AVIOutputStream avi = null;
        ProgressMonitor pm = new ProgressMonitor(this, "Converting to " + outFile.getName(), "Reading " + inFile.getName() + "...", 0, 2);
        try {
            in = new FileImageInputStream(inFile);
            pm.setProgress(1);
            SEQDecoder decoder = new SEQDecoder(in);
            decoder.setEnforce8BitColorModel(false);
            SEQMovieTrack track = new SEQMovieTrack();
            decoder.produce(track, false);
            pm.setMaximum(track.getFrameCount() + 2);
            int width = track.getWidth();
            int height = track.getHeight();

            BitmapImage bmp = new BitmapImage(width, height, track.getNbPlanes(), track.getFrame(0).getColorModel());
            bmp.setEnforceDirectColors(false);
            bmp.setPreferredChunkyColorModel(track.getFrame(0).getColorModel());
            BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_INDEXED, (IndexColorModel) track.getFrame(0).getColorModel());
            byte[] buf = ((DataBufferByte) img.getRaster().getDataBuffer()).getData();
            bmp.setBytePixels(buf);
            avi = new AVIOutputStream(outFile, AVIOutputStream.AVIVideoFormat.RAW,4);
            avi.setTimeScale(1);
            avi.setPalette((IndexColorModel) track.getFrame(0).getColorModel());
            avi.setFrameRate(track.getJiffies() / (int) track.getFrameDuration(0));
//            avi.setFrameRate((int) track.getFrameDuration(0));

            int[] imgRGBs = new int[16];
            int[] previousRGBs = new int[16];
            ((IndexColorModel) track.getFrame(0).getColorModel()).getRGBs(imgRGBs);
            ((IndexColorModel) track.getFrame(0).getColorModel()).getRGBs(previousRGBs);


            for (int i = 0, n = track.getFrameCount(); i < n; i++) {
                SEQFrame f = track.getFrame(i);
                ((IndexColorModel) f.getColorModel()).getRGBs(imgRGBs);
                if (!Arrays.equals(imgRGBs, previousRGBs)) {
                    img = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_INDEXED, (IndexColorModel) f.getColorModel());
                    buf = ((DataBufferByte) img.getRaster().getDataBuffer()).getData();
                    bmp.setBytePixels(buf);
                    System.arraycopy(imgRGBs, 0, previousRGBs, 0, imgRGBs.length);
                }

                f.decode(bmp, track);
                bmp.setPlanarColorModel(f.getColorModel());
                bmp.convertToChunky();
                buf[0] = (byte) i;
                avi.writeFrame(img);
                pm.setNote("Writing frame " + i + "...");
                pm.setProgress(i + 1);

            }
            pm.setNote("Writing AVI headers...");
            avi.close();
            avi = null;
        } finally {
            if (in != null) {
                in.close();
            }
            if (avi != null) {
                avi.close();
            }
            if (pm != null) {
                pm.close();
            }
        }*/
        return outFile;
    }
    private File convertToAVI_RLE8(File inFile) throws IOException {
        File outFile = new File(inFile + ".avi");
        /*
        for (int i = 2; outFile.exists() && i < 1000; i++) {
            outFile = new File(inFile + " " + i + ".avi");
        }

        ImageInputStream in = null;
        AVIOutputStream avi = null;
        ProgressMonitor pm = new ProgressMonitor(this, "Converting to " + outFile.getName(), "Reading " + inFile.getName() + "...", 0, 2);
        try {
            in = new FileImageInputStream(inFile);
            pm.setProgress(1);
            SEQDecoder decoder = new SEQDecoder(in);
            SEQMovieTrack track = new SEQMovieTrack();
            decoder.produce(track, false);
            pm.setMaximum(track.getFrameCount() + 2);
            int width = track.getWidth();
            int height = track.getHeight();

            BitmapImage bmp = new BitmapImage(width, height, track.getNbPlanes(), track.getFrame(0).getColorModel());
            bmp.setEnforceDirectColors(false);
            bmp.setPreferredChunkyColorModel(track.getFrame(0).getColorModel());
            BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_INDEXED, (IndexColorModel) track.getFrame(0).getColorModel());
            byte[] buf = ((DataBufferByte) img.getRaster().getDataBuffer()).getData();
            bmp.setBytePixels(buf);
            avi = new AVIOutputStream(outFile, AVIOutputStream.AVIVideoFormat.RLE,8);
            avi.setTimeScale(1);
            avi.setPalette((IndexColorModel) track.getFrame(0).getColorModel());
            avi.setFrameRate(track.getJiffies() / (int) track.getFrameDuration(0));
//            avi.setFrameRate((int) track.getFrameDuration(0));

            int[] imgRGBs = new int[16];
            int[] previousRGBs = new int[16];
            ((IndexColorModel) track.getFrame(0).getColorModel()).getRGBs(imgRGBs);
            ((IndexColorModel) track.getFrame(0).getColorModel()).getRGBs(previousRGBs);


            for (int i = 0, n = track.getFrameCount(); i < n && !pm.isCanceled(); i++) {
                SEQFrame f = track.getFrame(i);
                ((IndexColorModel) f.getColorModel()).getRGBs(imgRGBs);
                if (!Arrays.equals(imgRGBs, previousRGBs)) {
                    img = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_INDEXED, (IndexColorModel) f.getColorModel());
                    buf = ((DataBufferByte) img.getRaster().getDataBuffer()).getData();
                    bmp.setBytePixels(buf);
                    System.arraycopy(imgRGBs, 0, previousRGBs, 0, imgRGBs.length);
                }

                f.decode(bmp, track);
                bmp.setPlanarColorModel(f.getColorModel());
                bmp.convertToChunky();
                buf[0] = (byte) i;
                avi.writeFrame(img);
                pm.setNote("Writing frame " + i + "...");
                pm.setProgress(i + 1);

            }
            pm.setNote("Writing AVI headers...");
            avi.close();
            avi = null;
        } finally {
            if (in != null) {
                in.close();
            }
            if (avi != null) {
                avi.close();
            }
            if (pm != null) {
                pm.close();
            }
        }*/
        return outFile;
    }

    private File convertToQuickTime_PNG24(File inFile, boolean variableFramerate) throws IOException {
        File outFile = new File(inFile + ".mov");
        /*
        for (int i = 2; outFile.exists() && i < 1000; i++) {
            outFile = new File(inFile + " " + i + ".mov");
        }

        ImageInputStream in = null;
        QuickTimeWriter mov = null;
        ProgressMonitor pm = new ProgressMonitor(this, "Converting to " + outFile.getName(), "Reading " + inFile.getName() + "...", 0, 2);
        try {
            in = new FileImageInputStream(inFile);
            pm.setProgress(1);
            SEQDecoder decoder = new SEQDecoder(in);
            SEQMovieTrack track = new SEQMovieTrack();
            decoder.produce(track, false);
            if (variableFramerate) {
                pm.setNote("Removing duplicate frames...");
                int removed = removeDuplicateFrames(track);
               // System.out.println(removed + " frames removed");
            }

            pm.setMaximum(track.getFrameCount() + 2);
            int width = track.getWidth();
            int height = track.getHeight();

            BitmapImage bmp = new BitmapImage(width, height, track.getNbPlanes(), track.getFrame(0).getColorModel());
            bmp.setEnforceDirectColors(true);


            BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
            int[] buf = ((DataBufferInt) img.getRaster().getDataBuffer()).getData();
            bmp.setIntPixels(buf);
            mov = new QuickTimeWriter(outFile);
            mov.addVideoTrack(QuickTimeWriter.VIDEO_PNG, 6000, width, height);
            mov.setMovieTimeScale(6000);
            //            avi.setFrameRate((int) track.getFrameDuration(0));
            for (int i = 0, n = track.getFrameCount(); i < n && !pm.isCanceled(); i++) {
                SEQFrame f = track.getFrame(i);
                f.decode(bmp, track);
                bmp.setPlanarColorModel(f.getColorModel());
                // bmp.setPreferredChunkyColorModel(f.getVideoColorTable());
                bmp.convertToChunky();
                //img.setRGB(0, 0, width, height, bmp.getIntPixels(), 0, width);
                mov.writeFrame(0, img, f.getRelTime());
                pm.setNote("Writing frame " + i + "...");
                pm.setProgress(i + 1);
            }
            pm.setNote("Writing QuickTime headers...");
            mov.close();
            mov = null;
        } finally {
            if (in != null) {
                in.close();
            }
            if (mov != null) {
                mov.close();
            }
            if (pm != null) {
                pm.close();
            }
        }*/
        return outFile;
    }
    private File convertToQuickTime_RLE16(File inFile, boolean variableFramerate) throws IOException {
        File outFile = new File(inFile + ".mov");
        /*
        File tmpFile = new File(inFile + ".tmp.mov");
        for (int i = 2; outFile.exists() && i < 1000; i++) {
            outFile = new File(inFile + " " + i + ".mov");
            tmpFile = new File(inFile + " " + i + ".tmp.mov");
        }

        ImageInputStream in = null;
        QuickTimeWriter mov = null;
        ProgressMonitor pm = new ProgressMonitor(this, "Converting to " + outFile.getName(), "Reading " + inFile.getName() + "...", 0, 2);
        try {
            in = new FileImageInputStream(inFile);
            pm.setProgress(1);
            SEQDecoder decoder = new SEQDecoder(in);
            SEQMovieTrack track = new SEQMovieTrack();
            decoder.produce(track, false);
            if (variableFramerate) {
                pm.setNote("Removing duplicate frames...");
                int removed = removeDuplicateFrames(track);
               // System.out.println(removed + " frames removed");
            }

            pm.setMaximum(track.getFrameCount() + 3);
            int width = track.getWidth();
            int height = track.getHeight();

            BitmapImage bmp = new BitmapImage(width, height, track.getNbPlanes(), track.getFrame(0).getColorModel());
            bmp.setPreferredChunkyColorModel(new DirectColorModel(16,0x7c,0x3e,0x1f));


            BufferedImage img = new BufferedImage(width,height, BufferedImage.TYPE_USHORT_555_RGB);
            short[] buf = ((DataBufferUShort) img.getRaster().getDataBuffer()).getData();
            bmp.setShortPixels(buf);
            mov = new QuickTimeWriter(tmpFile);
            mov.addVideoTrack("rle ","Animation", 6000, width, height, 16, 30);
            mov.setMovieTimeScale(6000);
            //            avi.setFrameRate((int) track.getFrameDuration(0));
            for (int i = 0, n = track.getFrameCount(); i < n && !pm.isCanceled(); i++) {
                SEQFrame f = track.getFrame(i);
                f.decode(bmp, track);
                bmp.setPlanarColorModel(f.getColorModel());
                // bmp.setPreferredChunkyColorModel(f.getVideoColorTable());
                bmp.convertToChunky();
                //img.setRGB(0, 0, width, height, bmp.getIntPixels(), 0, width);
                mov.writeFrame(0, img, f.getRelTime());

                pm.setNote("Writing frame " + i + "...");
                pm.setProgress(i + 1);
            }
            pm.setNote("Writing QuickTime headers...");

            pm.setNote("Writing Web-optimized version of the movie.");
            
            mov.toWebOptimizedMovie(outFile,true);

            mov.close();
            tmpFile.delete();
            mov = null;
        } finally {
            if (in != null) {
                in.close();
            }
            if (mov != null) {
                mov.close();
            }
            if (pm != null) {
                pm.close();
            }
        }*/
        return outFile;
    }

    /** This does not work, because QuickTime does not use the color table
     * that we provide in the file. */
    private File convertToQuickTime_RAW8(File inFile, boolean removeDuplicates) throws IOException {
        File outFile = new File(inFile + ".mov");
        /*
        for (int i = 2; outFile.exists() && i < 1000; i++) {
            outFile = new File(inFile + " " + i + ".mov");
        }

        ImageInputStream in = null;
        QuickTimeWriter mov = null;
        ProgressMonitor pm = new ProgressMonitor(this, "Converting to " + outFile.getName(), "Reading " + inFile.getName() + "...", 0, 2);
        try {
            in = new FileImageInputStream(inFile);
            pm.setProgress(1);
            SEQDecoder decoder = new SEQDecoder(in);
            decoder.setEnforce8BitColorModel(true);
            SEQMovieTrack track = new SEQMovieTrack();
            decoder.produce(track, false);
            if (removeDuplicates) {
                pm.setNote("Removing duplicate frames...");
                int removed = removeDuplicateFrames(track);
               // System.out.println(removed + " frames removed");
            }
            pm.setMaximum(track.getFrameCount() + 2);
            int width = track.getWidth();
            int height = track.getHeight();

            SEQFrame f0 = track.getFrame(0);
            BitmapImage bmp = new BitmapImage(width, height, track.getNbPlanes(), f0.getColorModel());
            bmp.setPreferredChunkyColorModel(f0.getColorModel());
            BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_INDEXED, (IndexColorModel) f0.getColorModel());
            byte[] buf = ((DataBufferByte) img.getRaster().getDataBuffer()).getData();
            bmp.setBytePixels(buf);
            mov = new QuickTimeWriter(outFile);
            mov.setMovieTimeScale(6000);
            mov.addVideoTrack("raw ", "None", 6000, width, height, 8, 1);
            mov.setVideoColorTable(0, (IndexColorModel) f0.getColorModel());

            //            avi.setFrameRate((int) track.getFrameDuration(0));
            int[] imgRGBs = new int[16];
            int[] previousRGBs = new int[16];
            ((IndexColorModel) track.getFrame(0).getColorModel()).getRGBs(imgRGBs);
            ((IndexColorModel) track.getFrame(0).getColorModel()).getRGBs(previousRGBs);

            for (int i = 0, n = track.getFrameCount(); i < n && !pm.isCanceled(); i++) {
                SEQFrame f = track.getFrame(i);
                ((IndexColorModel) f.getColorModel()).getRGBs(imgRGBs);
                if (!Arrays.equals(imgRGBs, previousRGBs)) {
                    img = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_INDEXED, (IndexColorModel) f.getColorModel());
                    buf = ((DataBufferByte) img.getRaster().getDataBuffer()).getData();
                    bmp.setBytePixels(buf);
                    System.arraycopy(imgRGBs, 0, previousRGBs, 0, imgRGBs.length);
                }
                f.decode(bmp, track);
                bmp.setPlanarColorModel(f.getColorModel());
                bmp.setPreferredChunkyColorModel(f.getColorModel());
                bmp.convertToChunky();
                //img.setRGB(0, 0, width, height, bmp.getIntPixels(), 0, width);
                mov.writeFrame(0, img, f.getRelTime());
                pm.setNote("Writing frame " + i + "...");
                pm.setProgress(i + 1);
            }
            pm.setNote("Writing QuickTime headers...");
            mov.close();
            mov = null;
        } finally {
            if (in != null) {
                in.close();
            }
            if (mov != null) {
                mov.close();
            }
            if (pm != null) {
                pm.close();
            }
        }*/
        return outFile;
    }

    /** This does not work, because QuickTime does not use the color table
     * that we provide in the file. */
    private File convertToQuickTime_PNG8(File inFile, boolean variableFramerate) throws IOException {
        File outFile = new File(inFile + ".mov");
        /*
        for (int i = 2; outFile.exists() && i < 1000; i++) {
            outFile = new File(inFile + " " + i + ".mov");
        }

        ImageInputStream in = null;
        QuickTimeWriter mov = null;
        ProgressMonitor pm = new ProgressMonitor(this, "Converting to " + outFile.getName(), "Reading " + inFile.getName() + "...", 0, 2);
        try {
            in = new FileImageInputStream(inFile);
            pm.setProgress(1);
            SEQDecoder decoder = new SEQDecoder(in);
            decoder.setEnforce8BitColorModel(true);
            SEQMovieTrack track = new SEQMovieTrack();
            decoder.produce(track, false);
            if (variableFramerate) {
                pm.setNote("Removing duplicate frames...");
                int removed = removeDuplicateFrames(track);
               // System.out.println(removed + " frames removed");
            }

            pm.setMaximum(track.getFrameCount() + 2);
            int width = track.getWidth();
            int height = track.getHeight();

            SEQFrame f0 = track.getFrame(0);
            BitmapImage bmp = new BitmapImage(width, height, track.getNbPlanes(), f0.getColorModel());
            bmp.setPreferredChunkyColorModel(f0.getColorModel());
            BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_INDEXED, (IndexColorModel) f0.getColorModel());
            byte[] buf = ((DataBufferByte) img.getRaster().getDataBuffer()).getData();
            bmp.setBytePixels(buf);
            mov = new QuickTimeWriter(outFile);
            mov.setMovieTimeScale(6000);
            mov.addVideoTrack("png ", "PNG", 6000, width, height, 8, 1);
            mov.setVideoColorTable(0, (IndexColorModel) f0.getColorModel());

            //            avi.setFrameRate((int) track.getFrameDuration(0));
            int[] imgRGBs = new int[16];
            int[] previousRGBs = new int[16];
            ((IndexColorModel) track.getFrame(0).getColorModel()).getRGBs(imgRGBs);
            ((IndexColorModel) track.getFrame(0).getColorModel()).getRGBs(previousRGBs);

            for (int i = 0, n = track.getFrameCount(); i < n && !pm.isCanceled(); i++) {
                SEQFrame f = track.getFrame(i);
                ((IndexColorModel) f.getColorModel()).getRGBs(imgRGBs);
                if (!Arrays.equals(imgRGBs, previousRGBs)) {
                    img = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_INDEXED, (IndexColorModel) f.getColorModel());
                    buf = ((DataBufferByte) img.getRaster().getDataBuffer()).getData();
                    bmp.setBytePixels(buf);
                    System.arraycopy(imgRGBs, 0, previousRGBs, 0, imgRGBs.length);
                }
                f.decode(bmp, track);
                bmp.setPlanarColorModel(f.getColorModel());
                bmp.setPreferredChunkyColorModel(f.getColorModel());
                bmp.convertToChunky();
                //img.setRGB(0, 0, width, height, bmp.getIntPixels(), 0, width);
                mov.writeFrame(0, img, f.getRelTime());
                pm.setNote("Writing frame " + i + "...");
                pm.setProgress(i + 1);
            }
            pm.setNote("Writing QuickTime headers...");
            mov.close();
            mov = null;
        } finally {
            if (in != null) {
                in.close();
            }
            if (mov != null) {
                mov.close();
            }
            if (pm != null) {
                pm.close();
            }
        }*/
        return outFile;
    }

    private int removeDuplicateFrames(SEQMovieTrack track) {
        int width = track.getWidth();
        int height = track.getHeight();

        SEQFrame f0 = track.getFrame(0);
        BitmapImage bmp = new BitmapImage(width, height, track.getNbPlanes(), f0.getColorModel());
        bmp.setPreferredChunkyColorModel(f0.getColorModel());
        byte[] previousBmp = new byte[bmp.getBitmap().length];
        int[] previousColors = new int[16];
        int[] colors = new int[16];

        int removed = 0;
        SEQFrame previousF = f0;
        for (int i = 1, n = track.getFrameCount(); i < n; i++) {
            SEQFrame f = track.getFrame(i);
            f.decode(bmp, track);

            ((IndexColorModel) f.getColorModel()).getRGBs(colors);
            if (Arrays.equals(bmp.getBitmap(), previousBmp)
                    && Arrays.equals(colors, previousColors)) {
                previousF.setRelTime(previousF.getRelTime() + f.getRelTime());
                track.removeFrame(i);
                --n;
                --i;
                ++removed;
                continue;
            } else {
                System.arraycopy(colors, 0, previousColors, 0, 16);
                System.arraycopy(bmp.getBitmap(), 0, previousBmp, 0, previousBmp.length);
            }

            previousF = f;
        }
        return removed;
    }

    /** This method is called from within the constructor to
     * initialize the form.
     * WARNING: Do NOT modify this code. The content of this method is
     * always regenerated by the Form Editor.
     */
    @SuppressWarnings("unchecked")
    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
    private void initComponents() {

        outputGroup = new javax.swing.ButtonGroup();
        label = new javax.swing.JLabel();
        menuBar = new javax.swing.JMenuBar();
        optionsMenu = new javax.swing.JMenu();
        toAnimItem = new javax.swing.JRadioButtonMenuItem();
        toAVIItem = new javax.swing.JRadioButtonMenuItem();
        toQuickTimeItem = new javax.swing.JRadioButtonMenuItem();
        toIlbmItem = new javax.swing.JRadioButtonMenuItem();
        toPNGItem = new javax.swing.JRadioButtonMenuItem();
        jSeparator1 = new javax.swing.JPopupMenu.Separator();
        variableFrameRateItem = new javax.swing.JCheckBoxMenuItem();
        helpMenu = new javax.swing.JMenu();
        aboutItem = new javax.swing.JMenuItem();

        FormListener formListener = new FormListener();

        setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
        setTitle("SEQ to ILBM");

        label.setHorizontalAlignment(javax.swing.SwingConstants.CENTER);
        label.setText("Drop SEQ file here.");
        getContentPane().add(label, java.awt.BorderLayout.CENTER);

        optionsMenu.setText("Options");

        outputGroup.add(toAnimItem);
        toAnimItem.setSelected(true);
        toAnimItem.setText("Convert to ANIM");
        toAnimItem.setActionCommand("ANIM");
        toAnimItem.addActionListener(formListener);
        optionsMenu.add(toAnimItem);

        outputGroup.add(toAVIItem);
        toAVIItem.setText("Convert to AVI");
        toAVIItem.setActionCommand("AVI");
        toAVIItem.addActionListener(formListener);
        optionsMenu.add(toAVIItem);

        outputGroup.add(toQuickTimeItem);
        toQuickTimeItem.setText("Convert to QuickTime");
        toQuickTimeItem.setActionCommand("QuickTime");
        toQuickTimeItem.addActionListener(formListener);
        optionsMenu.add(toQuickTimeItem);

        outputGroup.add(toIlbmItem);
        toIlbmItem.setText("Convert to ILBM Image Sequence");
        toIlbmItem.setActionCommand("ILBM");
        toIlbmItem.addActionListener(formListener);
        optionsMenu.add(toIlbmItem);

        outputGroup.add(toPNGItem);
        toPNGItem.setText("Convert to PNG Image Sequence");
        toPNGItem.setActionCommand("PNG");
        toPNGItem.addActionListener(formListener);
        optionsMenu.add(toPNGItem);
        optionsMenu.add(jSeparator1);

        variableFrameRateItem.setSelected(true);
        variableFrameRateItem.setText("Use Variable Frame Rate");
        variableFrameRateItem.addActionListener(formListener);
        optionsMenu.add(variableFrameRateItem);

        menuBar.add(optionsMenu);

        helpMenu.setText("Help");

        aboutItem.setText("About");
        aboutItem.addActionListener(formListener);
        helpMenu.add(aboutItem);

        menuBar.add(helpMenu);

        setJMenuBar(menuBar);

        pack();
    }

    // Code for dispatching events from components to event handlers.

    private class FormListener implements java.awt.event.ActionListener {
        FormListener() {}
        public void actionPerformed(java.awt.event.ActionEvent evt) {
            if (evt.getSource() == toIlbmItem) {
                Main.this.outputFormatPerformed(evt);
            }
            else if (evt.getSource() == toAnimItem) {
                Main.this.outputFormatPerformed(evt);
            }
            else if (evt.getSource() == toAVIItem) {
                Main.this.outputFormatPerformed(evt);
            }
            else if (evt.getSource() == toQuickTimeItem) {
                Main.this.outputFormatPerformed(evt);
            }
            else if (evt.getSource() == aboutItem) {
                Main.this.aboutPerformed(evt);
            }
            else if (evt.getSource() == toPNGItem) {
                Main.this.outputFormatPerformed(evt);
            }
            else if (evt.getSource() == variableFrameRateItem) {
                Main.this.variableFrameRateItemPerformed(evt);
            }
        }
    }// </editor-fold>//GEN-END:initComponents

    private void outputFormatPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_outputFormatPerformed
        Preferences prefs = Preferences.userNodeForPackage(Main.class);

        prefs.put("outputFormat", evt.getActionCommand());
        updateEnabled();
        updateTitle();
    }//GEN-LAST:event_outputFormatPerformed

    private void aboutPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_aboutPerformed
        String version = Main.class.getPackage().getImplementationVersion();
        if (version == null) {
            version = "";
        }
        JOptionPane.showMessageDialog(this, "<html><b>SEQ Converter " + version + "</b><br><br>© Werner Randelshofer<br>All rights reserved.");
    }//GEN-LAST:event_aboutPerformed

    private void variableFrameRateItemPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_variableFrameRateItemPerformed
        Preferences prefs = Preferences.userNodeForPackage(Main.class);

        prefs.putBoolean("variableFrameRate", variableFrameRateItem.isSelected());
        updateTitle();
    }//GEN-LAST:event_variableFrameRateItemPerformed

    /**
     * @param args the command line arguments
     */
    public static void main(String args[]) {
        System.setProperty("apple.laf.useScreenMenuBar", "true");
        System.setProperty("com.apple.macos.useScreenMenuBar", "true");
        try {
            UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
        } catch (Exception ex) {
        }
        java.awt.EventQueue.invokeLater(new Runnable() {

            @Override
            public void run() {
                new Main().setVisible(true);
            }
        });
    }
    // Variables declaration - do not modify//GEN-BEGIN:variables
    private javax.swing.JMenuItem aboutItem;
    private javax.swing.JMenu helpMenu;
    private javax.swing.JPopupMenu.Separator jSeparator1;
    private javax.swing.JLabel label;
    private javax.swing.JMenuBar menuBar;
    private javax.swing.JMenu optionsMenu;
    private javax.swing.ButtonGroup outputGroup;
    private javax.swing.JRadioButtonMenuItem toAVIItem;
    private javax.swing.JRadioButtonMenuItem toAnimItem;
    private javax.swing.JRadioButtonMenuItem toIlbmItem;
    private javax.swing.JRadioButtonMenuItem toPNGItem;
    private javax.swing.JRadioButtonMenuItem toQuickTimeItem;
    private javax.swing.JCheckBoxMenuItem variableFrameRateItem;
    // End of variables declaration//GEN-END:variables

    private void updateTitle() {
        Preferences prefs = Preferences.userNodeForPackage(Main.class);
        setTitle("SEQ to " + prefs.get("outputFormat", "ANIM"));
    }

    private void updateEnabled() {
        Preferences prefs = Preferences.userNodeForPackage(Main.class);
        String format = prefs.get("outputFormat", "ANIM");
        variableFrameRateItem.setEnabled(format.equals("ANIM") || format.equals("QuickTime"));
    }
}
