///////////////////////////////////////////////////////////////////////////////
// For information as to what this class does, see the Javadoc, below.       //
// Copyright (C) 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2006,       //
// 2007, 2008, 2009, 2010, 2014, 2015, 2022 by Peter Spirtes, Richard        //
// Scheines, Joseph Ramsey, and Clark Glymour.                               //
//                                                                           //
// This program is free software; you can redistribute it and/or modify      //
// it under the terms of the GNU General Public License as published by      //
// the Free Software Foundation; either version 2 of the License, or         //
// (at your option) any later version.                                       //
//                                                                           //
// This program is distributed in the hope that it will be useful,           //
// but WITHOUT ANY WARRANTY; without even the implied warranty of            //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the             //
// GNU General Public License for more details.                              //
//                                                                           //
// You should have received a copy of the GNU General Public License         //
// along with this program; if not, write to the Free Software               //
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA //
///////////////////////////////////////////////////////////////////////////////
package edu.cmu.tetradapp.editor;

import edu.cmu.tetrad.data.DataModel;
import edu.cmu.tetrad.data.DataSet;
import edu.cmu.tetrad.data.Variable;
import edu.cmu.tetrad.graph.Node;
import edu.cmu.tetrad.util.JOptionUtils;

import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.TableColumnModelEvent;
import javax.swing.event.TableColumnModelListener;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableCellRenderer;
import java.awt.*;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.text.NumberFormat;
import java.util.EventObject;
import java.util.Hashtable;
import java.util.Map;

/**
 * Displays a DataSet object as a JTable.
 *
 * @author josephramsey
 */
public class TabularDataJTable extends JTable implements DataModelContainer,
        PropertyChangeListener {

    private Map<String, String> columnToTooltip;

    /**
     * States whether edits are allowed.
     */
    private boolean editable = true;

    public TabularDataJTable(DataSet model, Map<String, String> columnToTooltip) {
        this(model);
//		System.out.println("setting columnToTooltip " + columnToTooltip);
        this.columnToTooltip = columnToTooltip;
    }

    /**
     * Constructor. Takes a DataSet as a model.
     */
    public TabularDataJTable(DataSet model) {

        TabularDataTable dataModel = new TabularDataTable(model);

        dataModel.addPropertyChangeListener(this);
        setModel(dataModel);
        setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
//		System.out.println("dataModel: "+model.getColumnToTooltip());
        this.columnToTooltip
                = model.getColumnToTooltip() != null ? model.getColumnToTooltip() : new Hashtable<>();
        int rowCount = this.dataModel.getRowCount();
        int max = 0;

        while (rowCount > 0) {
            rowCount /= 10;
            max++;
        }
        // add cell renderer for columns 2-7
        // int vColIndex = 2;
        // TableColumn col = this.getColumnModel().getColumn(vColIndex);
        // Map<String, String> columnToTooltip = new Hashtable<String,
        // String>();
        // columnToTooltip.put("ADJ_COR", "This is a tooltip.");
        // col.setCellRenderer(new
        // GraphComparisonTableCellRenderer(columnToTooltip));

        // provide cell renderer the tooltip.
        addMouseListener(new MouseAdapter() {
            public void mousePressed(MouseEvent e) {
                int row = rowAtPoint(e.getPoint());
                int col = columnAtPoint(e.getPoint());

                if (row == 0) {
                    setRowSelectionAllowed(false);
                    setColumnSelectionAllowed(true);
                } else if (col == 0) {
                    setRowSelectionAllowed(true);
                    setColumnSelectionAllowed(false);
                } else {
                    setRowSelectionAllowed(true);
                    setColumnSelectionAllowed(true);
                }
            }
        });

        FontMetrics metrics = getFontMetrics(getFont());

        getColumnModel().getColumn(0).setMaxWidth(9 * max);
//		getColumnModel().getColumn(1).setMaxWidth(9 * 4);
        setRowHeight(metrics.getHeight() + 3);

        setRowSelectionAllowed(true);
        getColumnModel().setColumnSelectionAllowed(true);

        for (int i = 0; i < model.getNumColumns(); i++) {
            if (model.isSelected(model.getVariable(i))) {
                setRowSelectionAllowed(false);
                addColumnSelectionInterval(i + 1, i + 1);
            }
        }

        getColumnModel().addColumnModelListener(new TableColumnModelListener() {
            public void columnAdded(TableColumnModelEvent e) {
            }

            public void columnRemoved(TableColumnModelEvent e) {
            }

            public void columnMoved(TableColumnModelEvent e) {
            }

            public void columnMarginChanged(ChangeEvent e) {
            }

            /**
             * Sets the selection of columns in the model to what's in the
             * display.
             */
            public void columnSelectionChanged(ListSelectionEvent e) {
                if (e.getValueIsAdjusting()) {
                    return;
                }

                ListSelectionModel selectionModel = (ListSelectionModel) e
                        .getSource();
                DataSet dataSet = getDataSet();
                dataSet.clearSelection();

                if (!getRowSelectionAllowed()) {
                    for (int i = 0; i < dataSet.getNumColumns(); i++) {
                        if (selectionModel.isSelectedIndex(i + 1)) {
                            dataSet.setSelected(dataSet.getVariable(i), true);
                        }
                    }
                }
            }
        });

        setTransferHandler(new TabularDataTransferHandler());

        addFocusListener(new FocusAdapter() {
            public void focusLost(FocusEvent e) {
                JTable table = (JTable) e.getSource();
                TableCellEditor editor = table.getCellEditor();
                if (editor != null) {
                    editor.stopCellEditing();
                }
            }
        });
    }

    @Override
    public Component prepareRenderer(TableCellRenderer renderer, int rowIndex, int vColIndex) {
        Component c = super.prepareRenderer(renderer, rowIndex, vColIndex);
        if (c instanceof JComponent) {
            JComponent jc = (JComponent) c;

            Object o = getValueAt(rowIndex, vColIndex);

            if (o != null) {
                String tooltip = this.columnToTooltip.get(o.toString());
//				System.out.println("tooltip " + o + " "+ tooltip);
                if (tooltip != null) {
                    jc.setToolTipText(tooltip);
                }
            }
        }
        return c;
    }

    public void setEditable(boolean editable) {
        this.editable = editable;
    }

    public void setValueAt(Object aValue, int row, int column) {
        try {
            super.setValueAt(aValue, row, column);
        } catch (Exception e) {
            JOptionPane.showMessageDialog(JOptionUtils.centeringComp(), e
                    .getMessage(), "Error", JOptionPane.WARNING_MESSAGE);
        }
    }

    public TableCellEditor getCellEditor(int row, int column) {
        if (!this.editable) {
            return new DoNothingEditor();
        }
        if (row == 1) {
            return new VariableNameEditor();
        } //		else if (column == 1 && row >= 2) {
        //			return new MultiplierEditor();
        //		}
        else if (row > 1) {
            return new DataCellEditor();
        }

        return null;
    }

    public TableCellRenderer getCellRenderer(int row, int column) {
        if (column == 0) {
            return new RowNumberRenderer();
        } //		else if (column == 1 && row >= 1) {
        //			return new MultiplierRenderer();
        //		}
        else {
            if (row == 0 || row == 1) {
                return new VariableNameRenderer();
            }

            return new DataCellRenderer(this, getNumLeadingCols());
        }
    }

    /**
     * @return the underlying DataSet model.
     */
    public DataSet getDataSet() {
        TabularDataTable tableModelTabularData = (TabularDataTable) getModel();
        return tableModelTabularData.getDataSet();
    }

    public void setDataSet(DataSet data) {
        TabularDataTable tableModelTabularData = (TabularDataTable) getModel();
        tableModelTabularData.setDataSet(data);
    }

    public DataModel getDataModel() {
        return getDataSet();
    }

    public void deleteSelected() {
        TabularDataTable model = (TabularDataTable) getModel();
        DataSet dataSet = model.getDataSet();

        // When getRowSelectionAllowed() is false, getColumnSelectionAllowed() must be true, vise versa.
        // But both can be true since we can select a data cell - Zhou
        if (getColumnSelectionAllowed()) {
            int[] selectedCols = getSelectedColumns();

            TableCellEditor editor = getCellEditor();

            if (editor != null) {
                editor.stopCellEditing();
            }

            for (int i = selectedCols.length - 1; i >= 0; i--) {
//            for (int i = 0; i < selectedCols.length; i++) {
                // Adjust to 0 base
                selectedCols[i] -= getNumLeadingCols();
                // Then remove each individual column from model
                dataSet.removeColumn(selectedCols[i]);
            }

            // Using this causes error - Zhou
            //dataSet.removeCols(selectedCols);
        } else if (getRowSelectionAllowed()) {
            int[] selectedRows = getSelectedRows();

            TableCellEditor editor = getCellEditor();

            if (editor != null) {
                editor.stopCellEditing();
            }

            for (int i = 0; i < selectedRows.length; i++) {
                selectedRows[i] -= 2;
            }

            dataSet.removeRows(selectedRows);
        } else {
            throw new IllegalStateException("Only row deletion and column deltion supported.");
        }

        firePropertyChange("modelChanged", null, null);
        model.fireTableDataChanged();
    }

    public void clearSelected() {
        TabularDataTable model = (TabularDataTable) getModel();
        DataSet dataSet = model.getDataSet();

        if (!getRowSelectionAllowed()) {
            int[] selectedCols = getSelectedColumns();
            TableCellEditor editor = getCellEditor();

            if (editor != null) {
                editor.stopCellEditing();
            }

            for (int i = selectedCols.length - 1; i >= 0; i--) {
                if (selectedCols[i] < getNumLeadingCols()) {
                    continue;
                }

                int dataCol = selectedCols[i] - getNumLeadingCols();

                if (dataCol >= dataSet.getNumColumns()) {
                    continue;
                }

                Node variable = dataSet.getVariable(dataCol);
                Object missingValue = ((Variable) variable)
                        .getMissingValueMarker();

                for (int j = 0; j < dataSet.getNumRows(); j++) {
                    dataSet.setObject(j, dataCol, missingValue);
                }
            }
        } else if (!getColumnSelectionAllowed()) {
            int[] selectedRows = getSelectedRows();

            TableCellEditor editor = getCellEditor();
            if (editor != null) {
                editor.stopCellEditing();
            }

            for (int i = getColumnCount() - 1; i >= 0; i--) {
                if (i < getNumLeadingCols()) {
                    continue;
                }

                String colName = (String) (getValueAt(1, i));

                if (colName == null) {
                    continue;
                }

                int dataCol = i - getNumLeadingCols();

                Node variable = dataSet.getVariable(dataCol);
                Object missingValue = ((Variable) variable)
                        .getMissingValueMarker();

                for (int j = selectedRows.length - 1; j >= 0; j--) {
                    if (selectedRows[j] < 2) {
                        continue;
                    }

                    if (selectedRows[j] > dataSet.getNumRows() + 1) {
                        continue;
                    }

                    dataSet.setObject(selectedRows[j] - 2, dataCol,
                            missingValue);
                }
            }
        } else {
            int[] selectedRows = getSelectedRows();
            int[] selectedCols = getSelectedColumns();

            TableCellEditor editor = getCellEditor();

            if (editor != null) {
                editor.stopCellEditing();
            }

            for (int i = selectedCols.length - 1; i >= 0; i--) {
                if (selectedCols[i] < getNumLeadingCols()) {
                    continue;
                }

                int dataCol = selectedCols[i] - getNumLeadingCols();

                if (dataCol >= dataSet.getNumColumns()) {
                    continue;
                }

                String colName = (String) (getValueAt(1, selectedCols[i]));

                if (colName == null) {
                    continue;
                }

                Node variable = dataSet.getVariable(dataCol);
                Object missingValue = ((Variable) variable)
                        .getMissingValueMarker();

                for (int j = selectedRows.length - 1; j >= 0; j--) {
                    if (selectedRows[j] < 2) {
                        continue;
                    }

                    if (selectedRows[j] > dataSet.getNumRows() + 1) {
                        continue;
                    }

                    dataSet.setObject(selectedRows[j] - 2, dataCol,
                            missingValue);
                }
            }
        }

        firePropertyChange("modelChanged", null, null);
        model.fireTableDataChanged();
    }

    private int getNumLeadingCols() {
        /*
	  The number of initial "special" columns not used to display the data set.
         */
        return 1;
    }

    /**
     * @return true iff the given token is a legitimate value for the cell at
     * (row, col) in the table.
     */
    public boolean checkValueAt(String token, int col) {
        if (col < getNumLeadingCols()) {
            throw new IllegalArgumentException();
        }

        DataSet dataSet = getDataSet();
        int dataCol = col - getNumLeadingCols();
        // int dataCol = col;

        if (dataCol < dataSet.getNumColumns()) {
            Node variable = dataSet.getVariable(dataCol);
            return ((Variable) variable).checkValue(token);
        } else {
            return true;
        }
    }

    public void setShowCategoryNames(boolean selected) {
        TabularDataTable table = (TabularDataTable) getModel();
        table.setCategoryNamesShown(selected);
    }

    public boolean isShowCategoryNames() {
        TabularDataTable table = (TabularDataTable) getModel();
        return table.isCategoryNamesShown();
    }

    public void propertyChange(PropertyChangeEvent evt) {
        firePropertyChange(evt.getPropertyName(), evt.getOldValue(), evt.getNewValue());
    }

}

class RowNumberRenderer extends DefaultTableCellRenderer {

    public Component getTableCellRendererComponent(JTable table, Object value,
                                                   boolean isSelected, boolean hasFocus, int row, int column) {
        JLabel label = (JLabel) super.getTableCellRendererComponent(table,
                value, isSelected, hasFocus, row, column);

        if (row > 1) {
            setText(Integer.toString(row - 1));
            label.setHorizontalAlignment(SwingConstants.CENTER);
            label.setFont(new Font("SansSerif", Font.BOLD, 12));
        }

        return label;
    }
}

class VariableNameRenderer extends DefaultTableCellRenderer {

    public void setValue(Object value) {
        if (!(value instanceof String)) {
            value = "";
        }

        if (((String) value).contains("\b")) {
            return;
        }

        setText((String) value);
        setFont(new Font("SansSerif", Font.BOLD, 12));
        setHorizontalAlignment(SwingConstants.CENTER);
    }
}

class DoNothingEditor extends DefaultCellEditor {

    public DoNothingEditor() {
        super(new JTextField());
    }

    public boolean isCellEditable(EventObject anEvent) {
        return false;
    }
}

class VariableNameEditor extends DefaultCellEditor {

    private final JTextField textField;

    /**
     * Constructs a new number cell editor.
     */
    public VariableNameEditor() {
        super(new JTextField());

        this.textField = (JTextField) this.editorComponent;

        this.delegate = new EditorDelegate() {

            /**
             * Overrides delegate; sets the value of the textfield to the value
             * of the datum.
             *
             * @param value this value.
             */
            public void setValue(Object value) {
                if (!(value instanceof String)) {
                    value = "";
                }

                VariableNameEditor.this.textField.setText((String) value);
                VariableNameEditor.this.textField.setFont(new Font("SansSerif", Font.BOLD, 12));
                VariableNameEditor.this.textField.setHorizontalAlignment(SwingConstants.CENTER);
                VariableNameEditor.this.textField.selectAll();
            }

            /**
             * Overrides delegate; gets the text value from the cell to send
             * back to the model.
             *
             * @return this text value.
             */
            public Object getCellEditorValue() {
                return VariableNameEditor.this.textField.getText();
            }
        };

        this.textField.addActionListener(this.delegate);
    }
}

class DataCellRenderer extends DefaultTableCellRenderer {

    private final NumberFormat nf;
    private final DataSet dataSet;
    private final int numLeadingCols;

    public DataCellRenderer(TabularDataJTable tableTabular, int numLeadingCols) {
        this.dataSet = ((TabularDataTable) tableTabular.getModel()).getDataSet();
        this.numLeadingCols = numLeadingCols;
        this.nf = this.dataSet.getNumberFormat();
    }

    public void setValue(Object value) {
        if (value instanceof String) {
            setText((String) value);
        } else if (value instanceof Integer) {
            setText(value.toString());
        } else if (value instanceof Double) {
            setText(this.nf.format((double) (Double) value));
        } else {
            setText("");
        }
    }

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

        // Have to set the alignment here, since this is the only place the col
        // index of the component is available...
        Component c = super.getTableCellRendererComponent(table, value,
                isSelected, hasFocus, row, col);
        DefaultTableCellRenderer renderer = (DefaultTableCellRenderer) c;

        if (this.dataSet.getNumColumns() > 0 && col >= getNumLeadingCols()
                && col < this.dataSet.getNumColumns() + getNumLeadingCols()) {
            renderer.setHorizontalAlignment(SwingConstants.RIGHT);
        }

        return renderer;
    }

    private int getNumLeadingCols() {
        return this.numLeadingCols;
    }
}
