/*
 *    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., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

/*
 * ArffTableModel.java
 * Copyright (C) 2005 University of Waikato, Hamilton, New Zealand
 *
 */

package weka.gui.arffviewer;

import weka.core.Attribute;
import weka.core.Instance;
import weka.core.Instances;
import weka.core.Undoable;
import weka.core.converters.AbstractFileLoader;
import weka.core.converters.ConverterUtils;
import weka.filters.Filter;
import weka.filters.unsupervised.attribute.Reorder;
import weka.gui.ComponentHelper;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Vector;

import javax.swing.JOptionPane;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import javax.swing.table.TableModel;

/**
 * The model for the Arff-Viewer.
 *
 *
 * @author FracPete (fracpete at waikato dot ac dot nz)
 * @version $Revision: 9014 $
 */
public class ArffTableModel
  implements TableModel, Undoable {

  /** the listeners */
  private HashSet m_Listeners;
  /** the data */
  private Instances m_Data;
  /** whether notfication is enabled */
  private boolean m_NotificationEnabled;
  /** whether undo is active */
  private boolean m_UndoEnabled;
  /** whether to ignore changes, i.e. not adding to undo history */
  private boolean m_IgnoreChanges;
  /** the undo list (contains temp. filenames) */
  private Vector m_UndoList;
  /** whether the table is read-only */
  private boolean m_ReadOnly;

  /**
   * performs some initialization
   */
  private ArffTableModel() {
    super();

    m_Listeners           = new HashSet();
    m_Data                = null;
    m_NotificationEnabled = true;
    m_UndoList            = new Vector();
    m_IgnoreChanges       = false;
    m_UndoEnabled         = true;
    m_ReadOnly            = false;
  }

  /**
   * initializes the object and loads the given file
   *
   * @param filename	the file to load
   */
  public ArffTableModel(String filename) {
    this();

    if ( (filename != null) && (!filename.equals("")) )
      loadFile(filename);
  }

  /**
   * initializes the model with the given data
   *
   * @param data 	the data to use
   */
  public ArffTableModel(Instances data) {
    this();

    this.m_Data = data;
  }

  /**
   * returns whether the notification of changes is enabled
   *
   * @return 		true if notification of changes is enabled
   */
  public boolean isNotificationEnabled() {
    return m_NotificationEnabled;
  }

  /**
   * sets whether the notification of changes is enabled
   *
   * @param enabled	enables/disables the notification
   */
  public void setNotificationEnabled(boolean enabled) {
    m_NotificationEnabled = enabled;
  }

  /**
   * returns whether undo support is enabled
   *
   * @return 		true if undo support is enabled
   */
  public boolean isUndoEnabled() {
    return m_UndoEnabled;
  }

  /**
   * sets whether undo support is enabled
   *
   * @param enabled	whether to enable/disable undo support
   */
  public void setUndoEnabled(boolean enabled) {
    m_UndoEnabled = enabled;
  }

  /**
   * returns whether the model is read-only
   *
   * @return 		true if model is read-only
   */
  public boolean isReadOnly() {
    return m_ReadOnly;
  }

  /**
   * sets whether the model is read-only
   *
   * @param value	if true the model is set to read-only
   */
  public void setReadOnly(boolean value) {
    m_ReadOnly = value;
  }

  /**
   * loads the specified ARFF file
   *
   * @param filename	the file to load
   */
  private void loadFile(String filename) {
    AbstractFileLoader          loader;

    loader = ConverterUtils.getLoaderForFile(filename);

    if (loader != null) {
      try {
        loader.setFile(new File(filename));
        m_Data = loader.getDataSet();
      }
      catch (Exception e) {
        ComponentHelper.showMessageBox(
            null,
            Messages.getInstance().getString("ArffTableModel_LoadFile_ComponentHelperShowMessageBox_Text"),
            e.toString(),
            JOptionPane.OK_CANCEL_OPTION,
            JOptionPane.ERROR_MESSAGE );
        System.out.println(e);
        m_Data = null;
      }
    }
  }

  /**
   * sets the data
   *
   * @param data	the data to use
   */
  public void setInstances(Instances data) {
    m_Data = data;
  }

  /**
   * returns the data
   *
   * @return		the current data
   */
  public Instances getInstances() {
    return m_Data;
  }

  /**
   * returns the attribute at the given index, can be NULL if not an attribute
   * column
   *
   * @param columnIndex		the index of the column
   * @return			the attribute at the position
   */
  public Attribute getAttributeAt(int columnIndex) {
    if ( (columnIndex > 0) && (columnIndex < getColumnCount()) )
      return m_Data.attribute(columnIndex - 1);
    else
      return null;
  }

  /**
   * returns the TYPE of the attribute at the given position
   *
   * @param columnIndex		the index of the column
   * @return			the attribute type
   */
  public int getType(int columnIndex) {
    return getType(0, columnIndex);
  }

  /**
   * returns the TYPE of the attribute at the given position
   *
   * @param rowIndex		the index of the row
   * @param columnIndex		the index of the column
   * @return			the attribute type
   */
  public int getType(int rowIndex, int columnIndex) {
    int            result;

    result = Attribute.STRING;

    if (    (rowIndex >= 0) && (rowIndex < getRowCount())
         && (columnIndex > 0) && (columnIndex < getColumnCount()) )
      result = m_Data.instance(rowIndex).attribute(columnIndex - 1).type();

    return result;
  }

  /**
   * deletes the attribute at the given col index. notifies the listeners.
   *
   * @param columnIndex     the index of the attribute to delete
   */
  public void deleteAttributeAt(int columnIndex) {
    deleteAttributeAt(columnIndex, true);
  }

  /**
   * deletes the attribute at the given col index
   *
   * @param columnIndex     the index of the attribute to delete
   * @param notify          whether to notify the listeners
   */
  public void deleteAttributeAt(int columnIndex, boolean notify) {
    if ( (columnIndex > 0) && (columnIndex < getColumnCount()) ) {
      if (!m_IgnoreChanges)
        addUndoPoint();
      m_Data.deleteAttributeAt(columnIndex - 1);
      if (notify)
        notifyListener(new TableModelEvent(this, TableModelEvent.HEADER_ROW));
    }
  }

  /**
   * deletes the attributes at the given indices
   *
   * @param columnIndices	the column indices
   */
  public void deleteAttributes(int[] columnIndices) {
    int            i;

    Arrays.sort(columnIndices);

    addUndoPoint();

    m_IgnoreChanges = true;
    for (i = columnIndices.length - 1; i >= 0; i--)
      deleteAttributeAt(columnIndices[i], false);
    m_IgnoreChanges = false;

    notifyListener(new TableModelEvent(this, TableModelEvent.HEADER_ROW));
  }

  /**
   * renames the attribute at the given col index
   *
   * @param columnIndex		the index of the column
   * @param newName		the new name of the attribute
   */
  public void renameAttributeAt(int columnIndex, String newName) {
    if ( (columnIndex > 0) && (columnIndex < getColumnCount()) ) {
      addUndoPoint();
      m_Data.renameAttribute(columnIndex - 1, newName);
      notifyListener(new TableModelEvent(this, TableModelEvent.HEADER_ROW));
    }
  }

  /**
   * sets the attribute at the given col index as the new class attribute, i.e.
   * it moves it to the end of the attributes
   *
   * @param columnIndex		the index of the column
   */
  public void attributeAsClassAt(int columnIndex) {
    Reorder     reorder;
    String      order;
    int         i;

    if ( (columnIndex > 0) && (columnIndex < getColumnCount()) ) {
      addUndoPoint();

      try {
        // build order string (1-based!)
        order = "";
        for (i = 1; i < m_Data.numAttributes() + 1; i++) {
          // skip new class
          if (i == columnIndex)
            continue;

          if (!order.equals(""))
            order += ",";
          order += Integer.toString(i);
        }
        if (!order.equals(""))
          order += ",";
        order += Integer.toString(columnIndex);

        // process data
        reorder = new Reorder();
        reorder.setAttributeIndices(order);
        reorder.setInputFormat(m_Data);
        m_Data = Filter.useFilter(m_Data, reorder);

        // set class index
        m_Data.setClassIndex(m_Data.numAttributes() - 1);
      }
      catch (Exception e) {
        e.printStackTrace();
        undo();
      }

      notifyListener(new TableModelEvent(this, TableModelEvent.HEADER_ROW));
    }
  }

  /**
   * deletes the instance at the given index
   *
   * @param rowIndex		the index of the row
   */
  public void deleteInstanceAt(int rowIndex) {
    deleteInstanceAt(rowIndex, true);
  }

  /**
   * deletes the instance at the given index
   *
   * @param rowIndex		the index of the row
   * @param notify		whether to notify the listeners
   */
  public void deleteInstanceAt(int rowIndex, boolean notify) {
    if ( (rowIndex >= 0) && (rowIndex < getRowCount()) ) {
      if (!m_IgnoreChanges)
        addUndoPoint();
      m_Data.delete(rowIndex);
      if (notify)
        notifyListener(
            new TableModelEvent(
                this, rowIndex, rowIndex,
                TableModelEvent.ALL_COLUMNS, TableModelEvent.DELETE));
    }
  }

  /**
   * deletes the instances at the given positions
   *
   * @param rowIndices		the indices to delete
   */
  public void deleteInstances(int[] rowIndices) {
    int               i;

    Arrays.sort(rowIndices);

    addUndoPoint();

    m_IgnoreChanges = true;
    for (i = rowIndices.length - 1; i >= 0; i--)
      deleteInstanceAt(rowIndices[i], false);
    m_IgnoreChanges = false;

    notifyListener(
        new TableModelEvent(
            this, rowIndices[0], rowIndices[rowIndices.length - 1],
            TableModelEvent.ALL_COLUMNS, TableModelEvent.DELETE));
  }

  /**
   * sorts the instances via the given attribute
   *
   * @param columnIndex		the index of the column
   */
  public void sortInstances(int columnIndex) {
    if ( (columnIndex > 0) && (columnIndex < getColumnCount()) ) {
      addUndoPoint();
      m_Data.sort(columnIndex - 1);
      notifyListener(new TableModelEvent(this));
    }
  }

  /**
   * returns the column of the given attribute name, -1 if not found
   *
   * @param name		the name of the attribute
   * @return			the column index or -1 if not found
   */
  public int getAttributeColumn(String name) {
    int            i;
    int            result;

    result = -1;

    for (i = 0; i < m_Data.numAttributes(); i++) {
      if (m_Data.attribute(i).name().equals(name)) {
        result = i + 1;
        break;
      }
    }

    return result;
  }

  /**
   * returns the most specific superclass for all the cell values in the
   * column (always String)
   *
   * @param columnIndex		the column index
   * @return			the class of the column
   */
  public Class getColumnClass(int columnIndex) {
    Class       result;

    result = null;

    if ( (columnIndex >= 0) && (columnIndex < getColumnCount()) ) {
      if (columnIndex == 0)
        result = Integer.class;
      else if (getType(columnIndex) == Attribute.NUMERIC)
        result = Double.class;
      else
        result = String.class;   // otherwise no input of "?"!!!
    }

    return result;
  }

  /**
   * returns the number of columns in the model
   *
   * @return		the number of columns
   */
  public int getColumnCount() {
    int         result;

    result = 1;
    if (m_Data != null)
      result += m_Data.numAttributes();

    return result;
  }

  /**
   * checks whether the column represents the class or not
   *
   * @param columnIndex		the index of the column
   * @return			true if the column is the class attribute
   */
  private boolean isClassIndex(int columnIndex) {
    boolean        result;
    int            index;

    index  = m_Data.classIndex();
    result =    ((index == - 1) && (m_Data.numAttributes() == columnIndex))
             || (index == columnIndex - 1);

    return result;
  }

  /**
   * returns the name of the column at columnIndex
   *
   * @param columnIndex		the index of the column
   * @return			the name of the column
   */
  public String getColumnName(int columnIndex) {
    String      result;

    result = "";

    if ( (columnIndex >= 0) && (columnIndex < getColumnCount()) ) {
      if (columnIndex == 0) {
        result = Messages.getInstance().getString("ArffTableModel_GetColumnName_Result_Text_First");
      }
      else {
        if (m_Data != null) {
          if ( (columnIndex - 1 < m_Data.numAttributes()) ) {
            result = Messages.getInstance().getString("ArffTableModel_GetColumnName_Result_Text_Second");
            // name
            if (isClassIndex(columnIndex))
              result +=   Messages.getInstance().getString("ArffTableModel_GetColumnName_Result_Text_Third")
                + m_Data.attribute(columnIndex - 1).name()
                + Messages.getInstance().getString("ArffTableModel_GetColumnName_Result_Text_Forth");
            else
              result += m_Data.attribute(columnIndex - 1).name();

            // attribute type
            switch (getType(columnIndex)) {
              case Attribute.DATE:
                result += Messages.getInstance().getString("ArffTableModel_GetColumnName_Result_Date_Text");
                break;
              case Attribute.NOMINAL:
                result += Messages.getInstance().getString("ArffTableModel_GetColumnName_Result_Nominal_Text");
                break;
              case Attribute.STRING:
                result += Messages.getInstance().getString("ArffTableModel_GetColumnName_Result_String_Text");
                break;
              case Attribute.NUMERIC:
                result += Messages.getInstance().getString("ArffTableModel_GetColumnName_Result_Numeric_Text");
                break;
              case Attribute.RELATIONAL:
                result += Messages.getInstance().getString("ArffTableModel_GetColumnName_Result_Relational_Text");
                break;
              default:
                result += Messages.getInstance().getString("ArffTableModel_GetColumnName_Result_Default_Text");
            }

            result += Messages.getInstance().getString("ArffTableModel_GetColumnName_Result_Text_End");
          }
        }
      }
    }

    return result;
  }

  /**
   * returns the number of rows in the model
   *
   * @return		the number of rows
   */
  public int getRowCount() {
    if (m_Data == null)
      return 0;
    else
      return m_Data.numInstances();
  }

  /**
   * checks whether the value at the given position is missing
   *
   * @param rowIndex		the row index
   * @param columnIndex		the column index
   * @return			true if the value at the position is missing
   */
  public boolean isMissingAt(int rowIndex, int columnIndex) {
    boolean           result;

    result = false;

    if (    (rowIndex >= 0) && (rowIndex < getRowCount())
         && (columnIndex > 0) && (columnIndex < getColumnCount()) )
      result = (m_Data.instance(rowIndex).isMissing(columnIndex - 1));

    return result;
  }

  /**
   * returns the double value of the underlying Instances object at the
   * given position, -1 if out of bounds
   *
   * @param rowIndex		the row index
   * @param columnIndex		the column index
   * @return			the underlying value in the Instances object
   */
  public double getInstancesValueAt(int rowIndex, int columnIndex) {
    double	result;

    result = -1;

    if (    (rowIndex >= 0) && (rowIndex < getRowCount())
         && (columnIndex > 0) && (columnIndex < getColumnCount()) )
      result = m_Data.instance(rowIndex).value(columnIndex - 1);

    return result;
  }

  /**
   * returns the value for the cell at columnindex and rowIndex
   *
   * @param rowIndex		the row index
   * @param columnIndex		the column index
   * @return 			the value at the position
   */
  public Object getValueAt(int rowIndex, int columnIndex) {
    Object            result;
    String            tmp;

    result = null;

    if (    (rowIndex >= 0) && (rowIndex < getRowCount())
        && (columnIndex >= 0) && (columnIndex < getColumnCount()) ) {
      if (columnIndex == 0) {
        result = new Integer(rowIndex + 1);
      }
      else {
        if (isMissingAt(rowIndex, columnIndex)) {
          result = null;
        }
        else {
          switch (getType(columnIndex)) {
            case Attribute.DATE:
            case Attribute.NOMINAL:
            case Attribute.STRING:
            case Attribute.RELATIONAL:
              result = m_Data.instance(rowIndex).stringValue(columnIndex - 1);
              break;
            case Attribute.NUMERIC:
              result = new Double(m_Data.instance(rowIndex).value(columnIndex - 1));
              break;
            default:
              result = "-can't display-";
          }

          if (getType(columnIndex) != Attribute.NUMERIC) {
            if (result != null) {
              tmp = result.toString();
              // fix html tags, otherwise Java parser hangs
              if ((tmp.indexOf('<') > -1) || (tmp.indexOf('>') > -1)) {
                tmp = tmp.replace("<", "(");
                tmp = tmp.replace(">", ")");
              }
              // does it contain "\n" or "\r"? -> replace with red html tag
              if ( (tmp.indexOf("\n") > -1) || (tmp.indexOf("\r") > -1) ) {
                tmp = tmp.replaceAll("\\r\\n", "<font color=\"red\"><b>\\\\r\\\\n</b></font>");
                tmp = tmp.replaceAll("\\r", "<font color=\"red\"><b>\\\\r</b></font>");
                tmp = tmp.replaceAll("\\n", "<font color=\"red\"><b>\\\\n</b></font>");
                tmp = "<html>" + tmp + "</html>";
              }
              result = tmp;
            }
          }          
        }
      }
    }

    return result;
  }

  /**
   * returns true if the cell at rowindex and columnindexis editable
   *
   * @param rowIndex		the index of the row
   * @param columnIndex		the index of the column
   * @return			true if the cell is editable
   */
  public boolean isCellEditable(int rowIndex, int columnIndex) {
    return (columnIndex > 0) && !isReadOnly();
  }

  /**
   * sets the value in the cell at columnIndex and rowIndex to aValue.
   * but only the value and the value can be changed
   *
   * @param aValue		the new value
   * @param rowIndex		the row index
   * @param columnIndex		the column index
   */
  public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
    setValueAt(aValue, rowIndex, columnIndex, true);
  }

  /**
   * sets the value in the cell at columnIndex and rowIndex to aValue.
   * but only the value and the value can be changed
   *
   * @param aValue		the new value
   * @param rowIndex		the row index
   * @param columnIndex		the column index
   * @param notify		whether to notify the listeners
   */
  public void setValueAt(Object aValue, int rowIndex, int columnIndex, boolean notify) {
    int            type;
    int            index;
    String         tmp;
    Instance       inst;
    Attribute      att;
    Object         oldValue;

    if (!m_IgnoreChanges)
      addUndoPoint();

    oldValue = getValueAt(rowIndex, columnIndex);
    type     = getType(rowIndex, columnIndex);
    index    = columnIndex - 1;
    inst     = m_Data.instance(rowIndex);
    att      = inst.attribute(index);

    // missing?
    if (aValue == null) {
      inst.setValue(index, Instance.missingValue());
    }
    else {
      tmp = aValue.toString();

      switch (type) {
        case Attribute.DATE:
          try {
            att.parseDate(tmp);
            inst.setValue(index, att.parseDate(tmp));
          }
          catch (Exception e) {
            // ignore
          }
          break;

        case Attribute.NOMINAL:
          if (att.indexOfValue(tmp) > -1)
            inst.setValue(index, att.indexOfValue(tmp));
          break;

        case Attribute.STRING:
          inst.setValue(index, tmp);
          break;

        case Attribute.NUMERIC:
          try {
            Double.parseDouble(tmp);
            inst.setValue(index, Double.parseDouble(tmp));
          }
          catch (Exception e) {
            // ignore
          }
          break;

        case Attribute.RELATIONAL:
          try {
            inst.setValue(index, inst.attribute(index).addRelation((Instances) aValue));
          }
          catch (Exception e) {
            // ignore
          }
          break;

        default:
          throw new IllegalArgumentException(Messages.getInstance().getString("ArffTableModel_SetValueAt_Default_Error_Text_Front") + type + Messages.getInstance().getString("ArffTableModel_SetValueAt_Default_Error_Text_End"));
      }
    }

    // notify only if the value has changed!
    if (notify && (!("" + oldValue).equals("" + aValue)) )
        notifyListener(new TableModelEvent(this, rowIndex, columnIndex));
  }

  /**
   * adds a listener to the list that is notified each time a change to data
   * model occurs
   *
   * @param l		the listener to add
   */
  public void addTableModelListener(TableModelListener l) {
    m_Listeners.add(l);
  }

  /**
   * removes a listener from the list that is notified each time a change to
   * the data model occurs
   *
   * @param l		the listener to remove
   */
  public void removeTableModelListener(TableModelListener l) {
    m_Listeners.remove(l);
  }

  /**
   * notfies all listener of the change of the model
   *
   * @param e		the event to send to the listeners
   */
  public void notifyListener(TableModelEvent e) {
    Iterator                iter;
    TableModelListener      l;

    // is notification enabled?
    if (!isNotificationEnabled())
      return;

    iter = m_Listeners.iterator();
    while (iter.hasNext()) {
      l = (TableModelListener) iter.next();
      l.tableChanged(e);
    }
  }

  /**
   * removes the undo history
   */
  public void clearUndo() {
    m_UndoList = new Vector();
  }

  /**
   * returns whether an undo is possible, i.e. whether there are any undo points
   * saved so far
   *
   * @return returns TRUE if there is an undo possible
   */
  public boolean canUndo() {
    return !m_UndoList.isEmpty();
  }

  /**
   * undoes the last action
   */
  public void undo() {
    File                  tempFile;
    Instances             inst;
    ObjectInputStream     ooi;

    if (canUndo()) {
      // load file
      tempFile = (File) m_UndoList.get(m_UndoList.size() - 1);
      try {
        // read serialized data
        ooi = new ObjectInputStream(new BufferedInputStream(new FileInputStream(tempFile)));
        inst = (Instances) ooi.readObject();
        ooi.close();

        // set instances
        setInstances(inst);
        notifyListener(new TableModelEvent(this, TableModelEvent.HEADER_ROW));
        notifyListener(new TableModelEvent(this));
      }
      catch (Exception e) {
        e.printStackTrace();
      }
      tempFile.delete();

      // remove from undo
      m_UndoList.remove(m_UndoList.size() - 1);
    }
  }

  /**
   * adds an undo point to the undo history, if the undo support is enabled
   * @see #isUndoEnabled()
   * @see #setUndoEnabled(boolean)
   */
  public void addUndoPoint() {
    File                  tempFile;
    ObjectOutputStream    oos;

    // undo support currently on?
    if (!isUndoEnabled())
      return;

    if (getInstances() != null) {
      try {
        // temp. filename
        tempFile = File.createTempFile("arffviewer", null);
        tempFile.deleteOnExit();

        // serialize instances
        oos = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(tempFile)));
        oos.writeObject(getInstances());
        oos.flush();
        oos.close();

        // add to undo list
        m_UndoList.add(tempFile);
      }
      catch (Exception e) {
        e.printStackTrace();
      }
    }
  }
}
