/*
 * @(#)EXIFView.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.io.ByteArrayImageInputStream;
import org.monte.media.binary.BinaryPanel;
import org.monte.media.binary.StructTableModel;
import org.monte.media.exif.EXIFReader;
import org.monte.media.tiff.FileSegment;
import org.monte.media.tiff.IFDDataType;
import org.monte.media.tiff.IFDEntry;
import org.monte.media.tiff.TIFFDirectory;
import org.monte.media.tiff.TIFFField;
import org.monte.media.tiff.TIFFNode;
import org.monte.media.tiff.BaselineTagSet;
import org.monte.media.tiff.IFD;
import org.monte.media.tiff.TIFFTag;
import ch.randelshofer.util.Strings;
import java.awt.Cursor;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Stack;
import javax.activation.DataHandler;
import javax.activation.FileDataSource;
import javax.imageio.ImageIO;
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;

/**
 * Viewer for EXIF 2.2 and MPF metadata.
 * <p>
 * Sources:
 * <p>
 * Exchangeable image file format for digital still cameras: EXIF Version 2.2.
 * (April, 2002). Standard of Japan Electronics and Information Technology
 * Industries Association. JEITA CP-3451.
 * <a href="http://www.exif.org/Exif2-2.PDF">http://www.exif.org/Exif2-2.PDF</a>
 * <p>
 * Multi-Picture Format
 * (February 4, 2009). Standard of the Camera & Imaging Products Association.
 * CIPA DC-007-Translation-2009.
 * <a href="http://www.cipa.jp/english/hyoujunka/kikaku/pdf/DC-007_E.pdf">
 * http://www.cipa.jp/english/hyoujunka/kikaku/pdf/DC-007_E.pdf</a>
 *
 * @author Werner Randelshofer
 * @version 1.0 2009-12-26 Created.
 */
public class EXIFView extends AbstractStructView {

    /** Will not be closed until the view is disposed. */
    private ImageInputStream iin;

    private class IFDStructNode implements AbstractStructView.StructNode {

        private String name;
        private int count;
        /** Offset inside of the TIFF file. */
        private long offset;
        /** TIFF segments which hold the IFD. */
        private ArrayList<FileSegment> tiffSeg;
        private ArrayList<TIFFField> fieldList;
        private ArrayList<TIFFDirectory> directoryList;
        private long length;
        private IFD ifd;
        private int indexInParent;

        public IFDStructNode(TIFFDirectory dir) {
            this.name = dir.getName();
            this.count = dir.getCount();
            this.indexInParent=dir.getIndex();
            this.tiffSeg = dir.getFileSegments();
            this.offset = dir.getOffset();
            this.length = dir.getLength();
            this.ifd = dir.getIFD();
            fieldList = new ArrayList<TIFFField>();
            directoryList = new ArrayList<TIFFDirectory>();
        }

        public void add(TIFFField field) {
            fieldList.add(field);
        }

        public void add(TIFFDirectory directory) {
            directoryList.add(directory);
        }

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

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

        public int getCount() {
            return count;
        }

        @Override
        public long getSize() {
            if (tiffSeg == null) {
                return 0;
            } else {
                long size = 0;
                for (FileSegment seg : tiffSeg) {
                    size += seg.getLength();
                }
                return size;
            }
        }

        private ArrayList<FileSegment> getTIFFSegments() {
            return tiffSeg;
        }

        @Override
        public long getOffset() {
            if (tiffSeg == null || tiffSeg.size() == 0) {
                return offset;
            } else {
                return tiffSeg.get(0).getOffset();
            }
        }

        @Override
        public String getDescription() {
            StringBuilder buf = new StringBuilder();

            if (name.equals("Image") || name.equals("Track")) {
                buf.append("<p>Meta Data for " + name + " " + indexInParent + ".</p>");
            } else {
                if (name.equals("TIFF")) {
                    buf.append("<p>The tags in a baseline TIFF Image File Directory (TIFF IFD) describe the structure and characteristics of the image data contained in the file.</p>");
                } else if (name.equals("EXIF")) {
                    buf.append("<p>The tags in an \"Exchangeable\" Image File Directory (EXIF IFD) describe the camera settings and picture taking conditions under which the image was created.</p>");
                } else if (name.equals("Interoperability")) {
                    buf.append("<p>The \"Interoperability\" Image File Directory (Interoperability IFD) is composed of tags which support the interoperability between devices.</p>");
                } else if (name.equals("GPS")) {
                    buf.append("<p>The tags in a \"Global Positioning System\" Image File Directory (GPS IFD) provide world location data which was available when the picture was taken.</p>");
                } else if (name.equals("MPF")) {
                    buf.append("<p>The tags in the \"Multi Picture Format\" Image File Directory (MPF IFD) provide information about the images contained in the file.</p>");
                } else if (name.equals("MP Entry Information")) {
                    buf.append("<p>This list has been synthesized from the MPEntryInformation tag of the parent IFD.</p>");

                }
                if (offset != -1) {
                    buf.append("<p>The IFD is located at offset " + offset + " (0x" + Long.toHexString(offset) + ") inside of the TIFF stream.</p>");
                }
            }


            return buf.toString();
        }

        @Override
        public StructTableModel getStructTableModel() {
            return new EXIFTableModel(fieldList);
        }

        /** FIXME Output is used by EXIFView */
        private String toString(IFDEntry e) {
            return "IFD Entry: tag:0x" + Integer.toHexString(e.getTagNumber()) + " type:0x" + Integer.toHexString(e.getTypeNumber()) + " count:0x" + Long.toHexString(e.getCount()) + " valueOffset:0x" + Long.toHexString(e.getValueOffset());
        }

        @Override
        public java.util.List<BinaryPanel.Highlight> getRawDataHighlights() {
            ArrayList<BinaryPanel.Highlight> h = new ArrayList<BinaryPanel.Highlight>();
            h.add(new BinaryPanel.Highlight(offset, offset + 2, BinaryPanel.HIGHLIGHT_COLORS[5], "IFD Entry Count:" + getCount()));
            if (ifd != null && ifd.hasNextOffset()) {
                h.add(new BinaryPanel.Highlight(offset + length - 2, offset + length, BinaryPanel.HIGHLIGHT_COLORS[1], "Next IFD Pointer:0x" + Long.toHexString(ifd.getNextOffset())));
            }
            for (TIFFField field : fieldList) {
                if (field.getIFDEntry() != null && field.getIFDEntry().getDataOffset() != -1) {
                    IFDEntry entry = field.getIFDEntry();
                    if (entry.getTagNumber() >= 0) {// Only highlight non-synthetic entries
                        if (entry.isDataInValueOffset()) {
                            if (entry.getEntryOffset() != -1) {
                                h.add(new BinaryPanel.Highlight(entry.getEntryOffset(), entry.getEntryOffset() + 8, BinaryPanel.HIGHLIGHT_COLORS[2], toString(entry)));
                            }
                            h.add(new BinaryPanel.Highlight(entry.getDataOffset(), entry.getDataOffset() + 4, BinaryPanel.HIGHLIGHT_COLORS[3], toString(entry)));
                        } else {
                            if (entry.getEntryOffset() != -1) {
                                h.add(new BinaryPanel.Highlight(entry.getEntryOffset(), entry.getEntryOffset() + 8, BinaryPanel.HIGHLIGHT_COLORS[2], toString(entry)));
                            }
                            h.add(new BinaryPanel.Highlight(entry.getEntryOffset() + 8, entry.getEntryOffset() + 12, BinaryPanel.HIGHLIGHT_COLORS[1], toString(entry)));
                            h.add(new BinaryPanel.Highlight(entry.getDataOffset(), entry.getDataOffset() + entry.getLength(), BinaryPanel.HIGHLIGHT_COLORS[3], toString(entry)));
                        }
                    }
                }
            }
            for (TIFFDirectory dir : directoryList) {
                IFDEntry entry = dir.getIFDEntry();
                if (entry != null) {
                    if (entry.isDataInValueOffset()) {
                        if (entry.getEntryOffset() != -1) {
                            h.add(new BinaryPanel.Highlight(entry.getEntryOffset(), entry.getEntryOffset() + 8, BinaryPanel.HIGHLIGHT_COLORS[2], toString(entry)));
                        }
                        h.add(new BinaryPanel.Highlight(entry.getDataOffset(), entry.getDataOffset() + 4, BinaryPanel.HIGHLIGHT_COLORS[3], toString(entry)));
                    } else {
                        if (entry.getEntryOffset() != -1) {
                            h.add(new BinaryPanel.Highlight(entry.getEntryOffset(), entry.getEntryOffset() + 8, BinaryPanel.HIGHLIGHT_COLORS[2], toString(entry)));
                        }
                        h.add(new BinaryPanel.Highlight(entry.getEntryOffset() + 8, entry.getEntryOffset() + 12, BinaryPanel.HIGHLIGHT_COLORS[1], toString(entry)));
                        h.add(new BinaryPanel.Highlight(entry.getDataOffset(), entry.getDataOffset() + entry.getLength(), BinaryPanel.HIGHLIGHT_COLORS[3], toString(entry)));
                    }
                }
            }
            return h;
        }

        @Override
        public byte[] getRawData(long offset, int length) {
            byte[] buf = new byte[length];

            try {
                long offsetInSeg = offset;
                int offsetInBuf = 0;
                for (FileSegment seg : tiffSeg) {
                    if (offsetInSeg < seg.getLength()) {
                        iin.seek(seg.getOffset() + offsetInSeg);
                        int lengthInSeg = (int) Math.min(length, seg.getLength() - offsetInSeg);

                        // XXX - We need to account for readCount being smaller than what we actually want to read
                        int readCount = iin.read(buf, offsetInBuf, lengthInSeg);
                        offsetInBuf += readCount;
                    }

                    offsetInSeg -= seg.getLength();
                    if (offsetInSeg < 0) {
                        break;
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }

            return buf;
        }

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

        @Override
        public boolean hasRawData() {
            return tiffSeg != null && tiffSeg.size() > 0;
        }

        @Override
        public String toString() {
            return name + " " + indexInParent;
        }

        private long getLength() {
            return length;
        }

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

    private static class EXIFTableModel extends StructTableModel {

        private ArrayList<TIFFField> data;
        private HashMap<Enum, Object> map;
        private Class[] columnClasses = {String.class, String.class, String.class, Object.class};
        private String[] columnNames = {"ID", "Name", "Type", "Value"};

        public EXIFTableModel(ArrayList<TIFFField> data) {
            super(null, null);
            this.data = data;
        }

        @Override
        public int getRowCount() {
            return data.size();
        }

        @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) {
            TIFFField mde = data.get(row);
            switch (column) {
                case 0:
                    if (mde.getTag() != null && (mde.getTag().isSynthetic())) {
                        return "---";
                    } else {
                        return "0x" + Integer.toHexString(mde.getTagNumber());
                    }
                case 1:
                    TIFFTag tag = mde.getTag();
                    return (tag == null ? "unknown" : Strings.camelToWords(tag.toString()));
                case 2:
                    IFDDataType tt = mde.getType();
                    return (tt == null ? "" + mde.getType() : tt.toString())//
                            + (mde.getCount() == 1 ? "" : "[" + mde.getCount() + "]");
                case 3:
                default:
                    if (mde.getTag() != null) {
                        /*
                        if (mde.getTag().getNumber()==0x1408) {
                            System.out.println("EXIFView "+mde.getTag().getNumber());
                        }*/
                        return mde.getTag().prettyFormat(mde.getData());
                    } else {
                        return mde.getData();
                    }
            }
        }
    }

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

    /**
     * This method should be abstract but the form editor in NetBeans 3.4
     * does not suport abstract classes.
     */
    @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 {

                if (dh.getDataSource() instanceof FileDataSource) {
                    iin = new FileImageInputStream(((FileDataSource) dh.getDataSource()).getFile());
                } else {
                    iin = new MemoryCacheImageInputStream(dh.getInputStream());
                }
                EXIFReader reader = new EXIFReader(iin);
                reader.read();

                // Reorganize the tree structure created by the reader
                DefaultMutableTreeNode viewRoot = new DefaultMutableTreeNode();
                DefaultTreeModel dtm = new DefaultTreeModel(viewRoot);
                TIFFNode mdRoot = reader.getMetaDataTree();
                Stack<TIFFNode> mdStack = new Stack<TIFFNode>();
                Stack<DefaultMutableTreeNode> viewStack = new Stack<DefaultMutableTreeNode>();
                viewStack.push(viewRoot);
                if (mdRoot != null) {
                    for (Iterator<TIFFNode> i = mdRoot.preorderIterator(); i.hasNext();) {
                        TIFFNode item = i.next();
                        while (!mdStack.isEmpty() && item.getParent() != mdStack.peek()) {
                            mdStack.pop();
                            viewStack.pop();
                        }
                        mdStack.push(item);

                        if (item instanceof TIFFDirectory) {
                            TIFFDirectory dir = (TIFFDirectory) item;
                            IFDStructNode structNode = new IFDStructNode(dir);
                            DefaultMutableTreeNode viewNode = new DefaultMutableTreeNode(structNode);

                            ((DefaultMutableTreeNode) viewStack.peek()).add(viewNode);
                            IFDStructNode parentStructNode = (IFDStructNode) viewStack.peek().getUserObject();
                            if (parentStructNode != null) {
                                parentStructNode.add(dir);
                            }
                            viewStack.push(viewNode);
                        } else if (item instanceof TIFFField) {
                            TIFFField mde = (TIFFField) item;
                            IFDStructNode structNode = (IFDStructNode) viewStack.peek().getUserObject();
                            DefaultMutableTreeNode viewNode = viewStack.peek();

                            if (mde.getTag() == BaselineTagSet.getInstance().getTag(BaselineTagSet.TAG_JPEGThumbnailImage)) {
                                mde.setData(ImageIO.read((ImageInputStream) new ByteArrayImageInputStream((byte[]) mde.getData())));
                            }
                            structNode.add(mde);
                            viewStack.push(null);
                        }
                    }
                }

                if (viewRoot.getChildCount() == 1) {
                    dtm.setRoot(viewRoot.getChildAt(0));
                    viewRoot.remove(0);
                }

                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();
    }

    @Override
    protected void updateViewProperties() {
        super.updateViewProperties();
        getViewProperties().put("title", " as EXIF");


    }

    @Override
    public String toString() {
        return "EXIF Meta Data 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 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
}
