/*
 * @(#)SEQStructView.java  1.0  2010-12-28
 *
 * Copyright (c) 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.mp3.MP3ElementaryInputStream;
import org.monte.media.mp3.MP3ElementaryInputStream.Frame;
import org.monte.media.binary.*;
import ch.randelshofer.util.*;
import java.awt.Cursor;

import java.io.*;

import javax.activation.DataHandler;
import javax.activation.FileDataSource;
import javax.imageio.stream.FileImageInputStream;
import javax.imageio.stream.ImageInputStream;

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

/**
 * Displays the structure of an MP3 elementary stream.
 *
 * @author  Werner Randelshofer
 * @version 1.0 2010-12-28 Created.
 */
public class MP3StructView extends AbstractStructView {

    private InputStream inputStream;

    protected static class MP3Node
            implements AbstractStructView.StructNode {

        private MP3ElementaryInputStream.Frame frame;
        private byte[] data;
        private int indexInParent;
        private long offset;
        private MP3Model model;
        private String name;
        private File file;
        private long size;

        public MP3Node(MP3Model model, String name, File file, MP3ElementaryInputStream.Frame frame, byte[] data, int indexInParent, long offset, long size) {
            this.model = model;
            this.name = name;
            this.file = file;
            this.frame = frame;
            this.data = data;
            this.indexInParent = indexInParent;
            this.offset = offset;
            this.size = size;
        }

        @Override
        public String getName() {
            return name;
        }

        @Override
        public StructTableModel getStructTableModel() {
            if (name.startsWith("Header")) {
                StructParser p = MP3Model.getStructParser();
                String magic = frame.getVersion() + "." + frame.getLayer();
                byte[] d = frame.headerToByteArray();
                try {
                    return p.readStruct(magic, d);
                } catch (IOException e) {
                    e.printStackTrace();
                    return null;
                }
            } else if (name.startsWith("SideInfo")) {
                StructParser p = MP3Model.getStructParser();
                String magic = name;
                byte[] d = getRawData(0L,(int)size);
                try {
                    return p.readStruct(magic, d);
                } catch (IOException e) {
                    e.printStackTrace();
                    return null;
                }
            } else {
                return null;
            }
        }

        @Override
        public byte[] getRawData(long offset, int length) {
            byte[] d = data;
            if (frame != null) {
                d = frame.headerToByteArray();
            } else if (file != null) {
                FileImageInputStream in = null;
                try {
                    d = data = new byte[(int) size];
                    in = new FileImageInputStream(file);
                    in.skipBytes(this.offset);
                    in.read(data);
                    file = null;
                } catch (IOException e) {
                    e.printStackTrace();
                } finally {
                    if (in != null) {
                        try {
                            in.close();
                        } catch (IOException ex) {
                            ex.printStackTrace();
                        }
                    }
                }
            }

            if (d == null || offset == 0 && length == d.length) {
                return d;
            } else {
                byte[] bytes = new byte[length];
                System.arraycopy(d, 0, bytes, (int) offset, length);
                return bytes;
            }
        }

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

        @Override
        public String toString() {
            return getName()+" "+getSize();
        }

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

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

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

        @Override
        public String getDescription() {
            StructParser p = MP3Model.getStructParser();
            if (frame != null) {
                String magic = frame.getVersion() + "." + frame.getLayer();
                return p.getDescription(magic);
            } else if (name.startsWith("Frame")) {
                return p.getDescription("Frame");
            } else {
                return p.getDescription(name);
            }
        }

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

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

        @Override
        public File getFile() {

            return file;
        }
    }

    protected static class MP3Model
            extends DefaultTreeModel {

        private static StructParser structParser;

        public MP3Model(InputStream in, File file)
                throws IOException {
            super(new DefaultMutableTreeNode());
            // Parse the input stream
            if (!(in instanceof BufferedInputStream)) {
                in = new BufferedInputStream(in);
            }
            DefaultMutableTreeNode r = (DefaultMutableTreeNode) root;

            MP3ElementaryInputStream mp3 = new MP3ElementaryInputStream(in);
            int frameCount = 0;
            Frame previousFrame = null;
            for (Frame f = mp3.getNextFrame(); f != null; f = mp3.getNextFrame()) {
                if (frameCount == 0) {
                    if (mp3.getStreamPosition() > 4) {
                        r.add(new DefaultMutableTreeNode(new MP3Node(this, "Junk", file, null, null, 0, 0, mp3.getStreamPosition() - 4)));
                    }
                }
                if (previousFrame != null && previousFrame.getFrameOffset()+previousFrame.getFrameSize()<mp3.getStreamPosition()-f.getHeaderSize()) {
                        r.add(new DefaultMutableTreeNode(new MP3Node(this, "Junk", file, null, null, 0, previousFrame.getBodyOffset()+previousFrame.getBodySize(), mp3.getStreamPosition() - previousFrame.getBodyOffset()-previousFrame.getBodySize()-f.getHeaderSize())));
                }

                DefaultMutableTreeNode frameNode = new DefaultMutableTreeNode(new MP3Node(this, "Frame (" + frameCount+")", file, null, null, r.getChildCount(),f.getBodyOffset() - f.getHeaderSize(), f.getFrameSize()));
                r.add(frameNode);
                frameNode.add(new DefaultMutableTreeNode(new MP3Node(this, "Header", null, f, null, 0, f.getBodyOffset() - f.getHeaderSize(), f.getHeaderSize())));
                MP3Node node = new MP3Node(this,f.getSideInfoSize()==17? "SideInfo (Single Channel)" : "SideInfo", file, null, null, 0, f.getSideInfoOffset(),f.getSideInfoSize());
                frameNode.add(new DefaultMutableTreeNode(node));
                node = new MP3Node(this, "MainData", file, null, null, 0, f.getBodyOffset()+f.getSideInfoSize(), f.getBodySize()-f.getSideInfoSize());
                frameNode.add(new DefaultMutableTreeNode(node));
                frameCount++;
                previousFrame = f;
            }
                if (previousFrame != null && previousFrame.getFrameOffset()+previousFrame.getFrameSize()<mp3.getStreamPosition()) {
                        r.add(new DefaultMutableTreeNode(new MP3Node(this, "Junk", file, null, null, 0, previousFrame.getBodyOffset()+previousFrame.getBodySize(), mp3.getStreamPosition() - previousFrame.getBodyOffset()-previousFrame.getBodySize())));
                }
            //if (dataNode != null) {
            //   dataNode.size = mp3.getStreamPosition() - dataNode.offset;
            //}
        }

        private static StructParser getStructParser() {
            // Parse the CyberPaintSeqStruct's definitions.
            if (structParser == null) {
                BufferedReader reader = null;
                try {
                    reader = new BufferedReader(
                            new InputStreamReader(
                            SEQStructView.class.getResourceAsStream("MP3Structs.txt")));
                    structParser = (reader == null) ? new StructParser() : new StructParser(reader);
                } catch (NullPointerException e) {
                    InternalError error = new InternalError(e.toString());
                    error.initCause(e);
                    throw error;
                } catch (ParseException e) {
                    InternalError error = new InternalError(e.toString());
                    error.initCause(e);
                    throw error;
                } catch (IOException e) {
                    InternalError error = new InternalError(e.toString());
                    error.initCause(e);
                    throw error;
                } finally {
                    if (reader != null) {
                        try {
                            reader.close();
                        } catch (IOException ex) {
                            ex.printStackTrace();
                        }
                    }
                }
            }
            return structParser;

        }
    }

    /** Creates new form PictStructView */
    public MP3StructView() {
        //initComponents();
    }

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


        if (dh.getDataSource() instanceof FileDataSource) {
            file = ((FileDataSource) dh.getDataSource()).getFile();


        } else {
            file = null;


        }
        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 MP3Model(inputStream, file);


                } catch (IOException e) {
                    result = e;


                }
                try {
                    inputStream.close();


                } catch (IOException e) {
                }

                return result;


            }

            @Override
            public void finished() {
                Object result = get();


                if (result instanceof MP3Model) {
                    getTree().setModel((MP3Model) result);


                } else {
                    removeAll();
                    add(
                            new JLabel(result.toString()));


                    if (result instanceof Throwable) {
                        ((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;

        }
    }

    /** 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 Form Editor.
     */
    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
    private void initComponents() {
    }// </editor-fold>//GEN-END:initComponents
    // Variables declaration - do not modify//GEN-BEGIN:variables
    // End of variables declaration//GEN-END:variables
}
