/**
 * @(#)AVIOutputStream.java
 *
 * Copyright (c) 2011-2012 Werner Randelshofer, Goldau, 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 org.monte.media.avi;

import java.awt.image.ColorModel;
import org.monte.media.riff.RIFFChunk;
import org.monte.media.math.Rational;
import java.util.ArrayList;
import org.monte.media.Format;
import org.monte.media.riff.RIFFParser;
import java.awt.Dimension;
import java.awt.image.IndexColorModel;
import java.io.*;
import java.nio.ByteOrder;
import javax.imageio.stream.*;
import static java.lang.Math.*;
import static org.monte.media.FormatKeys.*;
import static org.monte.media.AudioFormatKeys.*;
import static org.monte.media.VideoFormatKeys.*;

/**
 * Provides low-level support for writing already encoded audio and video
 * samples into an AVI 1.0 file. <p> The length of an AVI 1.0 file is limited to
 * 1 GB. This class supports lengths of up to 4 GB, but such files may not work
 * on all players. <p> For detailed information about the AVI 1.0 file format
 * see:<br> <a
 * href="http://msdn.microsoft.com/en-us/library/ms779636.aspx">msdn.microsoft.com
 * AVI RIFF</a><br> <a
 * href="http://www.microsoft.com/whdc/archive/fourcc.mspx">www.microsoft.com
 * FOURCC for Video Compression</a><br> <a
 * href="http://www.saettler.com/RIFFMCI/riffmci.html">www.saettler.com
 * RIFF</a><br>
 *
 * @author Werner Randelshofer
 * @version $Id: AVIOutputStream.java 306 2013-01-04 16:19:29Z werner $
 */
public class AVIOutputStream extends AbstractAVIStream {

    /**
     * The states of the movie output stream.
     */
    protected static enum States {

        STARTED, FINISHED, CLOSED;
    }
    /**
     * The current state of the movie output stream.
     */
    protected States state = States.FINISHED;
    /**
     * This chunk holds the whole AVI content.
     */
    protected CompositeChunk aviChunk;
    /**
     * This chunk holds the movie frames.
     */
    protected CompositeChunk moviChunk;
    /**
     * This chunk holds the AVI Main Header.
     */
    protected FixedSizeDataChunk avihChunk;
    ArrayList<Sample> idx1 = new ArrayList<Sample>();

    /**
     * Creates a new instance.
     *
     * @param file the output file
     */
    public AVIOutputStream(File file) throws IOException {
        if (file.exists()) {
            file.delete();
        }
        this.out = new FileImageOutputStream(file);
        out.setByteOrder(ByteOrder.LITTLE_ENDIAN);
        this.streamOffset = 0;
    }

    /**
     * Creates a new instance.
     *
     * @param out the output stream.
     */
    public AVIOutputStream(ImageOutputStream out) throws IOException {
        this.out = out;
        this.streamOffset = out.getStreamPosition();
        out.setByteOrder(ByteOrder.LITTLE_ENDIAN);
    }

    /**
     * Adds a video track.
     *
     * @param fccHandler The 4-character code of the format.
     * @param scale The numerator of the sample rate.
     * @param rate The denominator of the sample rate.
     * @param width The width of a video image. Must be greater than 0.
     * @param height The height of a video image. Must be greater than 0.
     * @param depth The number of bits per pixel. Must be greater than 0.
     * @param syncInterval Interval for sync-samples. 0=automatic. 1=all frames
     * are keyframes. Values larger than 1 specify that for every n-th frame is
     * a keyframe.
     *
     * @return Returns the track index.
     *
     * @throws IllegalArgumentException if the width or the height is smaller
     * than 1.
     */
    public int addVideoTrack(String fccHandler, long scale, long rate, int width, int height, int depth, int syncInterval) throws IOException {
        ensureFinished();
        if (fccHandler == null || fccHandler.length() != 4) {
            throw new IllegalArgumentException("fccHandler must be 4 characters long:" + fccHandler);
        }
        VideoTrack vt = new VideoTrack(tracks.size(), typeToInt(fccHandler),//
                new Format(MediaTypeKey, MediaType.VIDEO,
                MimeTypeKey, MIME_AVI,
                EncodingKey, fccHandler,
                DataClassKey, byte[].class,
                WidthKey, width, HeightKey, height, DepthKey, depth,
                FixedFrameRateKey, true,
                FrameRateKey, new Rational(rate, scale)));
        vt.scale = scale;
        vt.rate = rate;
        vt.syncInterval = syncInterval;
        vt.frameLeft = 0;
        vt.frameTop = 0;
        vt.frameRight = width;
        vt.frameBottom = height;
        vt.bitCount = depth;
        vt.planes = 1; // must be 1

        if (depth == 4) {
            byte[] gray = new byte[16];
            for (int i = 0; i < gray.length; i++) {
                gray[i] = (byte) ((i << 4) | i);
            }
            vt.palette = new IndexColorModel(4, 16, gray, gray, gray);
        } else if (depth == 8) {
            byte[] gray = new byte[256];
            for (int i = 0; i < gray.length; i++) {
                gray[i] = (byte) i;
            }
            vt.palette = new IndexColorModel(8, 256, gray, gray, gray);
        }

        tracks.add(vt);
        return tracks.size() - 1;
    }

    /**
     * Adds an audio track.
     *
     * @param waveFormatTag The format of the audio stream given in MMREG.H, for
     * example 0x0001 for WAVE_FORMAT_PCM.
     * @param scale The numerator of the sample rate.
     * @param rate The denominator of the sample rate.
     * @param numberOfChannels The number of channels: 1 for mono, 2 for stereo.
     * @param sampleSizeInBits The number of bits in a sample: 8 or 16.
     * @param isCompressed Whether the sound is compressed.
     * @param frameDuration The frame duration, expressed in the media’s
     * timescale, where the timescale is equal to the sample rate. For
     * uncompressed formats, this field is always 1.
     * @param frameSize For uncompressed audio, the number of bytes in a sample
     * for a single channel (sampleSize divided by 8). For compressed audio, the
     * number of bytes in a frame.
     *
     * @throws IllegalArgumentException if the format is not 4 characters long,
     * if the time scale is not between 1 and 2^32, if the integer portion of
     * the sampleRate is not equal to the scale, if numberOfChannels is not 1 or
     * 2.
     * @return Returns the track index.
     */
    public int addAudioTrack(int waveFormatTag, //
            long scale, long rate, //
            int numberOfChannels, int sampleSizeInBits, //
            boolean isCompressed, //
            int frameDuration, int frameSize) throws IOException {
        ensureFinished();

        if (scale < 1 || scale > (2L << 32)) {
            throw new IllegalArgumentException("timeScale must be between 1 and 2^32:" + scale);
        }
        if (numberOfChannels != 1 && numberOfChannels != 2) {
            throw new IllegalArgumentException("numberOfChannels must be 1 or 2: " + numberOfChannels);
        }
        if (sampleSizeInBits != 8 && sampleSizeInBits != 16) {
            throw new IllegalArgumentException("sampleSize must be 8 or 16: " + numberOfChannels);
        }

        AudioTrack t = new AudioTrack(tracks.size(), typeToInt("\u0000\u0000\u0000\u0000"));
        t.wFormatTag = waveFormatTag;

        float afSampleRate = (float) rate / (float) scale;

        t.format = new Format(MediaTypeKey, MediaType.AUDIO,
                MimeTypeKey, MIME_AVI,
                EncodingKey, RIFFParser.idToString(waveFormatTag),
                SampleRateKey, Rational.valueOf(afSampleRate),
                SampleSizeInBitsKey, sampleSizeInBits,
                ChannelsKey, numberOfChannels,
                FrameSizeKey, frameSize,
                FrameRateKey, Rational.valueOf(afSampleRate),
                SignedKey, sampleSizeInBits != 8,
                ByteOrderKey, ByteOrder.LITTLE_ENDIAN);

        t.scale = scale;
        t.rate = rate;
        t.samplesPerSec = rate / scale;
        t.channels = numberOfChannels;
        t.avgBytesPerSec = t.samplesPerSec * frameSize;
        t.blockAlign = t.channels * sampleSizeInBits / 8;
        t.bitsPerSample = sampleSizeInBits;
        tracks.add(t);
        return tracks.size() - 1;
    }

    /**
     * Sets the global color palette.
     */
    public void setPalette(int track, ColorModel palette) {
        if (palette instanceof IndexColorModel) {
            ((VideoTrack) tracks.get(track)).palette = (IndexColorModel) palette;
        }
    }

    /**
     * Gets the dimension of a track.
     */
    public Dimension getVideoDimension(int track) {
        Track tr = tracks.get(track);
        if (tr instanceof VideoTrack) {
            VideoTrack vt = (VideoTrack) tr;
            Format fmt = vt.format;
            return new Dimension(fmt.get(WidthKey), fmt.get(HeightKey));
        } else {
            return new Dimension(0, 0);
        }
    }

    /**
     * Returns the contents of the extra track header. Returns null if the
     * header is not present. <p> Note: this method can only be performed before
     * media data has been written into the tracks.
     *
     * @param track
     * @param fourcc
     * @param data the extra header as a byte array
     * @throws IOException
     */
    public void putExtraHeader(int track, String fourcc, byte[] data) throws IOException {
        if (state == States.STARTED) {
            throw new IllegalStateException("Stream headers have already been written!");
        }
        Track tr = tracks.get(track);
        int id = RIFFParser.stringToID(fourcc);
        // Remove duplicate entries
        for (int i = tr.extraHeaders.size() - 1; i >= 0; i--) {
            if (tr.extraHeaders.get(i).getID() == id) {
                tr.extraHeaders.remove(i);
            }
        }

        // Add new entry
        RIFFChunk chunk = new RIFFChunk(STRH_ID, id, data.length, -1);
        chunk.setData(data);
        tr.extraHeaders.add(chunk);
    }

    /**
     * Returns the fourcc's of all extra stream headers.
     *
     * @param track
     * @return An array of fourcc's of all extra stream headers.
     * @throws IOException
     */
    public String[] getExtraHeaderFourCCs(int track) throws IOException {
        Track tr = tracks.get(track);
        String[] fourccs = new String[tr.extraHeaders.size()];
        for (int i = 0; i < fourccs.length; i++) {
            fourccs[i] = RIFFParser.idToString(tr.extraHeaders.get(i).getID());
        }
        return fourccs;
    }

    public void setName(int track, String name) {
        tracks.get(track).name = name;
    }

    /**
     * Sets the compression quality of a track. <p> A value of 0 stands for
     * "high compression is important" a value of 1 for "high image quality is
     * important". <p> Changing this value affects the encoding of video frames
     * which are subsequently written into the track. Frames which have already
     * been written are not changed. <p> This value has no effect on videos
     * encoded with lossless encoders such as the PNG format. <p> The default
     * value is 0.97.
     *
     * @param newValue
     */
    public void setCompressionQuality(int track, float newValue) {
        VideoTrack vt = (VideoTrack) tracks.get(track);
        vt.videoQuality = newValue;
    }

    /**
     * Returns the compression quality of a track.
     *
     * @return compression quality
     */
    public float getCompressionQuality(int track) {
        return ((VideoTrack) tracks.get(track)).videoQuality;
    }    /**
     * Sets the state of the QuickTimeOutpuStream to started. <p> If the state
     * is changed by this method, the prolog is written.
     */
    protected void ensureStarted() throws IOException {
        if (state != States.STARTED) {
            writeProlog();
            state = States.STARTED;
        }
    }

    /**
     * Sets the state of the QuickTimeOutpuStream to finished. <p> If the state
     * is changed by this method, the prolog is written.
     */
    protected void ensureFinished() throws IOException {
        if (state != States.FINISHED) {
            throw new IllegalStateException("Writer is in illegal state for this operation.");
        }
    }

    /**
     * Writes an already encoded palette change into the specified track. <p> If
     * a track contains palette changes, then all key frames must be immediately
     * preceeded by a palette change chunk which also is a key frame. If a key
     * frame is not preceeded by a key frame palette change chunk, it will be
     * downgraded to a delta frame.
     *
     * @throws IllegalArgumentException if the track is not a video track.
     */
    public void writePalette(int track, byte[] data, int off, int len, boolean isKeyframe) throws IOException {
        Track tr = tracks.get(track);
        if (!(tr instanceof VideoTrack)) {
            throw new IllegalArgumentException("Error: track " + track + " is not a video track.");
        }
        if (!isKeyframe && tr.samples.isEmpty()) {
            throw new IllegalStateException("The first sample in a track must be a keyframe.");
        }

        VideoTrack vt = (VideoTrack) tr;
        tr.flags |= STRH_FLAG_VIDEO_PALETTE_CHANGES;

        DataChunk paletteChangeChunk = new DataChunk(vt.twoCC | PC_ID);
        long offset = getRelativeStreamPosition();
        ImageOutputStream pOut = paletteChangeChunk.getOutputStream();
        pOut.write(data, off, len);
        moviChunk.add(paletteChangeChunk);
        paletteChangeChunk.finish();
        long length = getRelativeStreamPosition() - offset;
        Sample s = new Sample(paletteChangeChunk.chunkType, 0, offset, length, isKeyframe);
        tr.addSample(s);
        idx1.add(s);
        //tr.length+=0;  Length is not affected by this chunk!
        offset = getRelativeStreamPosition();
    }

    /**
     * Writes an already encoded sample from a file to the specified track. <p>
     * This method does not inspect the contents of the file. For example, Its
     * your responsibility to only append JPG files if you have chosen the JPEG
     * video format. <p> If you append all frames from files or from input
     * streams, then you have to explicitly set the dimension of the video track
     * before you call finish() or close().
     *
     * @param file The file which holds the sample data.
     *
     * @throws IllegalStateException if the duration is less than 1.
     * @throws IOException if writing the sample data failed.
     */
    public void writeSample(int track, File file, boolean isKeyframe) throws IOException {
        FileInputStream in = null;
        try {
            in = new FileInputStream(file);
            writeSample(track, in, isKeyframe);
        } finally {
            if (in != null) {
                in.close();
            }
        }
    }

    /**
     * Writes an already encoded sample from an input stream to the specified
     * track. <p> This method does not inspect the contents of the file. For
     * example, its your responsibility to only append JPG files if you have
     * chosen the JPEG video format. <p> If you append all frames from files or
     * from input streams, then you have to explicitly set the dimension of the
     * video track before you call finish() or close().
     *
     * @param track The track number.
     * @param in The input stream which holds the sample data.
     * @param isKeyframe True if the sample is a key frame.
     *
     * @throws IllegalArgumentException if the duration is less than 1.
     * @throws IOException if writing the sample data failed.
     */
    public void writeSample(int track, InputStream in, boolean isKeyframe) throws IOException {
        ensureStarted();

        Track tr = tracks.get(track);

        if (!isKeyframe && tr.samples.isEmpty()) {
            throw new IllegalStateException("The first sample in a track must be a keyframe.");
        }

        // If a stream has palette changes, then only palette change samples can
        // be marked as keyframe.
        if (isKeyframe && 0 != (tr.flags & STRH_FLAG_VIDEO_PALETTE_CHANGES)) {
            // If a keyframe sample is immediately preceeded by a palette change
            // we can raise the palette change to a keyframe.
            if (tr.samples.size() > 0) {
                Sample s = tr.samples.get(tr.samples.size() - 1);
                if ((s.chunkType & 0xffff) == PC_ID) {
                    s.isKeyframe = true;
                }
            }
            isKeyframe = false;
        }


        DataChunk dc = new DataChunk(tr.getSampleChunkFourCC(isKeyframe));
        moviChunk.add(dc);
        ImageOutputStream mdatOut = dc.getOutputStream();
        long offset = getRelativeStreamPosition();
        byte[] buf = new byte[512];
        int len;
        while ((len = in.read(buf)) != -1) {
            mdatOut.write(buf, 0, len);
        }
        long length = getRelativeStreamPosition() - offset;
        dc.finish();
        Sample s = new Sample(dc.chunkType, 1, offset, length, isKeyframe);
        tr.addSample(s);
        idx1.add(s);
        tr.length++;
        if (getRelativeStreamPosition() > 1L << 32) {
            throw new IOException("AVI file is larger than 4 GB");
        }
    }

    /**
     * Writes an already encoded sample from a byte array into a track. <p> This
     * method does not inspect the contents of the samples. The contents has to
     * match the format and dimensions of the media in this track. <p> If a
     * track contains palette changes, then all key frames must be immediately
     * preceeded by a palette change chunk. If a key frame is not preceeded by a
     * palette change chunk, it will be downgraded to a delta frame.
     *
     * @param track The track index.
     * @param data The encoded sample data.
     * @param off The startTime offset in the data.
     * @param len The number of bytes to write.
     * @param isKeyframe Whether the sample is a sync sample (keyframe).
     *
     * @throws IllegalArgumentException if the duration is less than 1.
     * @throws IOException if writing the sample data failed.
     */
    public void writeSample(int track, byte[] data, int off, int len, boolean isKeyframe) throws IOException {
        ensureStarted();
        Track tr = tracks.get(track);

        // The first sample in a track is always a key frame
        if (!isKeyframe && tr.samples.isEmpty()) {
            throw new IllegalStateException("The first sample in a track must be a keyframe.\nTrack="+track+", "+tr.format);
        }

        // If a stream has palette changes, then only palette change samples can
        // be marked as keyframe.
        if (isKeyframe && 0 != (tr.flags & STRH_FLAG_VIDEO_PALETTE_CHANGES)) {
            throw new IllegalStateException("Only palette changes can be marked as keyframe.\nTrack="+track+", "+tr.format);
        }

        DataChunk dc = new DataChunk(tr.getSampleChunkFourCC(isKeyframe), len);
        moviChunk.add(dc);
        ImageOutputStream mdatOut = dc.getOutputStream();
        long offset = getRelativeStreamPosition();
        mdatOut.write(data, off, len);
        long length = getRelativeStreamPosition() - offset;
        dc.finish();
        Sample s = new Sample(dc.chunkType, 1, offset, length, isKeyframe);
        tr.addSample(s);
        idx1.add(s);
        if (getRelativeStreamPosition() > 1L << 32) {
            throw new IOException("AVI file is larger than 4 GB");
        }
    }

    /**
     * Writes multiple already encoded samples from a byte array into a track.
     * <p> This method does not inspect the contents of the data. The contents
     * has to match the format and dimensions of the media in this track.
     *
     * @param track The track index.
     * @param sampleCount The number of samples.
     * @param data The encoded sample data.
     * @param off The startTime offset in the data.
     * @param len The number of bytes to write. Must be dividable by
     * sampleCount.
     * @param isKeyframe Whether the samples are sync samples. All samples must
     * either be sync samples or non-sync samples.
     *
     * @throws IllegalArgumentException if the duration is less than 1.
     * @throws IOException if writing the sample data failed.
     */
    public void writeSamples(int track, int sampleCount, byte[] data, int off, int len, boolean isKeyframe) throws IOException {
        ensureStarted();
        Track tr = tracks.get(track);
        if (tr.mediaType == AVIMediaType.AUDIO) {
            DataChunk dc = new DataChunk(tr.getSampleChunkFourCC(isKeyframe), len);
            moviChunk.add(dc);
            ImageOutputStream mdatOut = dc.getOutputStream();
            long offset = getRelativeStreamPosition();
            mdatOut.write(data, off, len);
            long length = getRelativeStreamPosition() - offset;
            dc.finish();
            Sample s = new Sample(dc.chunkType, sampleCount, offset, length, isKeyframe | tr.samples.isEmpty());
            tr.addSample(s);
            idx1.add(s);
            tr.length += sampleCount;
            if (getRelativeStreamPosition() > 1L << 32) {
                throw new IOException("AVI file is larger than 4 GB");
            }
        } else {
            for (int i = 0; i < sampleCount; i++) {
                writeSample(track, data, off, len / sampleCount, isKeyframe);
                off += len / sampleCount;
            }
        }
    }

    /**
     * Returns the duration of the track in media time scale.
     */
    public long getMediaDuration(int track) {
        Track tr = tracks.get(track);
        long duration = tr.startTime;
        if (!tr.samples.isEmpty()) {
            Sample s = tr.samples.get(tr.samples.size() - 1);
            duration += s.timeStamp + s.duration;
        }
        return duration;
    }

    /**
     * Closes the stream.
     *
     * @exception IOException if an I/O error has occurred
     */
    public void close() throws IOException {
        if (state == States.STARTED) {
            finish();
        }
        if (state != States.CLOSED) {
            out.close();
            state = States.CLOSED;
        }
    }

    /**
     * Finishes writing the contents of the AVI output stream without closing
     * the underlying stream. Use this method when applying multiple filters in
     * succession to the same output stream.
     *
     * @exception IllegalStateException if the dimension of the video track has
     * not been specified or determined yet.
     * @exception IOException if an I/O exception has occurred
     */
    public void finish() throws IOException {
        ensureOpen();
        if (state != States.FINISHED) {
            moviChunk.finish();
            writeEpilog();
            state = States.FINISHED;
        }
    }

    /**
     * Check to make sure that this stream has not been closed
     */
    private void ensureOpen() throws IOException {
        if (state == States.CLOSED) {
            throw new IOException("Stream closed");
        }
    }

    /**
     * Returns true if the limit for media samples has been reached. If this
     * limit is reached, no more samples should be added to the movie. <p> AVI
     * 1.0 files have a file size limit of 2 GB. This method returns true if a
     * file size of 1.8 GB has been reached.
     */
    public boolean isDataLimitReached() {
        try {
            return getRelativeStreamPosition() > (long) (1.8 * 1024 * 1024 * 1024);
        } catch (IOException ex) {
            return true;
        }
    }

    private void writeProlog() throws IOException {
        // The file has the following structure:
        //
        // .RIFF AVI
        // ..avih (AVI Header Chunk)
        // ..LIST strl (for each track)
        // ...strh (Stream Header Chunk)
        // ...strf (Stream Format Chunk)
        // ...**** (Extra Stream Header Chunks)
        // ...strn (Stream Name Chunk)
        // ..LIST movi
        // ...00dc (Compressed video data chunk in Track 00, repeated for each frame)
        // ..idx1 (List of video data chunks and their location in the file)

        // The RIFF AVI Chunk holds the complete movie
        aviChunk = new CompositeChunk(RIFF_ID, AVI_ID);
        CompositeChunk hdrlChunk = new CompositeChunk(LIST_ID, HDRL_ID);

        // Write empty AVI Main Header Chunk - we fill the data in later
        aviChunk.add(hdrlChunk);
        avihChunk = new FixedSizeDataChunk(AVIH_ID, 56);
        avihChunk.seekToEndOfChunk();
        hdrlChunk.add(avihChunk);

        // Write empty AVI Stream Header Chunk - we fill the data in later
        for (Track tr : tracks) {

            CompositeChunk strlChunk = new CompositeChunk(LIST_ID, STRL_ID);
            hdrlChunk.add(strlChunk);

            tr.strhChunk = new FixedSizeDataChunk(STRH_ID, 56);
            tr.strhChunk.seekToEndOfChunk();
            strlChunk.add(tr.strhChunk);

            tr.strfChunk = new FixedSizeDataChunk(STRF_ID, tr.getSTRFChunkSize());
            tr.strfChunk.seekToEndOfChunk();
            strlChunk.add(tr.strfChunk);

            for (RIFFChunk c : tr.extraHeaders) {
                DataChunk d = new DataChunk(c.getID(),
                        c.getSize());
                ImageOutputStream dout = d.getOutputStream();
                dout.write(c.getData());
                d.finish();
                strlChunk.add(d);
            }

            if (tr.name != null) {
                byte[] data = (tr.name + "\u0000").getBytes("ASCII");
                DataChunk d = new DataChunk(STRN_ID,
                        data.length);
                ImageOutputStream dout = d.getOutputStream();
                dout.write(data);
                d.finish();
                strlChunk.add(d);
            }
        }

        moviChunk = new CompositeChunk(LIST_ID, MOVI_ID);
        aviChunk.add(moviChunk);


    }

    private void writeEpilog() throws IOException {

        ImageOutputStream d;

        /* Create Idx1 Chunk and write data
         * -------------
         typedef struct _avioldindex {
         FOURCC  fcc;
         DWORD   cb;
         struct _avioldindex_entry {
         DWORD   dwChunkId;
         DWORD   flags;
         DWORD   dwOffset;
         DWORD   dwSize;
         } aIndex[];
         } AVIOLDINDEX;
         */
        {
            DataChunk idx1Chunk = new DataChunk(IDX1_ID);
            aviChunk.add(idx1Chunk);
            d = idx1Chunk.getOutputStream();
            long moviListOffset = moviChunk.offset + 8 + 8;

            {
                double movieTime = 0;
                int nTracks = tracks.size();
                int[] trackSampleIndex = new int[nTracks];
                long[] trackSampleCount = new long[nTracks];
                for (Sample s : idx1) {
                    d.setByteOrder(ByteOrder.BIG_ENDIAN);
                    d.writeInt(s.chunkType); // dwChunkId
                    d.setByteOrder(ByteOrder.LITTLE_ENDIAN);
                    // Specifies a FOURCC that identifies a stream in the AVI file. The
                    // FOURCC must have the form 'xxyy' where xx is the stream number and yy
                    // is a two-character code that identifies the contents of the stream:
                    //
                    // Two-character code   Description
                    //  db                  Uncompressed video frame
                    //  dc                  Compressed video frame
                    //  header                  Palette change
                    //  wb                  Audio data

                    d.writeInt(((s.chunkType & 0xffff) == PC_ID ? 0x100 : 0x0)//
                            | (s.isKeyframe ? 0x10 : 0x0)); // flags
                    // Specifies a bitwise combination of zero or more of the following
                    // flags:
                    //
                    // Value    Name            Description
                    // 0x10     AVIIF_KEYFRAME  The data chunk is a key frame.
                    // 0x1      AVIIF_LIST      The data chunk is a 'rec ' list.
                    // 0x100    AVIIF_NO_TIME   The data chunk does not affect the timing of the
                    //                          stream. For example, this flag should be set for
                    //                          palette changes.

                    d.writeInt((int) (s.offset - moviListOffset)); // dwOffset
                    // Specifies the location of the data chunk in the file. The value
                    // should be specified as an offset, in bytes, from the startTime of the
                    // 'movi' list; however, in some AVI files it is given as an offset from
                    // the startTime of the file.

                    d.writeInt((int) (s.length)); // dwSize
                    // Specifies the size of the data chunk, in bytes.
                }

            }

            idx1Chunk.finish();
        }

        /* Write Data into AVI Main Header Chunk
         * -------------
         * The AVIMAINHEADER structure defines global information in an AVI file.
         * see http://msdn.microsoft.com/en-us/library/ms779632(VS.85).aspx
         typedef struct _avimainheader {
         FOURCC fcc;
         DWORD  cb;
         DWORD  dwMicroSecPerFrame;
         DWORD  dwMaxBytesPerSec;
         DWORD  dwPaddingGranularity;
         DWORD  flags;
         DWORD  dwTotalFrames;
         DWORD  initialFrames;
         DWORD  dwStreams;
         DWORD  dwSuggestedBufferSize;
         DWORD  dwWidth;
         DWORD  dwHeight;
         DWORD  dwReserved[4];
         } AVIMAINHEADER; */
        {
            avihChunk.seekToStartOfData();
            d = avihChunk.getOutputStream();

            // compute largest buffer size
            long largestBufferSize = 0;
            long duration = 0;
            for (Track tr : tracks) {
                long trackDuration = 0;
                for (Sample s : tr.samples) {
                    trackDuration += s.duration;
                }
                duration = max(duration, trackDuration);
                for (Sample s : tr.samples) {
                    if (s.length > largestBufferSize) {
                        largestBufferSize = s.length;
                    }
                }
            }



            // FIXME compute dwMicroSecPerFrame properly!
            Track tt = tracks.get(0);

            d.writeInt((int) ((1000000L * tt.scale) / tt.rate)); // dwMicroSecPerFrame
            // Specifies the number of microseconds between frames.
            // This value indicates the overall timing for the file.

            d.writeInt((int)largestBufferSize); // dwMaxBytesPerSec
            // Specifies the approximate maximum data rate of the file.
            // This value indicates the number of bytes per second the system
            // must handle to present an AVI sequence as specified by the other
            // parameters contained in the main header and stream header chunks.

            d.writeInt(0); // dwPaddingGranularity
            // Specifies the alignment for data, in bytes. Pad the data to multiples
            // of this value.

            d.writeInt(0x10|0x100|0x800); // flags 
            // Contains a bitwise combination of zero or more of the following
            // flags:
            //
            // Value   Name         Description
            // 0x10    AVIF_HASINDEX Indicates the AVI file has an index.
            // 0x20    AVIF_MUSTUSEINDEX Indicates that application should use the
            //                      index, rather than the physical ordering of the
            //                      chunks in the file, to determine the order of
            //                      presentation of the data. For example, this flag
            //                      could be used to create a list of frames for
            //                      editing.
            // 0x100   AVIF_ISINTERLEAVED Indicates the AVI file is interleaved.
            // 0x800   AVIF_TRUST_CK_TYPE ???  
            // 0x1000  AVIF_WASCAPTUREFILE Indicates the AVI file is a specially
            //                      allocated file used for capturing real-time
            //                      video. Applications should warn the user before
            //                      writing over a file with this flag set because
            //                      the user probably defragmented this file.
            // 0x20000 AVIF_COPYRIGHTED Indicates the AVI file contains copyrighted
            //                      data and software. When this flag is used,
            //                      software should not permit the data to be
            //                      duplicated.

            /*long dwTotalFrames = 0;
             for (Track t : tracks) {
             dwTotalFrames += t.samples.size();
             }*/
            d.writeInt(tt.samples.size()); // dwTotalFrames
            // Specifies the total number of frames of data in the file.

            d.writeInt(0); // initialFrames
            // Specifies the initial frame for interleaved files. Noninterleaved
            // files should specify zero. If you are creating interleaved files,
            // specify the number of frames in the file prior to the initial frame
            // of the AVI sequence in this member.
            // To give the audio driver enough audio to work with, the audio data in
            // an interleaved file must be skewed from the video data. Typically,
            // the audio data should be moved forward enough frames to allow
            // approximately 0.75 seconds of audio data to be preloaded. The
            // dwInitialRecords member should be set to the number of frames the
            // audio is skewed. Also set the same value for the initialFrames
            // member of the AVISTREAMHEADER structure in the audio stream header

            d.writeInt(tracks.size()); // dwStreams
            // Specifies the number of streams in the file. For example, a file with
            // audio and video has two streams.

            d.writeInt((int) largestBufferSize); // dwSuggestedBufferSize
            // Specifies the suggested buffer size for reading the file. Generally,
            // this size should be large enough to contain the largest chunk in the
            // file. If set to zero, or if it is too small, the playback software
            // will have to reallocate memory during playback, which will reduce
            // performance. For an interleaved file, the buffer size should be large
            // enough to read an entire record, and not just a chunk.
            {
                VideoTrack vt = null;
                int width = 0, height = 0;
                // FIXME - Maybe we should support a global video dimension property
                for (Track tr : tracks) {
                    width = max(width, max(tr.frameLeft, tr.frameRight));
                    height = max(height, max(tr.frameTop, tr.frameBottom));
                }
                d.writeInt(width); // dwWidth
                // Specifies the width of the AVI file in pixels.

                d.writeInt(height); // dwHeight
                // Specifies the height of the AVI file in pixels.
            }
            d.writeInt(0); // dwReserved[0]
            d.writeInt(0); // dwReserved[1]
            d.writeInt(0); // dwReserved[2]
            d.writeInt(0); // dwReserved[3]
            // Reserved. Set this array to zero.
        }

        for (Track tr : tracks) {
            /* Write Data into AVI Stream Header Chunk
             * -------------
             * The AVISTREAMHEADER structure contains information about one stream
             * in an AVI file.
             * see http://msdn.microsoft.com/en-us/library/ms779638(VS.85).aspx
             typedef struct _avistreamheader {
             FOURCC fcc;
             DWORD  cb;
             FOURCC fccType;
             FOURCC fccHandler;
             DWORD  flags;
             WORD   priority;
             WORD   language;
             DWORD  initialFrames;
             DWORD  scale;
             DWORD  rate;
             DWORD  startTime;
             DWORD  dwLength;
             DWORD  dwSuggestedBufferSize;
             DWORD  quality;
             DWORD  dwSampleSize;
             struct {
             short int left;
             short int top;
             short int right;
             short int bottom;
             }  rcFrame;
             } AVISTREAMHEADER;
             */
            tr.strhChunk.seekToStartOfData();
            d = tr.strhChunk.getOutputStream();
            d.setByteOrder(ByteOrder.BIG_ENDIAN);
            d.writeInt(typeToInt(tr.mediaType.fccType)); // fccType: "vids" for video stream
            d.writeInt(tr.fccHandler); // fccHandler: specifies the codec
            d.setByteOrder(ByteOrder.LITTLE_ENDIAN);

            d.writeInt(tr.flags);
            // Contains any flags for the data stream. The bits in the high-order
            // word of these flags are specific to the type of data contained in the
            // stream. The following standard flags are defined:
            //
            // Value    Name        Description
            //          AVISF_DISABLED 0x00000001 Indicates this stream should not
            //                      be enabled by default.
            //          AVISF_VIDEO_PALCHANGES 0x00010000
            //                      Indicates this video stream contains
            //                      palette changes. This flag warns the playback
            //                      software that it will need to animate the
            //                      palette.

            d.writeShort(tr.priority); // priority: highest priority denotes default stream
            d.writeShort(tr.language); // language: language code (?)
            d.writeInt((int) tr.initialFrames); // initialFrames: how far audio data is ahead of the video frames
            d.writeInt((int) tr.scale); // scale: time scale
            d.writeInt((int) tr.rate); // rate: sample rate in scale units
            d.writeInt((int) tr.startTime); // startTime: starting time of stream
            d.writeInt((int) tr.length); // dwLength: length of stream ! WRONG

            long dwSuggestedBufferSize = 0;
            long dwSampleSize = -1; // => -1 indicates unknown
            for (Sample s : tr.samples) {
                if (s.length > dwSuggestedBufferSize) {
                    dwSuggestedBufferSize = s.length;
                }
                if (dwSampleSize == -1) {
                    dwSampleSize = s.length;
                } else if (dwSampleSize != s.length) {
                    dwSampleSize = 0;
                }
            }
            if (dwSampleSize == -1) {
                dwSampleSize = 0;
            }

            d.writeInt((int) dwSuggestedBufferSize); // dwSuggestedBufferSize
            // Specifies how large a buffer should be used to read this stream.
            // Typically, this contains a value corresponding to the largest chunk
            // present in the stream. Using the correct buffer size makes playback
            // more efficient. Use zero if you do not know the correct buffer size.

            d.writeInt(tr.quality); // quality
            // Specifies an indicator of the quality of the data in the stream.
            // Quality is represented as a number between 0 and 10,000.
            // For compressed data, this typically represents the value of the
            // quality parameter passed to the compression software. If set to –1,
            // drivers use the default quality value.

            d.writeInt(tr instanceof AudioTrack ? ((AudioTrack) tr).blockAlign : (int) dwSampleSize); // dwSampleSize
            // Specifies the size of a single sample of data. This is set to zero
            // if the samples can vary in size. If this number is nonzero, then
            // multiple samples of data can be grouped into a single chunk within
            // the file. If it is zero, each sample of data (such as a video frame)
            // must be in a separate chunk. For video streams, this number is
            // typically zero, although it can be nonzero if all video frames are
            // the same size. For audio streams, this number should be the same as
            // the blockAlign member of the WAVEFORMATEX structure describing the
            // audio.

            d.writeShort(tr.frameLeft); // rcFrame.left
            d.writeShort(tr.frameTop); // rcFrame.top
            d.writeShort(tr.frameRight); // rcFrame.right
            d.writeShort(tr.frameBottom); // rcFrame.bottom
            // Specifies the destination rectangle for a text or video stream within
            // the movie rectangle specified by the dwWidth and dwHeight members of
            // the AVI main header structure. The rcFrame member is typically used
            // in support of multiple video streams. Set this rectangle to the
            // coordinates corresponding to the movie rectangle to update the whole
            // movie rectangle. Units for this member are pixels. The upper-left
            // corner of the destination rectangle is relative to the upper-left
            // corner of the movie rectangle.

            if (tr instanceof VideoTrack) {
                VideoTrack vt = (VideoTrack) tr;
                Format vf = tr.format;

                /* Write BITMAPINFOHEADR Data into AVI Stream Format Chunk
                 /* -------------
                 * see http://msdn.microsoft.com/en-us/library/ms779712(VS.85).aspx
                 typedef struct tagBITMAPINFOHEADER {
                 DWORD  biSize;
                 LONG   width;
                 LONG   height;
                 WORD   planes;
                 WORD   bitCount;
                 DWORD  compression;
                 DWORD  sizeImage;
                 LONG   xPelsPerMeter;
                 LONG   yPelsPerMeter;
                 DWORD  clrUsed;
                 DWORD  clrImportant;
                 } BITMAPINFOHEADER;
                 */
                tr.strfChunk.seekToStartOfData();
                d = tr.strfChunk.getOutputStream();
                d.writeInt(40); // biSize: number of bytes required by the structure.
                d.writeInt(vf.get(WidthKey)); // width
                d.writeInt(vf.get(HeightKey)); // height
                d.writeShort(1); // planes
                d.writeShort(vf.get(DepthKey)); // bitCount

                String enc = vf.get(EncodingKey);
                if (enc.equals(ENCODING_AVI_DIB)) {
                    d.writeInt(0); // compression - BI_RGB for uncompressed RGB
                } else if (enc.equals(ENCODING_AVI_RLE)) {
                    if (vf.get(DepthKey) == 8) {
                        d.writeInt(1); // compression - BI_RLE8
                    } else if (vf.get(DepthKey) == 4) {
                        d.writeInt(2); // compression - BI_RLE4
                    } else {
                        throw new UnsupportedOperationException("RLE only supports 4-bit and 8-bit images");
                    }
                } else {
            d.setByteOrder(ByteOrder.BIG_ENDIAN);
                    d.writeInt(typeToInt(vt.format.get(EncodingKey))); // compression
            d.setByteOrder(ByteOrder.LITTLE_ENDIAN);
                }

                if (enc.equals(ENCODING_AVI_DIB)) {
                    d.writeInt(0); // sizeImage
                } else {
                    if (vf.get(DepthKey) == 4) {
                        d.writeInt(vf.get(WidthKey) * vf.get(HeightKey) / 2); // sizeImage
                    } else {
                        int bytesPerPixel = Math.max(1, vf.get(DepthKey) / 8);
                        d.writeInt(vf.get(WidthKey) * vf.get(HeightKey) * bytesPerPixel); // sizeImage
                    }
                }

                d.writeInt(0); // xPelsPerMeter
                d.writeInt(0); // yPelsPerMeter

                d.writeInt(vt.palette == null ? 0 : vt.palette.getMapSize()); // clrUsed

                d.writeInt(0); // clrImportant

                if (vt.palette != null) {
                    for (int i = 0, n = vt.palette.getMapSize(); i < n; ++i) {
                        /*
                         * typedef struct tagRGBQUAD {
                         BYTE rgbBlue;
                         BYTE rgbGreen;
                         BYTE rgbRed;
                         BYTE rgbReserved; // This member is reserved and must be zero.
                         } RGBQUAD;
                         */
                        d.write(vt.palette.getBlue(i));
                        d.write(vt.palette.getGreen(i));
                        d.write(vt.palette.getRed(i));
                        d.write(0);
                    }
                }
            } else if (tr instanceof AudioTrack) {
                AudioTrack at = (AudioTrack) tr;

                /* Write WAVEFORMATEX Data into AVI Stream Format Chunk
                 /* -------------
                 * see http://msdn.microsoft.com/en-us/library/dd757720(v=vs.85).aspx
                 typedef struct {
                 WORD  wFormatTag;
                 WORD  channels;
                 DWORD samplesPerSec;
                 DWORD avgBytesPerSec;
                 WORD  blockAlign;
                 WORD  bitsPerSample;
                 WORD  cbSize;
                 } WAVEFORMATEX;
                 */
                tr.strfChunk.seekToStartOfData();
                d = tr.strfChunk.getOutputStream();

                d.writeShort(at.wFormatTag); // wFormatTag: WAVE_FORMAT_PCM=0x0001
                d.writeShort(at.channels); // channels
                d.writeInt((int) at.samplesPerSec);// samplesPerSec
                d.writeInt((int) at.avgBytesPerSec); // avgBytesPerSec
                d.writeShort(at.blockAlign); //  blockAlign
                d.writeShort(at.bitsPerSample); // bitsPerSample

                d.writeShort(0); //cbSize
                // cbSize: Size, in bytes, of extra format information appended
                // to the end of the WAVEFORMATEX structure. This information 
                // can be used by non-PCM formats to store extra attributes for
                // the wFormatTag. If no extra information is required by the 
                // wFormatTag, this member must be set to zero. If this value is
                // 22, the format is most likely described using the 
                // WAVEFORMATEXTENSIBLE structure, of which WAVEFORMATEX is the
                // first member.
            }
        }

        // -----------------
        aviChunk.finish();
    }
}
