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

/*
 * Copyright (C) 2007 University of Waikato 
 */

package weka.core.neighboursearch;

import weka.core.CheckGOE;
import weka.core.CheckOptionHandler;
import weka.core.FastVector;
import weka.core.Instances;
import weka.core.SerializationHelper;
import weka.test.Regression;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.Random;

import junit.framework.TestCase;

/**
 * Abstract Test class for neighboursearch algorithms.
 *
 * @author FracPete (fracpete at waikato dot ac dot nz)
 * @version $Revision: 1.1 $
 */
public abstract class AbstractNearestNeighbourSearchTest
  extends TestCase {
  
  /** The NearestNeighbourSearch to be tested */
  protected NearestNeighbourSearch m_NearestNeighbourSearch;
  
  /** the OptionHandler tester */
  protected CheckOptionHandler m_OptionTester;
  
  /** for testing GOE stuff */
  protected CheckGOE m_GOETester;

  /** the dataset used for testing */
  protected Instances m_Instances;

  /** the number of neighbors to test */
  protected int m_NumNeighbors;

  /** for selecting random instances */
  protected Random m_Random;
  
  /**
   * Constructs the <code>AbstractNearestNeighbourSearchTest</code>. 
   * Called by subclasses.
   *
   * @param name the name of the test class
   */
  public AbstractNearestNeighbourSearchTest(String name) { 
    super(name); 
  }

  /**
   * Returns the Instances to be used in testing.
   * 
   * @return		the test instances
   * @throws Exception	if loading fails
   */
  protected Instances getInstances() throws Exception {
    Instances	result;
    
    result = new Instances(
		new BufferedReader(
		    new InputStreamReader(
			ClassLoader.getSystemResourceAsStream(
			    "weka/core/neighboursearch/anneal.arff"))));
    result.setClassIndex(result.numAttributes() - 1);
    
    return result;
  }
  
  /**
   * Configures the CheckOptionHandler uses for testing the option handling.
   * Sets the NearestNeighbourSearch returned from the 
   * getNearestNeighbourSearch() method
   * 
   * @return	the fully configured CheckOptionHandler
   * @see	#getNearestNeighbourSearch()
   */
  protected CheckOptionHandler getOptionTester() {
    CheckOptionHandler		result;
    
    result = new CheckOptionHandler();
    result.setOptionHandler(getNearestNeighbourSearch());
    result.setUserOptions(new String[0]);
    result.setSilent(true);
    
    return result;
  }
  
  /**
   * Configures the CheckGOE used for testing GOE stuff.
   * Sets the NearestNeighbourSearch returned from the 
   * getNearestNeighbourSearch() method.
   * 
   * @return	the fully configured CheckGOE
   * @see	#getNearestNeighbourSearch()
   */
  protected CheckGOE getGOETester() {
    CheckGOE		result;
    
    result = new CheckGOE();
    result.setObject(getNearestNeighbourSearch());
    result.setIgnoredProperties(result.getIgnoredProperties() + ",instances");
    result.setSilent(true);
    
    return result;
  }

  /**
   * Used to create an instance of a specific NearestNeighbourSearch.
   *
   * @return a suitably configured <code>NearestNeighbourSearch</code> value
   */
  public abstract NearestNeighbourSearch getNearestNeighbourSearch();
  
  /**
   * Called by JUnit before each test method. This implementation creates
   * the default NearestNeighbourSearch to test and loads a test set of Instances.
   *
   * @exception Exception if an error occurs reading the example instances.
   */
  protected void setUp() throws Exception {
    m_NearestNeighbourSearch = getNearestNeighbourSearch();
    m_OptionTester           = getOptionTester();
    m_GOETester              = getGOETester();
    m_Instances	             = getInstances();
    m_NumNeighbors           = 3;
    m_Random                 = new Random(1);
  }

  /** Called by JUnit after each test method */
  protected void tearDown() {
    m_NearestNeighbourSearch = null;
    m_OptionTester           = null;
    m_GOETester              = null;
    m_Instances	             = null;
    m_NumNeighbors           = 0;
    m_Random                 = null;
  }

  /**
   * tests whether the scheme declares a serialVersionUID.
   */
  public void testSerialVersionUID() {
    boolean     result;

    result = !SerializationHelper.needsUID(m_NearestNeighbourSearch.getClass());

    if (!result)
      fail("Doesn't declare serialVersionUID!");
  }
  
  /**
   * tests the listing of the options
   */
  public void testListOptions() {
    if (!m_OptionTester.checkListOptions())
      fail("Options cannot be listed via listOptions.");
  }
  
  /**
   * tests the setting of the options
   */
  public void testSetOptions() {
    if (!m_OptionTester.checkSetOptions())
      fail("setOptions method failed.");
  }
  
  /**
   * tests whether the default settings are processed correctly
   */
  public void testDefaultOptions() {
    if (!m_OptionTester.checkDefaultOptions())
      fail("Default options were not processed correctly.");
  }
  
  /**
   * tests whether there are any remaining options
   */
  public void testRemainingOptions() {
    if (!m_OptionTester.checkRemainingOptions())
      fail("There were 'left-over' options.");
  }
  
  /**
   * tests the whether the user-supplied options stay the same after setting.
   * getting, and re-setting again.
   * 
   * @see 	#getOptionTester()
   */
  public void testCanonicalUserOptions() {
    if (!m_OptionTester.checkCanonicalUserOptions())
      fail("setOptions method failed");
  }
  
  /**
   * tests the resetting of the options to the default ones
   */
  public void testResettingOptions() {
    if (!m_OptionTester.checkSetOptions())
      fail("Resetting of options failed");
  }
  
  /**
   * tests for a globalInfo method
   */
  public void testGlobalInfo() {
    if (!m_GOETester.checkGlobalInfo())
      fail("No globalInfo method");
  }
  
  /**
   * tests the tool tips
   */
  public void testToolTips() {
    if (!m_GOETester.checkToolTips())
      fail("Tool tips inconsistent");
  }

  /**
   * tests whether the number of instances returned by the algorithms is the
   * same as was requested
   */
  public void testNumberOfNeighbors() {
    int		i;
    int 	instIndex;
    Instances	neighbors;
    
    try {
      m_NearestNeighbourSearch.setInstances(m_Instances);
    }
    catch (Exception e) {
      fail("Failed setting the instances: " + e);
    }
    
    for (i = 1; i <= m_NumNeighbors; i++) {
      instIndex = m_Random.nextInt(m_Instances.numInstances());
      try {
	neighbors = m_NearestNeighbourSearch.kNearestNeighbours(
	    		m_Instances.instance(instIndex), i);
	assertEquals(
	    "Returned different number of neighbors than requested", 
	    i, neighbors.numInstances());
      }
      catch (Exception e) {
	fail(
	    "Failed for " + i + " neighbors on instance " + (instIndex+1) 
	    + ": " + e);
      }
    }
  }
  
  /**
   * tests whether the tokenizer correctly initializes in the
   * buildTokenizer method
   */
  public void testBuildInitialization() {
    String[][][]	results;
    Instances		inst;
    int			i;
    int			n;
    int			m;
    
    results = new String[2][m_Instances.numInstances()][m_NumNeighbors];
    
    // two runs of determining neighbors
    for (i = 0; i < 2; i++) {
      try {
	m_NearestNeighbourSearch.setInstances(m_Instances);
	
	for (n = 0; n < m_Instances.numInstances(); n++) {
	  for (m = 1; m <= m_NumNeighbors; m++) {
	    inst = m_NearestNeighbourSearch.kNearestNeighbours(
			m_Instances.instance(n), m);
	    results[i][n][m - 1] = inst.toString();
	  }
	}
      }
      catch (Exception e) {
	fail("Build " + (i + 1) + " failed: " + e);
      }
    }
    
    // compare the results
    for (n = 0; n < m_Instances.numInstances(); n++) {
      for (m = 1; m <= m_NumNeighbors; m++) {
	if (!results[0][n][m - 1].equals(results[1][n][m - 1]))
	  fail("Results differ: instance #" + (n+1) + " with " + m + " neighbors");
      }
    }
  }

  /**
   * Runs the NearestNeighbourSearch with the given data and returns the 
   * generated results.
   *
   * @param data	the data to use
   * @return 		a <code>FastVector</code> containing the results.
   * @throws Exception	if search fails
   */
  protected FastVector useNearestNeighbourSearch(Instances data) throws Exception {
    FastVector		result;
    int			i;
    int			n;
    int			m;
    Instances		inst;
    StringBuffer	item;
    
    m_NearestNeighbourSearch.setInstances(m_Instances);
    
    result = new FastVector();
    for (i = 0; i < m_Instances.numInstances(); i++) {
      item = new StringBuffer((i+1) + ". " + m_Instances.instance(i).toString() + ": ");
      for (n = 1; n <= m_NumNeighbors; n++) {
	inst = m_NearestNeighbourSearch.kNearestNeighbours(
	    		m_Instances.instance(i), n);
	item.append(" neighbors=" + n + ": ");
	for (m = 0; m < inst.numInstances(); m++) {
	  if (m > 0)
	    item.append("; ");
	  item.append("neighbor_" + (m+1) + "=" + inst.instance(m));
	}
      }
      result.addElement(item.toString());
    }
    
    return result;
      
  }

  /**
   * Returns a string containing all the results.
   *
   * @param tokens 	a <code>FastVector</code> containing the results
   * @return 		a <code>String</code> representing the vector of results.
   */
  protected String resultsToString(FastVector results) {
    StringBuffer sb = new StringBuffer();
    
    sb.append(results.size()).append(" results for " + m_Instances.relationName() + ":\n");
    for (int i = 0; i < results.size(); i++)
      sb.append(results.elementAt(i)).append('\n');
    
    return sb.toString();
  }

  /**
   * Runs a regression test -- this checks that the output of the tested
   * object matches that in a reference version. When this test is
   * run without any pre-existing reference output, the reference version
   * is created.
   */
  public void testRegression() {
    Regression	reg;
    FastVector	regressionResult;
    
    reg = new Regression(this.getClass());
    
    try {
      regressionResult = useNearestNeighbourSearch(m_Instances);
      reg.println(resultsToString(regressionResult));
    }
    catch (Exception e) {
      fail("Regression test failed: " + e);
    }
    
    try {
      String diff = reg.diff();
      if (diff == null) {
        System.err.println("Warning: No reference available, creating."); 
      } else if (!diff.equals("")) {
        fail("Regression test failed. Difference:\n" + diff);
      }
    } 
    catch (java.io.IOException ex) {
      fail("Problem during regression testing.\n" + ex);
    }
  }
}
