/*
 *    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.
 */

/*
 *    Saver.java
 *    Copyright (C) 2004 University of Waikato, Hamilton, New Zealand
 *
 */

package weka.gui.beans;

import java.io.IOException;
import java.io.ObjectInputStream;

import weka.core.Environment;
import weka.core.EnvironmentHandler;
import weka.core.Instances;
import weka.core.OptionHandler;
import weka.core.SerializedObject;
import weka.core.Utils;
import weka.core.converters.ArffSaver;
import weka.core.converters.DatabaseConverter;
import weka.core.converters.DatabaseSaver;

/**
 * Saves data sets using weka.core.converter classes
 * 
 * @author <a href="mailto:mutter@cs.waikato.ac.nz">Stefan Mutter</a>
 * @version $Revision: 9217 $
 * 
 */
public class Saver extends AbstractDataSink implements WekaWrapper,
    EnvironmentHandler {

  /** for serialization */
  private static final long serialVersionUID = 5371716690308950755L;

  /**
   * Holds the instances to be saved
   */
  private Instances m_dataSet;

  /**
   * Holds the structure
   */
  private Instances m_structure;

  /**
   * Global info for the wrapped loader (if it exists).
   */
  protected String m_globalInfo;

  /**
   * Thread for doing IO in
   */
  private transient SaveBatchThread m_ioThread;

  /**
   * Saver
   */
  private weka.core.converters.Saver m_Saver = new ArffSaver();
  private weka.core.converters.Saver m_SaverTemplate = m_Saver;

  /**
   * The relation name that becomes part of the file name
   */
  private String m_fileName;

  /**
   * Flag indicating that instances will be saved to database. Used because
   * structure information can only be sent after a database has been
   * configured.
   */
  private boolean m_isDBSaver;

  /**
   * For file-based savers - if true (default), relation name is used as the
   * primary part of the filename. If false, then the prefix is used as the
   * filename. Useful for preventing filenames from getting too long when there
   * are many filters in a flow.
   */
  private boolean m_relationNameForFilename = true;

  /**
   * Count for structure available messages
   */
  private int m_count;

  /**
   * The environment variables.
   */
  protected transient Environment m_env;

  private weka.core.converters.Saver makeCopy() throws Exception {
    return (weka.core.converters.Saver) new SerializedObject(m_SaverTemplate)
        .getObject();
  }

  private class SaveBatchThread extends Thread {
    private final DataSink m_DS;

    public SaveBatchThread(DataSink ds) {
      m_DS = ds;
    }

    @Override
    public void run() {
      try {
        m_visual.setAnimated();

        m_Saver.setInstances(m_dataSet);
        if (m_logger != null) {
          m_logger.statusMessage(statusMessagePrefix()
              + Messages.getInstance().getString(
                  "Saver_SaveBatchThread_Run_StatusMessage_Text_First")
              + m_dataSet.relationName()
              + Messages.getInstance().getString(
                  "Saver_SaveBatchThread_Run_StatusMessage_Text_Second"));
        }
        m_Saver.writeBatch();
        if (m_logger != null) {
          m_logger.logMessage(Messages.getInstance().getString(
              "Saver_SaveBatchThread_Run_LogMessage_Text_First")
              + statusMessagePrefix()
              + Messages.getInstance().getString(
                  "Saver_SaveBatchThread_Run_LogMessage_Text_Second"));
        }

      } catch (Exception ex) {
        if (m_logger != null) {
          m_logger.statusMessage(statusMessagePrefix()
              + Messages.getInstance().getString(
                  "Saver_SaveBatchThread_Run_StatusMessage_Text_Third"));
          m_logger.logMessage(Messages.getInstance().getString(
              "Saver_SaveBatchThread_Run_LogMessage_Text_Third")
              + statusMessagePrefix()
              + Messages.getInstance().getString(
                  "Saver_SaveBatchThread_Run_LogMessage_Text_Fourth")
              + ex.getMessage());
        }
        ex.printStackTrace();
      } finally {
        if (Thread.currentThread().isInterrupted()) {
          if (m_logger != null) {
            m_logger.logMessage(Messages.getInstance().getString(
                "Saver_SaveBatchThread_Run_LogMessage_Text_Fifth")
                + statusMessagePrefix()
                + Messages.getInstance().getString(
                    "Saver_SaveBatchThread_Run_LogMessage_Text_Sixth"));
          }
        }
        if (m_logger != null) {
          m_logger.statusMessage(statusMessagePrefix()
              + Messages.getInstance().getString(
                  "Saver_SaveBatchThread_Run_StatusMessage_Text_Fourth"));
        }
        block(false);
        m_visual.setStatic();
        m_ioThread = null;
      }
    }
  }

  /**
   * Function used to stop code that calls acceptTrainingSet. This is needed as
   * classifier construction is performed inside a separate thread of execution.
   * 
   * @param tf a <code>boolean</code> value
   */
  private synchronized void block(boolean tf) {

    if (tf) {
      try {
        if (m_ioThread.isAlive()) {
          wait();
        }
      } catch (InterruptedException ex) {
      }
    } else {
      notifyAll();
    }
  }

  /**
   * Returns true if. at this time, the bean is busy with some (i.e. perhaps a
   * worker thread is performing some calculation).
   * 
   * @return true if the bean is busy.
   */
  public boolean isBusy() {
    return (m_ioThread != null);
  }

  /**
   * Global info (if it exists) for the wrapped loader
   * 
   * @return the global info
   */
  public String globalInfo() {
    return m_globalInfo;
  }

  /** Contsructor */
  public Saver() {
    super();
    setSaverTemplate(m_Saver);
    m_fileName = "";
    m_dataSet = null;
    m_count = 0;

  }

  /**
   * Set a custom (descriptive) name for this bean
   * 
   * @param name the name to use
   */
  public void setCustomName(String name) {
    m_visual.setText(name);
  }

  /**
   * Get the custom (descriptive) name for this bean (if one has been set)
   * 
   * @return the custom name (or the default name)
   */
  public String getCustomName() {
    return m_visual.getText();
  }

  /**
   * Set environment variables to use.
   * 
   * @param env the environment variables to use
   */
  public void setEnvironment(Environment env) {
    m_env = env;
  }

  /**
   * Pass the environment variables on the the wrapped saver
   */
  private void passEnvOnToSaver() {
    // set environment variables
    if (m_SaverTemplate instanceof EnvironmentHandler && m_env != null) {
      ((EnvironmentHandler) m_Saver).setEnvironment(m_env);
    }
  }

  /**
   * Set the loader to use
   * 
   * @param saver a Saver
   */
  public void setSaverTemplate(weka.core.converters.Saver saver) {
    boolean loadImages = true;
    if (saver.getClass().getName()
        .compareTo(m_SaverTemplate.getClass().getName()) == 0) {
      loadImages = false;
    }
    m_SaverTemplate = saver;
    String saverName = saver.getClass().toString();
    saverName = saverName.substring(saverName.lastIndexOf('.') + 1,
        saverName.length());
    if (loadImages) {

      if (!m_visual.loadIcons(BeanVisual.ICON_PATH + saverName + ".gif",
          BeanVisual.ICON_PATH + saverName + "_animated.gif")) {
        useDefaultVisual();
      }
    }
    m_visual.setText(saverName);

    // get global info
    m_globalInfo = KnowledgeFlowApp.getGlobalInfo(m_SaverTemplate);
    if (m_SaverTemplate instanceof DatabaseConverter)
      m_isDBSaver = true;
    else
      m_isDBSaver = false;
  }

  /**
   * makes sure that the filename is valid, i.e., replaces slashes, backslashes
   * and colons with underscores ("_"). Also try to prevent filename from
   * becoming insanely long by removing package part of class names.
   * 
   * @param filename the filename to cleanse
   * @return the cleansed filename
   */
  protected String sanitizeFilename(String filename) {
    filename = filename.replaceAll("\\\\", "_").replaceAll(":", "_")
        .replaceAll("/", "_");
    filename = Utils.removeSubstring(filename,
        "weka.filters.supervised.instance.");
    filename = Utils.removeSubstring(filename,
        "weka.filters.supervised.attribute.");
    filename = Utils.removeSubstring(filename,
        "weka.filters.unsupervised.instance.");
    filename = Utils.removeSubstring(filename,
        "weka.filters.unsupervised.attribute.");
    filename = Utils.removeSubstring(filename, "weka.clusterers.");
    filename = Utils.removeSubstring(filename, "weka.associations.");
    filename = Utils.removeSubstring(filename, "weka.attributeSelection.");
    filename = Utils.removeSubstring(filename, "weka.estimators.");
    filename = Utils.removeSubstring(filename, "weka.datagenerators.");

    if (!m_isDBSaver && !m_relationNameForFilename) {
      filename = "";
      try {
        if (m_Saver.filePrefix().equals("")) {
          m_Saver.setFilePrefix("no-name");
        }
      } catch (Exception ex) {
        System.err.println(ex);
      }
    }

    return filename;
  }

  /**
   * Method reacts to a dataset event and starts the writing process in batch
   * mode
   * 
   * @param e a dataset event
   */
  @Override
  public synchronized void acceptDataSet(DataSetEvent e) {

    try {
      m_Saver = makeCopy();
    } catch (Exception ex) {
      if (m_logger != null) {
        m_logger.statusMessage(statusMessagePrefix()
            + Messages.getInstance().getString(
                "Saver_AcceptDataSet_StatusMessage_Text_First"));
        m_logger
            .logMessage(Messages.getInstance().getString(
                "Saver_AcceptDataSet_LogMessage_Text_First")
                + statusMessagePrefix()
                + Messages.getInstance().getString(
                    "Saver_AcceptDataSet_LogMessage_Text_Second")
                + ex.getMessage());
      }
    }
    passEnvOnToSaver();
    m_fileName = sanitizeFilename(e.getDataSet().relationName());
    m_dataSet = e.getDataSet();
    if (e.isStructureOnly() && m_isDBSaver
        && ((DatabaseSaver) m_SaverTemplate).getRelationForTableName()) {//
      ((DatabaseSaver) m_Saver).setTableName(m_fileName);
    }
    if (!e.isStructureOnly()) {
      if (!m_isDBSaver) {
        try {
          m_Saver.setDirAndPrefix(m_fileName, "");
        } catch (Exception ex) {
          System.out.println(ex);
        }
      }
      saveBatch();
      System.out
          .println(Messages.getInstance().getString(
              "Saver_AcceptDataSet_Text_First")
              + m_fileName
              + Messages.getInstance().getString(
                  "Saver_AcceptDataSet_Text_Second"));
    }
  }

  /**
   * Method reacts to a threshold data event ans starts the writing process in
   * batch mode.
   * 
   * @param e threshold data event.
   */
  @Override
  public synchronized void acceptDataSet(ThresholdDataEvent e) {
    try {
      m_Saver = makeCopy();
    } catch (Exception ex) {
      if (m_logger != null) {
        m_logger.statusMessage(statusMessagePrefix()
            + Messages.getInstance().getString(
                "Saver_AcceptDataSet_StatusMessage_Text_Second"));
        m_logger
            .logMessage(Messages.getInstance().getString(
                "Saver_AcceptDataSet_LogMessage_Text_Third")
                + statusMessagePrefix()
                + Messages.getInstance().getString(
                    "Saver_AcceptDataSet_LogMessage_Text_Fourth")
                + ex.getMessage());
      }
    }

    passEnvOnToSaver();
    m_fileName = sanitizeFilename(e.getDataSet().getPlotInstances()
        .relationName());
    m_dataSet = e.getDataSet().getPlotInstances();

    if (m_isDBSaver
        && ((DatabaseSaver) m_SaverTemplate).getRelationForTableName()) {//
      ((DatabaseSaver) m_Saver).setTableName(m_fileName);
      ((DatabaseSaver) m_Saver).setRelationForTableName(false);
    }

    if (!m_isDBSaver) {
      try {
        m_Saver.setDirAndPrefix(m_fileName, "");
      } catch (Exception ex) {
        System.out.println(ex);
      }
    }
    saveBatch();
    System.out.println(Messages.getInstance().getString(
        "Saver_AcceptDataSet_Text_Third")
        + m_fileName
        + Messages.getInstance().getString("Saver_AcceptDataSet_Text_Fourth"));
  }

  /**
   * Method reacts to a test set event and starts the writing process in batch
   * mode
   * 
   * @param e test set event
   */
  @Override
  public synchronized void acceptTestSet(TestSetEvent e) {

    try {
      m_Saver = makeCopy();
    } catch (Exception ex) {
      if (m_logger != null) {
        m_logger.statusMessage(statusMessagePrefix()
            + Messages.getInstance().getString(
                "Saver_AcceptTestSet_StatusMessage_Text_First"));
        m_logger
            .logMessage(Messages.getInstance().getString(
                "Saver_AcceptTestSet_LogMessage_Text_First")
                + statusMessagePrefix()
                + Messages.getInstance().getString(
                    "Saver_AcceptTestSet_LogMessage_Text_Second")
                + ex.getMessage());
      }
    }

    passEnvOnToSaver();
    m_fileName = sanitizeFilename(e.getTestSet().relationName());
    m_dataSet = e.getTestSet();
    if (e.isStructureOnly() && m_isDBSaver
        && ((DatabaseSaver) m_SaverTemplate).getRelationForTableName()) {
      ((DatabaseSaver) m_Saver).setTableName(m_fileName);
    }
    if (!e.isStructureOnly()) {
      if (!m_isDBSaver) {
        try {
          m_Saver.setDirAndPrefix(m_fileName, "_test_" + e.getSetNumber()
              + "_of_" + e.getMaxSetNumber());
        } catch (Exception ex) {
          System.out.println(ex);
        }
      } else {
        ((DatabaseSaver) m_Saver).setRelationForTableName(false);
        String setName = ((DatabaseSaver) m_Saver).getTableName();
        setName = setName.replaceFirst(
            "_[tT][eE][sS][tT]_[0-9]+_[oO][fF]_[0-9]+", "");
        ((DatabaseSaver) m_Saver).setTableName(setName + "_test_"
            + e.getSetNumber() + "_of_" + e.getMaxSetNumber());
      }
      saveBatch();
      System.out
          .println(Messages.getInstance().getString(
              "Saver_AcceptTestSet_Text_First")
              + e.getSetNumber()
              + Messages.getInstance().getString(
                  "Saver_AcceptTestSet_Text_Second")
              + e.getMaxSetNumber()
              + Messages.getInstance().getString(
                  "Saver_AcceptTestSet_Text_Third")
              + m_fileName
              + Messages.getInstance().getString(
                  "Saver_AcceptTestSet_Text_Fourth"));
    }
  }

  /**
   * Method reacts to a training set event and starts the writing process in
   * batch mode
   * 
   * @param e a training set event
   */
  @Override
  public synchronized void acceptTrainingSet(TrainingSetEvent e) {

    try {
      m_Saver = makeCopy();
    } catch (Exception ex) {
      if (m_logger != null) {
        m_logger.statusMessage(statusMessagePrefix()
            + Messages.getInstance().getString(
                "Saver_AcceptTrainingSet_StatusMessage_Text_First"));
        m_logger.logMessage(Messages.getInstance().getString(
            "Saver_AcceptTrainingSet_LogMessage_Text_First")
            + statusMessagePrefix()
            + Messages.getInstance().getString(
                "Saver_AcceptTrainingSet_LogMessage_Text_Second")
            + ex.getMessage());
      }
    }

    passEnvOnToSaver();
    m_fileName = sanitizeFilename(e.getTrainingSet().relationName());
    m_dataSet = e.getTrainingSet();
    if (e.isStructureOnly() && m_isDBSaver
        && ((DatabaseSaver) m_SaverTemplate).getRelationForTableName()) {
      ((DatabaseSaver) m_Saver).setTableName(m_fileName);
    }
    if (!e.isStructureOnly()) {
      if (!m_isDBSaver) {
        try {
          m_Saver.setDirAndPrefix(m_fileName, "_training_" + e.getSetNumber()
              + "_of_" + e.getMaxSetNumber());
        } catch (Exception ex) {
          System.out.println(ex);
        }
      } else {
        ((DatabaseSaver) m_Saver).setRelationForTableName(false);
        String setName = ((DatabaseSaver) m_Saver).getTableName();
        setName = setName.replaceFirst(
            "_[tT][rR][aA][iI][nN][iI][nN][gG]_[0-9]+_[oO][fF]_[0-9]+", "");
        ((DatabaseSaver) m_Saver).setTableName(setName + "_training_"
            + e.getSetNumber() + "_of_" + e.getMaxSetNumber());
      }
      saveBatch();
      System.out.println(Messages.getInstance().getString(
          "Saver_AcceptTrainingSet_Text_First")
          + e.getSetNumber()
          + Messages.getInstance().getString(
              "Saver_AcceptTrainingSet_Text_Second")
          + e.getMaxSetNumber()
          + Messages.getInstance().getString(
              "Saver_AcceptTrainingSet_Text_Third")
          + m_fileName
          + Messages.getInstance().getString(
              "Saver_AcceptTrainingSet_Text_Fourth"));
    }
  }

  /** Saves instances in batch mode */
  public synchronized void saveBatch() {

    m_Saver.setRetrieval(m_Saver.BATCH);
    /*
     * String visText = this.getName(); try { visText = (m_fileName.length() >
     * 0) ? m_fileName : m_Saver.filePrefix(); } catch (Exception ex) { }
     * m_visual.setText(visText);
     */
    m_ioThread = new SaveBatchThread(Saver.this);
    m_ioThread.setPriority(Thread.MIN_PRIORITY);
    m_ioThread.start();
    block(true);
  }

  /**
   * Methods reacts to instance events and saves instances incrementally. If the
   * instance to save is null, the file is closed and the saving process is
   * ended.
   * 
   * @param e instance event
   */
  @Override
  public synchronized void acceptInstance(InstanceEvent e) {

    if (e.getStatus() == e.FORMAT_AVAILABLE) {
      // start of a new stream
      try {
        m_Saver = makeCopy();
      } catch (Exception ex) {
        if (m_logger != null) {
          m_logger.statusMessage(statusMessagePrefix()
              + Messages.getInstance().getString(
                  "Saver_AcceptInstance_StatusMessage_Text_First"));
          m_logger.logMessage(Messages.getInstance().getString(
              "Saver_AcceptInstance_LogMessage_Text_First")
              + statusMessagePrefix()
              + Messages.getInstance().getString(
                  "Saver_AcceptInstance_LogMessage_Text_Second")
              + ex.getMessage());
        }
      }
      m_Saver.setRetrieval(m_Saver.INCREMENTAL);
      m_structure = e.getStructure();
      m_fileName = sanitizeFilename(m_structure.relationName());
      m_Saver.setInstances(m_structure);
      if (m_isDBSaver)
        if (((DatabaseSaver) m_SaverTemplate).getRelationForTableName()) {
          ((DatabaseSaver) m_Saver).setTableName(m_fileName);
          ((DatabaseSaver) m_Saver).setRelationForTableName(false);
        }
    }
    if (e.getStatus() == e.INSTANCE_AVAILABLE) {
      m_visual.setAnimated();
      if (m_count == 0) {
        passEnvOnToSaver();
        if (!m_isDBSaver) {
          try {
            m_Saver.setDirAndPrefix(m_fileName, "");
          } catch (Exception ex) {
            System.out.println(ex);
            m_visual.setStatic();
          }
        }
        m_count++;
      }
      try {
        /*
         * String visText = this.getName(); visText = (m_fileName.length() > 0)
         * ? m_fileName : m_Saver.filePrefix(); m_visual.setText(m_fileName);
         */
        m_Saver.writeIncremental(e.getInstance());
      } catch (Exception ex) {
        m_visual.setStatic();
        System.err.println(Messages.getInstance().getString(
            "Saver_AcceptInstance_Error_Text_First")
            + e.getInstance()
            + Messages.getInstance().getString(
                "Saver_AcceptInstance_Error_Text_Second"));
        ex.printStackTrace();
      }
    }
    if (e.getStatus() == e.BATCH_FINISHED) {
      try {
        if (m_count == 0) {
          passEnvOnToSaver();
          if (!m_isDBSaver) {
            try {
              m_Saver.setDirAndPrefix(m_fileName, "");
            } catch (Exception ex) {
              System.out.println(ex);
              m_visual.setStatic();
            }
          }
          m_count++;
        }
        m_Saver.writeIncremental(e.getInstance());
        if (e.getInstance() != null) {
          m_Saver.writeIncremental(null);
        }
        // m_firstNotice = true;
        m_visual.setStatic();
        System.out.println(Messages.getInstance().getString(
            "Saver_AcceptInstance_Text_First")
            + m_fileName
            + Messages.getInstance().getString(
                "Saver_AcceptInstance_Text_Second"));
        /*
         * String visText = this.getName(); visText = (m_fileName.length() > 0)
         * ? m_fileName : m_Saver.filePrefix(); m_visual.setText(visText);
         */
        m_count = 0;
      } catch (Exception ex) {
        m_visual.setStatic();
        System.err.println(Messages.getInstance().getString(
            "Saver_AcceptInstance_Text_Third"));
        ex.printStackTrace();
      }
    }
  }

  /**
   * Get the saver
   * 
   * @return a <code>weka.core.converters.Saver</code> value
   */
  public weka.core.converters.Saver getSaverTemplate() {
    return m_SaverTemplate;
  }

  /**
   * Set the saver
   * 
   * @param algorithm a Saver
   */
  public void setWrappedAlgorithm(Object algorithm) {
    if (!(algorithm instanceof weka.core.converters.Saver)) {
      throw new IllegalArgumentException(algorithm.getClass()
          + Messages.getInstance().getString(
              "Saver_SetWrappedAlgorithm_IllegalArgumentException_Text"));
    }
    setSaverTemplate((weka.core.converters.Saver) algorithm);
  }

  /**
   * Get the saver
   * 
   * @return a Saver
   */
  public Object getWrappedAlgorithm() {
    return getSaverTemplate();
  }

  /**
   * Set whether to use the relation name as the primary part of the filename.
   * If false, then the prefix becomes the filename.
   * 
   * @param r true if the relation name is to be part of the filename.
   */
  public void setRelationNameForFilename(boolean r) {
    m_relationNameForFilename = r;
  }

  /**
   * Get whether the relation name is the primary part of the filename.
   * 
   * @return true if the relation name is part of the filename.
   */
  public boolean getRelationNameForFilename() {
    return m_relationNameForFilename;
  }

  /** Stops the bean */
  @Override
  public void stop() {
    // tell the listenee (upstream bean) to stop
    if (m_listenee instanceof BeanCommon) {
      ((BeanCommon) m_listenee).stop();
    }

    // stop the io thread
    if (m_ioThread != null) {
      m_ioThread.interrupt();
      m_ioThread.stop();
      m_ioThread = null;
      m_visual.setStatic();
    }
  }

  private String statusMessagePrefix() {
    return getCustomName()
        + "$"
        + hashCode()
        + "|"
        + ((m_Saver instanceof OptionHandler) ? Utils
            .joinOptions(((OptionHandler) m_Saver).getOptions()) + "|" : "");
  }

  // Custom de-serialization in order to set default
  // environment variables on de-serialization
  private void readObject(ObjectInputStream aStream) throws IOException,
      ClassNotFoundException {
    aStream.defaultReadObject();

    // set a default environment to use
    m_env = Environment.getSystemWide();
  }

  /**
   * The main method for testing
   * 
   * @param args
   */
  public static void main(String[] args) {
    try {
      final javax.swing.JFrame jf = new javax.swing.JFrame();
      jf.getContentPane().setLayout(new java.awt.BorderLayout());

      final Saver tv = new Saver();

      jf.getContentPane().add(tv, java.awt.BorderLayout.CENTER);
      jf.addWindowListener(new java.awt.event.WindowAdapter() {
        @Override
        public void windowClosing(java.awt.event.WindowEvent e) {
          jf.dispose();
          System.exit(0);
        }
      });
      jf.setSize(800, 600);
      jf.setVisible(true);
    } catch (Exception ex) {
      ex.printStackTrace();
    }
  }

}
