/*
 * @(#)PictStructView.java  1.0  2006-12-28
 *
 * Copyright (c) 2006-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 ch.randelshofer.io.CopyingInputStream;
import org.monte.media.binary.*;
import ch.randelshofer.util.*;

import java.awt.*;
import java.io.*;
import java.util.HashSet;

import javax.activation.DataHandler;

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

/**
 * Displays the structure of a PICT file and parses well known chunks.
 * <a href="http://developer.apple.com/documentation/mac/QuickDraw/QuickDraw-458.html"
 * >http://developer.apple.com/documentation/mac/QuickDraw/QuickDraw-458.html</a>
 *
 * @author  Werner Randelshofer
 * @version 1.0 2006-12-28 Created.
 */
public class PictStructView extends AbstractStructView {
    private InputStream inputStream;
    private final static int PICTURE_ID = Integer.MIN_VALUE;
    private final static int HEADER_ID = Integer.MIN_VALUE + 1;
    private final static int ERROR_ID = Integer.MIN_VALUE + 2;
    
    protected static class PictNode
            implements AbstractStructView.StructNode {
        private PictModel model;
        private int id;
        private byte[] data;
        private int indexInParent;
        private int offset;
        
        public PictNode(PictModel model, int id, int offset, byte[] data) {
            this.model = model;
            this.id = id;
            this.offset = offset;
            this.data = data;
        }
        
        
        @Override
        public String getName() {
            return model.getName(id);
        }
        
        @Override
        public StructTableModel getStructTableModel() {
            return model.getStructTableModel(this);
        }
        @Override
        public byte[] getRawData(long offset, int length) {
            if (data == null || offset == 0 && length == data.length) {
                return data;
            } else {
                byte[] bytes = new byte[length];
                System.arraycopy(data, 0, bytes, (int) offset, length);
                return bytes;
            }
        }
        @Override
        public java.util.List<BinaryPanel.Highlight> getRawDataHighlights() {
            return null;
        }
        
        @Override
        public String toString() {
            return getName();
        }
        
        @Override
        public String getId() {
            switch (id) {
                case PICTURE_ID :
                case HEADER_ID :
                case ERROR_ID :
                    return "\u2013";
                default :
                    String hex = "0000"+Integer.toHexString(id);
                    return id+" (0x"+hex.substring(hex.length() - 4)+")";
            }
        }
        
        @Override
        public long getSize() {
            return data.length;
        }
        
        @Override
        public long getOffset() {
            return offset;
        }
        
        @Override
        public String getDescription() {
            return model.getDescription(id);
        }
        
        @Override
        public int getIndexInParent() {
            return indexInParent;
        }
        public void setIndexInParent(int cc) {
            indexInParent = cc;
        }
        @Override
        public boolean hasRawData() {
            return data != null;
        }

        @Override
        public File getFile() {
            return null;
        }
    }
    
    
    protected static class PictModel
            extends DefaultTreeModel {
        private static StructParser structParser;
        
        public PictModel(InputStream in)
        throws IOException {
            super(new DefaultMutableTreeNode());
            DefaultMutableTreeNode root = (DefaultMutableTreeNode) getRoot();
            
            if (! (in instanceof BufferedInputStream)) {
                in = new BufferedInputStream(in);
            }
            ByteArrayOutputStream buf = new ByteArrayOutputStream();
            CopyingInputStream cin = new CopyingInputStream(in, buf);
            DataInputStream din = new DataInputStream(cin);
            
            int offset = 0;
            // Test if there is a 512 byte header at the beginning of the stream
            // and skip it, if it is present.
            {
                din.mark(512);
                int first = din.read();
                int second = din.read();
                if (first == 0 && second == 0) {
                    din.skipBytes(510);
                    PictNode node = new PictNode(this, HEADER_ID, offset, buf.toByteArray());
                    DefaultMutableTreeNode pictNode = new DefaultMutableTreeNode(node);
                    root.add(pictNode);
                    offset += 512;
                    buf.reset();
                } else {
                    din.reset();
                    buf.reset();
                }
            }
            
            
            // Parse the PictStruct's definitions.
            if (structParser == null) {
                BufferedReader reader = null;
                try {
                    reader = new BufferedReader(
                            new InputStreamReader(
                            PictStructView.class.getResourceAsStream("PictStructs.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;
                } finally {
                    if (reader != null) {
                        reader.close();
                    }
                }
                
            }
            
            /**
             * The following structs start with a ushort length
             * field followed by (length - 2) data bytes.
             */
            HashSet<Integer> varLengthIds = new HashSet<Integer>();
            varLengthIds.add(0x0070); // FramePoly
            varLengthIds.add(0x0071); // PaintPoly
            varLengthIds.add(0x0072); // ErasePoly
            varLengthIds.add(0x0073); // InvertPoly
            varLengthIds.add(0x0074); // FillPoly
            
            /**
             * The following structs start with a PixMap header
             * followed by a struct followed by PixData bytes.
             */
            HashSet<Integer> pixDataIds = new HashSet<Integer>();
            pixDataIds.add(0x0090); // BitsRect
            pixDataIds.add(0x0091); // BitsRgn
            pixDataIds.add(0x0098); // PackBitsRect
            pixDataIds.add(0x0099); // PackBitsRgn
            pixDataIds.add(0x009a); // DirectBitsRect
            
            /**
             * The following structs need to be when they have an odd byte count
             */
            HashSet<Integer> padIds = new HashSet<Integer>();
            padIds.add(0x00a1); // Long Comment
            padIds.add(0x0028); // Long Text
            padIds.add(0x0029); // DH Text
            padIds.add(0x002a); // DV Text
            padIds.add(0x002b); // DHDV Text
            padIds.add(0x002c); // Font name
            
            structParser.readStruct("Picture", din);
            PictNode node = new PictNode(this, PICTURE_ID, offset, buf.toByteArray());
            DefaultMutableTreeNode pictNode = new DefaultMutableTreeNode(node);
            root.add(pictNode);
            while (true) {
                int id = 0;
                try {
                    id = din.readShort() & 0xffff;
                } catch (EOFException e) {
                    break;
                }
                offset += buf.size();
                buf.reset();
                
                if (varLengthIds.contains(id)) {
                    int length = din.readShort() & 0xffff;
                    din.readFully(new byte[length - 2]);
                } else if (pixDataIds.contains(id)) {
                    if (id != 0x0098) {
                        din.skipBytes(4); // baseAddr
                    }
                    int rowBytes = din.readShort() & 0x3fff; // mask out the two high bits
                    int boundsTop = din.readShort() & 0xffff;
                    din.skipBytes(2); // boundsLeft
                    int boundsBottom = din.readShort() & 0xffff;
                    din.skipBytes(2); // boundsRight
                    din.skipBytes(2); // pmVersion
                    int packType = din.readShort() & 0xffff;
                    int packSize = din.readInt();
                    din.skipBytes(8); // hRes, vRes
                    din.skipBytes(8); // pixelType, pixelSize, cmpCount, cmpSize
                    din.skipBytes(4); // planeBytes
                    din.skipBytes(4); // pmTable
                    din.skipBytes(4); // pmReserved
                    
                    switch (id) {
                        case 0x0090 : // BitsRect
                            // XXX implement me
                            //   ColorTable colorTable; //ColorTable record
                            //   Rect srcRect;       //source rectangle
                            //   Rect dstRect;       //destination rectangle
                            //   short mode;       //transfer mode (may include new transfer modes)
                            break;
                        case 0x0091 : // BitsRgn
                            // XXX implement me
                            //   ColorTable colorTable;
                            //   Rect srcRect;          //source rectangle
                            //   Rect dstRect;          //destination rectangle
                            //   short mode;          //transfer mode (may include new modes)
                            //   Rgn maskRgn;         //region for masking
                            break;
                        case 0x0098 : // PackBitsRect
                            din.skipBytes(4); // baseAddr (weirdo);
                            din.skipBytes(2); // ColorTable.flags
                            int size = din.readShort() & 0xffff; // number of entries
                            din.skipBytes((size + 1) * 8);
                            din.skipBytes(8); //    Rect srcRect;
                            din.skipBytes(8); //    Rect dstRect;
                            din.skipBytes(2); //    short mode;
                            break;
                        case 0x0099 : // PackBitsRgn
                            // XXX implement me
                            //    ColorTable colorTable;
                            //    Rect srcRect;
                            //    Rect dstRect;
                            //    short mode;
                            //    Rgn maskRgn;
                            break;
                        case 0x009a : // DirectBitsRect
                            din.skipBytes(8); //    Rect srcRect;
                            din.skipBytes(8); //    Rect dstRect;
                            din.skipBytes(2); //    short mode;
                            break;
                    }
                    
                    if (packType == 1 || rowBytes < 8) {
                        // data is unpacked
                        din.skipBytes(rowBytes * (boundsBottom - boundsTop));
                    } else if (packType == 2) {
                        // the high-order pad byte of a 32-bit direct pixel
                        // is dropped
                        din.skipBytes(rowBytes * 3 / 4 * (boundsBottom - boundsTop));
                    } else /*if (packType > 2)*/ {
                        // image contains (bounds.bottom - bounds.top) packed
                        // scan lines;
                        // each scan line consists of [byteCount] [data];
                        if (rowBytes > 250) {
                            //  byteCount is a word
                            int count = 0;
                            for (int i=boundsTop; i < boundsBottom; i++) {
                                int byteCount = din.readShort() & 0xffff;
                                System.out.println("word byte count:"+byteCount);
                                din.skipBytes(byteCount);
                                count += byteCount + 2;
                            }
                            // Add a pad byte
                            if (count % 2 == 1) {
                                din.skipBytes(1);
                            }
                        } else {
                            // byteCount is a byte
                            System.out.println("boundsTop="+boundsTop+" bott="+boundsBottom);
                            int count = 0;
                            for (int i=boundsTop; i < boundsBottom; i++) {
                                int byteCount = din.readByte() & 0xff;
                                System.out.println("byte byte count:"+byteCount);
                                din.skipBytes(byteCount);
                                count += byteCount + 1;
                            }
                            // Add a pad byte
                            if (count % 2 == 1) {
                                din.skipBytes(1);
                            }
                        }
                    }
                } else {
                    try {
                        structParser.readStruct(id, din);
                    } catch (IOException e) {
                        e.printStackTrace();
                        
                        // Fetch until end of file
                        while (din.skip(100) > 0) {
                        }
                        
                        node = new PictNode(this, id, offset, buf.toByteArray());
                        pictNode.add(new DefaultMutableTreeNode(node));
                        break;
                    }
                }
                
                if (padIds.contains(id) && buf.size() % 2 == 1) {
                   din.skip(1);
                }
                
                node = new PictNode(this, id, offset, buf.toByteArray());
                
                pictNode.add(new DefaultMutableTreeNode(node));
            }
            
        }
        public StructTableModel getStructTableModel(PictNode node) {
            try {
                byte[] data = node.getRawData(0, (int) node.getSize());
                if (data == null) {
                    return null;
                }
                if (node.id == PICTURE_ID) {
                    return structParser.readStruct("Picture", data);
                } else {
                    return structParser.readStruct(node.id, data);
                }
            } catch (IOException e) {
//System.out.println("IFFStructView.IFFModel.getStructTableModel(...):"+e);
//e.printStackTrace();
                return null;
            }
        }
        
        public String getName(int id) {
            switch (id) {
                case PICTURE_ID :
                    return "Picture";
                case HEADER_ID :
                    return "Header";
                default :
                    return structParser.getName(id);
            }
        }
        public String getDescription(int id) {
            switch (id) {
                case PICTURE_ID :
                    return "Picture";
                case HEADER_ID :
                    return "Header";
                default :
                    return structParser.getDescription(id);
            }
        }
    }
    
    /** Creates new form PictStructView */
    public PictStructView() {
        //initComponents();
    }
    
    @Override
    public void setCommandContext(String verb, DataHandler dh)
    throws IOException {
        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 PictModel(inputStream);
                } catch (IOException e) {
                    result = e;
                }
                try {
                    inputStream.close();
                } catch (IOException e) {}
                
                return result;
            }
            @Override
            public void finished() {
                Object result = get();
                if (result instanceof PictModel) {
                    getTree().setModel((PictModel) 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
    
}
