/*
 * @(#)RIFFStructView.java  1.0.4  2011-08-28
 *
 * Copyright (c) 2005-2011 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.AbortException;
import org.monte.media.ParseException;
import org.monte.media.riff.*;
import org.monte.media.binary.*;

import java.awt.*;
import java.io.*;
import java.text.NumberFormat;
import java.util.WeakHashMap;

import javax.activation.DataHandler;
import javax.activation.FileDataSource;

import javax.swing.*;
import javax.swing.tree.*;

/**
 * Displays the structure of an RIFF file and parses well known chunks.
 *
 * @author  Werner Randelshofer, Hausmatt 10, CH-6405 Immensee, Switzerland
 * @version 1.0.3 2011-08-28 Don't read data twice when showing struct view.
 * <br>1.0.3 2011-03-13 Fixes display of empty data chunks.
 * <br>1.0.2 2011-01-14 Use a buffered input stream.
 * <br>1.0.1 2009-03-27 RIFFChunks.txt is now encoded with UTF-8.
 * <br>1.0 2005-06-19 indexInParent property added.
 * <br>0.9 2005-01-09
 */
public class RIFFStructView extends AbstractStructView {

    private InputStream inputStream;

    protected static class RIFFChunkNode
            implements AbstractStructView.StructNode {

        private static NumberFormat numFormat = NumberFormat.getInstance();
        private RIFFModel model;
        private String type;
        private String id;
        private int size;
        private int offset;
        private byte[] data;
        private int childCount;
        private String parserMessage;
        private int indexInParent;
        private File file;

        public RIFFChunkNode(RIFFModel model, String type, String id, int size, int offset, byte[] data) {
            this.model = model;
            this.type = type;
            this.id = id;
            this.size = size;
            this.offset = offset;
            this.data = data;
        }

        public RIFFChunkNode(RIFFModel model, String type, String id, int size, int offset, File file) {
            this.model = model;
            this.type = type;
            this.id = id;
            this.size = size;
            this.offset = offset;
            this.file = file;
        }

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

        public String getType() {
            return type;
        }

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

        @Override
        public long getSize() {
            return size;
        }

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

        public void setChildCount(int cc) {
            childCount = cc;
        }

        public void setParserMessage(String message) {
            parserMessage = message;
        }

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

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


        @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) {
                try {
                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;
                } catch (OutOfMemoryError e) {
                    e.printStackTrace();
                    return null;
                }
            }
            return null;
        }

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

        @Override
        public String toString() {
            StringBuffer buf = new StringBuffer();
            if (!hasRawData()&&size>0) {
                buf.append(type);
                buf.append(' ');
                if (id.equals(RIFFParser.NULL_NUL_ID)) {
                    buf.append("0x00000000");
                } else {
                    if (id.length() == 8) {
                        buf.append(id.substring(0, 4));
                        buf.append(" (");
                        buf.append(id.substring(4));
                        buf.append(')');
                    } else {
                        buf.append(id);
                    }
                }
                buf.append(' ');
                buf.append(numFormat.format(size));
                buf.append(" (");
                buf.append(childCount);
                buf.append(')');
            } else {
                if (id.equals(RIFFParser.NULL_NUL_ID)) {
                    buf.append("0x00000000");
                } else {
                    if (id.length() == 8) {
                        buf.append(id.substring(0, 4));
                        buf.append(" (");
                        buf.append(id.substring(4));
                        buf.append(')');
                    } else {
                        buf.append(id);
                    }
                }
                buf.append(' ');
                buf.append(numFormat.format(size));
            }
            if (parserMessage != null) {
                buf.append(' ');
                buf.append(parserMessage);
            }

            return buf.toString();
        }

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

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

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

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

    protected static class RIFFModel
            extends DefaultTreeModel
            implements RIFFVisitor {

        private static StructParser structParser;
        private DefaultMutableTreeNode decodeNode;
        private final static int NULL_ID = RIFFParser.stringToID("    ");
        private final static int LIST_ID = RIFFParser.stringToID("LIST");
        private final static int strl_ID = RIFFParser.stringToID("strl");
        private final static int strh_ID = RIFFParser.stringToID("strh");
        private final static int strf_ID = RIFFParser.stringToID("strf");
        private final static int vids_ID = RIFFParser.stringToID("vids");
        private final static int auds_ID = RIFFParser.stringToID("auds");
        /** Holds the FourCC of the track type as specified in the strh-Chunk
         * of an AVI file.
         */
        private int trackType = RIFFParser.NULL_ID;
        private File file;

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

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

            long streamPosition=0;
            try {
                RIFFParser iff = new RIFFParser();
                if (file != null) {
                    iff.declareDataChunk(strl_ID, strh_ID);
                    iff.declareStopChunks();
                }
                decodeNode = root;
                BufferedInputStream bin=new BufferedInputStream(in);
                
                while (true)
                streamPosition=iff.parse(in, this);
            } catch (EOFException e) {
                // EOF's are shown as annotations in the tree
                // We print the stack trace here for debugging purposes only
                //e.printStackTrace();
                //throw new IOException(e.getMessage());
            } catch (ParseException e) {
                // Parse exceptions are shown as annotations in the tree
                // We print the stack trace here for debugging purposes only
                System.err.println("RIFFStructView ParseException near "+streamPosition+" 0x"+Long.toHexString(streamPosition));
                e.printStackTrace();
                //throw new IOException(e.getMessage());
            } catch (AbortException e) {
                throw new IOException(e.getMessage());
            } finally {
                in.close();
            }
        }

        public void registerChunks(RIFFParser iff) {
        }


            private WeakHashMap<Integer,String> idCache= new WeakHashMap<Integer,String>();

    /**
     * Convert an integer IFF identifier to String.
     *
     * @param	anInt ID to be converted.
     * @return	String representation of the ID.
     */
    private String idToString(int anInt) {
        String str=idCache.get(anInt);
        if (str!=null) return str;

        byte[] bytes = new byte[4];

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

        try {
             str= new String(bytes, "ASCII");
            idCache.put(anInt,str);
            return str;
        } catch (UnsupportedEncodingException e) {
            throw new InternalError(e.getMessage());
        }
    }


        @Override
        public void enterGroup(RIFFChunk group) {
            RIFFChunkNode groupNode = new RIFFChunkNode(
                    this,
                    idToString(group.getID()),
                    idToString(group.getType()),
                    (int) group.getSize(),
                    (int) group.getScan(),
                    (byte[]) null);
            groupNode.setIndexInParent(decodeNode.getChildCount());
            DefaultMutableTreeNode newNode = new DefaultMutableTreeNode(groupNode);
            decodeNode.add(newNode);
            decodeNode = newNode;

            // Reset the track type when we enter an AVI stream header list
            if (group.getID() == LIST_ID
                    && group.getType() == strl_ID) {
                trackType = NULL_ID;
            }

        }

        @Override
        public void leaveGroup(RIFFChunk group) {
            RIFFChunkNode groupNode = (RIFFChunkNode) decodeNode.getUserObject();
            groupNode.setChildCount(decodeNode.getChildCount());
            groupNode.setParserMessage(group.getParserMessage());
            decodeNode = (DefaultMutableTreeNode) decodeNode.getParent();
        }

        @Override
        public void visitChunk(RIFFChunk group, RIFFChunk chunk) {
            // Extract the track type from the stream header of an AVI file.
            if (group!=null&&group.getID() == LIST_ID
                    && group.getType() == strl_ID
                    && chunk.getID() == strh_ID) {
                try {
                    RIFFPrimitivesInputStream in = new RIFFPrimitivesInputStream(new ByteArrayInputStream(chunk.getData()));
                    trackType = in.readFourCC();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            String chunkID = idToString(chunk.getID());

            // Change the chunk id of the stream format chunk in an AVI file.
            if (group!=null&&group.getID() == LIST_ID
                    && group.getType() == strl_ID
                    && chunk.getID() == strf_ID) {
                if (trackType == vids_ID || trackType == auds_ID) {
                    chunkID += idToString(trackType);
                }
            }

            // Create a node for the chunk

            RIFFChunkNode chunkNode;
            if (file != null) {
                chunkNode = new RIFFChunkNode(
                        this,
                        idToString(group==null?0:group.getType()),
                        chunkID,
                        (int) chunk.getSize(),
                        (int) chunk.getScan(),
                        file);
            } else {
                chunkNode = new RIFFChunkNode(
                        this,
                        RIFFParser.idToString(group==null?0:group.getType()),
                        chunkID,
                        (int) chunk.getSize(),
                        (int) chunk.getScan(),
                        chunk.getData());
            }
            chunkNode.setIndexInParent(decodeNode.getChildCount());
            decodeNode.add(new DefaultMutableTreeNode(chunkNode));
        }

        private String getMagic(RIFFChunkNode chunkNode) {
            String type = chunkNode.getType();
            String id = chunkNode.getId();
            String magic = type + id;
            if (structParser.isMagicDeclared(magic) == false) {
                if (RIFFParser.isGroupID(RIFFParser.stringToID(type))) {
                    magic = type + "????";
                } else {
                    magic = "????" + id;
                }
            }
            return magic;
        }

        public String getName(RIFFChunkNode chunkNode) {
            String name = structParser.getName(getMagic(chunkNode));
            return (name == null) ? chunkNode.getType() + " " + chunkNode.getId() : name;
        }

        public String getDescription(RIFFChunkNode chunkNode) {
            return structParser.getDescription(getMagic(chunkNode));
        }

        public StructTableModel getStructTableModel(RIFFChunkNode chunkNode) {
            try {
                byte[] data = chunkNode.getRawData(0, (int) chunkNode.getSize());
                return (data == null) ? null : structParser.readStruct(getMagic(chunkNode), data);
            } catch (IOException e) {
                return null;
            }
        }

        @Override
        public boolean enteringGroup(RIFFChunk group) {
           return true;
        }
    }

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

    @Override
    public void setCommandContext(String verb, DataHandler dh)
            throws IOException {
        
        inputStream = new BufferedInputStream(dh.getInputStream());


        setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
        final File file;
        if (dh.getDataSource() instanceof FileDataSource) {
            file = ((FileDataSource) dh.getDataSource()).getFile();
        } else {
            file = null;
        }
        ch.randelshofer.gui.SwingWorker worker = new ch.randelshofer.gui.SwingWorker() {

            @Override
            public Object construct() {
                Object result;

                try {
                    result = new RIFFModel(file, inputStream);
                } catch (Throwable e) {
                    e.printStackTrace();
                    result = e;
                } finally {
                    try {
                        if (inputStream != null) {
                            inputStream.close();
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                return result;
            }

            @Override
            public void finished() {
                Object result = get();
                if (result instanceof RIFFModel) {
                    getTree().setModel((RIFFModel) result);
                } else {
                    removeAll();
                    add(new JLabel(result.toString()));
                    if (result instanceof Throwable) {
                        ((Throwable) result).printStackTrace();
                    }
                    validate();
                }
                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 "RIFF 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
}
