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

/*
 * LibLINEAR.java
 * Copyright (C) Benedikt Waldvogel 
 */
package weka.classifiers.functions;

import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.StringTokenizer;
import java.util.Vector;
import weka.classifiers.AbstractClassifier;

import weka.classifiers.Classifier;
import weka.core.Capabilities;
import weka.core.Instance;
import weka.core.Instances;
import weka.core.Option;
import weka.core.RevisionUtils;
import weka.core.SelectedTag;
import weka.core.Tag;
import weka.core.TechnicalInformation;
import weka.core.TechnicalInformationHandler;
import weka.core.Utils;
import weka.core.WekaException;
import weka.core.Capabilities.Capability;
import weka.core.TechnicalInformation.Type;
import weka.filters.Filter;
import weka.filters.unsupervised.attribute.NominalToBinary;
import weka.filters.unsupervised.attribute.Normalize;
import weka.filters.unsupervised.attribute.ReplaceMissingValues;

/**
  <!-- globalinfo-start -->
  * A wrapper class for the liblinear tools (the liblinear classes, typically the jar file, need to be in the classpath to use this classifier).<br/>
  * Rong-En Fan, Kai-Wei Chang, Cho-Jui Hsieh, Xiang-Rui Wang, Chih-Jen Lin (2008). LIBLINEAR - A Library for Large Linear Classification. URL http://www.csie.ntu.edu.tw/~cjlin/liblinear/.
  * <p/>
  <!-- globalinfo-end -->
 *
 <!-- technical-bibtex-start -->
 * BibTeX:
 * <pre>
 * &#64;misc{Fan2008,
 *    author = {Rong-En Fan and Kai-Wei Chang and Cho-Jui Hsieh and Xiang-Rui Wang and Chih-Jen Lin},
 *    note = {The Weka classifier works with version 1.33 of LIBLINEAR},
 *    title = {LIBLINEAR - A Library for Large Linear Classification},
 *    year = {2008},
 *    URL = {http://www.csie.ntu.edu.tw/\~cjlin/liblinear/}
 * }
 * </pre>
 * <p/>
 <!-- technical-bibtex-end -->
 *
 <!-- options-start -->
 * Valid options are: <p/>
 * 
 * <pre> -S &lt;int&gt;
 *  Set type of solver (default: 1)
 *    0 = L2-regularized logistic regression
 *    1 = L2-loss support vector machines (dual)
 *    2 = L2-loss support vector machines (primal)
 *    3 = L1-loss support vector machines (dual)
 *    4 = multi-class support vector machines by Crammer and Singer</pre>
 * 
 * <pre> -C &lt;double&gt;
 *  Set the cost parameter C
 *   (default: 1)</pre>
 * 
 * <pre> -Z
 *  Turn on normalization of input data (default: off)</pre>
 * 
 * <pre> -N
 *  Turn on nominal to binary conversion.</pre>
 * 
 * <pre> -M
 *  Turn off missing value replacement.
 *  WARNING: use only if your data has no missing values.</pre>
 * 
 * <pre> -P
 *  Use probability estimation (default: off)
 * currently for L2-regularized logistic regression only! </pre>
 * 
 * <pre> -E &lt;double&gt;
 *  Set tolerance of termination criterion (default: 0.01)</pre>
 * 
 * <pre> -W &lt;double&gt;
 *  Set the parameters C of class i to weight[i]*C
 *   (default: 1)</pre>
 * 
 * <pre> -B &lt;double&gt;
 *  Add Bias term with the given value if &gt;= 0; if &lt; 0, no bias term added (default: 1)</pre>
 * 
 * <pre> -D
 *  If set, classifier is run in debug mode and
 *  may output additional info to the console</pre>
 * 
 <!-- options-end -->
 *
 * @author  Benedikt Waldvogel (mail at bwaldvogel.de)
 * @version $Revision: 5917 $
 */
public class LibLINEAR
  extends AbstractClassifier implements TechnicalInformationHandler {

  /** the svm classname */
  protected final static String CLASS_LINEAR = "liblinear.Linear";

  /** the svm_model classname */
  protected final static String CLASS_MODEL = "liblinear.Model";

  /** the svm_problem classname */
  protected final static String CLASS_PROBLEM = "liblinear.Problem";

  /** the svm_parameter classname */
  protected final static String CLASS_PARAMETER = "liblinear.Parameter";

  /** the svm_parameter classname */
  protected final static String CLASS_SOLVERTYPE = "liblinear.SolverType";

  /** the svm_node classname */
  protected final static String CLASS_FEATURENODE = "liblinear.FeatureNode";

  /** serial UID */
  protected static final long serialVersionUID = 230504711;

  /** LibLINEAR Model */
  protected Object m_Model;


  public Object getModel() {
    return m_Model;
  }

  /** for normalizing the data */
  protected Filter m_Filter = null;

  /** normalize input data */
  protected boolean m_Normalize = false;

  /** SVM solver type L2-regularized logistic regression */
  public static final int SVMTYPE_L2_LR = 0;
  /** SVM solver type L2-loss support vector machines (dual) */
  public static final int SVMTYPE_L2LOSS_SVM_DUAL = 1;
  /** SVM solver type L2-loss support vector machines (primal) */
  public static final int SVMTYPE_L2LOSS_SVM = 2;
  /** SVM solver type L1-loss support vector machines (dual) */
  public static final int SVMTYPE_L1LOSS_SVM_DUAL = 3;
  /** SVM solver type multi-class support vector machines by Crammer and Singer */
  public static final int SVMTYPE_MCSVM_CS = 4;
  /** SVM solver types */
  public static final Tag[] TAGS_SVMTYPE = {
    new Tag(SVMTYPE_L2_LR, "L2-regularized logistic regression"),
    new Tag(SVMTYPE_L2LOSS_SVM_DUAL, "L2-loss support vector machines (dual)"),
    new Tag(SVMTYPE_L2LOSS_SVM, "L2-loss support vector machines (primal)"),
    new Tag(SVMTYPE_L1LOSS_SVM_DUAL, "L1-loss support vector machines (dual)"),
    new Tag(SVMTYPE_MCSVM_CS, "multi-class support vector machines by Crammer and Singer")
  };

  /** the SVM solver type */
  protected int m_SVMType = SVMTYPE_L2LOSS_SVM_DUAL;

  /** stopping criteria */
  protected double m_eps = 0.01;

  /** cost Parameter C */
  protected double m_Cost = 1;

  /** bias term value */
  protected double m_Bias = 1;

  protected int[] m_WeightLabel = new int[0];

  protected double[] m_Weight = new double[0];

  /** whether to generate probability estimates instead of +1/-1 in case of
   * classification problems */
  protected boolean m_ProbabilityEstimates = false;

  /** The filter used to get rid of missing values. */
  protected ReplaceMissingValues m_ReplaceMissingValues;

  /** The filter used to make attributes numeric. */
  protected NominalToBinary m_NominalToBinary;

  /** If true, the nominal to binary filter is applied */
  private boolean m_nominalToBinary = false;

  /** If true, the replace missing values filter is not applied */
  private boolean m_noReplaceMissingValues;

  /** whether the liblinear classes are in the Classpath */
  protected static boolean m_Present = false;
  static {
    try {
      Class.forName(CLASS_LINEAR);
      m_Present = true;
    }
    catch (Exception e) {
      m_Present = false;
    }
  }

  /**
   * Returns a string describing classifier
   *
   * @return a description suitable for displaying in the
   *         explorer/experimenter gui
   */
  public String globalInfo() {
    return
      "A wrapper class for the liblinear tools (the liblinear classes, typically "
      + "the jar file, need to be in the classpath to use this classifier).\n"
      + getTechnicalInformation().toString();
  }

  /**
   * Returns an instance of a TechnicalInformation object, containing
   * detailed information about the technical background of this class,
   * e.g., paper reference or book this class is based on.
   *
   * @return the technical information about this class
   */
  public TechnicalInformation getTechnicalInformation() {
    TechnicalInformation 	result;

    result = new TechnicalInformation(Type.MISC);
    result.setValue(TechnicalInformation.Field.AUTHOR, "Rong-En Fan and Kai-Wei Chang and Cho-Jui Hsieh and Xiang-Rui Wang and Chih-Jen Lin");
    result.setValue(TechnicalInformation.Field.TITLE, "LIBLINEAR - A Library for Large Linear Classification");
    result.setValue(TechnicalInformation.Field.YEAR, "2008");
    result.setValue(TechnicalInformation.Field.URL, "http://www.csie.ntu.edu.tw/~cjlin/liblinear/");
    result.setValue(TechnicalInformation.Field.NOTE, "The Weka classifier works with version 1.33 of LIBLINEAR");

    return result;
  }

  /**
   * Returns an enumeration describing the available options.
   *
   * @return an enumeration of all the available options.
   */
  public Enumeration listOptions() {
    Vector      result;

    result = new Vector();

    result.addElement(
        new Option(
          "\tSet type of solver (default: 1)\n"
          + "\t\t 0 = L2-regularized logistic regression\n"
          + "\t\t 1 = L2-loss support vector machines (dual)\n"
          + "\t\t 2 = L2-loss support vector machines (primal)\n"
          + "\t\t 3 = L1-loss support vector machines (dual)\n"
          + "\t\t 4 = multi-class support vector machines by Crammer and Singer",
          "S", 1, "-S <int>"));

    result.addElement(
        new Option(
          "\tSet the cost parameter C\n"
          + "\t (default: 1)",
          "C", 1, "-C <double>"));

    result.addElement(
        new Option(
          "\tTurn on normalization of input data (default: off)",
          "Z", 0, "-Z"));
    
    result.addElement(
        new Option("\tTurn on nominal to binary conversion.",
            "N", 0, "-N"));
    
    result.addElement(
        new Option("\tTurn off missing value replacement."
            + "\n\tWARNING: use only if your data has no missing "
            + "values.", "M", 0, "-M"));

    result.addElement(
        new Option(
          "\tUse probability estimation (default: off)\n" +
          "currently for L2-regularized logistic regression only! ",
          "P", 0, "-P"));

    result.addElement(
        new Option(
          "\tSet tolerance of termination criterion (default: 0.01)",
          "E", 1, "-E <double>"));

    result.addElement(
        new Option(
          "\tSet the parameters C of class i to weight[i]*C\n"
          + "\t (default: 1)",
          "W", 1, "-W <double>"));

    result.addElement(
        new Option(
          "\tAdd Bias term with the given value if >= 0; if < 0, no bias term added (default: 1)",
          "B", 1, "-B <double>"));

    Enumeration en = super.listOptions();
    while (en.hasMoreElements())
      result.addElement(en.nextElement());

    return result.elements();
  }

  /**
   * Sets the classifier options <p/>
   *
   <!-- options-start -->
   * Valid options are: <p/>
   * 
   * <pre> -S &lt;int&gt;
   *  Set type of solver (default: 1)
   *    0 = L2-regularized logistic regression
   *    1 = L2-loss support vector machines (dual)
   *    2 = L2-loss support vector machines (primal)
   *    3 = L1-loss support vector machines (dual)
   *    4 = multi-class support vector machines by Crammer and Singer</pre>
   * 
   * <pre> -C &lt;double&gt;
   *  Set the cost parameter C
   *   (default: 1)</pre>
   * 
   * <pre> -Z
   *  Turn on normalization of input data (default: off)</pre>
   * 
   * <pre> -N
   *  Turn on nominal to binary conversion.</pre>
   * 
   * <pre> -M
   *  Turn off missing value replacement.
   *  WARNING: use only if your data has no missing values.</pre>
   * 
   * <pre> -P
   *  Use probability estimation (default: off)
   * currently for L2-regularized logistic regression only! </pre>
   * 
   * <pre> -E &lt;double&gt;
   *  Set tolerance of termination criterion (default: 0.01)</pre>
   * 
   * <pre> -W &lt;double&gt;
   *  Set the parameters C of class i to weight[i]*C
   *   (default: 1)</pre>
   * 
   * <pre> -B &lt;double&gt;
   *  Add Bias term with the given value if &gt;= 0; if &lt; 0, no bias term added (default: 1)</pre>
   * 
   * <pre> -D
   *  If set, classifier is run in debug mode and
   *  may output additional info to the console</pre>
   * 
   <!-- options-end -->
   *
   * @param options     the options to parse
   * @throws Exception  if parsing fails
   */
  public void setOptions(String[] options) throws Exception {
    String      tmpStr;

    tmpStr = Utils.getOption('S', options);
    if (tmpStr.length() != 0)
      setSVMType(
          new SelectedTag(Integer.parseInt(tmpStr), TAGS_SVMTYPE));
    else
      setSVMType(
          new SelectedTag(SVMTYPE_L2LOSS_SVM_DUAL, TAGS_SVMTYPE));

    tmpStr = Utils.getOption('C', options);
    if (tmpStr.length() != 0)
      setCost(Double.parseDouble(tmpStr));
    else
      setCost(1);

    tmpStr = Utils.getOption('E', options);
    if (tmpStr.length() != 0)
      setEps(Double.parseDouble(tmpStr));
    else
      setEps(1e-3);

    setNormalize(Utils.getFlag('Z', options));
    
    setConvertNominalToBinary(Utils.getFlag('N', options));
    setDoNotReplaceMissingValues(Utils.getFlag('M', options));

    tmpStr = Utils.getOption('B', options);
    if (tmpStr.length() != 0)
      setBias(Double.parseDouble(tmpStr));
    else
      setBias(1);

    setWeights(Utils.getOption('W', options));

    setProbabilityEstimates(Utils.getFlag('P', options));
    
    super.setOptions(options);
  }

  /**
   * Returns the current options
   *
   * @return            the current setup
   */
  public String[] getOptions() {
    Vector        result;

    result  = new Vector();

    result.add("-S");
    result.add("" + m_SVMType);

    result.add("-C");
    result.add("" + getCost());

    result.add("-E");
    result.add("" + getEps());

    result.add("-B");
    result.add("" + getBias());

    if (getNormalize())
      result.add("-Z");
    
    if (getConvertNominalToBinary())
      result.add("-N");
    
    if (getDoNotReplaceMissingValues())
      result.add("-M");

    if (getWeights().length() != 0) {
      result.add("-W");
      result.add("" + getWeights());
    }

    if (getProbabilityEstimates())
      result.add("-P");

    return (String[]) result.toArray(new String[result.size()]);
  }

  /**
   * returns whether the liblinear classes are present or not, i.e. whether the
   * classes are in the classpath or not
   *
   * @return whether the liblinear classes are available
   */
  public static boolean isPresent() {
    return m_Present;
  }

  /**
   * Sets type of SVM (default SVMTYPE_L2)
   *
   * @param value       the type of the SVM
   */
  public void setSVMType(SelectedTag value) {
    if (value.getTags() == TAGS_SVMTYPE)
      m_SVMType = value.getSelectedTag().getID();
  }

  /**
   * Gets type of SVM
   *
   * @return            the type of the SVM
   */
  public SelectedTag getSVMType() {
    return new SelectedTag(m_SVMType, TAGS_SVMTYPE);
  }

  /**
   * Returns the tip text for this property
   *
   * @return tip text for this property suitable for
   *         displaying in the explorer/experimenter gui
   */
  public String SVMTypeTipText() {
    return "The type of SVM to use.";
  }

  /**
   * Sets the cost parameter C (default 1)
   *
   * @param value       the cost value
   */
  public void setCost(double value) {
    m_Cost = value;
  }

  /**
   * Returns the cost parameter C
   *
   * @return            the cost value
   */
  public double getCost() {
    return m_Cost;
  }

  /**
   * Returns the tip text for this property
   *
   * @return tip text for this property suitable for
   *         displaying in the explorer/experimenter gui
   */
  public String costTipText() {
    return "The cost parameter C.";
  }

  /**
   * Sets tolerance of termination criterion (default 0.001)
   *
   * @param value       the tolerance
   */
  public void setEps(double value) {
    m_eps = value;
  }

  /**
   * Gets tolerance of termination criterion
   *
   * @return            the current tolerance
   */
  public double getEps() {
    return m_eps;
  }

  /**
   * Returns the tip text for this property
   *
   * @return tip text for this property suitable for
   *         displaying in the explorer/experimenter gui
   */
  public String epsTipText() {
    return "The tolerance of the termination criterion.";
  }

  /**
   * Sets bias term value (default 1)
   * No bias term is added if value &lt; 0
   *
   * @param value       the bias term value
   */
  public void setBias(double value) {
    m_Bias = value;
  }

  /**
   * Returns bias term value (default 1)
   * No bias term is added if value &lt; 0
   *
   * @return             the bias term value
   */
  public double getBias() {
    return m_Bias;
  }

  /**
   * Returns the tip text for this property
   *
   * @return tip text for this property suitable for
   *         displaying in the explorer/experimenter gui
   */
  public String biasTipText() {
    return "If >= 0, a bias term with that value is added; " +
      "otherwise (<0) no bias term is added (default: 1).";
  }

  /**
   * Returns the tip text for this property
   *
   * @return tip text for this property suitable for
   *         displaying in the explorer/experimenter gui
   */
  public String normalizeTipText() {
    return "Whether to normalize the data.";
  }
  
  /**
   * whether to normalize input data
   *
   * @param value       whether to normalize the data
   */
  public void setNormalize(boolean value) {
    m_Normalize = value;
  }

  /**
   * whether to normalize input data
   *
   * @return            true, if the data is normalized
   */
  public boolean getNormalize() {
    return m_Normalize;
  }
  
  /**
   * Returns the tip text for this property
   *
   * @return tip text for this property suitable for
   *         displaying in the explorer/experimenter gui
   */
  public String convertNominalToBinaryTipText() {
    return "Whether to turn on conversion of nominal attributes "
      + "to binary.";
  }
  
  /**
   * Whether to turn on conversion of nominal attributes
   * to binary.
   * 
   * @param b true if nominal to binary conversion is to be
   * turned on
   */
  public void setConvertNominalToBinary(boolean b) {
    m_nominalToBinary = b;
  }
  
  /**
   * Gets whether conversion of nominal to binary is
   * turned on.
   * 
   * @return true if nominal to binary conversion is turned
   * on.
   */
  public boolean getConvertNominalToBinary() {
    return m_nominalToBinary;
  }
  
  /**
   * Returns the tip text for this property
   *
   * @return tip text for this property suitable for
   *         displaying in the explorer/experimenter gui
   */
  public String doNotReplaceMissingValuesTipText() {
    return "Whether to turn off automatic replacement of missing "
      + "values. WARNING: set to true only if the data does not "
      + "contain missing values.";
  }
  
  /**
   * Whether to turn off automatic replacement of missing values.
   * Set to true only if the data does not contain missing values.
   * 
   * @param b true if automatic missing values replacement is
   * to be disabled.
   */
  public void setDoNotReplaceMissingValues(boolean b) {
    m_noReplaceMissingValues = b;
  }
  
  /**
   * Gets whether automatic replacement of missing values is
   * disabled.
   * 
   * @return true if automatic replacement of missing values
   * is disabled.
   */
  public boolean getDoNotReplaceMissingValues() {
    return m_noReplaceMissingValues;
  }

  /**
   * Sets the parameters C of class i to weight[i]*C (default 1).
   * Blank separated list of doubles.
   *
   * @param weightsStr          the weights (doubles, separated by blanks)
   */
  public void setWeights(String weightsStr) {
    StringTokenizer       tok;
    int                   i;

    tok           = new StringTokenizer(weightsStr, " ");
    m_Weight      = new double[tok.countTokens()];
    m_WeightLabel = new int[tok.countTokens()];

    if (m_Weight.length == 0)
      System.out.println(
          "Zero Weights processed. Default weights will be used");

    for (i = 0; i < m_Weight.length; i++) {
      m_Weight[i]      = Double.parseDouble(tok.nextToken());
      m_WeightLabel[i] = i;
    }
  }

  /**
   * Gets the parameters C of class i to weight[i]*C (default 1).
   * Blank separated doubles.
   *
   * @return            the weights (doubles separated by blanks)
   */
  public String getWeights() {
    String      result;
    int         i;

    result = "";
    for (i = 0; i < m_Weight.length; i++) {
      if (i > 0)
        result += " ";
      result += Double.toString(m_Weight[i]);
    }

    return result;
  }

  /**
   * Returns the tip text for this property
   *
   * @return tip text for this property suitable for
   *         displaying in the explorer/experimenter gui
   */
  public String weightsTipText() {
    return "The weights to use for the classes, if empty 1 is used by default.";
  }

  /**
   * Returns whether probability estimates are generated instead of -1/+1 for
   * classification problems.
   *
   * @param value       whether to predict probabilities
   */
  public void setProbabilityEstimates(boolean value) {
    m_ProbabilityEstimates = value;
  }

  /**
   * Sets whether to generate probability estimates instead of -1/+1 for
   * classification problems.
   *
   * @return            true, if probability estimates should be returned
   */
  public boolean getProbabilityEstimates() {
    return m_ProbabilityEstimates;
  }

  /**
   * Returns the tip text for this property
   *
   * @return tip text for this property suitable for
   *         displaying in the explorer/experimenter gui
   */
  public String probabilityEstimatesTipText() {
    return "Whether to generate probability estimates instead of -1/+1 for classification problems " +
      "(currently for L2-regularized logistic regression only!)";
  }

  /**
   * sets the specified field
   *
   * @param o           the object to set the field for
   * @param name        the name of the field
   * @param value       the new value of the field
   */
  protected void setField(Object o, String name, Object value) {
    Field       f;

    try {
      f = o.getClass().getField(name);
      f.set(o, value);
    }
    catch (Exception e) {
      e.printStackTrace();
    }
  }

  /**
   * sets the specified field in an array
   *
   * @param o           the object to set the field for
   * @param name        the name of the field
   * @param index       the index in the array
   * @param value       the new value of the field
   */
  protected void setField(Object o, String name, int index, Object value) {
    Field       f;

    try {
      f = o.getClass().getField(name);
      Array.set(f.get(o), index, value);
    }
    catch (Exception e) {
      e.printStackTrace();
    }
  }

  /**
   * returns the current value of the specified field
   *
   * @param o           the object the field is member of
   * @param name        the name of the field
   * @return            the value
   */
  protected Object getField(Object o, String name) {
    Field       f;
    Object      result;

    try {
      f      = o.getClass().getField(name);
      result = f.get(o);
    }
    catch (Exception e) {
      e.printStackTrace();
      result = null;
    }

    return result;
  }

  /**
   * sets a new array for the field
   *
   * @param o           the object to set the array for
   * @param name        the name of the field
   * @param type        the type of the array
   * @param length      the length of the one-dimensional array
   */
  protected void newArray(Object o, String name, Class type, int length) {
    newArray(o, name, type, new int[]{length});
  }

  /**
   * sets a new array for the field
   *
   * @param o           the object to set the array for
   * @param name        the name of the field
   * @param type        the type of the array
   * @param dimensions  the dimensions of the array
   */
  protected void newArray(Object o, String name, Class type, int[] dimensions) {
    Field       f;

    try {
      f = o.getClass().getField(name);
      f.set(o, Array.newInstance(type, dimensions));
    }
    catch (Exception e) {
      e.printStackTrace();
    }
  }

  /**
   * executes the specified method and returns the result, if any
   *
   * @param o                   the object the method should be called from
   * @param name                the name of the method
   * @param paramClasses        the classes of the parameters
   * @param paramValues         the values of the parameters
   * @return                    the return value of the method, if any (in that case null)
   */
  protected Object invokeMethod(Object o, String name, Class[] paramClasses, Object[] paramValues) {
    Method      m;
    Object      result;

    result = null;

    try {
      m      = o.getClass().getMethod(name, paramClasses);
      result = m.invoke(o, paramValues);
    }
    catch (Exception e) {
      e.printStackTrace();
      result = null;
    }

    return result;
  }

  /**
   * transfers the local variables into a svm_parameter object
   *
   * @return the configured svm_parameter object
   */
  protected Object getParameters() {
    Object      result;
    int         i;

    try {
      Class solverTypeEnumClass = Class.forName(CLASS_SOLVERTYPE);
      Object[] enumValues = solverTypeEnumClass.getEnumConstants();
      Object solverType = enumValues[m_SVMType];

      Class[] constructorClasses = new Class[] { solverTypeEnumClass, double.class, double.class };
      Constructor parameterConstructor = Class.forName(CLASS_PARAMETER).getConstructor(constructorClasses);

      result = parameterConstructor.newInstance(solverType, Double.valueOf(m_Cost),
          Double.valueOf(m_eps));

      if (m_Weight.length > 0) {
        invokeMethod(result, "setWeights", new Class[] { double[].class, int[].class },
            new Object[] { m_Weight, m_WeightLabel });
      }
    }
    catch (Exception e) {
      e.printStackTrace();
      result = null;
    }

    return result;
  }

  /**
   * returns the svm_problem
   *
   * @param vx the x values
   * @param vy the y values
   * @param max_index
   * @return the Problem object
   */
  protected Object getProblem(List<Object> vx, List<Integer> vy, int max_index) {
    Object      result;

    try {
      result = Class.forName(CLASS_PROBLEM).newInstance();

      setField(result, "l", Integer.valueOf(vy.size()));
      setField(result, "n", Integer.valueOf(max_index));
      setField(result, "bias", getBias());

      newArray(result, "x", Class.forName(CLASS_FEATURENODE), new int[]{vy.size(), 0});
      for (int i = 0; i < vy.size(); i++)
        setField(result, "x", i, vx.get(i));

      newArray(result, "y", Integer.TYPE, vy.size());
      for (int i = 0; i < vy.size(); i++)
        setField(result, "y", i, vy.get(i));
    }
    catch (Exception e) {
      e.printStackTrace();
      result = null;
    }

    return result;
  }

  /**
   * returns an instance into a sparse liblinear array
   *
   * @param instance	the instance to work on
   * @return		the liblinear array
   * @throws Exception	if setup of array fails
   */
  protected Object instanceToArray(Instance instance) throws Exception {
    int     index;
    int     count;
    int     i;
    Object  result;

    // determine number of non-zero attributes
    count = 0;

    for (i = 0; i < instance.numValues(); i++) {
      if (instance.index(i) == instance.classIndex())
        continue;
      if (instance.valueSparse(i) != 0)
        count++;
    }

    if (m_Bias >= 0) {
      count++;
    }

    Class[] intDouble = new Class[] { int.class, double.class };
    Constructor nodeConstructor = Class.forName(CLASS_FEATURENODE).getConstructor(intDouble);

    // fill array
    result = Array.newInstance(Class.forName(CLASS_FEATURENODE), count);
    index  = 0;
    for (i = 0; i < instance.numValues(); i++) {

      int idx = instance.index(i);
      double val = instance.valueSparse(i);

      if (idx == instance.classIndex())
        continue;
      if (val == 0)
        continue;

      Object node = nodeConstructor.newInstance(Integer.valueOf(idx+1), Double.valueOf(val));
      Array.set(result, index, node);
      index++;
    }

    // add bias term
    if (m_Bias >= 0) {
      Integer idx = Integer.valueOf(instance.numAttributes()+1);
      Double value = Double.valueOf(m_Bias);
      Object node = nodeConstructor.newInstance(idx, value);
      Array.set(result, index, node);
    }

    return result;
  }
  /**
   * Computes the distribution for a given instance.
   *
   * @param instance 		the instance for which distribution is computed
   * @return 			the distribution
   * @throws Exception 		if the distribution can't be computed successfully
   */
  public double[] distributionForInstance (Instance instance) throws Exception {

    if (!getDoNotReplaceMissingValues()) {
      m_ReplaceMissingValues.input(instance);
      m_ReplaceMissingValues.batchFinished();
      instance = m_ReplaceMissingValues.output();
    }

    if (getConvertNominalToBinary() 
        && m_NominalToBinary != null) {
      m_NominalToBinary.input(instance);
      m_NominalToBinary.batchFinished();
      instance = m_NominalToBinary.output();
    }

    if (m_Filter != null) {
      m_Filter.input(instance);
      m_Filter.batchFinished();
      instance = m_Filter.output();
    }

    Object x = instanceToArray(instance);
    double v;
    double[] result = new double[instance.numClasses()];
    if (m_ProbabilityEstimates) {
      if (m_SVMType != SVMTYPE_L2_LR) {
        throw new WekaException("probability estimation is currently only " +
            "supported for L2-regularized logistic regression");
      }

      int[] labels = (int[])invokeMethod(m_Model, "getLabels", null, null);
      double[] prob_estimates = new double[instance.numClasses()];

      v = ((Integer) invokeMethod(
            Class.forName(CLASS_LINEAR).newInstance(),
            "predictProbability",
            new Class[]{
              Class.forName(CLASS_MODEL),
        Array.newInstance(Class.forName(CLASS_FEATURENODE), Array.getLength(x)).getClass(),
        Array.newInstance(Double.TYPE, prob_estimates.length).getClass()},
        new Object[]{ m_Model, x, prob_estimates})).doubleValue();

      // Return order of probabilities to canonical weka attribute order
      for (int k = 0; k < prob_estimates.length; k++) {
        result[labels[k]] = prob_estimates[k];
      }
    }
    else {
      v = ((Integer) invokeMethod(
            Class.forName(CLASS_LINEAR).newInstance(),
            "predict",
            new Class[]{
              Class.forName(CLASS_MODEL),
        Array.newInstance(Class.forName(CLASS_FEATURENODE), Array.getLength(x)).getClass()},
        new Object[]{
          m_Model,
        x})).doubleValue();

      assert (instance.classAttribute().isNominal());
      result[(int) v] = 1;
    }

    return result;
  }

  /**
   * Returns default capabilities of the classifier.
   *
   * @return      the capabilities of this classifier
   */
  public Capabilities getCapabilities() {
    Capabilities result = super.getCapabilities();
    result.disableAll();

    // attributes
    result.enable(Capability.NOMINAL_ATTRIBUTES);
    result.enable(Capability.NUMERIC_ATTRIBUTES);
    result.enable(Capability.DATE_ATTRIBUTES);
//    result.enable(Capability.MISSING_VALUES);

    // class
    result.enable(Capability.NOMINAL_CLASS);
    result.enable(Capability.MISSING_CLASS_VALUES);
    return result;
  }

  /**
   * builds the classifier
   *
   * @param insts       the training instances
   * @throws Exception  if liblinear classes not in classpath or liblinear
   *                    encountered a problem
   */
  public void buildClassifier(Instances insts) throws Exception {
    m_NominalToBinary = null;
    m_Filter = null;
    
    if (!isPresent())
      throw new Exception("liblinear classes not in CLASSPATH!");

    // remove instances with missing class
    insts = new Instances(insts);
    insts.deleteWithMissingClass();
    
    if (!getDoNotReplaceMissingValues()) {
      m_ReplaceMissingValues = new ReplaceMissingValues();
      m_ReplaceMissingValues.setInputFormat(insts);
      insts = Filter.useFilter(insts, m_ReplaceMissingValues);
    }
    
    // can classifier handle the data?
    // we check this here so that if the user turns off
    // replace missing values filtering, it will fail
    // if the data actually does have missing values
    getCapabilities().testWithFail(insts);

    if (getConvertNominalToBinary()) {
      insts = nominalToBinary(insts);
    }

    if (getNormalize()) {
      m_Filter = new Normalize();
      m_Filter.setInputFormat(insts);
      insts = Filter.useFilter(insts, m_Filter);
    }

    List<Integer> vy = new ArrayList<Integer>(insts.numInstances());
    List<Object> vx = new ArrayList<Object>(insts.numInstances());
    int max_index = 0;

    for (int d = 0; d < insts.numInstances(); d++) {
      Instance inst = insts.instance(d);
      Object x = instanceToArray(inst);
      int m = Array.getLength(x);
      if (m > 0)
        max_index = Math.max(max_index, ((Integer) getField(Array.get(x, m - 1), "index")).intValue());
      vx.add(x);
      double classValue = inst.classValue();
      int classValueInt = (int)classValue;
      if (classValueInt != classValue) throw new RuntimeException("unsupported class value: " + classValue);
      vy.add(Integer.valueOf(classValueInt));
    }

    if (!m_Debug) {
      invokeMethod(
          Class.forName(CLASS_LINEAR).newInstance(),
          "disableDebugOutput", null, null);
    } else {
      invokeMethod(
          Class.forName(CLASS_LINEAR).newInstance(),
          "enableDebugOutput", null, null);
    }

    // reset the PRNG for regression-stable results
    invokeMethod(
        Class.forName(CLASS_LINEAR).newInstance(),
        "resetRandom", null, null);

    // train model
    m_Model = invokeMethod(
        Class.forName(CLASS_LINEAR).newInstance(),
        "train",
        new Class[]{
          Class.forName(CLASS_PROBLEM),
            Class.forName(CLASS_PARAMETER)},
            new Object[]{
              getProblem(vx, vy, max_index),
            getParameters()});
  }

  /**
   * turns on nominal to binary filtering
   * if there are not only numeric attributes
   */
  private Instances nominalToBinary( Instances insts ) throws Exception {
    boolean onlyNumeric = true;
    for (int i = 0; i < insts.numAttributes(); i++) {
      if (i != insts.classIndex()) {
        if (!insts.attribute(i).isNumeric()) {
          onlyNumeric = false;
          break;
        }
      }
    }

    if (!onlyNumeric) {
      m_NominalToBinary = new NominalToBinary();
      m_NominalToBinary.setInputFormat(insts);
      insts = Filter.useFilter(insts, m_NominalToBinary);
    }
    return insts;
  }

  /**
   * returns a string representation
   *
   * @return            a string representation
   */
  public String toString() {
    return "LibLINEAR wrapper";
  }

  /**
   * Returns the revision string.
   *
   * @return		the revision
   */
  public String getRevision() {
    return RevisionUtils.extract("$Revision: 5917 $");
  }

  /**
   * Main method for testing this class.
   *
   * @param args the options
   */
  public static void main(String[] args) {
    runClassifier(new LibLINEAR(), args);
  }
}

