/*
 * Copyright 2020 Yurii Serhiichuk
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package com.github.xsavikx.androidscreencast.api.recording;

import com.github.xsavikx.androidscreencast.api.recording.atom.AtomType;
import com.github.xsavikx.androidscreencast.api.recording.atom.CompositeAtom;
import com.github.xsavikx.androidscreencast.api.recording.atom.DataAtom;
import com.github.xsavikx.androidscreencast.api.recording.atom.WideDataAtom;
import com.github.xsavikx.androidscreencast.api.recording.exception.OutputStreamAlreadyClosedException;
import com.github.xsavikx.androidscreencast.exception.IORuntimeException;

import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.stream.FileImageOutputStream;
import javax.imageio.stream.ImageOutputStream;
import javax.imageio.stream.MemoryCacheImageOutputStream;
import java.awt.image.BufferedImage;
import java.awt.image.WritableRaster;
import java.io.*;
import java.nio.file.Files;
import java.util.Date;
import java.util.LinkedList;

import static com.github.xsavikx.androidscreencast.api.recording.atom.AtomType.*;
import static com.google.common.base.Preconditions.*;
import static com.google.common.collect.Lists.newLinkedList;

/**
 * Implementation of QuickTime video encoder.
 * For more information see <a href="developer.apple.com/library/content/documentation/QuickTime/QTFF/QTFFPreface/qtffPreface.html">QuickTime File Format Specification</a>
 */
public final class QuickTimeOutputStream {

    private static final int UNSPECIFIED = -1;
    private static final float DEFAULT_QUALITY = 0.9f;
    private static final int DEFAULT_TIME_SCALE = 600;

    /**
     * Output stream of the QuickTimeOutputStream.
     */
    private final ImageOutputStream out;
    /**
     * Current video format.
     */
    private final VideoFormat videoFormat;
    /**
     * Quality of JPEG encoded video frames.
     */
    private float quality = DEFAULT_QUALITY;
    /**
     * Creation time of the movie output stream.
     */
    private Date creationTime;
    /**
     * Width of the video frames. All frames must have the same width. The value -1 is used to mark unspecified width.
     */
    private int imgWidth = UNSPECIFIED;
    /**
     * Height of the video frames. All frames must have the same height. The value -1 is used to mark unspecified height.
     */
    private int imgHeight = UNSPECIFIED;
    /**
     * The timeScale of the movie. A time value that indicates the time scale for this media-that is, the number of time units that pass per second in
     * its time coordinate system.
     */
    private int timeScale = DEFAULT_TIME_SCALE;
    /**
     * The current state of the movie output stream.
     */
    private States state = States.FINISHED;
    /**
     * List of video frames.
     */
    private final LinkedList<Sample> videoFrames;
    /**
     * This atom holds the movie frames.
     */
    private WideDataAtom mdatAtom;

    /**
     * Creates a new output stream with the specified image videoFormat and framerate.
     *
     * @param file   the output file
     * @param format Selects an encoder for the video format "JPG" or "PNG".
     * @throws IllegalArgumentException if videoFormat is null or if framerate is less or equal to 0
     */
    public QuickTimeOutputStream(File file, VideoFormat format) throws IOException {
        checkNotNull(file, "Result file should not be null.");
        checkNotNull(format, "Video format must not be null.");
        Files.deleteIfExists(file.toPath());
        out = new FileImageOutputStream(file);
        this.videoFormat = format;
        this.videoFrames = newLinkedList();
    }

    /**
     * Closes the movie file as well as the stream being filtered.
     */
    public void close() {
        if (state == States.STARTED) {
            finish();
        }
        if (state != States.CLOSED) {
            try {
                out.close();
            } catch (IOException e) {
                throw new IORuntimeException(e);
            }
            state = States.CLOSED;
        }
    }

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

    /**
     * Sets the state of the QuickTimeOutpuStream to started.
     * <p>
     * If the state is changed by this method, the prolog is written.
     */
    private void ensureStarted() {
        if (state != States.STARTED) {
            creationTime = new Date();
            writeProlog();
            mdatAtom = new WideDataAtom(MEDIA_DATA, out);
            state = States.STARTED;
        }
    }

    /**
     * Finishes writing the contents of the QuickTime output stream without closing the underlying stream. Use this method when applying multiple
     * filters in succession to the same output stream.
     *
     * @throws IllegalStateException if the dimension of the video track has not been specified or determined yet.
     */
    private void finish() {
        ensureOpen();
        if (state != States.FINISHED) {
            checkState(imgWidth != UNSPECIFIED && imgHeight != UNSPECIFIED,
                    "Image width and height must be specified, but were: width=%s, height=%s", imgWidth, imgHeight);
            mdatAtom.finish();
            writeEpilog();
            state = States.FINISHED;
            imgWidth = imgHeight = UNSPECIFIED;
        }
    }

    /**
     * Returns the time scale of this media.
     *
     * @return time scale
     */
    public int getTimeScale() {
        return timeScale;
    }

    /**
     * Sets the time scale for this media, that is, the number of time units that pass per second in its time coordinate system.
     * <p>
     * The default value is 600.
     *
     * @param timeScale
     */
    public void setTimeScale(int timeScale) {
        checkArgument(timeScale > 0, "timeScale must be greater 0, but was %s", timeScale);
        this.timeScale = timeScale;
    }

    /**
     * Returns the video compression quality.
     *
     * @return video compression quality
     */
    public float getVideoCompressionQuality() {
        return quality;
    }

    /**
     * Sets the compression quality of the video track. 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 frames which are subsequently written to the QuickTimeOutputStream. Frames which have already been written are not
     * changed.
     * <p>
     * This value has no effect on videos encoded with the PNG format.
     * <p>
     * The default value is 0.9.
     *
     * @param quality
     */
    public void setVideoCompressionQuality(float quality) {
        checkArgument(quality >= 0f && quality <= 1f, "Compression quality should be between 0 and 1, but was %s", quality);
        this.quality = quality;
    }

    /**
     * Sets the dimension of the video track.
     * <p>
     * You need to explicitly set the dimension, if you add all frames from files or input streams.
     * <p>
     * If you add frames from buffered images, then QuickTimeOutputStream can determine the video dimension from the image width and height.
     *
     * @param width
     * @param height
     */
    public void setVideoDimension(int width, int height) {
        checkArgument(width > 1 && height > 1,
                "width and height must be greater than 0, but were: width=%s, height=%s", width, height);
        this.imgWidth = width;
        this.imgHeight = height;
    }

    private void writeEpilog() {
        try {
            Date modificationTime = new Date();
            int duration = 0;
            for (Sample s : videoFrames) {
                duration += s.duration;
            }
            /* Movie Atom */
            CompositeAtom movieAtom = new CompositeAtom(MOVIE, out);
            movieAtom.add(createMovieHeaderAtom(modificationTime, duration, out));
            movieAtom.add(createTrackAtom(modificationTime, duration, out));
            movieAtom.finish();
        } catch (IOException e) {
            throw new IORuntimeException(e);
        }
    }

    /**
     * Track Atom
     *
     * @param modificationTime calendar date and time of last modification
     * @param duration         time value that indicates duration of video
     * @param out              ImageOutputStream for this data atom
     * @return filled Track Atom
     * @throws IOException if any write operation fails
     */
    private CompositeAtom createTrackAtom(Date modificationTime, int duration, ImageOutputStream out) throws IOException {
        CompositeAtom trackAtom = new CompositeAtom(TRACK, out);
        trackAtom.add(createTrackHeaderAtom(modificationTime, duration, out));
        trackAtom.add(createMediaAtom(modificationTime, duration, out));
        return trackAtom;
    }


    /**
     * Track Header Atom
     * The track header atom specifies the characteristics of a single track within a movie. A track header atom
     * contains a size field that specifies the number of bytes and a type field that indicates the format of the data (defined by the atom type
     * 'tkhd').
     * <p>
     * typedef struct { byte version; byte flag0; byte flag1; byte set TrackHeaderFlags flag2; mactimestamp creationTime; mactimestamp
     * modificationTime; int trackId; byte[4] reserved; int duration; byte[8] reserved; short layer; short alternateGroup; short volume; byte[2]
     * reserved; int[9] matrix; int trackWidth; int trackHeight; } trackHeaderAtom;
     *
     * @param modificationTime calendar date and time of last modification
     * @param duration         time value that indicates duration of video
     * @param out              ImageOutputStream for this data atom
     * @return filled Track Header Atom
     * @throws IOException if any write operation fails
     */
    private DataAtom createTrackHeaderAtom(Date modificationTime, int duration, ImageOutputStream out) throws IOException {
        DataAtom leaf = new DataAtom(TRACK_HEADER, out);
        DataAtomOutputStream d = leaf.getOutputStream();
        d.write(0); // version
        // A 1-byte specification of the version of this track header.

        d.write(0); // flag[0]
        d.write(0); // flag[1]
        d.write(0xf); // flag[2]
        // Three bytes that are reserved for the track header flags. These flags
        // indicate how the track is used in the movie. The following flags are
        // valid (all flags are enabled when set to 1):
        //
        // Track enabled
        // Indicates that the track is enabled. Flag value is 0x0001.
        // Track in movie
        // Indicates that the track is used in the movie. Flag value is
        // 0x0002.
        // Track in preview
        // Indicates that the track is used in the movie's preview. Flag
        // value is 0x0004.
        // Track in poster
        // Indicates that the track is used in the movie's poster. Flag
        // value is 0x0008.

        d.writeMacTimestamp(creationTime); // creationTime
        // A 32-bit integer that indicates the calendar date and time (expressed
        // in seconds since midnight, January 1, 1904) when the track header was
        // created. It is strongly recommended that this value should be
        // specified using coordinated universal time (UTC).

        d.writeMacTimestamp(modificationTime); // modificationTime
        // A 32-bit integer that indicates the calendar date and time (expressed
        // in seconds since midnight, January 1, 1904) when the track header was
        // changed. It is strongly recommended that this value should be
        // specified using coordinated universal time (UTC).

        d.writeInt(1); // trackId
        // A 32-bit integer that uniquely identifies the track. The value 0
        // cannot be used.

        d.writeInt(0); // reserved;
        // A 32-bit integer that is reserved for use by Apple. Set this field to
        // 0.

        d.writeInt(duration); // duration
        // A time value that indicates the duration of this track (in the
        // movie's time coordinate system). Note that this property is derived
        // from the track's edits. The value of this field is equal to the sum
        // of the durations of all of the track's edits. If there is no edit
        // list, then the duration is the sum of the sample durations, converted
        // into the movie timescale.

        d.writeLong(0); // reserved
        // An 8-byte value that is reserved for use by Apple. Set this field to
        // 0.

        d.writeShort(0); // layer;
        // A 16-bit integer that indicates this track's spatial priority in its
        // movie. The QuickTime Movie Toolbox uses this value to determine how
        // tracks overlay one another. Tracks with lower layer values are
        // displayed in front of tracks with higher layer values.

        d.writeShort(0); // alternate group
        // A 16-bit integer that specifies a collection of movie tracks that
        // contain alternate data for one another. QuickTime chooses one track
        // from the group to be used when the movie is played. The choice may be
        // based on such considerations as playback quality, language, or the
        // capabilities of the computer.

        d.writeShort(0); // volume
        // A 16-bit fixed-point value that indicates how loudly this track's
        // sound is to be played. A value of 1.0 indicates normal volume.

        d.writeShort(0); // reserved
        // A 16-bit integer that is reserved for use by Apple. Set this field to
        // 0.

        d.writeFixed16D16(1f); // matrix[0]
        d.writeFixed16D16(0f); // matrix[1]
        d.writeFixed2D30(0f); // matrix[2]
        d.writeFixed16D16(0f); // matrix[3]
        d.writeFixed16D16(1f); // matrix[4]
        d.writeFixed2D30(0); // matrix[5]
        d.writeFixed16D16(0); // matrix[6]
        d.writeFixed16D16(0); // matrix[7]
        d.writeFixed2D30(1f); // matrix[8]
        // The matrix structure associated with this track.
        // See Figure 2-8 for an illustration of a matrix structure:
        // http://developer.apple.com/documentation/QuickTime/QTFF/QTFFChap2/chapter_3_section_3.html#//apple_ref/doc/uid/TP40000939-CH204-32967

        d.writeFixed16D16(imgWidth); // width
        // A 32-bit fixed-point number that specifies the width of this track in
        // pixels.

        d.writeFixed16D16(imgHeight); // height
        // A 32-bit fixed-point number that indicates the height of this track
        // in pixels.
        return leaf;
    }

    /**
     * Media Atom
     *
     * @param modificationTime calendar date and time of last modification
     * @param duration         time value that indicates duration of video
     * @param out              ImageOutputStream for this data atom
     * @return filled Media Atom
     * @throws IOException if any write operation fails
     */
    private CompositeAtom createMediaAtom(Date modificationTime, int duration, ImageOutputStream out) throws IOException {
        CompositeAtom mediaAtom = new CompositeAtom(MEDIA, out);
        mediaAtom.add(createMediaHeaderAtom(modificationTime, duration, out));
        mediaAtom.add(createMediaHandlerAtom(out));
        mediaAtom.add(createMediaInformationAtom(out));
        return mediaAtom;
    }

    /**
     * Media Header atom
     * typedef struct { byte version; byte[3] flags; mactimestamp creationTime; mactimestamp modificationTime; int
     * timeScale; int duration; short language; short quality; } mediaHeaderAtom;
     *
     * @param modificationTime calendar date and time of last modification
     * @param duration         time value that indicates duration of video
     * @param out              ImageOutputStream for this data atom
     * @return filled Media Header Atom
     * @throws IOException if any write operation fails
     */
    private DataAtom createMediaHeaderAtom(Date modificationTime, int duration, ImageOutputStream out) throws IOException {
        DataAtom mediaHangler = new DataAtom(MEDIA_HEADER, out);
        DataAtomOutputStream d = mediaHangler.getOutputStream();
        d.write(0); // version
        // One byte that specifies the version of this header atom.

        d.write(0); // flag[0]
        d.write(0); // flag[1]
        d.write(0); // flag[2]
        // Three bytes of space for media header flags. Set this field to 0.

        d.writeMacTimestamp(creationTime); // creationTime
        // A 32-bit integer that specifies (in seconds since midnight, January
        // 1, 1904) when the media atom was created. It is strongly recommended
        // that this value should be specified using coordinated universal time
        // (UTC).

        d.writeMacTimestamp(modificationTime); // modificationTime
        // A 32-bit integer that specifies (in seconds since midnight, January
        // 1, 1904) when the media atom was changed. It is strongly recommended
        // that this value should be specified using coordinated universal time
        // (UTC).

        d.writeInt(timeScale); // timeScale
        // A time value that indicates the time scale for this media-that is,
        // the number of time units that pass per second in its time coordinate
        // system.

        d.writeInt(duration); // duration
        // The duration of this media in units of its time scale.

        d.writeShort(0); // language;
        // A 16-bit integer that specifies the language code for this media.
        // See "Language Code Values" for valid language codes:
        // http://developer.apple.com/documentation/QuickTime/QTFF/QTFFChap4/chapter_5_section_2.html#//apple_ref/doc/uid/TP40000939-CH206-27005

        d.writeShort(0); // quality
        // A 16-bit integer that specifies the media's playback quality-that is,
        // its suitability for playback in a given environment.
        return mediaHangler;
    }

    /**
     * Create Handler Atom
     *
     * @param out           ImageOutputStream for this data atom
     * @param componentType componentType - A four-character code that identifies the type of the handler.
     *                      Only two values are valid for this field: 'mhlr' for media handlers and 'dhlr' for data handlers.
     * @return filled Handler Atom
     * @throws IOException if any write operation fails
     */
    private DataAtom createHandlerAtom(ImageOutputStream out, AtomType componentType) throws IOException {
        DataAtom handlerAtom = new DataAtom(HANDLER, out);

        /*
         * typedef struct { byte version; byte[3] flags; magic componentType; magic componentSubtype; magic componentManufacturer; int componentFlags; int
         * componentFlagsMask; cstring componentName; } handlerReferenceAtom;
         */
        DataAtomOutputStream d = handlerAtom.getOutputStream();
        d.write(0); // version
        // A 1-byte specification of the version of this handler information.

        d.write(0); // flag[0]
        d.write(0); // flag[1]
        d.write(0); // flag[2]
        // A 3-byte space for handler information flags. Set this field to 0.

        d.writeType(componentType); // componentType
        // A four-character code that identifies the type of the handler. Only
        // two values are valid for this field: 'mhlr' for media handlers and
        // 'dhlr' for data handlers.

        d.writeType(VIDEO); // componentSubtype
        // A four-character code that identifies the type of the media handler
        // or data handler. For media handlers, this field defines the type of
        // data-for example, 'vide' for video data or 'soun' for sound data.
        //
        // For data handlers, this field defines the data reference type-for
        // example, a component subtype value of 'alis' identifies a file alias.

        d.writeInt(0); // componentManufacturer
        // Reserved. Set to 0.

        d.writeInt(0); // componentFlags
        // Reserved. Set to 0.

        d.writeInt(0); // componentFlagsMask
        // Reserved. Set to 0.

        d.write(0); // componentName (empty string)
        // A (counted) string that specifies the name of the component-that is,
        // the media handler used when this media was created. This field may
        // contain a zero-length (empty) string.
        return handlerAtom;
    }

    /**
     * Media Handler Atom
     *
     * @param out ImageOutputStream for this data atom
     * @return filled Media Handler Atom
     * @throws IOException if any write operation fails
     */
    private DataAtom createMediaHandlerAtom(ImageOutputStream out) throws IOException {
        return createHandlerAtom(out, MEDIA_HANDLER);
    }

    /**
     * Media Information atom
     *
     * @param out ImageOutputStream for this data atom
     * @return filled Media Information Atom
     * @throws IOException if any write operation fails
     */
    private CompositeAtom createMediaInformationAtom(ImageOutputStream out) throws IOException {
        CompositeAtom mediaInformationAtom = new CompositeAtom(MEDIA_INFORMATION, out);
        mediaInformationAtom.add(createVideoMediaInformationAtom(out));
        mediaInformationAtom.add(createHandleReferenceAtom(out));
        mediaInformationAtom.add(createDataInformationAtom(out));
        mediaInformationAtom.add(createSampleTableAtom(out));
        return mediaInformationAtom;
    }

    /**
     * Video media information atom
     *
     * @param out ImageOutputStream for this data atom
     * @return filled Video Media Information Atom
     * @throws IOException if any write operation fails
     */
    private DataAtom createVideoMediaInformationAtom(ImageOutputStream out) throws IOException {
        DataAtom videoMediaInformationAtom = new DataAtom(VIDEO_MEDIA_INFORMATION, out);

        /*
         * typedef struct { byte version; byte flag1; byte flag2; byte set vmhdFlags flag3; short graphicsMode; ushort[3] opcolor; }
         * videoMediaInformationHeaderAtom;
         */
        DataAtomOutputStream d = videoMediaInformationAtom.getOutputStream();
        d.write(0); // version
        // One byte that specifies the version of this header atom.

        d.write(0); // flag[0]
        d.write(0); // flag[1]
        d.write(0x1); // flag[2]
        // Three bytes of space for media header flags.
        // This is a compatibility flag that allows QuickTime to distinguish
        // between movies created with QuickTime 1.0 and newer movies. You
        // should always set this flag to 1, unless you are creating a movie
        // intended for playback using version 1.0 of QuickTime. This flag's
        // value is 0x0001.

        d.writeShort(0x40); // graphicsMode (0x40 = ditherCopy)
        // A 16-bit integer that specifies the transfer mode. The transfer mode
        // specifies which Boolean operation QuickDraw should perform when
        // drawing or transferring an image from one location to another.
        // See "Graphics Modes" for a list of graphics modes supported by
        // QuickTime:
        // http://developer.apple.com/documentation/QuickTime/QTFF/QTFFChap4/chapter_5_section_5.html#//apple_ref/doc/uid/TP40000939-CH206-18741

        d.writeUShort(0); // opcolor[0]
        d.writeUShort(0); // opcolor[1]
        d.writeUShort(0); // opcolor[2]
        // Three 16-bit values that specify the red, green, and blue colors for
        // the transfer mode operation indicated in the graphics mode field.
        return videoMediaInformationAtom;
    }

    /**
     * Handle reference atom
     * The handler reference atom specifies the media handler component that
     * is to be used to interpret the media's data. The handler reference
     * atom has an atom type value of 'hdlr'.
     *
     * @param out ImageOutputStream for this data atom
     * @return filled Handle Reference Atom
     * @throws IOException if any write operation fails
     */
    private DataAtom createHandleReferenceAtom(ImageOutputStream out) throws IOException {
        return createHandlerAtom(out, DATA_HANDLER);
    }

    /**
     * Data Information Atom
     *
     * @param out ImageOutputStream for this data atom
     * @return filled Data Information Atom
     * @throws IOException if any write operation fails
     */
    private CompositeAtom createDataInformationAtom(ImageOutputStream out) throws IOException {
        CompositeAtom dataInformationAtom = new CompositeAtom(DATA_INFORMATION, out);
        dataInformationAtom.add(createDataReferenceAtom(out));
        return dataInformationAtom;
    }

    /**
     * Data reference atom
     * Data reference atoms contain tabular data that instructs the data
     * handler component how to access the media's data.
     *
     * @param out ImageOutputStream for this data atom
     * @return filled Data Reference Atom
     * @throws IOException if any write operation fails
     */
    private DataAtom createDataReferenceAtom(ImageOutputStream out) throws IOException {

        DataAtom leaf = new DataAtom(DATA_REFERENCE, out);

        /*
         * typedef struct { ubyte version; ubyte[3] flags; int numberOfEntries; dataReferenceEntry dataReference[numberOfEntries]; } dataReferenceAtom;
         *
         * set { dataRefSelfReference=1 // I am not shure if this is the correct value for this flag } drefEntryFlags;
         *
         * typedef struct { int size; magic type; byte version; ubyte flag1; ubyte flag2; ubyte set drefEntryFlags flag3; byte[size - 12] data; }
         * dataReferenceEntry;
         */
        DataAtomOutputStream d = leaf.getOutputStream();
        d.write(0); // version
        // A 1-byte specification of the version of this data reference atom.

        d.write(0); // flag[0]
        d.write(0); // flag[1]
        d.write(0); // flag[2]
        // A 3-byte space for data reference flags. Set this field to 0.

        d.writeInt(1); // numberOfEntries
        // A 32-bit integer containing the count of data references that follow.

        d.writeInt(12); // dataReference.size
        // A 32-bit integer that specifies the number of bytes in the data
        // reference.

        d.writeType(FILE_ALIAS); // dataReference.type
        // A 32-bit integer that specifies the type of the data in the data
        // reference. Table 2-4 lists valid type values:
        // http://developer.apple.com/documentation/QuickTime/QTFF/QTFFChap2/chapter_3_section_4.html#//apple_ref/doc/uid/TP40000939-CH204-38840

        d.write(0); // dataReference.version
        // A 1-byte specification of the version of the data reference.

        d.write(0); // dataReference.flag1
        d.write(0); // dataReference.flag2
        d.write(0x1); // dataReference.flag3
        // A 3-byte space for data reference flags. There is one defined flag.
        //
        // Self reference
        // This flag indicates that the media's data is in the same file as
        // the movie atom. On the Macintosh, and other file systems with
        // multifork files, set this flag to 1 even if the data resides in
        // a different fork from the movie atom. This flag's value is
        // 0x0001.
        return leaf;
    }

    /**
     * Sample Table Atom
     *
     * @param out ImageOutputStream for this data atom
     * @return filled Sample Table Atom
     * @throws IOException if any write operation fails
     */
    private CompositeAtom createSampleTableAtom(ImageOutputStream out) throws IOException {
        CompositeAtom sampleTableAtom = new CompositeAtom(SAMPLE_TABLE, out);
        sampleTableAtom.add(createSampleDescriptionAtom(out));
        sampleTableAtom.add(createTimeToSampleAtom(out));
        sampleTableAtom.add(createSamplesToChunksMappingAtom(out));
        sampleTableAtom.add(createSamplesSizeAtom(out));
        sampleTableAtom.add(createChunkOffsetTableAtom(out));
        return sampleTableAtom;
    }

    /**
     * Sample Description atom
     * The sample description atom stores information that allows you to
     * decode samples in the media. The data stored in the sample
     * description varies, depending on the media type. For example, in the
     * case of video media, the sample descriptions are image description
     * structures. The sample description information for each media type is
     * explained in "Media Data Atom Types":
     * <p>
     * http://developer.apple.com/documentation/QuickTime/QTFF/QTFFChap3/chapter_4_section_1.html#//apple_ref/doc/uid/TP40000939-CH205-SW1
     *
     * @param out ImageOutputStream for this data atom
     * @return filled Sample Description Atom
     * @throws IOException if any write operation fails
     */
    private DataAtom createSampleDescriptionAtom(ImageOutputStream out) throws IOException {
        DataAtom sampleDescriptionAtom = new DataAtom(SAMPLE_DESCRIPTION, out);

        /*
         * typedef struct { byte version; byte[3] flags; int numberOfEntries; sampleDescriptionEntry sampleDescriptionTable[numberOfEntries]; }
         * sampleDescriptionAtom;
         *
         * typedef struct { int size; magic type; byte[6] reserved; // six bytes that must be zero short dataReferenceIndex; // A 16-bit integer that
         * contains the index of the data reference to use to retrieve data associated with samples that use this sample description. Data references are
         * stored in data reference atoms. byte[size - 16] data; } sampleDescriptionEntry;
         */
        DataAtomOutputStream d = sampleDescriptionAtom.getOutputStream();
        d.write(0); // version
        // A 1-byte specification of the version of this sample description
        // atom.

        d.write(0); // flag[0]
        d.write(0); // flag[1]
        d.write(0); // flag[2]
        // A 3-byte space for sample description flags. Set this field to 0.

        d.writeInt(1); // number of Entries
        // A 32-bit integer containing the number of sample descriptions that
        // follow.

        // A 32-bit integer indicating the number of bytes in the sample
        // description.
        switch (videoFormat) {
            case RAW: {
                writeRawSampleDescriptionAtomData(d);
                break;
            }
            case JPG: {
                writeJpgSampleDescriptionAtomData(d);
                break;
            }
            case PNG: {
                writePngSampleDescriptionAtomData(d);
                break;
            }
            default:
                throw new IllegalStateException("Such video format is not supported: " + videoFormat);
        }
        return sampleDescriptionAtom;
    }

    private void writeRawSampleDescriptionAtomData(DataAtomOutputStream d) throws IOException {
        d.writeInt(86); // sampleDescriptionTable[0].size
        d.writeType(RAW); // sampleDescriptionTable[0].type

        // A 32-bit integer indicating the format of the stored data.
        // This depends on the media type, but is usually either the
        // compression format or the media type.

        d.write(new byte[6]); // sampleDescriptionTable[0].reserved
        // Six bytes that must be set to 0.

        d.writeShort(1); // sampleDescriptionTable[0].dataReferenceIndex
        // A 16-bit integer that contains the index of the data
        // reference to use to retrieve data associated with samples
        // that use this sample description. Data references are stored
        // in data reference atoms.

        // Video Sample Description
        // ------------------------
        // The format of the following fields is described here:
        // http://developer.apple.com/documentation/QuickTime/QTFF/QTFFChap3/chapter_4_section_2.html#//apple_ref/doc/uid/TP40000939-CH205-BBCGICBJ

        d.writeShort(0); // sampleDescriptionTable.videoSampleDescription.version
        // A 16-bit integer indicating the version number of the
        // compressed data. This is set to 0, unless a compressor has
        // changed its data format.

        d.writeShort(0); // sampleDescriptionTable.videoSampleDescription.revisionLevel
        // A 16-bit integer that must be set to 0.

        d.writeType(JAVA); // sampleDescriptionTable.videoSampleDescription.manufacturer
        // A 32-bit integer that specifies the developer of the
        // compressor that generated the compressed data. Often this
        // field contains 'appl' to indicate Apple Computer, Inc.

        d.writeInt(0); // sampleDescriptionTable.videoSampleDescription.temporalQuality
        // A 32-bit integer containing a value from 0 to 1023 indicating
        // the degree of temporal compression.

        d.writeInt(512); // sampleDescriptionTable.videoSampleDescription.spatialQuality
        // A 32-bit integer containing a value from 0 to 1024 indicating
        // the degree of spatial compression.

        d.writeUShort(imgWidth); // sampleDescriptionTable.videoSampleDescription.width
        // A 16-bit integer that specifies the width of the source image
        // in pixels.

        d.writeUShort(imgHeight); // sampleDescriptionTable.videoSampleDescription.height
        // A 16-bit integer that specifies the height of the source image in
        // pixels.

        d.writeFixed16D16(72.0); // sampleDescriptionTable.videoSampleDescription.horizontalResolution
        // A 32-bit fixed-point number containing the horizontal
        // resolution of the image in pixels per inch.

        d.writeFixed16D16(72.0); // sampleDescriptionTable.videoSampleDescription.verticalResolution
        // A 32-bit fixed-point number containing the vertical
        // resolution of the image in pixels per inch.

        d.writeInt(0); // sampleDescriptionTable.videoSampleDescription.dataSize
        // A 32-bit integer that must be set to 0.

        d.writeShort(1); // sampleDescriptionTable.videoSampleDescription.frameCount
        // A 16-bit integer that indicates how many frames of compressed
        // data are stored in each sample. Usually set to 1.

        d.writePString("None", 32); // sampleDescriptionTable.videoSampleDescription.compressorName
        // A 32-byte Pascal string containing the name of the compressor
        // that created the image, such as "jpeg".

        d.writeShort(24); // sampleDescriptionTable.videoSampleDescription.depth
        // A 16-bit integer that indicates the pixel depth of the
        // compressed image. Values of 1, 2, 4, 8 ,16, 24, and 32
        // indicate the depth of color images. The value 32 should be
        // used only if the image contains an alpha channel. Values of
        // 34, 36, and 40 indicate 2-, 4-, and 8-bit grayscale,
        // respectively, for grayscale images.

        d.writeShort(-1); // sampleDescriptionTable.videoSampleDescription.colorTableID
        // A 16-bit integer that identifies which color table to use.
        // If this field is set to -1, the default color table should be
        // used for the specified depth. For all depths below 16 bits
        // per pixel, this indicates a standard Macintosh color table
        // for the specified depth. Depths of 16, 24, and 32 have no
        // color table.
    }

    private void writeJpgSampleDescriptionAtomData(DataAtomOutputStream d) throws IOException {
        d.writeInt(86); // sampleDescriptionTable[0].size
        d.writeType(JPEG); // sampleDescriptionTable[0].type

        // A 32-bit integer indicating the format of the stored data.
        // This depends on the media type, but is usually either the
        // compression format or the media type.

        d.write(new byte[6]); // sampleDescriptionTable[0].reserved
        // Six bytes that must be set to 0.

        d.writeShort(1); // sampleDescriptionTable[0].dataReferenceIndex
        // A 16-bit integer that contains the index of the data
        // reference to use to retrieve data associated with samples
        // that use this sample description. Data references are stored
        // in data reference atoms.

        // Video Sample Description
        // ------------------------
        // The format of the following fields is described here:
        // http://developer.apple.com/documentation/QuickTime/QTFF/QTFFChap3/chapter_4_section_2.html#//apple_ref/doc/uid/TP40000939-CH205-BBCGICBJ

        d.writeShort(0); // sampleDescriptionTable.videoSampleDescription.version
        // A 16-bit integer indicating the version number of the
        // compressed data. This is set to 0, unless a compressor has
        // changed its data format.

        d.writeShort(0); // sampleDescriptionTable.videoSampleDescription.revisionLevel
        // A 16-bit integer that must be set to 0.

        d.writeType(JAVA); // sampleDescriptionTable.videoSampleDescription.manufacturer
        // A 32-bit integer that specifies the developer of the
        // compressor that generated the compressed data. Often this
        // field contains 'appl' to indicate Apple Computer, Inc.

        d.writeInt(0); // sampleDescriptionTable.videoSampleDescription.temporalQuality
        // A 32-bit integer containing a value from 0 to 1023 indicating
        // the degree of temporal compression.

        d.writeInt(512); // sampleDescriptionTable.videoSampleDescription.spatialQuality
        // A 32-bit integer containing a value from 0 to 1024 indicating
        // the degree of spatial compression.

        d.writeUShort(imgWidth); // sampleDescriptionTable.videoSampleDescription.width
        // A 16-bit integer that specifies the width of the source image
        // in pixels.

        d.writeUShort(imgHeight); // sampleDescriptionTable.videoSampleDescription.height
        // A 16-bit integer that specifies the height of the source image in
        // pixels.

        d.writeFixed16D16(72.0); // sampleDescriptionTable.videoSampleDescription.horizontalResolution
        // A 32-bit fixed-point number containing the horizontal
        // resolution of the image in pixels per inch.

        d.writeFixed16D16(72.0); // sampleDescriptionTable.videoSampleDescription.verticalResolution
        // A 32-bit fixed-point number containing the vertical
        // resolution of the image in pixels per inch.

        d.writeInt(0); // sampleDescriptionTable.videoSampleDescription.dataSize
        // A 32-bit integer that must be set to 0.

        d.writeShort(1); // sampleDescriptionTable.videoSampleDescription.frameCount
        // A 16-bit integer that indicates how many frames of compressed
        // data are stored in each sample. Usually set to 1.

        d.writePString("Photo - JPEG", 32); // sampleDescriptionTable.videoSampleDescription.compressorName
        // A 32-byte Pascal string containing the name of the compressor
        // that created the image, such as "jpeg".

        d.writeShort(24); // sampleDescriptionTable.videoSampleDescription.depth
        // A 16-bit integer that indicates the pixel depth of the
        // compressed image. Values of 1, 2, 4, 8 ,16, 24, and 32
        // indicate the depth of color images. The value 32 should be
        // used only if the image contains an alpha channel. Values of
        // 34, 36, and 40 indicate 2-, 4-, and 8-bit grayscale,
        // respectively, for grayscale images.

        d.writeShort(-1); // sampleDescriptionTable.videoSampleDescription.colorTableID
        // A 16-bit integer that identifies which color table to use.
        // If this field is set to -1, the default color table should be
        // used for the specified depth. For all depths below 16 bits
        // per pixel, this indicates a standard Macintosh color table
        // for the specified depth. Depths of 16, 24, and 32 have no
        // color table.
    }

    private void writePngSampleDescriptionAtomData(DataAtomOutputStream d) throws IOException {
        d.writeInt(86); // sampleDescriptionTable[0].size
        d.writeType(PNG); // sampleDescriptionTable[0].type
        // A 32-bit integer indicating the format of the stored data.
        // This depends on the media type, but is usually either the
        // compression format or the media type.

        d.write(new byte[6]); // sampleDescriptionTable[0].reserved
        // Six bytes that must be set to 0.

        d.writeShort(1); // sampleDescriptionTable[0].dataReferenceIndex
        // A 16-bit integer that contains the index of the data
        // reference to use to retrieve data associated with samples
        // that use this sample description. Data references are stored
        // in data reference atoms.

        // Video Sample Description
        // ------------------------
        // The format of the following fields is described here:
        // http://developer.apple.com/documentation/QuickTime/QTFF/QTFFChap3/chapter_4_section_2.html#//apple_ref/doc/uid/TP40000939-CH205-BBCGICBJ

        d.writeShort(0); // sampleDescriptionTable.videoSampleDescription.version
        // A 16-bit integer indicating the version number of the
        // compressed data. This is set to 0, unless a compressor has
        // changed its data format.

        d.writeShort(0); // sampleDescriptionTable.videoSampleDescription.revisionLevel
        // A 16-bit integer that must be set to 0.

        d.writeType(JAVA); // sampleDescriptionTable.videoSampleDescription.manufacturer
        // A 32-bit integer that specifies the developer of the
        // compressor that generated the compressed data. Often this
        // field contains 'appl' to indicate Apple Computer, Inc.

        d.writeInt(0); // sampleDescriptionTable.videoSampleDescription.temporalQuality
        // A 32-bit integer containing a value from 0 to 1023 indicating
        // the degree of temporal compression.

        d.writeInt(512); // sampleDescriptionTable.videoSampleDescription.spatialQuality
        // A 32-bit integer containing a value from 0 to 1024 indicating
        // the degree of spatial compression.

        d.writeUShort(imgWidth); // sampleDescriptionTable.videoSampleDescription.width
        // A 16-bit integer that specifies the width of the source image
        // in pixels.

        d.writeUShort(imgHeight); // sampleDescriptionTable.videoSampleDescription.height
        // A 16-bit integer that specifies the height of the source image in
        // pixels.

        d.writeFixed16D16(72.0); // sampleDescriptionTable.videoSampleDescription.horizontalResolution
        // A 32-bit fixed-point number containing the horizontal
        // resolution of the image in pixels per inch.

        d.writeFixed16D16(72.0); // sampleDescriptionTable.videoSampleDescription.verticalResolution
        // A 32-bit fixed-point number containing the vertical
        // resolution of the image in pixels per inch.

        d.writeInt(0); // sampleDescriptionTable.videoSampleDescription.dataSize
        // A 32-bit integer that must be set to 0.

        d.writeShort(1); // sampleDescriptionTable.videoSampleDescription.frameCount
        // A 16-bit integer that indicates how many frames of compressed
        // data are stored in each sample. Usually set to 1.

        d.writePString("PNG", 32); // sampleDescriptionTable.videoSampleDescription.compressorName
        // A 32-byte Pascal string containing the name of the compressor
        // that created the image, such as "jpeg".

        d.writeShort(24); // sampleDescriptionTable.videoSampleDescription.depth
        // A 16-bit integer that indicates the pixel depth of the
        // compressed image. Values of 1, 2, 4, 8 ,16, 24, and 32
        // indicate the depth of color images. The value 32 should be
        // used only if the image contains an alpha channel. Values of
        // 34, 36, and 40 indicate 2-, 4-, and 8-bit grayscale,
        // respectively, for grayscale images.

        d.writeShort(-1); // sampleDescriptionTable.videoSampleDescription.colorTableID
        // A 16-bit integer that identifies which color table to use.
        // If this field is set to -1, the default color table should be
        // used for the specified depth. For all depths below 16 bits
        // per pixel, this indicates a standard Macintosh color table
        // for the specified depth. Depths of 16, 24, and 32 have no
        // color table.
    }

    /**
     * sample size atom
     * The sample size atom contains the sample count and a table giving the
     * size of each sample. This allows the media data itself to be
     * unframed. The total number of samples in the media is always
     * indicated in the sample count. If the default size is indicated, then
     * no table follows.
     *
     * @param out ImageOutputStream for this data atom
     * @return filled Sample Size Atom
     * @throws IOException if any write operation fails
     */
    private DataAtom createSamplesSizeAtom(ImageOutputStream out) throws IOException {
        DataAtom samplesSizeAtom = new DataAtom(SAMPLE_SIZE, out);

        /*
         * typedef struct { byte version; byte[3] flags; int sampleSize; int numberOfEntries; sampleSizeTable sampleSizeTable[numberOfEntries]; }
         * sampleSizeAtom;
         *
         * typedef struct { int size; } sampleSizeTable;
         */
        DataAtomOutputStream d = samplesSizeAtom.getOutputStream();
        d.write(0); // version
        // A 1-byte specification of the version of this time-to-sample atom.

        d.write(0); // flag[0]
        d.write(0); // flag[1]
        d.write(0); // flag[2]
        // A 3-byte space for time-to-sample flags. Set this field to 0.

        d.writeUInt(0); // sample size
        // A 32-bit integer specifying the sample size. If all the samples are
        // the same size, this field contains that size value. If this field is
        // set to 0, then the samples have different sizes, and those sizes are
        // stored in the sample size table.

        d.writeUInt(videoFrames.size()); // number of entries
        // A 32-bit integer containing the count of entries in the sample size
        // table.

        for (Sample s : videoFrames) {
            d.writeUInt(s.length); // sample size
            // The size field contains the size, in bytes, of the sample in
            // question. The table is indexed by sample number-the first entry
            // corresponds to the first sample, the second entry is for the
            // second sample, and so on.
        }
        return samplesSizeAtom;
    }

    /**
     * chunk offset table atom
     * The chunk-offset table gives the index of each chunk into the
     * containing file. There are two variants, permitting the use of
     * 32-bit or 64-bit offsets. The latter is useful when managing very
     * large movies. Only one of these variants occurs in any single
     * instance of a sample table atom.
     *
     * @param out ImageOutputStream for this data atom
     * @return filled Chunk Offset Atom
     * @throws IOException if any write operation fails
     */
    private DataAtom createChunkOffsetTableAtom(ImageOutputStream out) throws IOException {
        if (videoFrames.size() == 0 || videoFrames.getLast().offset <= 0xffffffffL) {
            return create32BitChunkOffsetTableAtom(out);
        }
        return create64BitChunkOffsetTableAtom(out);
    }

    /**
     * 32-bit chunk offset table atom
     *
     * @param out ImageOutputStream for this data atom
     * @return filled 32-bit Chunk Offset Table Atom
     * @throws IOException if any write operation fails
     */
    private DataAtom create32BitChunkOffsetTableAtom(ImageOutputStream out) throws IOException {
        DataAtom chunkOffsetAtom = new DataAtom(STANDARD_CHUNK_OFFSET_TABLE, out);

        /*
         * typedef struct { byte version; byte[3] flags; int numberOfEntries; chunkOffsetTable chunkOffsetTable[numberOfEntries]; } chunkOffsetAtom;
         *
         * typedef struct { int offset; } chunkOffsetTable;
         */
        DataAtomOutputStream d = chunkOffsetAtom.getOutputStream();
        d.write(0); // version
        // A 1-byte specification of the version of this time-to-sample
        // atom.

        d.write(0); // flag[0]
        d.write(0); // flag[1]
        d.write(0); // flag[2]
        // A 3-byte space for time-to-sample flags. Set this field to 0.

        d.writeUInt(videoFrames.size()); // number of entries
        // A 32-bit integer containing the count of entries in the chunk
        // offset table.

        for (Sample s : videoFrames) {
            d.writeUInt(s.offset); // offset
            // The offset contains the byte offset from the beginning of the
            // data stream to the chunk. The table is indexed by chunk
            // number-the first table entry corresponds to the first chunk,
            // the second table entry is for the second chunk, and so on.
        }
        return chunkOffsetAtom;
    }

    /**
     * 64-bit chunk offset table atom
     *
     * @param out ImageOutputStream for this data atom
     * @return filled 64-bit Chunk Offset Table Atom
     * @throws IOException if any write operation fails
     */
    private DataAtom create64BitChunkOffsetTableAtom(ImageOutputStream out) throws IOException {
        DataAtom chunkOffsetAtom = new DataAtom(WIDE_CHUNK_OFFSET_TABLE, out);

        /*
         * typedef struct { byte version; byte[3] flags; int numberOfEntries; chunkOffsetTable chunkOffset64Table[numberOfEntries]; } chunkOffset64Atom;
         *
         * typedef struct { long offset; } chunkOffset64Table;
         */
        DataAtomOutputStream d = chunkOffsetAtom.getOutputStream();
        d.write(0); // version
        // A 1-byte specification of the version of this time-to-sample
        // atom.

        d.write(0); // flag[0]
        d.write(0); // flag[1]
        d.write(0); // flag[2]
        // A 3-byte space for time-to-sample flags. Set this field to 0.

        d.writeUInt(videoFrames.size()); // number of entries
        // A 32-bit integer containing the count of entries in the chunk
        // offset table.

        for (Sample s : videoFrames) {
            d.writeLong(s.offset); // offset
            // The offset contains the byte offset from the beginning of the
            // data stream to the chunk. The table is indexed by chunk
            // number-the first table entry corresponds to the first chunk,
            // the second table entry is for the second chunk, and so on.
        }
        return chunkOffsetAtom;
    }

    /**
     * Sample to chunk atom
     * The sample-to-chunk atom contains a table that maps samples to chunks
     * in the media data stream. By examining the sample-to-chunk atom, you
     * can determine the chunk that contains a specific sample.
     *
     * @param out ImageOutputStream for this data atom
     * @return filled Sample To Chunk Atom
     * @throws IOException if any write operation fails
     */
    private DataAtom createSamplesToChunksMappingAtom(ImageOutputStream out) throws IOException {

        DataAtom samplesToChunksMappingAtom = new DataAtom(SAMPLE_TO_CHUNK_MAPPING, out);

        /*
         * typedef struct { byte version; byte[3] flags; int numberOfEntries; sampleToChunkTable sampleToChunkTable[numberOfEntries]; } sampleToChunkAtom;
         *
         * typedef struct { int firstChunk; int samplesPerChunk; int sampleDescription; } sampleToChunkTable;
         */
        DataAtomOutputStream d = samplesToChunksMappingAtom.getOutputStream();
        d.write(0); // version
        // A 1-byte specification of the version of this time-to-sample atom.

        d.write(0); // flag[0]
        d.write(0); // flag[1]
        d.write(0); // flag[2]
        // A 3-byte space for time-to-sample flags. Set this field to 0.

        d.writeInt(1); // number of entries
        // A 32-bit integer containing the count of entries in the
        // sample-to-chunk table.

        d.writeInt(1); // first chunk
        // The first chunk number using this table entry.

        d.writeInt(1); // samples per chunk
        // The number of samples in each chunk.

        d.writeInt(1); // sample description
        // The identification number associated with the sample description for
        // the sample. For details on sample description atoms, see "Sample
        // Description Atoms.":
        // http://developer.apple.com/documentation/QuickTime/QTFF/QTFFChap2/chapter_3_section_5.html#//apple_ref/doc/uid/TP40000939-CH204-25691
        return samplesToChunksMappingAtom;
    }

    /**
     * Time to Sample atom
     * Time-to-sample atoms store duration information for a media's
     * samples, providing a mapping from a time in a media to the
     * corresponding data sample. The time-to-sample atom has an atom type
     * of 'stts'.
     *
     * @param out ImageOutputStream for this data atom
     * @return filled Time to Sample Atom
     * @throws IOException if any write operation fails
     */
    private DataAtom createTimeToSampleAtom(ImageOutputStream out) throws IOException {

        DataAtom timeToSampleAtom = new DataAtom(TIME_TO_SAMPLE_MAPPING, out);

        /*
         * typedef struct { byte version; byte[3] flags; int numberOfEntries; timeToSampleTable timeToSampleTable[numberOfEntries]; } timeToSampleAtom;
         *
         * typedef struct { int sampleCount; int sampleDuration; } timeToSampleTable;
         */
        DataAtomOutputStream d = timeToSampleAtom.getOutputStream();
        d.write(0); // version
        // A 1-byte specification of the version of this time-to-sample atom.

        d.write(0); // flag[0]
        d.write(0); // flag[1]
        d.write(0); // flag[2]
        // A 3-byte space for time-to-sample flags. Set this field to 0.

        // count runs of video frame durations
        int runCount = 1;
        int prevDuration = videoFrames.size() == 0 ? 0 : videoFrames.get(0).duration;
        for (Sample s : videoFrames) {
            if (s.duration != prevDuration) {
                runCount++;
                prevDuration = s.duration;
            }
        }
        d.writeInt(runCount); // numberOfEntries
        // A 32-bit integer containing the count of entries in the
        // time-to-sample table.

        int runLength = 0;
        prevDuration = videoFrames.size() == 0 ? 0 : videoFrames.get(0).duration;
        for (Sample s : videoFrames) {
            if (s.duration != prevDuration) {
                if (runLength > 0) {
                    d.writeInt(runLength); // timeToSampleTable[0].sampleCount
                    // A 32-bit integer that specifies the number of consecutive
                    // samples that have the same duration.

                    d.writeInt(prevDuration); // timeToSampleTable[0].sampleDuration
                    // A 32-bit integer that specifies the duration of each
                    // sample.
                }
                prevDuration = s.duration;
                runLength = 1;
            } else {
                runLength++;
            }
        }
        if (runLength > 0) {
            d.writeInt(runLength); // timeToSampleTable[0].sampleCount
            // A 32-bit integer that specifies the number of consecutive
            // samples that have the same duration.

            d.writeInt(prevDuration); // timeToSampleTable[0].sampleDuration
            // A 32-bit integer that specifies the duration of each
            // sample.
        }
        return timeToSampleAtom;
    }

    /**
     * Movie Header Atom -------------
     * The data contained in this atom defines characteristics of the entire QuickTime movie, such as time scale and
     * duration. It has an atom type value of 'mvhd'.
     * <p>
     * typedef struct { byte version; byte[3] flags; mactimestamp creationTime; mactimestamp modificationTime; int timeScale; int duration; int
     * preferredRate; short preferredVolume; byte[10] reserved; int[9] matrix; int previewTime; int previewDuration; int posterTime; int
     * selectionTime; int selectionDuration; int currentTime; int nextTrackId; } movieHeaderAtom;
     *
     * @param duration         time value that indicates duration of video
     * @param modificationTime calendar date and time of last modification
     * @param out              ImageOutputStream for this data atom
     * @return filled Movie Header Atom
     * @throws IOException if any write operation fails
     */
    private DataAtom createMovieHeaderAtom(Date modificationTime, int duration, ImageOutputStream out) throws IOException {
        DataAtom movieHeader = new DataAtom(MOVIE_HEADER, out);
        DataAtomOutputStream d = movieHeader.getOutputStream();
        d.writeByte(0); // version
        // A 1-byte specification of the version of this movie header atom.

        d.writeByte(0); // flags[0]
        d.writeByte(0); // flags[1]
        d.writeByte(0); // flags[2]
        // Three bytes of space for future movie header flags.

        d.writeMacTimestamp(creationTime); // creationTime
        // A 32-bit integer that specifies the calendar date and time (in
        // seconds since midnight, January 1, 1904) when the movie atom was
        // created. It is strongly recommended that this value should be
        // specified using coordinated universal time (UTC).

        d.writeMacTimestamp(modificationTime); // modificationTime
        // A 32-bit integer that specifies the calendar date and time (in
        // seconds since midnight, January 1, 1904) when the movie atom was
        // changed. BooleanIt is strongly recommended that this value should be
        // specified using coordinated universal time (UTC).

        d.writeInt(timeScale); // timeScale
        // A time value that indicates the time scale for this movie-that is,
        // the number of time units that pass per second in its time coordinate
        // system. A time coordinate system that measures time in sixtieths of a
        // second, for example, has a time scale of 60.

        d.writeInt(duration); // duration
        // A time value that indicates the duration of the movie in time scale
        // units. Note that this property is derived from the movie's tracks.
        // The value of this field corresponds to the duration of the longest
        // track in the movie.

        d.writeFixed16D16(1d); // preferredRate
        // A 32-bit fixed-point number that specifies the rate at which to play
        // this movie. A value of 1.0 indicates normal rate.

        d.writeShort(256); // preferredVolume
        // A 16-bit fixed-point number that specifies how loud to play this
        // movie's sound. A value of 1.0 indicates full volume.

        d.write(new byte[10]); // reserved;
        // Ten bytes reserved for use by Apple. Set to 0.

        d.writeFixed16D16(1f); // matrix[0]
        d.writeFixed16D16(0f); // matrix[1]
        d.writeFixed2D30(0f); // matrix[2]
        d.writeFixed16D16(0f); // matrix[3]
        d.writeFixed16D16(1f); // matrix[4]
        d.writeFixed2D30(0); // matrix[5]
        d.writeFixed16D16(0); // matrix[6]
        d.writeFixed16D16(0); // matrix[7]
        d.writeFixed2D30(1f); // matrix[8]
        // The matrix structure associated with this movie. A matrix shows how
        // to map points from one coordinate space into another. See "Matrices"
        // for a discussion of how display matrices are used in QuickTime:
        // http://developer.apple.com/documentation/QuickTime/QTFF/QTFFChap4/chapter_5_section_4.html#//apple_ref/doc/uid/TP40000939-CH206-18737

        d.writeInt(0); // previewTime
        // The time value in the movie at which the preview begins.

        d.writeInt(0); // previewDuration
        // The duration of the movie preview in movie time scale units.

        d.writeInt(0); // posterTime
        // The time value of the time of the movie poster.

        d.writeInt(0); // selectionTime
        // The time value for the start time of the current selection.

        d.writeInt(0); // selectionDuration
        // The duration of the current selection in movie time scale units.

        d.writeInt(0); // currentTime;
        // The time value for current time position within the movie.

        d.writeInt(2); // nextTrackId
        // A 32-bit integer that indicates a value to use for the track ID
        // number of the next track added to this movie. Note that 0 is not a
        // valid track ID value.
        return movieHeader;
    }

    /**
     * Writes a frame to the video track.
     * <p>
     * If the dimension of the video track has not been specified yet, it is derived from the first buffered image added to the QuickTimeOutputStream.
     *
     * @param image    The frame image.
     * @param duration The duration of the frame in time scale units.
     * @throws IllegalArgumentException if the duration is less than 1, or if the dimension of the frame does not match the dimension of the video track.
     * @throws IORuntimeException       if writing the image failed.
     */
    public void writeFrame(BufferedImage image, int duration) {
        checkArgument(duration >= 0, "duration should be greater than 0, but was %s", duration);
        ensureOpen();
        ensureStarted();

        // Get the dimensions of the first image
        if (imgWidth == UNSPECIFIED) {
            imgWidth = image.getWidth();
            imgHeight = image.getHeight();
        } else {
            // The dimension of the image must match the dimension of the video
            // track
            checkState(imgWidth == image.getWidth() && imgHeight == image.getHeight(),
                    "Dimensions of image[%s] (width=%s, height=%s) differs from image[0] (width=%s, height=%s",
                    videoFrames.size(), image.getWidth(), image.getHeight(), imgWidth, imgHeight);
        }
        try {
            long offset = out.getStreamPosition();
            switch (videoFormat) {
                case RAW: {
                    WritableRaster raster = image.getRaster();
                    int[] raw = new int[imgWidth * 3]; // holds a scanline of raw image
                    // data with 3 channels of 32
                    // bit data
                    byte[] bytes = new byte[imgWidth * 3]; // holds a scanline of raw
                    // image data with 3
                    // channels of 8 bit data
                    int n = imgWidth * 3;
                    for (int y = 0; y < imgHeight; y++) {
                        raster.getPixels(0, y, imgWidth, 1, raw);
                        for (int k = 0; k < n; k++) {
                            bytes[k] = (byte) raw[k];
                        }
                        mdatAtom.getOutputStream().write(bytes);
                    }
                    break;
                }
                case JPG: {
                    ImageWriter iw = ImageIO.getImageWritersByMIMEType("image/jpeg").next();
                    ImageWriteParam iwParam = iw.getDefaultWriteParam();
                    iwParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
                    iwParam.setCompressionQuality(quality);
                    MemoryCacheImageOutputStream imgOut = new MemoryCacheImageOutputStream(mdatAtom.getOutputStream());
                    iw.setOutput(imgOut);
                    IIOImage img = new IIOImage(image, null, null);
                    iw.write(null, img, iwParam);
                    iw.dispose();
                    break;
                }
                case PNG:
                default: {
                    ImageWriter iw = ImageIO.getImageWritersByMIMEType("image/png").next();
                    ImageWriteParam iwParam = iw.getDefaultWriteParam();
                    MemoryCacheImageOutputStream imgOut = new MemoryCacheImageOutputStream(mdatAtom.getOutputStream());
                    iw.setOutput(imgOut);
                    IIOImage img = new IIOImage(image, null, null);
                    iw.write(null, img, iwParam);
                    iw.dispose();
                    break;
                }
            }
            long length = out.getStreamPosition() - offset;
            videoFrames.add(new Sample(duration, offset, length));
        } catch (IOException e) {
            throw new IORuntimeException(e);
        }
    }

    /**
     * Writes a frame from a file to the video track.
     * <p>
     * This method does not inspect the contents of the file. The contents has to match the video format. For example, it is your responsibility to only
     * add JPG files if you have chosen the JPEG video format.
     * <p>
     * If you add 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 stop().
     *
     * @param file     The file which holds the image data.
     * @param duration The duration of the frame in time scale units.
     * @throws IllegalStateException if the duration is less than 1.
     * @throws IORuntimeException    if writing the image failed.
     */
    public void writeFrame(File file, int duration) {
        try (FileInputStream in = new FileInputStream(file)) {
            writeFrame(in, duration);
        } catch (IOException e) {
            throw new IORuntimeException(e);
        }
    }

    /**
     * Writes a frame to the video track.
     * <p>
     * This method does not inspect the contents of the input stream. The contents has to match the video format. For example, it is your responsibility
     * to only add JPG files if you have chosen the JPEG video format.
     * <p>
     * If you add 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 stop().
     *
     * @param in       The input stream which holds the image data.
     * @param duration The duration of the frame in time scale units.
     * @throws IllegalArgumentException if the duration is less than 1.
     * @throws IORuntimeException       if writing the image failed.
     */
    private void writeFrame(InputStream in, int duration) {
        checkArgument(duration >= 0, "duration should be greater than 0, but was %s", duration);
        ensureOpen();
        ensureStarted();
        try {
            long offset = out.getStreamPosition();
            try (OutputStream mdatOut = mdatAtom.getOutputStream()) {
                byte[] buf = new byte[512];
                int len;
                while ((len = in.read(buf)) != -1) {
                    mdatOut.write(buf, 0, len);
                }
                long length = out.getStreamPosition() - offset;
                videoFrames.add(new Sample(duration, offset, length));
            }
        } catch (IOException e) {
            throw new IORuntimeException(e);
        }
    }

    private void writeProlog() {
        try {
            /*
             * File type atom
             *
             * typedef struct { magic brand; bcd4 versionYear; bcd2 versionMonth; bcd2 versionMinor; magic[4] compatibleBrands; } ftypAtom;
             */
            DataAtom ftypAtom = new DataAtom(FILE_TYPE, out);
            DataAtomOutputStream d = ftypAtom.getOutputStream();
            d.writeType(QUICK_TIME); // brand
            d.writeBCD4(2005); // versionYear
            d.writeBCD2(3); // versionMonth
            d.writeBCD2(0); // versionMinor
            d.writeType(QUICK_TIME); // compatibleBrands
            d.writeInt(0); // compatibleBrands (0 is used to denote no value)
            d.writeInt(0); // compatibleBrands (0 is used to denote no value)
            d.writeInt(0); // compatibleBrands (0 is used to denote no value)
            ftypAtom.finish();
        } catch (IOException e) {
            throw new IORuntimeException(e);
        }
    }

    /**
     * The states of the movie output stream.
     */
    private enum States {
        STARTED, FINISHED, CLOSED
    }

    /**
     * Supported video formats.
     */
    public enum VideoFormat {
        RAW, JPG, PNG
    }

    /**
     * QuickTime stores media data in samples. A sample is a single element in a sequence of time-ordered data. Samples are stored in the mdat atom.
     */
    private static class Sample {

        /**
         * Offset of the sample relative to the start of the QuickTime file.
         */
        private final long offset;
        /**
         * Data length of the sample.
         */
        private final long length;
        /**
         * The duration of the sample in time scale units.
         */
        private final int duration;

        /**
         * Creates a new sample.
         *
         * @param duration The duration of the sample in time scale units.
         * @param offset   Offset of the sample relative to the start of the QuickTime file.
         * @param length   Data length of the sample.
         */
        Sample(int duration, long offset, long length) {
            this.duration = duration;
            this.offset = offset;
            this.length = length;
        }
    }
}
