/*
 * @(#)TIFFStructView.java  1.0  2009-12-26
 * 
 * Copyright (c) 2009 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.binary.BinaryPanel;
import org.monte.media.binary.StructTableModel;
import org.monte.media.tiff.IFD;
import org.monte.media.tiff.IFDEntry;
import org.monte.media.tiff.TIFFInputStream;
import org.monte.media.tiff.IFDDataType;
import org.monte.media.tiff.BaselineTagSet;
import java.awt.Cursor;
import java.io.File;
import java.io.IOException;
import javax.activation.DataHandler;
import javax.activation.FileDataSource;
import javax.imageio.stream.FileImageInputStream;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.stream.MemoryCacheImageInputStream;
import javax.swing.JLabel;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import org.jhotdraw.gui.Worker;

/**
 * TIFFStructView.
 *
 * @author Werner Randelshofer
 * @version 1.0 2009-12-26 Created.
 */
public class TIFFStructView extends AbstractStructView {

    private ImageInputStream iin;
    private TIFFInputStream in;
    private DefaultTreeModel contents;

    protected class IFDStructNode
            implements AbstractStructView.StructNode {

        private IFD ifd;
        private int index;

        public IFDStructNode(IFD ifd, int index) {
            this.ifd = ifd;
            this.index = index;
        }

        @Override
        public String getName() {
            return "IFD";
        }

        @Override
        public String getId() {
            return "IFD";
        }

        @Override
        public long getSize() {
            return 12 * ifd.getCount() + 4 + 2;
        }

        @Override
        public long getOffset() {
            return ifd.getOffset();
        }

        @Override
        public String getDescription() {
            return "An Image File Directory (IFD) consists of a 2-byte count of "
                    + "the number of directory entries (i.e., the number of fields), "
                    + "followed by a sequence of 12-byte field entries, followed "
                    + "by a 4-byte offset of the next IFD (or 0 if none). "
                    + "There must be at least 1 IFD in a TIFF file and each IFD "
                    + "must have at least one entry.";
        }

        @Override
        public StructTableModel getStructTableModel() {
            return new IFDStructTableModel(ifd);
        }

        @Override
        public byte[] getRawData(long offset, int length) {
            byte[] b = new byte[length];
            try {
                int count = in.read(ifd.getOffset() + offset, b, 0, length);
            } catch (IOException ex) {
                ex.printStackTrace();
            }
            return b;
        }
        @Override
        public java.util.List<BinaryPanel.Highlight> getRawDataHighlights() {
 //           return new int[][] {{(int)ifd.getOffset(),(int)ifd.getOffset()+ifd.getCount()*12}};
            return null;
        }

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

        @Override
        public boolean hasRawData() {
            return true;
        }

        @Override
        public String toString() {
            return "IFD (" + index + ")";
        }

        @Override
        public File getFile() {
            return null;
        }
    }
    protected class IFDEntryStructNode
            implements AbstractStructView.StructNode {

        private IFDEntry entry;
        private int index;

        public IFDEntryStructNode(IFDEntry entry, int index) throws IOException {
            this.entry = entry;
            this.index = index;
            entry.getLength();
        }

        @Override
        public String getName() {
            return BaselineTagSet.getInstance().getTag(entry.getTagNumber()).toString();
        }

        @Override
        public String getId() {
            return Integer.toString(entry.getTagNumber());
        }

        @Override
        public long getSize() {
            return entry.getLength();
        }

        @Override
        public long getOffset() {
            return entry.getValueOffset();
        }

        @Override
        public String getDescription() {
            return ""+BaselineTagSet.getInstance().getTag(entry.getTagNumber())+": "+entry.getCount()+" values of type "+IFDDataType.valueOf(entry.getTypeNumber());
        }

        @Override
        public StructTableModel getStructTableModel() {
            try {
                IFDDataType vt = IFDDataType.valueOf(entry.getTypeNumber());
            return (vt==IFDDataType.BYTE) ? null:new IFDEntryStructTableModel(entry);
            } catch (IOException e) {
                e.printStackTrace();
                return null;
            }
        }

        @Override
        public byte[] getRawData(long offset, int length) {
            byte[] b = new byte[length];
            try {
                int count = in.read(getOffset() + offset, b, 0, length);
            } catch (IOException ex) {
                ex.printStackTrace();
            }
            return b;
        }
        @Override
        public java.util.List<BinaryPanel.Highlight> getRawDataHighlights() {
            return null;
        }

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

        @Override
        public boolean hasRawData() {
            return true;
        }

        @Override
        public String toString() {
            return ""+BaselineTagSet.getInstance().getTag(entry.getTagNumber())+" value";
        }

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

    private class IFDStructTableModel extends StructTableModel {

        private IFD ifd;
        private String[] columnNames={"Name","Type","Count","Value or Offset"};
        private Class[] columnClasses={String.class,String.class,Integer.class,Long.class};

        public IFDStructTableModel(IFD ifd) {
            super(null, null);
            this.ifd = ifd;
        }

        @Override
        public int getRowCount() {
            return ifd.getCount();
        }

        @Override
        public int getColumnCount() {
            return columnNames.length;
        }

        @Override
        public String getColumnName(int column) {
            return columnNames[column];
        }


        @Override
        public Class getColumnClass(int column) {
            return columnClasses[column];
        }

        @Override
        public Object getValueAt(int row, int column) {
            IFDEntry entry = ifd.getEntries().get(row);
            switch (column) {
                case 0:
                    if (BaselineTagSet.getInstance().getTag(entry.getTagNumber())==null) {
                    return "unknown ("+entry.getTagNumber()+")"  ;
                    } else {
                    return BaselineTagSet.getInstance().getTag(entry.getTagNumber());
                    }
                case 1:
                    if (IFDDataType.valueOf(entry.getTypeNumber())==null) {
                    return "unknown ("+entry.getTypeNumber()+")";
                    } else {
                    return IFDDataType.valueOf(entry.getTypeNumber());
                    }
                case 2:
                    return entry.getCount();
                case 3:
                default:
                    return entry.getValueOffset();
            }
        }
    }
    private class IFDEntryStructTableModel extends StructTableModel {

        private IFDEntry entry;
        private String[] columnNames={"Value"};
        private Class[] columnClasses={Object.class};
        private Object[] data;

        public IFDEntryStructTableModel(IFDEntry entry) throws IOException {
            super(null, null);
            this.entry = entry;
            Object d = entry.readData(in);
            if (d instanceof int[]) {
                int[] ints = (int[])d;
                data=new Object[ints.length];
                for (int i=0;i<ints.length;i++) {
                    data[i]=ints[i];
                }
            } else if (d instanceof long[]) {
                long[] longs = (long[])d;
                data=new Object[longs.length];
                for (int i=0;i<longs.length;i++) {
                    data[i]=longs[i];
                }
            } else if (d instanceof short[]) {
                short[] shorts = (short[])d;
                data=new Object[shorts.length];
                for (int i=0;i<shorts.length;i++) {
                    data[i]=shorts[i];
                }
            } else if (d instanceof float[]) {
                float[] floats = (float[])d;
                data=new Object[floats.length];
                for (int i=0;i<floats.length;i++) {
                    data[i]=floats[i];
                }
            } else if (d instanceof Object[]) {
                data=(Object[])d;
            } else {
                data=new Object[] {d};
            }
        }

        @Override
        public int getRowCount() {
            return (int)entry.getCount();
        }

        @Override
        public int getColumnCount() {
            return columnNames.length;
        }

        @Override
        public String getColumnName(int column) {
            return columnNames[column];
        }


        @Override
        public Class getColumnClass(int column) {
            return columnClasses[column];
        }

        @Override
        public Object getValueAt(int row, int column) {
            switch (column) {
                case 0:
                default:
                    return data[row];
            }
        }
    }

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

    @Override
    public void setCommandContext(String verb, final DataHandler dh)
            throws IOException {
        setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));

        Worker<DefaultTreeModel> worker = new Worker<DefaultTreeModel>() {

            @Override
            public DefaultTreeModel construct() throws IOException {
                DefaultMutableTreeNode root = new DefaultMutableTreeNode();
                DefaultTreeModel dtm = new DefaultTreeModel(root);
                if (dh.getDataSource() instanceof FileDataSource) {
                    iin = new FileImageInputStream(((FileDataSource) dh.getDataSource()).getFile());
                } else {
                    iin = new MemoryCacheImageInputStream(dh.getInputStream());
                }
                in = new TIFFInputStream(iin);
                long nextOffset = in.getFirstIFDOffset();
                int ifdCount = 0;
                while (nextOffset != 0) {
                    IFD ifd = in.readIFD(in.getFirstIFDOffset());
                    DefaultMutableTreeNode ifdNode=new DefaultMutableTreeNode(new IFDStructNode(ifd, ifdCount++), true);
                    root.add(ifdNode);
                    int entryCount=0;
                    for (IFDEntry entry : ifd.getEntries()) {
                        if (!entry.isDataInValueOffset()) {
                            ifdNode.add(new DefaultMutableTreeNode(new IFDEntryStructNode(entry, entryCount++), false));
                        }
                    }

                    nextOffset = ifd.getNextOffset();
                }
                return dtm;
            }

            @Override
            protected void failed(Throwable error) {
                removeAll();
                add(new JLabel(error.toString()));
                error.printStackTrace();
                validate();
            }

            @Override
            protected void done(DefaultTreeModel dtm) {
                getTree().setModel(dtm);
                getTree().expandRow(0);
                getTree().setSelectionRow(0);
                getSplitPane().setDividerLocation(getTree().getPreferredSize().width + 20);
            }

            @Override
            protected void finished() {
                setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
            }
        };
        worker.start();
    }

    @Override
    public void dispose() {
        if (iin != null) {
            try {
                iin.close();
            } catch (IOException ex) {
                // ignore
            }
            iin = null;
        }
        super.dispose();
    }

    /** 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.
     */
    @SuppressWarnings("unchecked")
    // <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
}
