/*
 *   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 3 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, see <http://www.gnu.org/licenses/>.
 */

/*
 *    InputMappedClassifier.java
 *    Copyright (C) 2010-2012 University of Waikato, Hamilton, New Zealand
 *
 */

package weka.classifiers.misc;

import java.io.Serializable;
import java.util.Enumeration;
import java.util.Vector;

import weka.classifiers.Classifier;
import weka.classifiers.SingleClassifierEnhancer;
import weka.core.AdditionalMeasureProducer;
import weka.core.Attribute;
import weka.core.Capabilities;
import weka.core.Capabilities.Capability;
import weka.core.DenseInstance;
import weka.core.Drawable;
import weka.core.Environment;
import weka.core.EnvironmentHandler;
import weka.core.Instance;
import weka.core.Instances;
import weka.core.Option;
import weka.core.OptionHandler;
import weka.core.RevisionUtils;
import weka.core.SerializationHelper;
import weka.core.Utils;
import weka.core.WeightedInstancesHandler;

/**
 <!-- globalinfo-start -->
 * Wrapper classifier that addresses incompatible training and test data by building a mapping between the training data that a classifier has been built with and the incoming test instances' structure. Model attributes that are not found in the incoming instances receive missing values, so do incoming nominal attribute values that the classifier has not seen before. A new classifier can be trained or an existing one loaded from a file.
 * <p/>
 <!-- globalinfo-end -->
 * 
 <!-- options-start -->
 * Valid options are: <p/>
 * 
 * <pre> -I
 *  Ignore case when matching attribute names and nominal values.</pre>
 * 
 * <pre> -M
 *  Suppress the output of the mapping report.</pre>
 * 
 * <pre> -trim
 *  Trim white space from either end of names before matching.</pre>
 * 
 * <pre> -L &lt;path to model to load&gt;
 *  Path to a model to load. If set, this model
 *  will be used for prediction and any base classifier
 *  specification will be ignored. Environment variables
 *  may be used in the path (e.g. ${HOME}/myModel.model)</pre>
 * 
 * <pre> -D
 *  If set, classifier is run in debug mode and
 *  may output additional info to the console</pre>
 * 
 * <pre> -W
 *  Full name of base classifier.
 *  (default: weka.classifiers.rules.ZeroR)</pre>
 * 
 * <pre> 
 * Options specific to classifier weka.classifiers.rules.ZeroR:
 * </pre>
 * 
 * <pre> -D
 *  If set, classifier is run in debug mode and
 *  may output additional info to the console</pre>
 * 
 <!-- options-end -->
 * 
 * @author Mark Hall (mhall{[at]}pentaho{[dot]}com)
 * @version $Revision: 8034 $
 *
 */
public class InputMappedClassifier extends SingleClassifierEnhancer 
  implements Serializable, OptionHandler, Drawable, WeightedInstancesHandler,
  AdditionalMeasureProducer, EnvironmentHandler {
  
  /** For serialization */
  private static final long serialVersionUID = 4901630631723287761L;

  /** The path to the serialized model to use (if any) */
  protected String m_modelPath = "";
   
  /** The header of the last known set of incoming test instances */
  protected transient Instances m_inputHeader;
  
  /** The instances structure used to train the classifier with */
  protected Instances m_modelHeader;
  
  /** Handle any environment variables used in the model path */
  protected transient Environment m_env;
  
  /** Map from model attributes to incoming attributes */
  protected transient int[] m_attributeMap;
  
  protected transient int[] m_attributeStatus;
  
  /** For each model attribute, map from incoming nominal values to model
   * nominal values */
  protected transient int[][] m_nominalValueMap;
  
  /** Trim white space from both ends of attribute names and nominal values? */
  protected boolean m_trim = true;
  
  /** Ignore case when matching attribute names and nominal values? */
  protected boolean m_ignoreCase = true;
  
  /** Dont output mapping report if set to true */
  protected boolean m_suppressMappingReport = false;
  
  /** 
   * If true, then a call to buildClassifier() will not overwrite
   * any test structure that has been recorded with the current training
   * structure. This is useful for getting a correct mapping report
   * output in toString() after buildClassifier has been called and
   * before any test instance has been seen. Test structure and mapping
   * will get reset if a test instance is received whose structure does
   * not match the recorded test structure.
   */
  protected boolean m_initialTestStructureKnown = false;
  
  /** Holds values for instances constructed for prediction */
  protected double[] m_vals;

  /**
   * Returns a string describing this classifier
   * 
   * @return a description of the classifier suitable for
   * displaying in the explorer/experimenter gui
   */
  public String globalInfo() {
    return "Wrapper classifier that addresses incompatible training and test " +
    		"data by building a mapping between the training data that " +
    		"a classifier has been built with and the incoming test instances' " +
    		"structure. Model attributes that are not found in the incoming " +
    		"instances receive missing values, so do incoming nominal attribute " +
    		"values that the classifier has not seen before. A new classifier " +
    		"can be trained or an existing one loaded from a file.";
  }
  
  /**
   * Set the environment variables to use
   * 
   * @param env the environment variables to use
   */
  public void setEnvironment(Environment env) {
    m_env = env;
  }
  
  /**
   * Returns the tip text for this property
   * 
   * @return tip text for this property suitable for
   * displaying in the explorer/experimenter gui
   */
  public String ignoreCaseForNamesTipText() {
    return "Ignore case when matching attribute names and nomina values.";
  }
  
  /**
   * Set whether to ignore case when matching attribute names and
   * nominal values.
   * 
   * @param ignore true if case is to be ignored
   */
  public void setIgnoreCaseForNames(boolean ignore) {
    m_ignoreCase = ignore;
  }
  
  /**
   * Get whether to ignore case when matching attribute names
   * and nominal values.
   * 
   * @return true if case is to be ignored.
   */
  public boolean getIgnoreCaseForNames() {
    return m_ignoreCase;
  }
  
  /**
   * Returns the tip text for this property
   * 
   * @return tip text for this property suitable for
   * displaying in the explorer/experimenter gui
   */
  public String trimTipText() {
    return "Trim white space from each end of attribute names and " +
    		"nominal values before matching.";
  }
  
  /**
   * Set whether to trim white space from each end of names
   * before matching.
   * 
   * @param trim true to trim white space.
   */
  public void setTrim(boolean trim) {
    m_trim = trim;
  }
  
  /**
   * Get whether to trim white space from each end of names
   * before matching.
   * 
   * @return true if white space is to be trimmed.
   */
  public boolean getTrim() {
    return m_trim;
  }
  
  /**
   * Returns the tip text for this property
   * 
   * @return tip text for this property suitable for
   * displaying in the explorer/experimenter gui
   */
  public String suppressMappingReportTipText() {
    return "Don't output a report of model-to-input mappings.";
  }
  
  /**
   * Set whether to suppress output the report of model to input mappings.
   * 
   * @param suppress true to suppress this output.
   */
  public void setSuppressMappingReport(boolean suppress) {
    m_suppressMappingReport = suppress;
  }
  
  /**
   * Get whether to suppress output the report of model to input mappings.
   * 
   * @return true if this output is to be suppressed.
   */
  public boolean getSuppressMappingReport() {
    return m_suppressMappingReport;
  }
  
  /**
   * Returns the tip text for this property
   * 
   * @return tip text for this property suitable for
   * displaying in the explorer/experimenter gui
   */
  public String modelPathTipText() {
    return "Set the path from which to load a model. " +
    		"Loading occurs when the first test instance " +
    		"is received. Environment variables can be used in the " +
    		"supplied path.";
  }
  
  /**
   * Set the path from which to load a model. Loading occurs when the
   * first test instance is received or getModelHeader() is called
   * programatically. Environment variables can be used in the
   * supplied path - e.g. ${HOME}/myModel.model.
   * 
   * @param modelPath the path to the model to load.
   * @throws Exception if a problem occurs during loading.
   */
  public void setModelPath(String modelPath) throws Exception {
    if (m_env == null) {
      m_env = Environment.getSystemWide();
    }
    
    m_modelPath = modelPath;
    
    //loadModel(modelPath);
  }
  
  /**
   * Get the path used for loading a model.
   * 
   * @return the path used for loading a model.
   */
  public String getModelPath() {
    return m_modelPath;
  }
  
  /**
   * Returns default capabilities of the classifier.
   *
   * @return      the capabilities of this classifier
   */
  public Capabilities getCapabilities() {
    Capabilities result = super.getCapabilities();
    
    result.disable(Capability.RELATIONAL_ATTRIBUTES);
    
    return result;
  }
  
  /**
   * Returns an enumeration describing the available options.
   * 
   <!-- options-start -->
   * Valid options are: <p/>
   * 
   * <pre> -I
   *  Ignore case when matching attribute names and nominal values.</pre>
   * 
   * <pre> -M
   *  Suppress the output of the mapping report.</pre>
   * 
   * <pre> -trim
   *  Trim white space from either end of names before matching.</pre>
   * 
   * <pre> -L &lt;path to model to load&gt;
   *  Path to a model to load. If set, this model
   *  will be used for prediction and any base classifier
   *  specification will be ignored. Environment variables
   *  may be used in the path (e.g. ${HOME}/myModel.model)</pre>
   * 
   * <pre> -D
   *  If set, classifier is run in debug mode and
   *  may output additional info to the console</pre>
   * 
   * <pre> -W
   *  Full name of base classifier.
   *  (default: weka.classifiers.rules.ZeroR)</pre>
   * 
   * <pre> 
   * Options specific to classifier weka.classifiers.rules.ZeroR:
   * </pre>
   * 
   * <pre> -D
   *  If set, classifier is run in debug mode and
   *  may output additional info to the console</pre>
   * 
   <!-- options-end -->
   *
   * @return an enumeration of all the available options.
   */
  public Enumeration<Option> listOptions() {
    Vector<Option> newVector = new Vector(4);
    
    newVector.addElement(new Option("\tIgnore case when matching attribute " +
    		"names and nominal values.", "I", 0, "-I"));
    newVector.addElement(new Option("\tSuppress the output of the mapping report.",
        "M", 0, "-M"));
    newVector.addElement(new Option("\tTrim white space from either end of names " +
    		"before matching.", "trim", 0, "-trim"));
    newVector.addElement(new Option("\tPath to a model to load. If set, this model" +
    		"\n\twill be used for prediction and any base classifier" +
    		"\n\tspecification will be ignored. Environment variables" +
    		"\n\tmay be used in the path (e.g. ${HOME}/myModel.model)",
    		"L", 1, "-L <path to model to load>"));
    
    Enumeration<Option> enu = super.listOptions();
    while (enu.hasMoreElements()) {
      newVector.addElement(enu.nextElement());
    }
    
    return newVector.elements();
  }
  
  /**
   * Parses a given list of options. <p/>
   *
   <!-- options-start -->
   * Valid options are: <p/>
   * 
   * <pre> -I
   *  Ignore case when matching attribute names and nominal values.</pre>
   * 
   * <pre> -M
   *  Suppress the output of the mapping report.</pre>
   * 
   * <pre> -trim
   *  Trim white space from either end of names before matching.</pre>
   * 
   * <pre> -L &lt;path to model to load&gt;
   *  Path to a model to load. If set, this model
   *  will be used for prediction and any base classifier
   *  specification will be ignored. Environment variables
   *  may be used in the path (e.g. ${HOME}/myModel.model)</pre>
   * 
   * <pre> -D
   *  If set, classifier is run in debug mode and
   *  may output additional info to the console</pre>
   * 
   * <pre> -W
   *  Full name of base classifier.
   *  (default: weka.classifiers.rules.ZeroR)</pre>
   * 
   * <pre> 
   * Options specific to classifier weka.classifiers.rules.ZeroR:
   * </pre>
   * 
   * <pre> -D
   *  If set, classifier is run in debug mode and
   *  may output additional info to the console</pre>
   * 
   <!-- options-end -->
   *
   * Options after -- are passed to the designated classifier.<p>
   *
   * @param options the list of options as an array of strings
   * @throws Exception if an option is not supported
   */
  public void setOptions(String[] options) throws Exception {
    setIgnoreCaseForNames(Utils.getFlag('I', options));
    setSuppressMappingReport(Utils.getFlag('M', options));
    setTrim(Utils.getFlag("trim", options));
    
    String modelPath = Utils.getOption('L', options);
    if (modelPath.length() > 0) {
      setModelPath(modelPath);
    }
    
    super.setOptions(options);
  }
  
  /**
   * Gets the current settings of the Classifier.
   *
   * @return an array of strings suitable for passing to setOptions
   */
  public String[] getOptions() {
    String[] superOptions = super.getOptions();
    String[] options = new String[superOptions.length + 5];
    
    int current = 0;
    if (getIgnoreCaseForNames()) {
      options[current++] = "-I";
    }
    if (getSuppressMappingReport()) {
      options[current++] = "-M";
    }
    if (getTrim()) {
      options[current++] = "-trim";
    }
    
    if (getModelPath() != null && getModelPath().length() > 0) {
      options[current++] = "-L";
      options[current++] = getModelPath();
    }

    System.arraycopy(superOptions, 0, options, current, 
        superOptions.length);

    current += superOptions.length;
    while (current < options.length) {
      options[current++] = "";
    }
    return options;
  }  
  
  /**
   * Set the test structure (if known in advance) that we are likely
   * to see. If set, then a call to buildClassifier() will not overwrite
   * any test structure that has been recorded with the current training
   * structure. This is useful for getting a correct mapping report
   * output in toString() after buildClassifier has been called and
   * before any test instance has been seen. Test structure and mapping
   * will get reset if a test instance is received whose structure does
   * not match the recorded test structure.
   * 
   * @param testStructure the structure of the test instances that
   * we are likely to see (if known in advance)
   */
  public void setTestStructure(Instances testStructure) {
    m_inputHeader = testStructure;
    m_initialTestStructureKnown = true;
  }
  
  /**
   * Set the structure of the data used to create the model. This method
   * is useful for clients who have an existing in-memory model that they'd
   * like to wrap in the InputMappedClassifier
   * 
   * @param modelHeader the structure of the data used to build the wrapped
   * model
   */
  public void setModelHeader(Instances modelHeader) {
    m_modelHeader = modelHeader;
  }
  
  private void loadModel(String modelPath) throws Exception {
    if (modelPath != null && modelPath.length() > 0) {
      try {
        if (m_env == null) {
          m_env = Environment.getSystemWide();
        }
        
        modelPath = m_env.substitute(modelPath);
      } catch (Exception ex) {
        // ignore any problems
      }
      
      try {
        Object[] modelAndHeader = SerializationHelper.readAll(modelPath);
        
        if (modelAndHeader.length != 2) {
          throw new Exception("[InputMappedClassifier] serialized model file " +
                          "does not seem to contain both a model and " +
                          "the instances header used in training it!");
        } else {
          setClassifier((Classifier)modelAndHeader[0]);
          m_modelHeader = (Instances)modelAndHeader[1];
        }
      } catch (Exception ex) {
        ex.printStackTrace();
      }
    }
  }
  
  /**
   * Build the classifier
   *
   * @param data the training data to be used for generating the
   * bagged classifier.
   * @throws Exception if the classifier could not be built successfully
   */
  public void buildClassifier(Instances data) throws Exception {
    if (!m_initialTestStructureKnown) {
      m_inputHeader = new Instances(data, 0);
    }
    
    m_attributeMap = null;
    
    if (m_modelPath != null && m_modelPath.length() > 0) {
      return; // Don't build a classifier if a path has been specified
    }
    
    // can classifier handle the data?
    getCapabilities().testWithFail(data);
    
    m_Classifier.buildClassifier(data);
    //m_loadedClassifier = m_Classifier;
    m_modelHeader = new Instances(data, 0);
  }
  
  private boolean stringMatch(String one, String two) {
    if (m_trim) {
      one = one.trim(); two = two.trim();
    }
    
    if (m_ignoreCase) {
      return one.equalsIgnoreCase(two);
    } else {
      return one.equals(two);
    }
  }
    
  /**
   * Helper method to pad/truncate strings
   *
   * @param s String to modify
   * @param pad character to pad with
   * @param len length of final string
   * @return final String
   */
  private String getFixedLengthString(String s, char pad, int len) {

    String padded = null;
    if (len <= 0) {
      return s;
    }
    // truncate?
    if (s.length() >= len) {
      return s.substring(0, len);
    } else {
      char [] buf = new char[len - s.length()];
      for (int j = 0; j < len - s.length(); j++) {
        buf[j] = pad;
      }
      padded = s + new String(buf);
    }

    return padded;
  }
   
  private StringBuffer createMappingReport() {
    StringBuffer result = new StringBuffer();
    result.append("Attribute mappings:\n\n");
    
    int maxLength = 0;
    for (int i = 0; i < m_modelHeader.numAttributes(); i++) {
      if (m_modelHeader.attribute(i).name().length() > maxLength) {
        maxLength = m_modelHeader.attribute(i).name().length();        
      }
    }
    maxLength += 12;
    
    int minLength = 16;
    String headerS = "Model attributes";
    String sep = "----------------";

    if (maxLength < minLength) {
      maxLength = minLength;
    }
    
    headerS = getFixedLengthString(headerS, ' ', maxLength);
    sep = getFixedLengthString(sep, '-', maxLength);
    sep += "\t    ----------------\n";
    headerS += "\t    Incoming attributes\n";
    result.append(headerS);
    result.append(sep);
    
    for (int i = 0; i < m_modelHeader.numAttributes(); i++) {
      Attribute temp = m_modelHeader.attribute(i);
      String attName = "("
        + ((temp.isNumeric())
           ? "numeric)"
           : "nominal)") 
        + " " + temp.name();
      attName = getFixedLengthString(attName, ' ', maxLength);
      attName +=  "\t--> ";
      result.append(attName);
      String inAttNum = "";
      if (m_attributeStatus[i] == NO_MATCH) {
        inAttNum += "- ";
        result.append(inAttNum + "missing (no match)\n");
      } else if (m_attributeStatus[i] == TYPE_MISMATCH) {       
        inAttNum += (m_attributeMap[i] + 1) + " ";
        result.append(inAttNum + "missing (type mis-match)\n");
      } else {
        Attribute inAtt = m_inputHeader.attribute(m_attributeMap[i]);
        String inName = "" + (m_attributeMap[i] + 1) + " (" +
        ((inAtt.isNumeric())
            ? "numeric)"
            : "nominal)")
            + " " + inAtt.name();
        result.append(inName + "\n");
      }
    }
    
    return result;
  }
 
  protected static final int NO_MATCH = -1;
  protected static final int TYPE_MISMATCH = -2;
  protected static final int OK = -3;
  
  private boolean regenerateMapping() throws Exception {
    loadModel(m_modelPath); // load a model (if specified)
    
    if (m_modelHeader == null) {
      return false;
    }
    
    m_attributeMap = new int[m_modelHeader.numAttributes()];
    m_attributeStatus = new int[m_modelHeader.numAttributes()];
    m_nominalValueMap = new int[m_modelHeader.numAttributes()][];
    
    for (int i = 0; i < m_modelHeader.numAttributes(); i++) {
      String modelAttName = m_modelHeader.attribute(i).name();
      m_attributeStatus[i] = NO_MATCH;
      
      for (int j = 0; j < m_inputHeader.numAttributes(); j++) {
        String incomingAttName = m_inputHeader.attribute(j).name();
        if (stringMatch(modelAttName, incomingAttName)) {
          m_attributeMap[i] = j;
          m_attributeStatus[i] = OK;
          
          Attribute modelAtt = m_modelHeader.attribute(i);
          Attribute incomingAtt = m_inputHeader.attribute(j);
          
          // check types
          if (modelAtt.type() != incomingAtt.type()) {
            m_attributeStatus[i] = TYPE_MISMATCH;
            break;
          }          
          
          // now check nominal values (number, names...)
          if (modelAtt.numValues() != incomingAtt.numValues()) {
            System.out.println("[InputMappedClassifier] Warning: incoming nominal " +
            		"attribute " + incomingAttName + " does not have the same " +
            				"number of values as model attribute "
            		+ modelAttName);
            
          }
          
          if (modelAtt.isNominal() && incomingAtt.isNominal()) {
            int[] valuesMap = new int[incomingAtt.numValues()];
            for (int k = 0; k < incomingAtt.numValues(); k++) {
              String incomingNomValue = incomingAtt.value(k);
              int indexInModel = modelAtt.indexOfValue(incomingNomValue);
              if (indexInModel < 0) {
                valuesMap[k] = NO_MATCH;
              } else {
                valuesMap[k] = indexInModel;
              }
            }
            m_nominalValueMap[i] = valuesMap;
          }
        }
      }
    }

    
    return true;
  }
  
  /**
   * Return the instance structure that the encapsulated model was built with.
   * If the classifier will be built from scratch by InputMappedClassifier then
   * this method just returns the default structure that is passed in as argument.
   * 
   * @param defaultH the default instances structure
   * @return the instances structure used to create the encapsulated model
   * @throws Exception if a problem occurs
   */
  public Instances getModelHeader(Instances defaultH) throws Exception {
    loadModel(m_modelPath);
    
    // If the model header is null, then we must be going to build from
    // scratch in buildClassifier. Therefore, just return the supplied default,
    // since this has to match what we will build with
    Instances toReturn = (m_modelHeader == null) ? defaultH : m_modelHeader;
    
    return new Instances(toReturn, 0);
  }
  
  // get the mapped class index (i.e. the index in the incoming data of
  // the attribute that the model uses as the class
  public int getMappedClassIndex() throws Exception {
    if (m_modelHeader == null) {
      throw new Exception("[InputMappedClassifier] No model available!");
    }
    
    if (m_attributeMap[m_modelHeader.classIndex()] == NO_MATCH) {
      return -1;
    }
    
    return m_attributeMap[m_modelHeader.classIndex()];
  }
  
  public Instance constructMappedInstance(Instance incoming) throws Exception {
    
    boolean regenerateMapping = false;
    
    if (m_inputHeader == null) {
      m_inputHeader = incoming.dataset();
      regenerateMapping = true;
      m_initialTestStructureKnown = false;
    } else if (!m_inputHeader.equalHeaders(incoming.dataset())) {
      /*System.out.println("[InputMappedClassifier] incoming data does not match " +
                "last known input format - regenerating mapping...");
      System.out.println("Incoming\n" + new Instances(incoming.dataset(), 0));
      System.out.println("Stored input header\n" + new Instances(m_inputHeader, 0));
      System.out.println("Model header\n" + new Instances(m_modelHeader, 0)); */
      m_inputHeader = incoming.dataset();
      
      regenerateMapping = true;
      m_initialTestStructureKnown = false;
    } else if (m_attributeMap == null) {
      regenerateMapping = true;
      m_initialTestStructureKnown = false;
    }
    
    if (regenerateMapping) {
      regenerateMapping();
      m_vals = null;
      
      if (!m_suppressMappingReport) {
        StringBuffer result = createMappingReport();
        System.out.println(result.toString());
      }
    }    
    
    m_vals = new double[m_modelHeader.numAttributes()];
    
    for (int i = 0; i < m_modelHeader.numAttributes(); i++) {
      if (m_attributeStatus[i] == OK) {
        Attribute modelAtt = m_modelHeader.attribute(i);
        Attribute incomingAtt = m_inputHeader.attribute(m_attributeMap[i]);
        
        if (Utils.isMissingValue(incoming.value(m_attributeMap[i]))) {
          m_vals[i] = Utils.missingValue();
          continue;
        }
        
        if (modelAtt.isNumeric()) {
          m_vals[i] = incoming.value(m_attributeMap[i]);
        } else if (modelAtt.isNominal()) {
          int mapVal = m_nominalValueMap[i][(int)incoming.value(m_attributeMap[i])];
          
          if (mapVal == NO_MATCH) {
            m_vals[i] = Utils.missingValue();
          } else {
            m_vals[i] = mapVal;
          }
        }
      } else {
        m_vals[i] = Utils.missingValue();
      }
    }
    
    Instance newInst = new DenseInstance(incoming.weight(), m_vals);
    newInst.setDataset(m_modelHeader);

    return newInst;
  }
  
  public double classifyInstance(Instance inst) throws Exception {
    Instance converted = constructMappedInstance(inst);
    return m_Classifier.classifyInstance(converted);
  }
  
  public double[] distributionForInstance(Instance inst) throws Exception {            
    
    Instance converted = constructMappedInstance(inst);
    return m_Classifier.distributionForInstance(converted);
  }
  
  public String toString() {
    StringBuffer buff = new StringBuffer();
    
    buff.append("InputMappedClassifier:\n\n");
    
    try {
      loadModel(m_modelPath);
    } catch (Exception ex) {
      return "[InputMappedClassifier] Problem loading model.";
    }
    
    if (m_modelPath != null && m_modelPath.length() > 0) {
      buff.append("Model sourced from: " + m_modelPath + "\n\n");
    }
    
    /*if (m_loadedClassifier != null) {
      buff.append(m_loadedClassifier);
    } else { */
      buff.append(m_Classifier);
    //}
      
      
      if (!m_suppressMappingReport && m_inputHeader != null) {
        try {
          regenerateMapping();
        } catch (Exception ex) {
          ex.printStackTrace();
          return "[InputMappedClassifier] Problem loading model.";
        }
        if (m_attributeMap != null) {
          buff.append("\n" + createMappingReport().toString());
        }
      }
    
    return buff.toString();
  }
  
  /**
   *  Returns the type of graph this classifier
   *  represents.
   *  
   *  @return the type of graph
   */   
  public int graphType() {
    
    if (m_Classifier instanceof Drawable)
      return ((Drawable)m_Classifier).graphType();
    else 
      return Drawable.NOT_DRAWABLE;
  }
  
  /**
   * Returns an enumeration of the additional measure names
   * @return an enumeration of the measure names
   */
  public Enumeration enumerateMeasures() {
    Vector newVector = new Vector();
    
    if (m_Classifier instanceof AdditionalMeasureProducer) {
      Enumeration en = ((AdditionalMeasureProducer)m_Classifier).
        enumerateMeasures();
      while (en.hasMoreElements()) {
        String mname = (String)en.nextElement();
        newVector.addElement(mname);
      }
    }
    return newVector.elements();
  }
  
  /**
   * Returns the value of the named measure
   * @param additionalMeasureName the name of the measure to query for its value
   * @return the value of the named measure
   * @throws IllegalArgumentException if the named measure is not supported
   */
  public double getMeasure(String additionalMeasureName) {
     if (m_Classifier instanceof AdditionalMeasureProducer) {
      return ((AdditionalMeasureProducer)m_Classifier).
        getMeasure(additionalMeasureName);
    } else {
      throw new IllegalArgumentException(additionalMeasureName 
                          + " not supported (InputMappedClassifier)");
    }
  }
  
  /**
   * Returns graph describing the classifier (if possible).
   *
   * @return the graph of the classifier in dotty format
   * @throws Exception if the classifier cannot be graphed
   */
  public String graph() throws Exception {
    
    if (m_Classifier != null && 
        m_Classifier instanceof Drawable)
      return ((Drawable)m_Classifier).graph();
    else throw new Exception("Classifier: " + getClassifierSpec()
                             + " cannot be graphed");
  }
  
  /**
   * Returns the revision string.
   * 
   * @return            the revision
   */
  public String getRevision() {
    return RevisionUtils.extract("$Revision: 8034 $");
  }
  
  /**
   * Main method for testing this class.
   *
   * @param argv should contain the following arguments:
   * -t training file [-T test file] [-c class index]
   */
  public static void main(String [] argv) {
    runClassifier(new InputMappedClassifier(), argv);
  }

}

