/*
 * @(#)QuickTimeInputStream.java  
 * 
 * Copyright (c) 2012 Werner Randelshofer, Immensee, Switzerland.
 * All rights reserved.
 * 
 * You may not use, copy or modify this file, except in compliance with the
 * license agreement you entered into with Werner Randelshofer.
 * For details see accompanying license terms.
 */
package org.monte.media.quicktime;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteOrder;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.zip.InflaterInputStream;
import javax.imageio.stream.FileImageInputStream;
import javax.imageio.stream.ImageInputStream;
import org.monte.media.io.ImageInputStreamAdapter;

/**
 * Provides low-level support for reading encoded audio and video samples from
 * a QuickTime file.
 *
 * @author Werner Randelshofer
 * @version $Id: QuickTimeInputStream.java 172 2012-02-25 08:17:11Z werner $
 */
public class QuickTimeInputStream extends AbstractQuickTimeStream {

    /** Same as DataAtom in super class, but used for reading the meta data. */
    private static class InputAtom {

        private String type;
        private byte[] data;

        public InputAtom(String type, byte[] data) {
            this.type = type;
            this.data = data;
        }
    }
    /** The image input stream. */
    protected final ImageInputStream in;
    /** This variable is set to true when all meta-data has been read from
     * the file.
     */
    private boolean isRealized = false;
    static final HashSet<String> compositeAtoms;

    static {
        compositeAtoms = new HashSet<String>();
        compositeAtoms.add("moov");
        compositeAtoms.add("cmov");
        compositeAtoms.add("gmhd");
        compositeAtoms.add("trak");
        compositeAtoms.add("tref");
        compositeAtoms.add("meta"); // sometimes has a special 4 byte header before its contents
        compositeAtoms.add("ilst");
        compositeAtoms.add("mdia");
        compositeAtoms.add("minf");
        compositeAtoms.add("udta");
        compositeAtoms.add("stbl");
        compositeAtoms.add("dinf");
        compositeAtoms.add("edts");
        compositeAtoms.add("clip");
        compositeAtoms.add("matt");
        compositeAtoms.add("rmra");
        compositeAtoms.add("rmda");
        compositeAtoms.add("tapt");
        compositeAtoms.add("mvex");
    }

    /**
     * Creates a new instance.
     *
     * @param file the input file
     */
    public QuickTimeInputStream(File file) throws IOException {

        this.in = new FileImageInputStream(file);
        in.setByteOrder(ByteOrder.BIG_ENDIAN);
        this.streamOffset = 0;
    }

    /**
     * Creates a new instance.
     *
     * @param in the input stream.
     */
    public QuickTimeInputStream(ImageInputStream in) throws IOException {
        this.in = in;
        this.streamOffset = in.getStreamPosition();
        in.setByteOrder(ByteOrder.BIG_ENDIAN);
    }

    public int getTrackCount() throws IOException {
        ensureRealized();
        throw new UnsupportedOperationException("Not supported yet.");
    }

    public long getMovieDuration() throws IOException {
        ensureRealized();
        long duration = 0;
        for (Track t : tracks) {
            duration = Math.max(duration, t.getTrackDuration(movieTimeScale));
        }
        return duration;
    }

    /**
     * Returns the time scale of the movie.
     *
     * @return time scale
     * @see #setMovieTimeScale(long)
     */
    public long getMovieTimeScale() throws IOException {
        ensureRealized();
        return movieTimeScale;
    }

    /**
     * Returns the time scale of the media in a track.
     *
     * @param track Track index.
     * @return time scale
     * @see #setMovieTimeScale(long)
     */
    public long getMediaTimeScale(int track) throws IOException {
        ensureRealized();
        return tracks.get(track).mediaTimeScale;
    }

    /**
     * Returns the media duration of a track in the media's time scale.
     *
     * @param track Track index.
     * @return media duration
     */
    public long getMediaDuration(int track) throws IOException {
        ensureRealized();
        return tracks.get(track).mediaDuration;
    }

    /** Ensures that all meta-data has been read from the file. */
    protected void ensureRealized() throws IOException {
        if (!isRealized) {
            isRealized = true;
            readAllMetadata();
        }
    }

    private void readAllMetadata() throws IOException {
        long remainingSize = in.length();
        if (remainingSize == -1) {
            remainingSize = Long.MAX_VALUE;
        }

        in.seek(0);
        readAllMetadata(new DataAtomInputStream(new ImageInputStreamAdapter(in)), remainingSize, new HashMap<String, InputAtom>(), null);
    }

    private void readAllMetadata(DataAtomInputStream in, long remainingSize, HashMap<String, InputAtom> atoms, String path) throws IOException {
        long pos = 0;

        InputAtom atom;

        while (remainingSize > 0) {
            long size = in.readInt() & 0xffffffffL;
            int headerSize = 8;
            if (size == 0) {
                // A zero len indicates that the size is the remainder of
                // the parent atom.
                size = remainingSize;

                // skip 4 bytes after zero size.
                if (headerSize + 4 <= remainingSize) {
                    in.skipBytes(4);
                    headerSize += 4;
                }

            } else if (size == 1) {
                // A size of 1 indicates a 64 bit size field
                headerSize = 16;
                size = in.readLong();
            }
            String type;
            long atomSize = size;
            if (size > remainingSize) {
                //System.out.println("QuickTimeStructView truncating size: " + size + " to:" + (remainingSize));
                size = remainingSize;
            }

            if (size - headerSize >= 0) {
                type = intToType(in.readInt());
                //System.out.println("QuickTimeStructView " + type + " size:" + size + " remaining:" + remainingSize);
            } else {
                type = "";
            }
            remainingSize -= size;

            // "stsd" atoms have a special structure if one of their parent
            // atoms contains a "vmhd" or a "smhd" child atom.
            if (type.equals("stsd")) {
                // handle stsd chunk
            }

            if (compositeAtoms.contains(type) && size - headerSize >= 8) {
                if (type.equals("trak")) {
                    atoms.clear();
                    readAllMetadata(in, size - headerSize, atoms, path == null ? type : path + "." + type);
                    parseTrack(atoms);
                } else {
                    readAllMetadata(in, size - headerSize, atoms, path == null ? type : path + "." + type);
                }
            } else {
                byte[] data;
                if (type.equals("mdat")) {
                    data = new byte[0];
                    long skipped = 0;
                    while (skipped < size - headerSize) {
                        long skipValue = in.skipBytes(size - headerSize - skipped);
                        if (skipValue > 0) {
                            skipped += skipValue;
                        } else {
                            throw new IOException("unable to skip");
                        }
                    }
                } else {
                    if (size < headerSize) {
                        // Pad atom?
                        data = new byte[0];
                    } else {
                        data = new byte[(int) (size - headerSize)];
                        in.readFully(data);
                    }
                }
                atom = new InputAtom(type, data);
                atoms.put(path == null ? type : path + "." + type, atom);

                if (type.equals("cmvd")) {
                    try {
                        InputStream in2 = new InflaterInputStream(new ByteArrayInputStream(data, 4, data.length - 4));
                        ByteArrayOutputStream out = new ByteArrayOutputStream();
                        int b;
                        while ((b = in2.read()) != -1) {
                            out.write(b);
                        }
                        in2.close();
                        out.close();

                        byte[] decompressed = out.toByteArray();
                        readAllMetadata(new DataAtomInputStream(
                                new ByteArrayInputStream(decompressed)),
                                decompressed.length,
                                atoms, path);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                } else if (type.equals("mvhd")) {
                    parseMovieHeader(data);
                }
            }
            pos += size;
        }
    }

    public void close() throws IOException {
        in.close();
    }

    /** Parses the mvhd atom. */
    private void parseMovieHeader(byte[] data) throws IOException {
        /* 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'.
         *
         * typedef struct {
        byte version;
        byte[3] flags;
        mactimestamp creationTime;
        mactimestamp modificationTime;
        int timeScale;
        int duration;
        fixed16d16 preferredRate;
        fixed8d8 preferredVolume;
        byte[10] reserved;
        fixed16d16 matrixA;
        fixed16d16 matrixB;
        fixed2d30 matrixU;
        fixed16d16 matrixC;
        fixed16d16 matrixD;
        fixed2d30 matrixV;
        fixed16d16 matrixX;
        fixed16d16 matrixY;
        fixed2d30 matrixW;
        int previewTime;
        int previewDuration;
        int posterTime;
        int selectionTime;
        int selectionDuration;
        int currentTime;
        int nextTrackId;
        } movieHeaderAtom; 
         */
        DataAtomInputStream i = new DataAtomInputStream(new ByteArrayInputStream(data));
        int version = i.readByte();
        i.skipBytes(3);// flags
        creationTime = i.readMacTimestamp();
        modificationTime = i.readMacTimestamp();
        movieTimeScale = i.readUInt();
        long movieDuration = i.readUInt();
        double preferredRate = i.readFixed16D16();
        double preferredVolume = i.readFixed8D8();
        i.skipBytes(10);//reserved
        double[] matrix = new double[9];
        // {a, b, u,
        //  c, d, v,
        //  tx,ty,w} // X- and Y-Translation
        movieMatrix[0] = i.readFixed16D16();
        movieMatrix[1] = i.readFixed16D16();
        movieMatrix[2] = i.readFixed2D30();
        movieMatrix[3] = i.readFixed16D16();
        movieMatrix[4] = i.readFixed16D16();
        movieMatrix[5] = i.readFixed2D30();
        movieMatrix[6] = i.readFixed16D16();
        movieMatrix[7] = i.readFixed16D16();
        movieMatrix[8] = i.readFixed2D30();
        long previewTime = i.readUInt();
        long previewDuration = i.readUInt();
        long posterTime = i.readUInt();
        long selectionTime = i.readUInt();
        long selectionDuration = i.readUInt();
        long currentTime = i.readUInt();
        long nextTrackId = i.readUInt();
    }

    /** Parses track atoms. */
    private void parseTrack(HashMap<String, InputAtom> atoms) throws IOException {
        for (String p : atoms.keySet()) {
            System.out.println("QuickTimeInputStream " + p);
        }
        HashMap<String, Object> hdlrMap = parseHdlr(atoms.get("moov.trak.mdia.hdlr").data);
        String trackType = (String) hdlrMap.get("componentSubtype");
        Track t;
        if ("vide".equals(trackType)) {
            t = new VideoTrack();
            parseVideoTrack((VideoTrack)t,atoms);
        } else if ("soun".equals(trackType)) {
            t = new AudioTrack();
            parseAudioTrack((AudioTrack)t,atoms);
        } else {
            throw new IOException("Unsupported track type: " + trackType);
        }
        tracks.add(t);
    }

    private void parseVideoTrack(VideoTrack t, HashMap<String, InputAtom> atoms) throws IOException {
        parseTkhd(t,atoms.get("moov.track.tkhd").data);
    }

    private void parseAudioTrack(AudioTrack t, HashMap<String, InputAtom> atoms) throws IOException {
        parseTkhd(t,atoms.get("moov.track.tkhd").data);
    }

    /** Parses a tkhd atom. */
    private void parseTkhd(Track t,byte[] data) throws IOException {
        /*
        // Enumeration for track header flags
        set {
            TrackEnable = 0x1, // enabled track
            TrackInMovie = 0x2, // track in playback
            TrackInPreview = 0x4, // track in preview
            TrackInPoster = 0x8 // track in poster
        } TrackHeaderFlags;


        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;
            fixed8d8 volume;
            byte[2] reserved;
            fixed16d16 matrixA;
            fixed16d16 matrixB;
            fixed2d30 matrixU;
            fixed16d16 matrixC;
            fixed16d16 matrixD;
            fixed2d30 matrixV;
            fixed16d16 matrixX;
            fixed16d16 matrixY;
            fixed2d30 matrixW;
            fixed16d16 trackWidth;
            fixed16d16 trackHeight;
        } trackHeaderAtom;
         */
        DataAtomInputStream i = new DataAtomInputStream(new ByteArrayInputStream(data));

        int version = i.readByte();
        i.skipBytes(2);
        int trackHeaderFlags=i.readByte();
        Date creationTime=i.readMacTimestamp();
        Date modificationTime=i.readMacTimestamp();
        int trackId=i.readInt();
        i.skipBytes(4);
        long duration=i.readUInt();
        i.skipBytes(8);
        int layer=i.readUShort();
        int alternatGroup=i.readUShort();
        double volume=i.readFixed8D8();
        i.skipBytes(2);

        // {a, b, u,
        //  c, d, v,
        //  tx,ty,w} // X- and Y-Translation
        t.matrix[0] = i.readFixed16D16();
        t.matrix[1] = i.readFixed16D16();
        t.matrix[2] = i.readFixed2D30();
        t.matrix[3] = i.readFixed16D16();
        t.matrix[4] = i.readFixed16D16();
        t.matrix[5] = i.readFixed2D30();
        t.matrix[6] = i.readFixed16D16();
        t.matrix[7] = i.readFixed16D16();
        t.matrix[8] = i.readFixed2D30();
        
        t.width=i.readFixed16D16();
        t.height=i.readFixed16D16();
    }
    /** Parses a hdlr atom. */
    private HashMap<String, Object> parseHdlr(byte[] data) throws IOException {
        /*
        typedef struct {
        byte version;
        byte[3] flags;
        magic componentType;
        magic componentSubtype;
        magic componentManufacturer;
        int componentFlags;
        int componentFlagsMask;
        pstring componentName;
        ubyte[] extraData;
        } handlerReferenceAtom;
         */
        DataAtomInputStream i = new DataAtomInputStream(new ByteArrayInputStream(data));

        int version = i.readByte();
        i.skipBytes(3);
        String componentType = i.readType();
        String componentSubtype = i.readType();
        String componentManufactureer = i.readType();
        int componentFlags = i.readInt();
        int componentFlagsMask = i.readInt();
        String componentName = i.readPString();

        HashMap<String, Object> m = new HashMap<String, Object>();
        m.put("componentSubtype", componentSubtype);
        return m;
    }
}
