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

/*
 *   RemoteBoundaryVisualizerSubTask.java
 *   Copyright (C) 2003 University of Waikato, Hamilton, New Zealand
 *
 */

package weka.gui.boundaryvisualizer;

import weka.classifiers.Classifier;
import weka.classifiers.AbstractClassifier;
import weka.core.Instance;
import weka.core.DenseInstance;
import weka.core.Instances;
import weka.core.Utils;
import weka.experiment.Task;
import weka.experiment.TaskStatusInfo;

import java.util.Random;

/**
 * Class that encapsulates a sub task for distributed boundary
 * visualization. Produces probability distributions for each pixel
 * in one row of the visualization.
 *
 * @author <a href="mailto:mhall@cs.waikato.ac.nz">Mark Hall</a>
 * @version $Revision: 5987 $
 * @since 1.0
 * @see Task
 */
public class RemoteBoundaryVisualizerSubTask implements Task {

  // status information for this sub task
  private TaskStatusInfo m_status = new TaskStatusInfo();

  // the result of this sub task
  private RemoteResult m_result;

  // which row are we doing
  private int m_rowNumber;

  // width and height of the visualization
  private int m_panelHeight;
  private int m_panelWidth;

  // the classifier to use
  private Classifier m_classifier;

  // the kernel density estimator
  private DataGenerator m_dataGenerator;

  // the training data
  private Instances m_trainingData;

  // attributes for visualizing on (fixed dimensions)
  private int m_xAttribute;
  private int m_yAttribute;

  // pixel width and height in terms of attribute values
  private double m_pixHeight;
  private double m_pixWidth;

  // min, max of these attributes
  private double m_minX;
  private double m_minY;
  private double m_maxX;
  private double m_maxY;

  // number of samples to take from each region in the fixed dimensions
  private int m_numOfSamplesPerRegion = 2;

  // number of samples per kernel = base ^ (# non-fixed dimensions)
  private int m_numOfSamplesPerGenerator;
  private double m_samplesBase = 2.0;

  // A random number generator 
  private Random m_random;

  private double [] m_weightingAttsValues;
  private boolean [] m_attsToWeightOn;
  private double [] m_vals;
  private double [] m_dist;
  private Instance m_predInst;
  
  /**
   * Set the row number for this sub task
   *
   * @param rn the row number
   */
  public void setRowNumber(int rn) {
    m_rowNumber = rn;
  }

  /**
   * Set the width of the visualization
   *
   * @param pw the width
   */
  public void setPanelWidth(int pw) {
    m_panelWidth = pw;
  }

  /**
   * Set the height of the visualization
   *
   * @param ph the height
   */
  public void setPanelHeight(int ph) {
    m_panelHeight = ph;
  }

  /**
   * Set the height of a pixel
   *
   * @param ph the height of a pixel
   */
  public void setPixHeight(double ph) {
    m_pixHeight = ph;
  }

  /**
   * Set the width of a pixel
   *
   * @param pw the width of a pixel
   */
  public void setPixWidth(double pw) {
    m_pixWidth = pw;
  }

  /**
   * Set the classifier to use
   *
   * @param dc the classifier
   */
  public void setClassifier(Classifier dc) {
    m_classifier = dc;
  }

  /**
   * Set the density estimator to use
   *
   * @param dg the density estimator
   */
  public void setDataGenerator(DataGenerator dg) {
    m_dataGenerator = dg;
  }

  /**
   * Set the training data
   *
   * @param i the training data
   */
  public void setInstances(Instances i) {
    m_trainingData = i;
  }

  /**
   * Set the minimum and maximum values of the x axis fixed dimension
   *
   * @param minx a <code>double</code> value
   * @param maxx a <code>double</code> value
   */
  public void setMinMaxX(double minx, double maxx) {
    m_minX = minx; m_maxX = maxx;
  }

  /**
   * Set the minimum and maximum values of the y axis fixed dimension
   *
   * @param miny a <code>double</code> value
   * @param maxy a <code>double</code> value
   */
  public void setMinMaxY(double miny, double maxy) {
    m_minY = miny; m_maxY = maxy;
  }

  /**
   * Set the x axis fixed dimension
   *
   * @param xatt an <code>int</code> value
   */
  public void setXAttribute(int xatt) {
    m_xAttribute = xatt;
  }

  /**
   * Set the y axis fixed dimension
   *
   * @param yatt an <code>int</code> value
   */
  public void setYAttribute(int yatt) {
    m_yAttribute = yatt;
  }

  /**
   * Set the number of points to uniformly sample from a region (fixed
   * dimensions).
   *
   * @param num an <code>int</code> value
   */
  public void setNumSamplesPerRegion(int num) {
    m_numOfSamplesPerRegion = num;
  }

  /**
   * Set the base for computing the number of samples to obtain from each
   * generator. number of samples = base ^ (# non fixed dimensions)
   *
   * @param ksb a <code>double</code> value
   */
  public void setGeneratorSamplesBase(double ksb) {
    m_samplesBase = ksb;
  }

  /**
   * Perform the sub task
   */
  public void execute() {

    m_random = new Random(m_rowNumber * 11);
    m_dataGenerator.setSeed(m_rowNumber * 11);
    m_result = new RemoteResult(m_rowNumber, m_panelWidth);
    m_status.setTaskResult(m_result);
    m_status.setExecutionStatus(TaskStatusInfo.PROCESSING);

    try {
      m_numOfSamplesPerGenerator = 
	(int)Math.pow(m_samplesBase, m_trainingData.numAttributes()-3);
      if (m_trainingData == null) {
	throw new Exception("No training data set (BoundaryPanel)");
      }
      if (m_classifier == null) {
	throw new Exception("No classifier set (BoundaryPanel)");
      }
      if (m_dataGenerator == null) {
	throw new Exception("No data generator set (BoundaryPanel)");
      }
      if (m_trainingData.attribute(m_xAttribute).isNominal() || 
	m_trainingData.attribute(m_yAttribute).isNominal()) {
	throw new Exception("Visualization dimensions must be numeric "
			    +"(RemoteBoundaryVisualizerSubTask)");
      }
      
      m_attsToWeightOn = new boolean[m_trainingData.numAttributes()];
      m_attsToWeightOn[m_xAttribute] = true;
      m_attsToWeightOn[m_yAttribute] = true;
      
      // generate samples
      m_weightingAttsValues = new double [m_attsToWeightOn.length];
      m_vals = new double[m_trainingData.numAttributes()];
      m_predInst = new DenseInstance(1.0, m_vals);
      m_predInst.setDataset(m_trainingData);

      System.err.println("Executing row number "+m_rowNumber);
      for (int j = 0; j < m_panelWidth; j++) {
	double [] preds = calculateRegionProbs(j, m_rowNumber);
	m_result.setLocationProbs(j, preds);
	m_result.
	  setPercentCompleted((int)(100 * ((double)j / (double)m_panelWidth)));
      }
    } catch (Exception ex) {
      m_status.setExecutionStatus(TaskStatusInfo.FAILED);
      m_status.setStatusMessage("Row "+m_rowNumber+" failed.");
      System.err.print(ex);
      return;
    }

    // finished
    m_status.setExecutionStatus(TaskStatusInfo.FINISHED);
    m_status.setStatusMessage("Row "+m_rowNumber+" completed successfully.");
  }


  private double [] calculateRegionProbs(int j, int i) throws Exception {
    double [] sumOfProbsForRegion = 
      new double [m_trainingData.classAttribute().numValues()];

    for (int u = 0; u < m_numOfSamplesPerRegion; u++) {
      
      double [] sumOfProbsForLocation = 
	new double [m_trainingData.classAttribute().numValues()];
      
      m_weightingAttsValues[m_xAttribute] = getRandomX(j);
      m_weightingAttsValues[m_yAttribute] = getRandomY(m_panelHeight-i-1);
      
      m_dataGenerator.setWeightingValues(m_weightingAttsValues);
      
      double [] weights = m_dataGenerator.getWeights();
      double sumOfWeights = Utils.sum(weights);
      int [] indices = Utils.sort(weights);
      
      // Prune 1% of weight mass
      int [] newIndices = new int[indices.length];
      double sumSoFar = 0; 
      double criticalMass = 0.99 * sumOfWeights;
      int index = weights.length - 1; int counter = 0;
      for (int z = weights.length - 1; z >= 0; z--) {
	newIndices[index--] = indices[z];
	sumSoFar += weights[indices[z]];
	counter++;
	if (sumSoFar > criticalMass) {
	  break;
	}
      }
      indices = new int[counter];
      System.arraycopy(newIndices, index + 1, indices, 0, counter);
      
      for (int z = 0; z < m_numOfSamplesPerGenerator; z++) {
        
	m_dataGenerator.setWeightingValues(m_weightingAttsValues);
	double [][] values = m_dataGenerator.generateInstances(indices);
        
	for (int q = 0; q < values.length; q++) {
	  if (values[q] != null) {
	    System.arraycopy(values[q], 0, m_vals, 0, m_vals.length);
	    m_vals[m_xAttribute] = m_weightingAttsValues[m_xAttribute];
	    m_vals[m_yAttribute] = m_weightingAttsValues[m_yAttribute];
            
	    // classify the instance
	    m_dist = m_classifier.distributionForInstance(m_predInst);

	    for (int k = 0; k < sumOfProbsForLocation.length; k++) {
	      sumOfProbsForLocation[k] += (m_dist[k] * weights[q]); 
	    }
	  }
	}
      }
      
      for (int k = 0; k < sumOfProbsForRegion.length; k++) {
	sumOfProbsForRegion[k] += (sumOfProbsForLocation[k] * sumOfWeights); 
      }
    }
    
    // average
    Utils.normalize(sumOfProbsForRegion);

    // cache
    double [] tempDist = new double[sumOfProbsForRegion.length];
    System.arraycopy(sumOfProbsForRegion, 0, tempDist, 
		     0, sumOfProbsForRegion.length);
		
    return tempDist;
  }

  /**
   * Return a random x attribute value contained within
   * the pix'th horizontal pixel
   *
   * @param pix the horizontal pixel number
   * @return a value in attribute space
   */
  private double getRandomX(int pix) {

    double minPix =  m_minX + (pix * m_pixWidth);

    return minPix + m_random.nextDouble() * m_pixWidth;
  }

  /**
   * Return a random y attribute value contained within
   * the pix'th vertical pixel
   *
   * @param pix the vertical pixel number
   * @return a value in attribute space
   */
  private double getRandomY(int pix) {
    
    double minPix = m_minY + (pix * m_pixHeight);
    
    return minPix +  m_random.nextDouble() * m_pixHeight;
  }
  
  /**
   * Return status information for this sub task
   *
   * @return a <code>TaskStatusInfo</code> value
   */
  public TaskStatusInfo getTaskStatus() {
    return m_status;
  }
}
