/*
 * @(#)QuickTimeStructView.java  1.3.2  2011-01-07
 *
 * Copyright (c) 2000-2010 Werner Randelshofer, Immensee, Switzerland.
 * All rights reserved.
 *
 * You may not use, copy or modify this file, except in compliance with the
 * license agreement you entered into with Werner Randelshofer.
 * For details see accompanying license terms.
 */
package ch.randelshofer.view;

import org.monte.media.ParseException;
import org.monte.media.binary.*;
import org.monte.media.io.ByteArrayImageInputStream;
import ch.randelshofer.util.*;

import java.awt.*;
import java.io.*;
import java.text.NumberFormat;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.zip.*;

import javax.activation.DataHandler;

import javax.activation.FileDataSource;
import javax.imageio.stream.FileCacheImageInputStream;
import javax.imageio.stream.FileImageInputStream;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.stream.MemoryCacheImageInputStream;
import javax.swing.*;
import javax.swing.tree.*;

/**
 * Displays the structure of a QuickTime file and parses well known atoms.
 *
 * @author  Werner Randelshofer, Hausmatt 10, CH-6405 Immensee, Switzerland
 * @version 1.3.2 2011-01-07 Don't give up if "cmvd" atom can not be decompressed.
 * <br>1.3.1 2010-12-29 Fixes handling of 0 size atoms inside "udta" atom.
 * <br>1.3 2010-04-01 Replaced DataInputStream by ImageInputStream.
 * <br>1.2.4 2009-03-27 QuickTimeAtoms.txt is now encoded with UTF-8.
 * <br>1.2.3 2008-07-01 Improved handling of -14 sized atoms.
 * <br>1.2.1 2007-12-11 Added 'meta' and 'ilst' to composite atoms. 
 * <br>1.2 2005-06-19 indexInParent property added.
 * <br>1.1.2 2004-12-26 QTModel left file QuickTimeAtoms.txt open.
 * <br>1.1.1 2001-06-17 Conversion to JDK 1.3 in progress.
 * <br> 1.1   2000-10-09 Super class AbstractStructView instead of JPanel.
 * <br> 1.0  2000-06-13
 */
public class QuickTimeStructView extends AbstractStructView {

    private InputStream inputStream;

    protected static class QTAtom
            implements AbstractStructView.StructNode {

        private static NumberFormat numFormat = NumberFormat.getInstance();
        private QTModel model;
        private DefaultMutableTreeNode node;
        private String id;
        private long size;
        private long offset;
        private byte[] data;
        private int indexInParent;
        private File file;

        public QTAtom(QTModel model, String id, long offset, long size) {
            this.model = model;
            this.id = id;
            this.size = size;
            this.offset = offset;
        }

        public QTAtom(QTModel model, String id, long offset, long size, byte[] data) {
            this.model = model;
            this.id = id;
            this.size = size;
            this.offset = offset;
            this.data = data;
        }

        public QTAtom(QTModel model, String id, long offset, long size, File file) {
            this.model = model;
            this.id = id;
            this.size = size;
            this.offset = offset;
            this.file = file;
        }

        @Override
        public byte[] getRawData(long offset, int len) {
            if (len <= 0) {
                return new byte[0];
            }
            if (data != null) {
                if (offset == 0 && len == data.length) {
                    return data;
                } else {
                    byte[] bytes = new byte[len];
                    System.arraycopy(data, 0, bytes, (int) offset, Math.min(data.length, len));
                    return bytes;
                }
            } else if (file != null) {
                byte[] bytes = new byte[len];
                RandomAccessFile raf = null;
                try {
                    raf = new RandomAccessFile(file, "r");
                    raf.seek(this.offset + offset);
                    raf.read(bytes);
                } catch (IOException e) {
                    e.printStackTrace();
                    file = null;
                } finally {
                    if (raf != null) {
                        try {
                            raf.close();
                        } catch (IOException ex) {
                            ex.printStackTrace();
                            file = null;
                        }
                    }
                }
                return bytes;
            }
            return null;
        }

        @Override
        public java.util.List<BinaryPanel.Highlight> getRawDataHighlights() {
            return null;
        }

        @Override
        public long getSize() {
            return size - 8;  // the len includes the header of the atom
        }

        @Override
        public long getOffset() {
            return offset;
        }

        @Override
        public String getName() {
            return model.getName(this);
        }

        @Override
        public String getId() {
            return id;
        }

        @Override
        public String getDescription() {
            return model.getDescription(this);
        }

        @Override
        public StructTableModel getStructTableModel() {
            return model.getStructTableModel(this);
        }

        @Override
        public String toString() {
            StringBuffer b = new StringBuffer();
            /*String addr = Long.toHexString(pos);
            b.append("00000000".substring(addr.length()));
            b.append(addr);
            b.append("> ");
             */ b.append(id);
            b.append(' ');
            b.append(numFormat.format(size - 8));
            return b.toString();
        }

        @Override
        public int getIndexInParent() {
            return indexInParent;
        }

        public void setIndexInParent(int cc) {
            indexInParent = cc;
        }

        @Override
        public boolean hasRawData() {
            return data != null || file != null;
        }

        @Override
        public File getFile() {
            return file;
        }
    }

    protected static class QTModel extends DefaultTreeModel {

        private static StructParser structParser;
        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");
        }

        public QTModel(File file, InputStream in)
                throws IOException {
            super(new DefaultMutableTreeNode());
            DefaultMutableTreeNode root = (DefaultMutableTreeNode) getRoot();

            if (structParser == null) {
                BufferedReader reader = null;
                try {
                    reader = new BufferedReader(
                            new InputStreamReader(
                            QuickTimeStructView.class.getResourceAsStream("QuickTimeAtoms.txt"), "UTF-8"));
                    structParser = (reader == null) ? new StructParser() : new StructParser(reader);
                } catch (ParseException e) {
                    InternalError ie= new InternalError(e.toString());
                    ie.initCause(e);
                    throw ie;
                } finally {
                    if (reader != null) {
                        reader.close();
                    }
                }
            }

            ImageInputStream iin = null;
            try {
                iin = (file == null) ? new FileCacheImageInputStream(in, null) : new FileImageInputStream(file);
                decode(file, //
                        iin,//
                        root, Long.MAX_VALUE, 0);
            } catch (EOFException e) {
                // It's ok to encounter an EOF exception because QuickTime
                // does not have a marker that tells us when the file is finished.
            } catch (ParseException e) {
                root.add(new DefaultMutableTreeNode(new QuickTimeStructView.QTAtom(this, e.toString(), 0, 8)));
            } finally {
                IOException ex = null;
                try {
                    iin.close();
                } catch (IOException e) {
                    ex = e;
                }
                try {
                    in.close();
                } catch (IOException e) {
                    ex = e;
                }
                if (ex != null) {
                    throw ex;
                }
            }
        }

        public String getName(QTAtom atom) {
            String id = atom.getId();
            String name = structParser.getName(id);

            return (name == null) ? id : name;
        }

        public String getDescription(QTAtom atom) {
            String id = atom.getId();
            return structParser.getDescription(id);
        }

        public StructTableModel getStructTableModel(QTAtom atom) {


            if (structParser.isTypeDeclared(atom.getId())) {
                try {
                    byte[] data = atom.getRawData(0, (int) atom.getSize());
                    return (data == null) ? null : structParser.readStruct(atom.getId(), data);
                } catch (IOException e) {
                    return null;
                }
            } else {
                return null;
            }
        }

        private void decode(File file, ImageInputStream in, DefaultMutableTreeNode parentNode, long remainingSize, long pos)
                throws IOException, ParseException {
            DefaultMutableTreeNode node;
            QTAtom 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 && (parentNode == null||parentNode.getUserObject()==null)) {
                    // 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 = typeToString(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")) {
                    DefaultMutableTreeNode p = parentNode;
                    while (p != null) {
                        for (int i = 0, n = p.getChildCount(); i < n; i++) {
                            QTAtom pca = (QTAtom) ((DefaultMutableTreeNode) p.getChildAt(i)).getUserObject();
                            if (pca.getId().equals("vmhd")
                                    || pca.getId().equals("smhd")) {
                                type = type + " (" + pca.getId() + ")";
                                break;
                            }
                        }
                        p = (DefaultMutableTreeNode) p.getParent();
                    }
                }

                if (compositeAtoms.contains(type) && size - headerSize >= 8) {
                    QTAtom parentAtom = (QTAtom) parentNode.getUserObject();
                    atom = new QTAtom(this, type, pos + headerSize, atomSize);
                    node = new DefaultMutableTreeNode(atom);
                    atom.setIndexInParent(parentNode.getChildCount());
                    parentNode.add(node);

                    decode(file, in, node, size - headerSize, pos + headerSize);
                } else {
                    byte[] data;
                    if (size < headerSize) {
                        // Pad atom?
                        data = new byte[0];
                        atom = new QTAtom(this, type, pos + headerSize, size, data);
                    } else if (file == null || type.equals("cmvd")) {
                        data = new byte[(int) size - headerSize];
                        in.readFully(data, 0, data.length);
                        atom = new QTAtom(this, type, pos + headerSize, size, data);

                    } else {
                        data = null;
                        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");
                            }
                        }
                        atom = new QTAtom(this, type, pos + headerSize, size, file);
                    }

                    atom.setIndexInParent(parentNode.getChildCount());
                    node = new DefaultMutableTreeNode(atom);
                    parentNode.add(node);

                    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();
                        decode(null,
                                new ByteArrayImageInputStream(decompressed),
                                node,
                                decompressed.length,
                                pos + headerSize);
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }
                pos += size;
                //              System.out.println("QuickTimeStructView "+type+" pos:"+pos+" stream pos:"+in.getStreamPosition());
            }
        }

        /**
         * Convert an integer Atom identifier to String.
         *
         * @param  ID to be converted.
         * @return  String representation of the ID.
         */
        private String typeToString(int type) {
            byte[] bytes = new byte[4];

            bytes[0] = (byte) (type >>> 24);
            bytes[1] = (byte) (type >>> 16);
            bytes[2] = (byte) (type >>> 8);
            bytes[3] = (byte) (type >>> 0);
            try {

                return new String(bytes, "MacRoman");
            } catch (UnsupportedEncodingException ex) {
                return new String(bytes);
            }
        }
    }

    /** Initializes the Form */
    public QuickTimeStructView() {
        //initComponents ();
    }

    @Override
    public void setCommandContext(String verb, DataHandler dh)
            throws IOException {
        final File file;
        if (dh.getDataSource() instanceof FileDataSource) {
            file = ((FileDataSource) dh.getDataSource()).getFile();
        } else {
            file = null;
        }
        inputStream = new BufferedInputStream(dh.getInputStream());
        setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));

        ch.randelshofer.gui.SwingWorker worker = new ch.randelshofer.gui.SwingWorker() {

            @Override
            public Object construct() {
                Object result;

                try {
                    result = new QTModel(file, inputStream);
                } catch (Exception e) {
                    result = e;
                }
                try {
                    inputStream.close();
                } catch (IOException e) {
                }

                return result;
            }

            @Override
            public void finished() {
                Object result = get();
                if (result instanceof QTModel) {
                    getTree().setModel((QTModel) result);
                } else {
                    removeAll();
                    add(new JLabel(result.toString()));
                    ((Throwable) result).printStackTrace();
                }
                setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
                getTree().expandRow(0);
                getTree().setSelectionRow(0);
                getSplitPane().setDividerLocation(getTree().getPreferredSize().width + 20);
            }
        };
        worker.start();
    }

    @Override
    public void dispose() {
        if (inputStream != null) {
            try {
                inputStream.close();
            } catch (IOException e) {
            }
            inputStream = null;
        }
    }

    @Override
    public String toString() {
        return "QuickTime Struct View";
    }

    /** This method is called from within the constructor to
     * initialize the form.
     * WARNING: Do NOT modify this code. The content of this method is
     * always regenerated by the FormEditor.
     */
    private void initComponents () {//GEN-BEGIN:initComponents
        setLayout (new java.awt.BorderLayout ());

    }//GEN-END:initComponents
// Variables declaration - do not modify//GEN-BEGIN:variables
// End of variables declaration//GEN-END:variables
}
