/*
 *    OptionsHandler.java
 *    Copyright (C) 2012-2013 Aristotle University of Thessaloniki, Greece
 */
package dbn.data;

import dbn.classifier.neural.DeepBeliefNetwork;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import mulan.data.InvalidDataFormatException;
import mulan.data.MultiLabelInstances;
import weka.core.Utils;
import weka.filters.Filter;
import weka.filters.unsupervised.attribute.Normalize;

/**
 * This class handles the user's options of the main class. It is a control
 * class that takes the user's parameters and converts them to objects of their
 * appropriate class. Then the user can get all the parameters using the getters
 * without any additional process. There is a set of parameters that this handler
 * can convert and the options have specific codes.<br />
 * <br />
 * The options are:<br />
 * <code>-path "folder/path/"</code> the path where train, test and info files are 
 *      placed.<br />
 * <code>-train "train/ARFF/file.arff"</code> the train dataset in ARFF format. If 
 *      path option is set, user mustn't put the path again.<br />
 * <code>-test "test/ARFF/file.arff"</code> the test dataset in ARFF format. If path 
 *      option is set, user mustn't put the path again.<br />
 * <code>-load "DBN/file.dbn"</code> the path where the {@link DeepBeliefNetwork} has 
 *      been saved. The <code>path</code> option doesn't concern this option.<br />
 * <code>-info "XML/file.xml"</code> the info XML file. If path option is set, user mustn't 
 *      put the path again. User can use <code>targets</code> option instead.<br />
 * <code>-targets integer</code> the number of targets (labels). User can use
 *      <code>info</code> option instead.<br />
 * <code>-ge integer</code> the number of greedyEpochs that the classifier will repeat
 *      the greedy learning for every RBM.<br />
 * <code>-be integer</code> the number of greedyEpochs that the classifier
 *      will repeat the back-propagation training for every instance.<br />
 * <code>-m double</code> the momentum parameter. This parameter specifies the
 *      urge value.<br />
 * <code>-lr double</code> an annealing parameter that handles the step size.
 *      A big learning rate will occur a fast gradient descent and undesired 
 *      solutions.<br />
 * <code>-wcost double</code> the weight cost option focuses to the weights
 *      of the neural network.<br />
 * <code>-l [first layer's number of features] ... [last layer's number of features]</code>
 *      the number of units on each layer.<br />
 * <code>-threshold [double]</code> this option sets the value of the threshold.<br />
 * <code>-debug</code> if this option is set, the program prints the process of
 *      the training task.
 * 
 * @author Evripidis Gkanias
 */
public class OptionsHandler {
    /**
     * Maps every option of the user interface with a specific name.
     */
    private final HashMap<String,String> DECODER = new HashMap();
    
    /**
     * The path of the folder that contains the XML info file, the train and 
     * test datasets. <br />
     * It is initialized to the current folder.
     */
    private String folderPath = "";
    
    /**
     * The {@link DeepBeliefNetwork} that have been loaded. <br />
     * If the user does not load a {@link DeepBeliefNetwork}, it will be constructed
     * using the layers array.
     */
    private DeepBeliefNetwork nn = null;
    
    /**
     * the train dataset in ARFF format.
     */
    private String train = null;
    
    /**
     * the test dataset in ARFF format.
     */
    private String test = null;
    
    /**
     * the info XML file name.
     */
    private static String xmlInfoPath = null;
    
    /**
     * the number of targets (labels)
     */
    private static int targets = 0;
    
    /**
     * An {@link ArrayList<Integer>} that contains the number of units per layer.
     */
    private ArrayList<Integer> layers = new ArrayList();
    
    /**
     * The name of the option that is to be handled at the moment.
     */
    private String type = null;
    
    /** 
     * The number of greedy training epochs for every {@link RBM}. <br />
     * Default value is 100.
     */
    private int greedyEpochs = 100;
    
    /**
     * The number of back-propagation training epochs. <br />
     * Default value is 10.
     * */
    private int bpEpochs = 10;
    
    /**
     * The momentum value that specifies the training urge. <br />
     * Default value is 0.6.
     */
    private double momentum = .6;
    
    /**
     * The classifier's learning rate. <br />
     * Default value is 0.05.
     */
    private double learningRate = 0.05;
    
    /**
     * The regularization cost term for weights decay.
     * Must be greater than 0 and no more than 1.<br />
     * Default value is 0.00001.
     */
    private double weightCost = 0.00001;
    
    /**
     * The threshold that is used for the bipartition. <br />
     * Default value is -1.
     */
    private double threshold = -1.;
    
    /**
     * Shows if the classifier prints the process of the training or not. <br />
     * Default value is <code>false</code>
     */
    private boolean debug = false;
    
    /**
     * The constructor. Initializes the map to contain all the necessary options.
     */
    public OptionsHandler() {
        this.DECODER.put("train","training arff file");
        this.DECODER.put("test","testing arff file");
        this.DECODER.put("targets", "number of targets");
        this.DECODER.put("l","layers");
        this.DECODER.put("ge","greedy epochs");
        this.DECODER.put("be","bp epochs");
        this.DECODER.put("m","momentum");
        this.DECODER.put("lr","learning rate");
        this.DECODER.put("wcost","weight cost");
        this.DECODER.put("threshold","threshold");
        this.DECODER.put("debug","debug");
        this.DECODER.put("load","load dbn file");
    }
    
    /**
     * Sets all the fields values using the user's set options.<br />
     * Throws {@link IOException} when the input files are not set or invalid.<br />
     * Throws {@link Exception} for other reasons.
     * 
     * @param args the users options
     * @throws IOException
     * @throws Exception 
     */
    public void setArgs(String[] args) throws IOException, Exception {
        try {
            this.folderPath = Utils.getOption("path", args);
        } catch (Exception ex) {}
        try {
            if ((xmlInfoPath = Utils.getOption("info", args)).equals("")) {
                xmlInfoPath = null;
            } else {
                xmlInfoPath = folderPath + xmlInfoPath;
            }
        } catch (Exception ex) {}
        try {
            targets = Integer.parseInt(Utils.getOption("targets", args));
        } catch (Exception ex) {}
        try {
            if (Utils.getFlag("debug", args)) debug = true;
        } catch (Exception ex) {}
        
        String flag;
        for (String option : this.DECODER.keySet()) {
            try {
                if ((flag = Utils.getOption(option, args)).equals("")) continue;
            } catch (Exception ex) {
                continue;
            }
            
            switch (this.type = this.DECODER.get(option)) {
                case "layers":
                    this.layers = new ArrayList();
                    this.layers.add(Integer.parseInt(flag));
                    for (int i = 0; i < args.length-1; i++) {
                        if (args[i].equals("") && args[i+1].matches("[0-9]+")) {
                            this.layers.add(Integer.parseInt(args[i+1]));
                            args[i+1] = "";
                        }
                    }
                    break;
                default:
                    this.readArg(flag);
                    break;
            }
        }
        
    }
    
    /**
     * Gets an argument (user's option) and converts it to an appropriate form.<br />
     * Throws {@link InvalidDataFormatException} when cannot load train or test data.
     * 
     * @param arg the user;s option
     * @throws InvalidDataFormatException 
     */
    private void readArg(String arg) throws InvalidDataFormatException {
        switch (this.type) {
            case "training arff file":
                this.train = arg;
                this.type = "";
                break;
            case "testing arff file":
                this.test = arg;
                this.type = "";
                break;
            case "greedy epochs":
                this.greedyEpochs = Integer.parseInt(arg);
                break;
            case "bp epochs":
                this.bpEpochs = Integer.parseInt(arg);
                break;
            case "momentum":
                this.momentum = Double.parseDouble(arg);
                break;
            case "learning rate":
                this.learningRate = Double.parseDouble(arg);
                break;
            case "threshold":
                this.threshold = Double.parseDouble(arg);
                break;
            case "weight cost":
                this.weightCost = Double.parseDouble(arg);
                break;
            case "load dbn file":
                try {
                    this.nn = DBNFile.GetFromFile(arg);
                } catch (Exception ex) {
                    this.nn = null;
                }
                break;
        }
    }
    
    /**
     * Gets an ARFF path and returns the {@link MultiLabelInstances}
     * object for the specific file.<br />
     * Throws an {@link InvalidDataFormatException} when the file is not in the
     * appropriate format.
     * 
     * @param path the file's path
     * @return the dataset in a {@link MultiLabelInstances} form
     * @throws InvalidDataFormatException 
     */
    public static MultiLabelInstances GetData(String path) throws InvalidDataFormatException {
        MultiLabelInstances mli;
        try {
            if (xmlInfoPath != null) {
                mli = new MultiLabelInstances(path, xmlInfoPath);
            } else {
                mli = new MultiLabelInstances(path, targets);
            }
            
            Normalize norm = new Normalize();
            
            if (norm.setInputFormat(mli.getDataSet())) {
                mli = mli.reintegrateModifiedDataSet(Filter.useFilter(mli.getDataSet(), norm));
            }
        } catch (Exception ex) {
            throw new InvalidDataFormatException(ex.getMessage()+"\n"
                    + "Try -train \"your/path/here.arff\"");
        }
        
        return mli;
    }
    
    /**
     * Gets the training dataset in a {@link MultiLabelInstances} form.
     * 
     * @return the training set in a {@link MultiLabelInstances} form
     * @throws InvalidDataFormatException when the input file is not in the
     * appropriate format (ARFF)
     */
    public MultiLabelInstances getTrainSet() throws InvalidDataFormatException {
        return GetData(this.getTrainPath());
    }
    
    /**
     * Gets the testing dataset in a {@link MultiLabelInstances} form.
     * 
     * @return the testing dataset in a {@link MultiLabelInstances} form
     * @throws InvalidDataFormatException when the input file is not in the
     * appropriate format (ARFF)
     */
    public MultiLabelInstances getTestSet() throws InvalidDataFormatException {
        return GetData(this.getTestPath());
    }
    
    /**
     * Gets the training set's path.
     * 
     * @return the training set's path
     * @throws NullPointerException when the training set's file path has not set
     */
    public String getTrainPath() {
        if ((this.folderPath+this.train).equals(""))
            throw new NullPointerException("Training dataset path has not set!"
                    + " Try -train \"your/path/here.arff\"");
        
        return this.folderPath+this.train;
    }
    
    /**
     * Gets the testing set's path.
     * 
     * @return the testing set's path
     * @throws NullPointerException when the testing set's file path has not set
     */
    public String getTestPath() {
        if (this.test == null || this.test.equals(""))
            return this.getTrainPath();
        
        return this.folderPath+this.test;
    }
       
    /**
     * Gets an array with the number of units for each layer.
     * 
     * @return the array with the number of units for each layer.
     */
    public int[] getLayers() {
        int[] t_layers = new int[this.layers.size()];
        for (int i = 0; i < this.layers.size(); i++) {
            t_layers[i] = this.layers.get(i);
        }
        if (this.layers == null)
            throw new NullPointerException("Number of units per layer has not set."
                    + " Try -l {layers} in your arguments path. e.g. \"-l 4 5 3\"");
        
        return t_layers;
    }
    
    /**
     * Gets number of greedy training epochs. <br />
     * Default value is 20.
     * 
     * @return greedy training epochs
     */
    public int getGreedyEpochs(){
        return this.greedyEpochs;
    }
    
    /**
     * Gets number of back-propagation training epochs. <br />
     * Default value is 2.
     * 
     * @return back-propagation training epochs per
     */
    public int getBPEpochs(){
        return this.bpEpochs;
    }
    
    /**
     * Gets the momentum. <br />
     * The default value is 0.6.
     * 
     * @return momentum
     */
    public double getMomentum() {
        return this.momentum;
    }
    
    /**
     * Gets the learning rate. <br />
     * The default value is 0.05.
     * 
     * @return learning rate
     */
    public double getLearningRate() {
        return this.learningRate;
    }
    
    /**
     * Gets a value of the regularization cost term for weights decay. <br />
     * The default value is 0.00001
     * 
     * @return regularization cost
     */
    public double getWeightCost() {
        return this.weightCost;
    }
    
    /**
     * Informs if user has chosen the option "debug". <br />
     * The default value is <code>false</code>
     * 
     * @return <code>true</code> if user has chosen the option "debug"<br />
     *         <code>false</code> if user hasn't chosen the option "debug"
     */
    public boolean getDebug() {
        return this.debug;
    }
    
    /**
     * Gets the threshold. <br />
     * The default value is -1.
     * 
     * @return threshold
     */
    public double getThreshold() {
        return this.threshold;
    }
    
    /**
     * Gets the {@link DeepBeliefNetwork}. If user has set the option "load"
     * the NN will be loaded from the hard drive, otherwise this function will 
     * construct a new neural network.
     * 
     * @return the loaded {@link DeepBeliefNetwork}. If user hasn't chosen to load
     * a neural network, it will return a new one.
     */
    public DeepBeliefNetwork getNN() {
        if (this.nn == null)
            return new DeepBeliefNetwork(this.getLayers());
        return this.nn;
    }
    
    /**
     * Gets as a parameter a file path {@link String} and returns the name of 
     * the file without the path and the ending.
     * 
     * @param path the file's path
     * @return  the file's name
     */
    public static String GetFileName(String path) {
        int pos;
        String fileName;
        if ((pos = path.lastIndexOf("/")) >= 0 ||
                (pos = path.lastIndexOf("\\")) >= 0) {
            fileName = path.substring(pos + 1);
        } else {
            fileName = path;
        }
        
        if ((pos = fileName.lastIndexOf(".")) >= 0) {
            fileName = fileName.substring(0, pos);
        }
        
        return fileName;
    }
}
