/*
 * @(#)AbstractStructView.java  2.2  2010-01-06
 *
 * Copyright (c) 2005-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 ch.randelshofer.gui.SwingWorker;
import org.monte.media.binary.*;
import ch.randelshofer.util.*;
import java.awt.*;
import java.awt.dnd.*;
import java.awt.event.*;
import java.beans.*;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import javax.swing.*;
import javax.swing.tree.*;
import java.text.NumberFormat;
import javax.swing.border.EmptyBorder;
import javax.swing.border.LineBorder;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumnModel;

/**
 * AbstractStructView.
 *
 * @author  Werner Randelshofer
 * @version 2.2 2010-01-06 Added support for highlighting an area of the raw
 * data.
 * <br>2.1 2008-11-02 Added "Export Struct..." popup menu to tree.
 * <br>2.0 2002-06-18 Reworked.
 * <br>1.1 2002-02-10 DnD-support added.
 * <br>1.0 2000-09-28
 */
public class AbstractStructView extends AbstractView implements ItemListener {

    private static NumberFormat numFormat = NumberFormat.getInstance();
    private BinaryPanel binaryPanel;
    private JTable table;
    private JCheckBoxMenuItem showStructItem;
    /**
     * Resource bundle for internationalisation.
     */
    private ResourceBundleUtil resource;

    protected static interface StructNode {

        public String getName();

        public String getId();

        public long getSize();

        public long getOffset();

        /** If the node is file based, returns the file otherwise returns null. */
        public File getFile();

        public String getDescription();

        public StructTableModel getStructTableModel();

        public byte[] getRawData(long offset, int length);

        public java.util.List<BinaryPanel.Highlight> getRawDataHighlights();

        /**
         * Returns the index of the node in its parent.
         */
        public int getIndexInParent();

        public boolean hasRawData();
    }

    private static class ImageCellRenderer extends DefaultTableCellRenderer {

        private BinaryPanel binaryPanel = new BinaryPanel();
        private EmptyBorder emptyBorder = new EmptyBorder(1, 1, 1, 1);
        private LineBorder focusBorder = new LineBorder(new Color(0x808080));

        @Override
        public Component getTableCellRendererComponent(JTable table, Object value,
                boolean isSelected, boolean hasFocus, int row, int column) {

            if (value instanceof byte[]) {
                binaryPanel.setModel(new ByteArrayBinaryModel((byte[]) value));
                binaryPanel.setBorder((hasFocus) ? focusBorder : emptyBorder);
                binaryPanel.setFont(binaryPanel.getFont().deriveFont(table.getFont().getSize2D() + 2));
                return binaryPanel;
            } else {

                Component c = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
                setVerticalAlignment(TOP);
                if (value instanceof Image) {
                    setIcon(new ImageIcon((Image) value));
                    setText(null);
                } else {
                    setIcon(null);
                    if (value instanceof Object[]) {
                        StringBuilder buf = new StringBuilder();
                        for (Object o : (Object[]) value) {
                            if (buf.length() != 0) {
                                buf.append(", ");
                            }
                            buf.append("" + o);
                        }
                        setText(buf.toString());
                    } else if (value instanceof int[]) {
                        StringBuilder buf = new StringBuilder();
                        int[] data = (int[]) value;
                        for (int i = 0; i < data.length; i++) {
                            if (i != 0) {
                                buf.append(", ");
                            }
                            buf.append(data[i]);
                        }
                        setText(buf.toString());
                    }
                }
                return c;
            }
        }
    }

    /** Initializes the Form */
    public AbstractStructView() {
        resource = ResourceBundleUtil.getLAFBundle("ch.randelshofer.view.AbstractStructView");
        initComponents();
        splitPane.setBorder(null);
        rightPanel.setVisible(false);

        tree.setModel(new DefaultTreeModel(new DefaultMutableTreeNode()));
        binaryPanel = new BinaryPanel();
        binaryScrollPane.setViewportView(binaryPanel);
        binaryScrollPane.setVisible(false);
        table = new JTable();
        table.setCellSelectionEnabled(true);
        table.setDragEnabled(true);
        table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
        ImageCellRenderer icr = new ImageCellRenderer();
        table.setDefaultRenderer(Image.class, icr);
        table.setDefaultRenderer(Object.class, icr);
        table.getRowHeight(4);
        tableScrollPane.setViewportView(table);
        tableScrollPane.setVisible(false);
        table.setDragEnabled(true);

        // Menu Items
        showStructItem = new JCheckBoxMenuItem(resource.getString("showStruct"));
        showStructItem.setMnemonic(resource.getMnemonic("showStructMnem"));
        showStructItem.setAccelerator(resource.getKeyStroke("showStructAcc"));
        showStructItem.addItemListener(this);

        setViewProperties(null);
        setApplicationProperties(null);

        final JPopupMenu popupMenu = new JPopupMenu();
        popupMenu.add(new AbstractAction("Export Struct...") {

            private JFileChooser exportChooser;

            @Override
            public void actionPerformed(ActionEvent e) {
                if (exportChooser == null) {
                    exportChooser = new JFileChooser("Export Struct");
                }
                if (exportChooser.showSaveDialog(tree) == JFileChooser.APPROVE_OPTION) {
                    new SwingWorker() {

                        @Override
                        public Object construct() {
                            try {
                                exportStruct((StructNode) (((DefaultMutableTreeNode) tree.getSelectionPath().getLastPathComponent()).getUserObject()), exportChooser.getSelectedFile());
                                return null;
                            } catch (IOException ex) {
                                return ex;
                            }
                        }

                        @Override
                        public void finished() {
                            if (getValue() instanceof Exception) {
                                Exception ex = (Exception) getValue();
                                ex.printStackTrace();
                                JOptionPane.showMessageDialog(tree, ex.toString());
                            }
                        }
                    }.start();
                }
            }
        });
        // Set the component to show the popup menu
        tree.addMouseListener(new MouseAdapter() {

            @Override
            public void mousePressed(MouseEvent evt) {
                if (evt.isPopupTrigger()) {
                    if (tree.getSelectionCount() == 1) {
                        popupMenu.show(evt.getComponent(), evt.getX(), evt.getY());
                    }
                }
            }

            @Override
            public void mouseReleased(MouseEvent evt) {
                if (evt.isPopupTrigger()) {
                    if (tree.getSelectionCount() == 1) {
                        popupMenu.show(evt.getComponent(), evt.getX(), evt.getY());
                    }
                }
            }
        });
    }

    protected void exportStruct(StructNode node, File file) throws IOException {
        FileOutputStream out = new FileOutputStream(file);
        try {
            out.write(node.getRawData(0, (int) node.getSize()));
        } finally {
            out.close();
        }
    }

    @Override
    public void dispose() {
        super.dispose();
        if (binaryPanel!=null) {
            binaryPanel.getModel().close();
        }
    }

    @Override
    public void mergeViewMenuItems(JMenu m) {
        m.add(showStructItem);
    }

    @Override
    protected void updateViewProperties() {
        updateShowStruct();
        getViewProperties().put("title", " as Struct");
    }

    @Override
    public void propertyChange(PropertyChangeEvent event) {
        String key = event.getPropertyName();
        Object source = event.getSource();
        if (source == getViewProperties()) {
            Object newValue = event.getNewValue();
            if (key.equals("showStruct")) {
                updateShowStruct();

            }
        }
    }

    private void updateShowStruct() {
        showStructItem.setSelected(isShowStruct());
        treeValueChanged(null);
    }

    public boolean isShowStruct() {
        return getViewProperties().getProperty("showStruct", "true").equals("true");
    }

    /**
     * Returns true if this viewer has a scrollpane of its own.
     * The container that contains this viewers uses this method to
     * determine if it should wrap a scrollpane around the viewer.
     *
     * @return  true if this viewer has a scrollpane of its own.
     */
    @Override
    public boolean hasScrollPane() {
        return true;
    }

    protected JSplitPane getSplitPane() {
        return splitPane;
    }

    protected JTree getTree() {
        return tree;
    }

    /**
     * This method adds the given drop target listener to
     * all components of the viewer.
     */
    @Override
    public void addDropTargetListener(DropTargetListener listener) {
        new DropTarget(this, listener);
        new DropTarget(binaryPanel, listener);
        new DropTarget(table, listener);
        new DropTarget(splitPane, listener);
        new DropTarget(treeScrollPane, listener);
        new DropTarget(rightPanel, listener);
        new DropTarget(tree, listener);
        new DropTarget(headPanel, listener);
        new DropTarget(tableScrollPane, listener);
        new DropTarget(binaryScrollPane, listener);
        new DropTarget(nameLabel, listener);
        new DropTarget(idLabel, listener);
        new DropTarget(idField, listener);
        new DropTarget(sizeLabel, listener);
        new DropTarget(sizeField, listener);
        new DropTarget(offsetLabel, listener);
        new DropTarget(offsetField, listener);
        new DropTarget(indexLabel, listener);
        new DropTarget(indexField, listener);
        new DropTarget(descriptionLabel, listener);
    }

    /** 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.
     */
    private void initComponents() {//GEN-BEGIN:initComponents
        java.awt.GridBagConstraints gridBagConstraints;

        splitPane = new javax.swing.JSplitPane();
        treeScrollPane = new javax.swing.JScrollPane();
        tree = new javax.swing.JTree();
        rightPanel = new javax.swing.JPanel();
        headPanel = new javax.swing.JPanel();
        nameLabel = new javax.swing.JLabel();
        descriptionLabel = new javax.swing.JLabel();
        idLabel = new javax.swing.JLabel();
        idField = new javax.swing.JTextArea();
        indexLabel = new javax.swing.JLabel();
        indexField = new javax.swing.JTextArea();
        sizeLabel = new javax.swing.JLabel();
        sizeField = new javax.swing.JTextArea();
        offsetLabel = new javax.swing.JLabel();
        offsetField = new javax.swing.JTextArea();
        binaryScrollPane = new javax.swing.JScrollPane();
        tableScrollPane = new javax.swing.JScrollPane();

        setLayout(new java.awt.BorderLayout());

        setPreferredSize(new java.awt.Dimension(720, 480));
        splitPane.setOneTouchExpandable(true);
        treeScrollPane.setMinimumSize(new java.awt.Dimension(0, 0));
        tree.setRootVisible(false);
        tree.setShowsRootHandles(true);
        tree.addTreeSelectionListener(new javax.swing.event.TreeSelectionListener() {
            public void valueChanged(javax.swing.event.TreeSelectionEvent evt) {
                treeValueChanged(evt);
            }
        });

        treeScrollPane.setViewportView(tree);

        splitPane.setLeftComponent(treeScrollPane);

        rightPanel.setLayout(new java.awt.BorderLayout());

        rightPanel.setBorder(new javax.swing.border.EmptyBorder(new java.awt.Insets(0, 11, 11, 11)));
        rightPanel.setMinimumSize(new java.awt.Dimension(0, 0));
        headPanel.setLayout(new java.awt.GridBagLayout());

        headPanel.setBorder(new javax.swing.border.EmptyBorder(new java.awt.Insets(0, 0, 4, 0)));
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridx = 0;
        gridBagConstraints.gridy = 0;
        gridBagConstraints.gridwidth = java.awt.GridBagConstraints.REMAINDER;
        gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL;
        gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTH;
        gridBagConstraints.weightx = 1.0;
        headPanel.add(nameLabel, gridBagConstraints);

        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridy = 1;
        gridBagConstraints.gridwidth = java.awt.GridBagConstraints.REMAINDER;
        gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL;
        gridBagConstraints.insets = new java.awt.Insets(0, 0, 10, 0);
        headPanel.add(descriptionLabel, gridBagConstraints);

        idLabel.setText("ID:");
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridy = 2;
        gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST;
        headPanel.add(idLabel, gridBagConstraints);

        idField.setEditable(false);
        idField.setOpaque(false);
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridy = 2;
        gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST;
        gridBagConstraints.insets = new java.awt.Insets(0, 2, 0, 0);
        headPanel.add(idField, gridBagConstraints);

        indexLabel.setText("Index:");
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridy = 2;
        gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST;
        gridBagConstraints.insets = new java.awt.Insets(0, 10, 0, 0);
        headPanel.add(indexLabel, gridBagConstraints);

        indexField.setEditable(false);
        indexField.setOpaque(false);
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridy = 2;
        gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST;
        gridBagConstraints.insets = new java.awt.Insets(0, 2, 0, 0);
        headPanel.add(indexField, gridBagConstraints);

        sizeLabel.setText("Size:");
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridy = 2;
        gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST;
        gridBagConstraints.insets = new java.awt.Insets(0, 10, 0, 0);
        headPanel.add(sizeLabel, gridBagConstraints);

        sizeField.setEditable(false);
        sizeField.setOpaque(false);
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridy = 2;
        gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST;
        gridBagConstraints.insets = new java.awt.Insets(0, 2, 0, 0);
        headPanel.add(sizeField, gridBagConstraints);

        offsetLabel.setText("Offset:");
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridy = 2;
        gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST;
        gridBagConstraints.insets = new java.awt.Insets(0, 10, 0, 0);
        headPanel.add(offsetLabel, gridBagConstraints);

        offsetField.setEditable(false);
        offsetField.setOpaque(false);
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST;
        gridBagConstraints.insets = new java.awt.Insets(0, 2, 0, 0);
        headPanel.add(offsetField, gridBagConstraints);

        rightPanel.add(headPanel, java.awt.BorderLayout.NORTH);

        rightPanel.add(binaryScrollPane, java.awt.BorderLayout.CENTER);

        rightPanel.add(tableScrollPane, java.awt.BorderLayout.CENTER);

        splitPane.setRightComponent(rightPanel);

        add(splitPane, java.awt.BorderLayout.CENTER);

    }//GEN-END:initComponents

    private void treeValueChanged(javax.swing.event.TreeSelectionEvent evt) {//GEN-FIRST:event_treeValueChanged
        TreePath path = tree.getSelectionPath();
        if (path == null) {
            rightPanel.setVisible(false);
        } else {
            setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
            Object obj = ((DefaultMutableTreeNode) path.getLastPathComponent()).getUserObject();

            if (obj != null && obj instanceof StructNode) {
                StructNode node = (StructNode) obj;
                rightPanel.setVisible(true);
                nameLabel.setText(
                        (node.getName() == null) ? "" : "<html><h2>" + node.getName() + "</h2>");
                idField.setText(node.getId());
                indexField.setVisible(node.getIndexInParent()!=-1);
                indexLabel.setVisible(node.getIndexInParent()!=-1);
                indexField.setText(numFormat.format(node.getIndexInParent()));
                sizeField.setText(numFormat.format(node.getSize()) + " (0x" + Long.toHexString(node.getSize()) + ")");
                offsetField.setText(numFormat.format(node.getOffset()) + " (0x" + Long.toHexString(node.getOffset()) + ")");

                descriptionLabel.setText(
                        (node.getDescription() == null) ? "" : "<html><head>"
                        + "<style type=\"text/css\">"
                        + "b { font: 13pt \"Dialog\" }"
                        + "p { font: 13pt \"Dialog\"; margin-bottom: 8px }"
                        + "</style>"
                        + "</head>" + node.getDescription());
                if (node.hasRawData() /*&& node.getSize() < (20 * 1024 * 1024)*/) {
                    StructTableModel struct;
                    if (isShowStruct() && (struct = node.getStructTableModel()) != null) {
                        table.setModel(struct);
                        int[] columnWidth = new int[table.getColumnCount()];
                        for (int r = 0, m = table.getRowCount(); r < m; r++) {
                            int rowHeight = 16;
                            for (int c = 0, n = table.getColumnCount(); c < n; c++) {
                                TableCellRenderer tcr = table.getCellRenderer(r, c);
                                Component cmp = tcr.getTableCellRendererComponent(table, struct.getValueAt(r, c), false, false, r, c);
                                Dimension preferredSize = cmp.getPreferredSize();
                                rowHeight = Math.max(rowHeight, preferredSize.height);
                                columnWidth[c] = Math.max(columnWidth[c], preferredSize.width);
                            }
                            table.setRowHeight(r, rowHeight);
                        }
                        TableColumnModel cm = table.getColumnModel();
                        for (int i = 0; i < columnWidth.length; i++) {
                            cm.getColumn(i).setPreferredWidth(columnWidth[i] + 4);
                        }

                        rightPanel.remove(binaryScrollPane);
                        rightPanel.add(tableScrollPane, BorderLayout.CENTER);
                        //  table.sizeColumnsToFit(-1);
                        tableScrollPane.setVisible(true);
                        tableScrollPane.invalidate();
                    } else {
                        table.setModel(new DefaultTableModel());
                        if (node.getFile() != null) {
                            binaryPanel.getModel().close();

                            try {
                                binaryPanel.setModel(new FileBasedBinaryModel(node.getFile(), node.getOffset(), Math.max(0,node.getSize())));
                            } catch (IOException ex) {
                                binaryPanel.setModel(new ByteArrayBinaryModel(node.getRawData(0, Math.max(0,(int) node.getSize()))));
                            }
                        } else {
                            binaryPanel.setModel(new ByteArrayBinaryModel(node.getRawData(0, Math.max(0,(int) node.getSize()))));
                        }
                        binaryPanel.setHighlights(node.getRawDataHighlights());
                        rightPanel.remove(tableScrollPane);
                        rightPanel.add(binaryScrollPane, BorderLayout.CENTER);
                        binaryScrollPane.setVisible(true);
                        binaryScrollPane.getVerticalScrollBar().setUnitIncrement(13);
                        binaryScrollPane.invalidate();
                    }
                } else {
                    tableScrollPane.setVisible(false);
                    binaryScrollPane.setVisible(false);
                }
            } else {
                rightPanel.setVisible(false);
            }
            rightPanel.validate();
            if (binaryScrollPane.isVisible()) {
                binaryScrollPane.repaint();
            } else {
                tableScrollPane.repaint();
            }
            rightPanel.repaint();
            setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
        }
    }//GEN-LAST:event_treeValueChanged

    @Override
    public void itemStateChanged(ItemEvent e) {
        if (e.getSource() == showStructItem) {
            getViewProperties().put("showStruct", showStructItem.isSelected() ? "true" : "false");
        }
    }
    // Variables declaration - do not modify//GEN-BEGIN:variables
    private javax.swing.JScrollPane binaryScrollPane;
    private javax.swing.JLabel descriptionLabel;
    private javax.swing.JPanel headPanel;
    private javax.swing.JTextArea idField;
    private javax.swing.JLabel idLabel;
    private javax.swing.JTextArea indexField;
    private javax.swing.JLabel indexLabel;
    private javax.swing.JLabel nameLabel;
    private javax.swing.JTextArea offsetField;
    private javax.swing.JLabel offsetLabel;
    private javax.swing.JPanel rightPanel;
    private javax.swing.JTextArea sizeField;
    private javax.swing.JLabel sizeLabel;
    private javax.swing.JSplitPane splitPane;
    private javax.swing.JScrollPane tableScrollPane;
    private javax.swing.JTree tree;
    private javax.swing.JScrollPane treeScrollPane;
    // End of variables declaration//GEN-END:variables
}
