package cooltable;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.GridLayout;
import java.awt.Point;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JViewport;
import javax.swing.KeyStroke;
import javax.swing.ListSelectionModel;
import javax.swing.SwingUtilities;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.JTableHeader;
import javax.swing.table.TableColumn;
import javax.swing.table.TableColumnModel;
import javax.swing.table.TableModel;

/**
 * A cool table with dynamic frozen columns.
 *
 * @author Kurt Riede
 */
public class CoolTable extends JScrollPane {

    public static void main(String[] args) {
        final CoolTable coolTable = new CoolTable(new DefaultTableModel(20, 10), 2);
        JFrame frame = new JFrame("Cool Table Demo");
        frame.getContentPane().setLayout(new GridLayout(2, 1));
        JPanel buttonPanel = new JPanel();
        buttonPanel.setLayout(new FlowLayout());
        JButton button2 = new JButton("Freeze 2 columns");
        JButton button4 = new JButton("Freeze 4 columns");
        JButton button6 = new JButton("Freeze 6 columns");
        button2.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                coolTable.setFrozenColumns(2);
            }
        });
        button4.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                coolTable.setFrozenColumns(4);
            }
        });
        button6.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                coolTable.setFrozenColumns(6);
            }
        });
        buttonPanel.add(button2);
        buttonPanel.add(button4);
        buttonPanel.add(button6);
        frame.getContentPane().add(buttonPanel);
        frame.getContentPane().add(coolTable);
        frame.setSize(600, 300);
        Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
        Dimension frameSize = frame.getSize();
        if (frameSize.height > screenSize.height) {
            frameSize.height = screenSize.height;
        }
        if (frameSize.width > screenSize.width) {
            frameSize.width = screenSize.width;
        }
        frame.setLocation((screenSize.width - frameSize.width) / 2, (screenSize.height - frameSize.height) / 2);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setVisible(true);
    }

    private final JTable lockedTable;

    private final JTable scrollTable;

    int frozenColumns = 0;

    private final JScrollPaneAdjuster adjuster;

    public CoolTable(TableModel model, int numFrozenColumns) {
        super();
        adjuster = new JScrollPaneAdjuster(this);
        frozenColumns = numFrozenColumns;
        // create the two tables
        lockedTable = new JTable(model);
        lockedTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
        scrollTable = new JTable(model);
        scrollTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
        setViewportView(scrollTable);

        // Put the locked-column table in the row header
        JViewport viewport = new JViewport();
        viewport.setBackground(Color.white);
        viewport.setView(lockedTable);
        setRowHeader(viewport);

        // Put the header of the locked-column table in the top left corner
        // of the scoll pane
        JTableHeader lockedHeader = lockedTable.getTableHeader();
        lockedHeader.setReorderingAllowed(false);
        lockedHeader.setResizingAllowed(false);
        setCorner(JScrollPane.UPPER_LEFT_CORNER, lockedHeader);

        scrollTable.getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
        lockedTable.setSelectionModel(scrollTable.getSelectionModel());
        lockedTable.getTableHeader().setReorderingAllowed(false);
        lockedTable.getTableHeader().setResizingAllowed(false);
        lockedTable.putClientProperty("terminateEditOnFocusLost", Boolean.TRUE);
        scrollTable.putClientProperty("terminateEditOnFocusLost", Boolean.TRUE);

        // Remove the fixed columns from the main table
        TableColumnModel scrollColumnModel = scrollTable.getColumnModel();
        for (int i = 0; i < frozenColumns; i++) {
            scrollColumnModel.removeColumn(scrollColumnModel.getColumn(0));
        }
        // Remove the non-fixed columns from the fixed table
        TableColumnModel lockedColumnModel = lockedTable.getColumnModel();
        while (lockedTable.getColumnCount() > frozenColumns) {
            lockedColumnModel.removeColumn(lockedColumnModel.getColumn(frozenColumns));
        }
        // Add the fixed table to the scroll pane
        lockedTable.setPreferredScrollableViewportSize(lockedTable.getPreferredSize());

        // set a new action for the tab key
        // todo search actions by action name (not by KeyStroke)
        final Action lockedTableNextColumnCellAction = getAction(lockedTable, KeyEvent.VK_TAB, 0);
        final Action scrollTableNextColumnCellAction = getAction(scrollTable, KeyEvent.VK_TAB, 0);
        final Action lockedTablePrevColumnCellAction = getAction(lockedTable, KeyEvent.VK_TAB, InputEvent.SHIFT_DOWN_MASK);
        final Action scrollTablePrevColumnCellAction = getAction(scrollTable, KeyEvent.VK_TAB, InputEvent.SHIFT_DOWN_MASK);

        setAction(lockedTable, "selectNextColumn", new LockedTableSelectNextColumnCellAction(lockedTableNextColumnCellAction));
        setAction(scrollTable, "selectNextColumn", new ScrollTableSelectNextColumnCellAction(scrollTableNextColumnCellAction));
        setAction(lockedTable, "selectPreviousColumn", new LockedTableSelectPreviousColumnCellAction(lockedTablePrevColumnCellAction));
        setAction(scrollTable, "selectPreviousColumn", new ScrollTableSelectPreviousColumnCellAction(scrollTablePrevColumnCellAction));
        
        setAction(lockedTable, "selectNextColumnCell", new LockedTableSelectNextColumnCellAction(lockedTableNextColumnCellAction));
        setAction(scrollTable, "selectNextColumnCell", new ScrollTableSelectNextColumnCellAction(scrollTableNextColumnCellAction));
        setAction(lockedTable, "selectPreviousColumnCell", new LockedTableSelectPreviousColumnCellAction(lockedTablePrevColumnCellAction));
        setAction(scrollTable, "selectPreviousColumnCell", new ScrollTableSelectPreviousColumnCellAction(scrollTablePrevColumnCellAction));

        setAction(scrollTable, "selectFirstColumn", new ScrollableSelectFirstColumnCellAction());
        setAction(lockedTable, "selectLastColumn", new LockedTableSelectLastColumnCellAction());
    }

    private void setAction(JComponent component, String name, Action action) {
        component.getActionMap().put(name, action);
    }

    private void setAction(JComponent component, String name, int keyCode, int modifiers, Action action) {
        final int condition = JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT;
        final KeyStroke keyStroke = KeyStroke.getKeyStroke(keyCode, modifiers);
        component.getInputMap(condition).put(keyStroke, name);
        component.getActionMap().put(name, action);
    }

    private Action getAction(JComponent component, int keyCode, int modifiers) {
        final int condition = JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT;
        final KeyStroke keyStroke = KeyStroke.getKeyStroke(keyCode, modifiers);
        Object object = component.getInputMap(condition).get(keyStroke);
        if (object == null) {
            if (component.getParent() instanceof JComponent) {
                return getAction((JComponent) component.getParent(), keyCode, modifiers);
            } else {
                return null;
            }
        } else {
            return scrollTable.getActionMap().get(object);
        }
    }

    protected int nextRow(JTable table) {
        int row = table.getSelectedRow() + 1;
        if (row == table.getRowCount()) {
            row = 0;
        }
        return row;
    }

    private int previousRow(JTable table) {
        int row = table.getSelectedRow() - 1;
        if (row == -1) {
            row = table.getRowCount() - 1;
        }
        return row;
    }

    public final int getFrozenColumns() {
        return frozenColumns;
    }

    public final void setFrozenColumns(final int numFrozenColumns) {
        rearrangeColumns(numFrozenColumns);
        frozenColumns = numFrozenColumns;
    }

    private void rearrangeColumns(final int numFrozenColumns) {
        TableColumnModel scrollColumnModel = scrollTable.getColumnModel();
        TableColumnModel lockedColumnModel = lockedTable.getColumnModel();
        if (frozenColumns < numFrozenColumns) {
            // move columns from scrollable to fixed table
            for (int i = frozenColumns; i < numFrozenColumns; i++) {
                TableColumn column = scrollColumnModel.getColumn(0);
                lockedColumnModel.addColumn(column);
                scrollColumnModel.removeColumn(column);
            }
            lockedTable.setPreferredScrollableViewportSize(lockedTable.getPreferredSize());
        } else if (frozenColumns > numFrozenColumns) {
            // move columns from fixed to scrollable table
            for (int i = numFrozenColumns; i < frozenColumns; i++) {
                TableColumn column = lockedColumnModel.getColumn(lockedColumnModel.getColumnCount() - 1);
                scrollColumnModel.addColumn(column);
                scrollColumnModel.moveColumn(scrollColumnModel.getColumnCount() - 1, 0);
                lockedColumnModel.removeColumn(column);
            }
            lockedTable.setPreferredScrollableViewportSize(lockedTable.getPreferredSize());
        }
    }

    public class JScrollPaneAdjuster implements PropertyChangeListener, Serializable {

        private JScrollPane pane;

        private transient Adjuster x, y;

        public JScrollPaneAdjuster(JScrollPane pane) {
            this.pane = pane;
            this.x = new Adjuster(pane.getViewport(), pane.getColumnHeader(), Adjuster.X);
            this.y = new Adjuster(pane.getViewport(), pane.getRowHeader(), Adjuster.Y);
            pane.addPropertyChangeListener(this);
        }

        public void dispose() {
            x.dispose();
            y.dispose();
            pane.removePropertyChangeListener(this);
            pane = null;
        }

        public void propertyChange(PropertyChangeEvent e) {
            String name = e.getPropertyName();
            if (name.equals("viewport")) {
                x.setViewport((JViewport) e.getNewValue());
                y.setViewport((JViewport) e.getNewValue());
            } else if (name.equals("rowHeader")) {
                y.setHeader((JViewport) e.getNewValue());
            } else if (name.equals("columnHeader")) {
                x.setHeader((JViewport) e.getNewValue());
            }
        }

        private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
            in.defaultReadObject();
            x = new Adjuster(pane.getViewport(), pane.getColumnHeader(), Adjuster.X);
            y = new Adjuster(pane.getViewport(), pane.getRowHeader(), Adjuster.Y);
        }

        private class Adjuster implements ChangeListener, Runnable {

            public static final int X = 1, Y = 2;
            private JViewport viewport, header;
            private int type;

            public Adjuster(JViewport viewport, JViewport header, int type) {
                this.viewport = viewport;
                this.header = header;
                this.type = type;
                if (header != null)
                    header.addChangeListener(this);
            }

            public void setViewport(JViewport newViewport) {
                viewport = newViewport;
            }

            public void setHeader(JViewport newHeader) {
                if (header != null)
                    header.removeChangeListener(this);
                header = newHeader;
                if (header != null)
                    header.addChangeListener(this);
            }

            public void stateChanged(ChangeEvent e) {
                if (viewport == null || header == null)
                    return;
                if (type == X) {
                    if (viewport.getViewPosition().x != header.getViewPosition().x)
                        SwingUtilities.invokeLater(this);
                } else {
                    if (viewport.getViewPosition().y != header.getViewPosition().y)
                        SwingUtilities.invokeLater(this);
                }
            }

            public void run() {
                if (viewport == null || header == null)
                    return;
                Point v = viewport.getViewPosition(), h = header.getViewPosition();
                if (type == X) {
                    if (v.x != h.x)
                        viewport.setViewPosition(new Point(h.x, v.y));
                } else {
                    if (v.y != h.y)
                        viewport.setViewPosition(new Point(v.x, h.y));
                }
            }

            public void dispose() {
                if (header != null)
                    header.removeChangeListener(this);
                viewport = header = null;
            }
        }
    }

    private final class LockedTableSelectLastColumnCellAction extends AbstractAction {
        private LockedTableSelectLastColumnCellAction() {
            super();
        }
        public void actionPerformed(ActionEvent e) {
            if (e.getSource() == lockedTable) {
                lockedTable.transferFocus();
            }
            scrollTable.changeSelection(scrollTable.getSelectedRow(), scrollTable.getColumnCount() - 1, false, false);
        }
    }

    private final class ScrollableSelectFirstColumnCellAction extends AbstractAction {
        private ScrollableSelectFirstColumnCellAction() {
            super();
        }
        public void actionPerformed(ActionEvent e) {
            if (e.getSource() == scrollTable) {
                scrollTable.transferFocusBackward();
            }
            lockedTable.changeSelection(lockedTable.getSelectedRow(), 0, false, false);
        }
    }

    private final class LockedTableSelectNextColumnCellAction extends AbstractAction {
        private final Action lockedTableNextColumnCellAction;
        private LockedTableSelectNextColumnCellAction(Action lockedTableNextColumnCellAction) {
            super();
            this.lockedTableNextColumnCellAction = lockedTableNextColumnCellAction;
        }
        public void actionPerformed(ActionEvent e) {
            if (lockedTable.getSelectedColumn() == lockedTable.getColumnCount() - 1) {
                lockedTable.transferFocus();
                scrollTable.changeSelection(lockedTable.getSelectedRow(), 0, false, false);
            } else {
                lockedTableNextColumnCellAction.actionPerformed(e);
            }
        }
    }

    private final class ScrollTableSelectNextColumnCellAction extends AbstractAction {
        private final Action scrollTableNextColumnCellAction;
        private ScrollTableSelectNextColumnCellAction(Action scrollTableNextColumnCellAction) {
            super();
            this.scrollTableNextColumnCellAction = scrollTableNextColumnCellAction;
        }
        public void actionPerformed(ActionEvent e) {
            if (scrollTable.getSelectedColumn() == scrollTable.getColumnCount() - 1) {
                scrollTable.transferFocusBackward();
                lockedTable.changeSelection(nextRow(scrollTable), 0, false, false);
                return;
            } else {
                scrollTableNextColumnCellAction.actionPerformed(e);
            }
        }
    }

    private final class ScrollTableSelectPreviousColumnCellAction extends AbstractAction {
        private final Action scrollTablePrevColumnCellAction;
        private ScrollTableSelectPreviousColumnCellAction(Action scrollTablePrevColumnCellAction) {
            super();
            this.scrollTablePrevColumnCellAction = scrollTablePrevColumnCellAction;
        }
        public void actionPerformed(ActionEvent e) {
            if (scrollTable.getSelectedColumn() == 0) {
                scrollTable.transferFocusBackward();
                lockedTable.changeSelection(scrollTable.getSelectedRow(), lockedTable.getColumnCount() - 1, false, false);
                return;
            } else {
                scrollTablePrevColumnCellAction.actionPerformed(e);
            }
        }
    }

    private final class LockedTableSelectPreviousColumnCellAction extends AbstractAction {
        private final Action lockedTablePrevColumnCellAction;
        private LockedTableSelectPreviousColumnCellAction(Action lockedTablePrevColumnCellAction) {
            super();
            this.lockedTablePrevColumnCellAction = lockedTablePrevColumnCellAction;
        }
        public void actionPerformed(ActionEvent e) {
            if (lockedTable.getSelectedColumn() == 0) {
                lockedTable.transferFocus();
                scrollTable.changeSelection(previousRow(scrollTable), scrollTable.getColumnCount() - 1, false, false);
                return;
            } else {
                lockedTablePrevColumnCellAction.actionPerformed(e);
            }
        }
    }
}