/*
 * Copyright (c) 2009, Swedish Institute of Computer Science.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. Neither the name of the Institute nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE INSTITUTE AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE INSTITUTE OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 *
 *
 * -----------------------------------------------------------------
 *
 * Author  : Niclas Finne
 * Created : 2009-05-20
 * Updated : $Date: 2009/06/12 14:12:59 $
 *           $Revision: 1.1 $
 */

package org.contikios.cooja.dialogs;
import java.awt.Component;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.Arrays;

import javax.swing.JTable;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;
import javax.swing.table.TableColumnModel;
import javax.swing.table.TableModel;

/**
 * This code was originally based on a Java tip from Rob Camick at
 * Java Tips Weblog, http://tips4java.wordpress.com.
 */
public class TableColumnAdjuster implements PropertyChangeListener, TableModelListener {

  private final JTable table;
  private int spacing;
  private boolean isColumnHeaderIncluded;
  private boolean isColumnDataIncluded;
  private boolean isOnlyAdjustLarger;
  private boolean isDynamicAdjustment;

  /*
   *  Specify the table and use default spacing
   */
  public TableColumnAdjuster(JTable table) {
    this(table, 6);
  }

  private boolean[] adjustColumns;

  /*
   *  Specify the table and spacing
   */
  public TableColumnAdjuster(JTable table, int spacing) {
    this.table = table;
    this.spacing = spacing;
    TableColumnModel tcm = table.getColumnModel();
    adjustColumns = new boolean[tcm.getColumnCount()];
    Arrays.fill(adjustColumns, true);

    setColumnHeaderIncluded(true);
    setColumnDataIncluded(true);
    setOnlyAdjustLarger(true);
    setDynamicAdjustment(false);
    
  }

  public void packColumns() {
    TableColumnModel tcm = table.getColumnModel();
    for (int i = 0, n = tcm.getColumnCount(); i < n; i++) {
      packColumn(i);
    }
  }

  public void packColumn(final int column) {
    adjustColumn(column, false);
  }

  /*
   *  Adjust the widths of all the columns in the table
   */
  public void adjustColumns() {
    TableColumnModel tcm = table.getColumnModel();
    for (int i = 0, n = tcm.getColumnCount(); i < n; i++) {
      if (adjustColumns[i]) {
        adjustColumn(i, isOnlyAdjustLarger);
      }
    }
  }

  public void setAdjustColumn(int i, boolean adjust) {
    adjustColumns[i] = adjust;
  }
  
  /*
   *  Adjust the width of the specified column in the table
   */
  public void adjustColumn(int column) {
    adjustColumn(column, isOnlyAdjustLarger);
  }

  private void adjustColumn(int column, boolean onlyAdjustLarger) {
    if (!adjustColumns[column]) {
      return;
    }
    int viewColumn = table.convertColumnIndexToView(column);
    if (viewColumn < 0) {
      return;
    }
    TableColumn tableColumn = table.getColumnModel().getColumn(viewColumn);
    if (! tableColumn.getResizable()) {
      return;
    }

    int columnHeaderWidth = getColumnHeaderWidth(tableColumn, column);
    int columnDataWidth   = getColumnDataWidth(tableColumn, column);
    int preferredWidth    = Math.max(columnHeaderWidth, columnDataWidth);
    updateTableColumn(tableColumn, preferredWidth, onlyAdjustLarger);
  }

  /*
   *  Calculate the width based on the column name
   */
  private int getColumnHeaderWidth(TableColumn tableColumn, int column) {
    if (! isColumnHeaderIncluded) return 0;

    Object value = tableColumn.getHeaderValue();
    TableCellRenderer renderer = tableColumn.getHeaderRenderer();

    if (renderer == null) {
      renderer = table.getTableHeader().getDefaultRenderer();
    }

    Component c = renderer.getTableCellRendererComponent(table, value, false, false, -1, column);
    return c.getPreferredSize().width;
  }

  /*
   *  Calculate the width based on the widest cell renderer for the
   *  given column.
   */
  private int getColumnDataWidth(TableColumn tableColumn, int column) {
    if (! isColumnDataIncluded) return 0;

    int preferredWidth = 0;
    int maxWidth = tableColumn.getMaxWidth();

    for (int row = 0, n = table.getRowCount(); row < n; row++) {
      preferredWidth = Math.max(preferredWidth, getCellDataWidth(row, column));

      //  We've exceeded the maximum width, no need to check other rows
      if (preferredWidth >= maxWidth) {
        break;
      }
    }

    return preferredWidth;
  }

  /*
   *  Get the preferred width for the specified cell
   */
  private int getCellDataWidth(int row, int column) {
    //  Invoke the renderer for the cell to calculate the preferred width

    TableCellRenderer cellRenderer = table.getCellRenderer(row, column);
    Object value = table.getModel().getValueAt(row, column);
    Component c = cellRenderer.getTableCellRendererComponent(table, value, false, false, row, column);
    int width = c.getPreferredSize().width + table.getIntercellSpacing().width;
    return width;
  }

  /*
   *  Update the TableColumn with the newly calculated width
   */
  private void updateTableColumn(TableColumn tableColumn, int width, boolean onlyAdjustLarger) {
    int currentWidth = tableColumn.getWidth();
    width += spacing;

    // Don't shrink the column width if onlyAdjustLarger is set
    if (width != currentWidth && (!onlyAdjustLarger || width > currentWidth)) {
      table.getTableHeader().setResizingColumn(tableColumn);
      tableColumn.setWidth(width);
      tableColumn.setPreferredWidth(width);
    }
  }

  private void adjustColumnsForNewRows(int firstRow, int lastRow) {
    TableColumnModel tcm = table.getColumnModel();
    for (int column = 0, n = tcm.getColumnCount(); column < n; column++) {
      if (!adjustColumns[column]) {
        continue;
      }
      int viewColumn = table.convertColumnIndexToView(column);
      if (viewColumn < 0) {
        continue;
      }
      TableColumn tableColumn = tcm.getColumn(viewColumn);
      if (! tableColumn.getResizable()) {
        continue;
      }
      // Find max width for the new rows (only adjust if wider)
      int width = 0;
      for (int row = firstRow; row <= lastRow; row++) {
        int w = getCellDataWidth(row, column);
        if (w > width) {
          width = w;
        }
      }
      updateTableColumn(tableColumn, width, true);
    }
  }

  /*
   *  Indicates whether to include the header in the width calculation
   */
  public void setColumnHeaderIncluded(boolean isColumnHeaderIncluded) {
    this.isColumnHeaderIncluded = isColumnHeaderIncluded;
  }

  /*
   *  Indicates whether to include the model data in the width calculation
   */
  public void setColumnDataIncluded(boolean isColumnDataIncluded) {
    this.isColumnDataIncluded = isColumnDataIncluded;
  }

  /*
   *  Indicates whether columns can only be increased in size
   */
  public void setOnlyAdjustLarger(boolean isOnlyAdjustLarger) {
    this.isOnlyAdjustLarger = isOnlyAdjustLarger;
  }

  /*
   *  Indicate whether changes to the model should cause the width to be
   *  dynamically recalculated.
   */
  public void setDynamicAdjustment(boolean isDynamicAdjustment) {
    if (this.isDynamicAdjustment != isDynamicAdjustment) {
      this.isDynamicAdjustment = isDynamicAdjustment;
      if (isDynamicAdjustment) {
        table.addPropertyChangeListener(this);
        table.getModel().addTableModelListener(this);
      } else {
        table.removePropertyChangeListener(this);
        table.getModel().removeTableModelListener(this);
      }
    }
  }

  //
  //  Implement the PropertyChangeListener
  //
  public void propertyChange(PropertyChangeEvent e) {
    //  When the TableModel changes we need to update the listeners
    //  and column widths

    if ("model".equals(e.getPropertyName())) {
      if (this.isDynamicAdjustment) {
        TableModel model = (TableModel)e.getOldValue();
        model.removeTableModelListener(this);

        model = (TableModel)e.getNewValue();
        model.addTableModelListener(this);
      }
      adjustColumns();
    }
  }

  //
  //  Implement the TableModelListener
  //
  public void tableChanged(final TableModelEvent e) {
    if (e.getType() == TableModelEvent.INSERT) {
      adjustColumnsForNewRows(e.getFirstRow(), e.getLastRow());

    } else if (e.getType() == TableModelEvent.UPDATE) {
      int column = e.getColumn();
      int lastRow = e.getLastRow();
      // Last row might be set higher than the row count if all rows have
      // been updated
      if (lastRow >= table.getRowCount()) {
        lastRow = table.getRowCount() - 1;
      }
      if (column == TableModelEvent.ALL_COLUMNS) {
        // All columns have been updated
        if (isOnlyAdjustLarger) {
          int firstRow = e.getFirstRow();
          if (firstRow >= 0) {
            // Handle the rows as new rows since they should only increase
            // width as needed
            adjustColumnsForNewRows(firstRow, lastRow);
          }
        } else {
          // Could be an increase or decrease so check all rows, all columns
          adjustColumns();
        }

      } else if (isOnlyAdjustLarger) {
        //  Only need to worry about an increase in width for these cells
        if (!adjustColumns[column]) {
          return;
        }
        int viewColumn = table.convertColumnIndexToView(column);
        if (viewColumn < 0) {
          // Column is not visible
        } else {
          TableColumn tableColumn = table.getColumnModel().getColumn(viewColumn);
          if (tableColumn.getResizable()) {
            int firstRow = e.getFirstRow();
            int width = 0;
            if (firstRow < 0) {
              // Header changed
              width = getColumnHeaderWidth(tableColumn, column);
            } else {
              for (int row = e.getFirstRow(); row <= lastRow; row++) {
                int w = getCellDataWidth(row, column);
                if (w > width) {
                  width = w;
                }
              }
            }
            updateTableColumn(tableColumn, width, true);
          }
        }
      } else {
        // Could be an increase or decrease so check all rows
        adjustColumn(column, false);
      }

    } else {
      // Some rows have been deleted.
      if (!isOnlyAdjustLarger) {
        adjustColumns();
      }
    }
  }

}
