/*
 * This file is part of the UEA Time Series Machine Learning (TSML) toolbox.
 *
 * The UEA TSML toolbox 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.
 *
 * The UEA TSML toolbox 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 the UEA TSML toolbox. If not, see <https://www.gnu.org/licenses/>.
 */
 
package tsml.classifiers.legacy.elastic_ensemble;

import java.io.File;
import java.io.FileWriter;
import java.text.DecimalFormat;
import java.util.Scanner;

import experiments.data.DatasetLoading;
import tsml.classifiers.EnhancedAbstractClassifier;
import tsml.classifiers.legacy.elastic_ensemble.fast_elastic_ensemble.CandidateNN;
import tsml.classifiers.legacy.elastic_ensemble.fast_elastic_ensemble.utils.SequenceStatsCache;
import utilities.InstanceTools;
import tsml.classifiers.SaveParameterInfo;
import weka.core.DenseInstance;
import weka.core.Instance;
import weka.core.Instances;

/**
 * An abstract class to allow for distance-specific optimisations of DTW for use
 * in the ElasticEnsemble. This class allows for univariate and multivariate
 * time series to be used; the multivariate version calculates distances as the
 * sum of individual distance calculations between common dimensions of two
 * instances (using the same parameter setting on all channels).
 * <p>
 * E.G. a DTW implementation with window = 0.5 (50%) for two instances with 10
 * channels would calculate the DTW distance separately for each channel, and
 * sum the 10 distances together.
 *
 * @author Jason Lines (j.lines@uea.ac.uk)
 */
public abstract class Efficient1NN extends EnhancedAbstractClassifier implements SaveParameterInfo{
    protected Instances train;
    protected Instances[] trainGroup;
    protected String classifierIdentifier;
    protected boolean allowLoocv = true;
    protected boolean singleParamCv = false;

    private boolean fileWriting = false;
    private boolean individualCvParamFileWriting = false;
    private String outputDir;
    private String datasetName;
    private int resampleId;

    public Efficient1NN() {
        super(CANNOT_ESTIMATE_OWN_PERFORMANCE);
    }
    
    /**
     * Abstract method to calculates the distance between two Instance objects
     *
     * @param first
     * @param second
     * @param cutOffValue a best-so-far value to allow early abandons
     * @return the distance between first and second. If early abandon occurs, it will return Double.MAX_VALUE.
     */
    public abstract double distance(Instance first, Instance second, double cutOffValue);

    /**
     * Multi-dimensional equivalent of the univariate distance method. Iterates
     * through channels calculating distances independently using the same param
     * options, summing together at the end to return a single distance.
     *
     * @param first
     * @param second
     * @param cutOffValue
     * @return
     */
    public double distance(Instance[] first, Instance[] second, double cutOffValue) {
        double sum = 0;
        double decliningCutoff = cutOffValue;
        double thisDist;
        for (int d = 0; d < first.length; d++) {
            thisDist = this.distance(first[d], second[d], decliningCutoff);
            sum += thisDist;
            if (sum > cutOffValue) {
                return Double.MAX_VALUE;
            }
            decliningCutoff -= thisDist;
        }

        return sum;
    }

    public abstract double lowerBound(final Instance query, final Instance candidate, final int queryIndex, final int candidateIndex);

    public abstract double lowerBound(final Instance query, final Instance candidate, final int queryIndex, final int candidateIndex, final double cutOffValue);

    public abstract double lowerBound(final Instance query, final Instance candidate, final int queryIndex, final int candidateIndex, final SequenceStatsCache cache);

    public abstract double lowerBound(final Instance query, final Instance candidate, final int queryIndex, final int candidateIndex, final double cutOffValue, final SequenceStatsCache cache);


    /**
     * Utility method for easy cross-validation experiments. Each inheriting
     * class has 100 param options to select from (some dependent on information
     * for the training data). Passing in the training data and a param
     *
     * @param train
     * @param paramId
     */
    public abstract void setParamsFromParamId(Instances train, int paramId);

    public void buildClassifier(Instances train) throws Exception {
        this.train = train;
        this.trainGroup = null;
    }

    public void buildClassifier(Instances[] trainGroup) throws Exception {
        this.train = null;
        this.trainGroup = trainGroup;
    }

    @Override
    public double classifyInstance(Instance instance) throws Exception {

        double bsfDistance = Double.MAX_VALUE;
        // for tie splitting
        int[] classCounts = new int[this.train.numClasses()];

        double thisDist;

        for (Instance i : this.train) {
            thisDist = distance(instance, i, bsfDistance);
            if (thisDist < bsfDistance) {
                bsfDistance = thisDist;
                classCounts = new int[train.numClasses()];
                classCounts[(int) i.classValue()]++;
            } else if (thisDist == bsfDistance) {
                classCounts[(int) i.classValue()]++;
            }
        }

        double bsfClass = -1;
        double bsfCount = -1;
        for (int c = 0; c < classCounts.length; c++) {
            if (classCounts[c] > bsfCount) {
                bsfCount = classCounts[c];
                bsfClass = c;
            }
        }

        return bsfClass;
    }

    public double classifyInstanceMultivariate(Instance[] instance) throws Exception {

        if (this.trainGroup == null) {
            throw new Exception("Error: this configuration is for multivariate data");
        }

        double bsfDistance = Double.MAX_VALUE;
        // for tie splitting
        int[] classCounts = new int[this.trainGroup[0].numClasses()];

        double thisDist;

        Instance[] trainInstancesByDimension;
        for (int i = 0; i < this.trainGroup[0].numInstances(); i++) {
            trainInstancesByDimension = new Instance[this.trainGroup.length];
            for (int j = 0; j < trainInstancesByDimension.length; j++) {
                trainInstancesByDimension[j] = this.trainGroup[j].instance(i);
            }

            thisDist = distance(instance, trainInstancesByDimension, bsfDistance);
            if (thisDist < bsfDistance) {
                bsfDistance = thisDist;
                classCounts = new int[trainGroup[0].numClasses()];
                classCounts[(int) trainGroup[0].instance(i).classValue()]++;
            } else if (thisDist == bsfDistance) {
                classCounts[(int) trainGroup[0].instance(i).classValue()]++;
            }
        }

        double bsfClass = -1;
        double bsfCount = -1;
        for (int c = 0; c < classCounts.length; c++) {
            if (classCounts[c] > bsfCount) {
                bsfCount = classCounts[c];
                bsfClass = c;
            }
        }

        return bsfClass;
    }

    @Override
    public double[] distributionForInstance(Instance instance) throws Exception {

        double bsfDistance = Double.MAX_VALUE;
        // for tie splitting
        int[] classCounts = new int[this.train.numClasses()];

        double thisDist;
        int sumOfBest = 0;

        for (Instance i : this.train) {
            thisDist = distance(instance, i, bsfDistance);
            if (thisDist < bsfDistance) {
                bsfDistance = thisDist;
                classCounts = new int[train.numClasses()];
                classCounts[(int) i.classValue()]++;
                sumOfBest = 1;
            } else if (thisDist == bsfDistance) {
                classCounts[(int) i.classValue()]++;
                sumOfBest++;
            }
        }

        double[] classDistributions = new double[this.train.numClasses()];
        for (int c = 0; c < classCounts.length; c++) {
            classDistributions[c] = (double) classCounts[c] / sumOfBest;
        }

        return classDistributions;
    }

    public void setClassifierIdentifier(String classifierIdentifier) {
        this.classifierIdentifier = classifierIdentifier;
    }

    public String getClassifierIdentifier() {
        return classifierIdentifier;
    }

    
    // could parallelise here
//    public void writeLOOCVOutput(String tscProblemDir, String datasetName, int resampleId, String outputResultsDir, boolean tidyUp) throws Exception{    
//        for(int paramId = 0; paramId < 100; paramId++){
//            writeLOOCVOutput(tscProblemDir, datasetName, resampleId, outputResultsDir, paramId);
//        }    
//        parseLOOCVResults(tscProblemDir, datasetName, resampleId, outputResultsDir, tidyUp);
//    }
//    
//    public double writeLOOCVOutput(String tscProblemDir, String datasetName, int resampleId, String outputResultsDir, int paramId) throws Exception{
//        new File(outputResultsDir+classifierIdentifier+"/Predictions/"+datasetName+"/loocvForParamOptions/").mkdirs();
//        
//        Instances train = ClassifierTools.loadDataThrowable(tscProblemDir+datasetName+"/"+datasetName+"_TRAIN");
//        Instances test = ClassifierTools.loadDataThrowable(tscProblemDir+datasetName+"/"+datasetName+"_TEST");
//        
//        if(resampleId!=0){
//            Instances[] temp = InstanceTools.resampleTrainAndTestInstances(train, test, resampleId);
//            train = temp[0];
//            test = temp[1];
//        }
//        
//        this.setParamsFromParamId(paramId);
//        
//        Instances trainLoocv;
//        Instance testLoocv;
//        
//        int correct = 0;
//        double pred, actual;
//        for(int i = 0; i < train.numInstances(); i++){
//            trainLoocv = new Instances(train);
//            testLoocv = trainLoocv.remove(i);
//            actual = testLoocv.classValue();
//            this.buildClassifier(train);
//            pred = this.classifyInstance(testLoocv);
//            if(pred==actual){
//                correct++;
//            }
//        }
//        
//        return (double)correct/train.numInstances();
//    }


    public void setFileWritingOn(String outputDir, String datasetName, int resampleId) {
        this.fileWriting = true;
        this.outputDir = outputDir;
        this.datasetName = datasetName;
        this.resampleId = resampleId;
    }

    public void setIndividualCvFileWritingOn(String outputDir, String datasetName, int resampleId) {
        this.individualCvParamFileWriting = true;
        this.outputDir = outputDir;
        this.datasetName = datasetName;
        this.resampleId = resampleId;
    }


    public double[] loocv(Instances train) throws Exception {
        double[] accAndPreds = null;
        String parsedFileName = this.outputDir + this.classifierIdentifier + "/Predictions/" + datasetName + "/trainFold" + resampleId + ".csv";

//        System.out.println(parsedFileName);

        if (fileWriting) {
            File existing = new File(parsedFileName);
            if (existing.exists()) {
//                throw new Exception("Parsed results already exist for this measure: "+ parsedFileName);
                Scanner scan = new Scanner(existing);
                scan.useDelimiter("\n");
                scan.next(); // skip header line
                int paramId = Integer.parseInt(scan.next().trim().split(",")[0]);
                if (this.allowLoocv) {
                    this.setParamsFromParamId(train, paramId);
                }
                this.buildClassifier(train);
                accAndPreds = new double[train.numInstances() + 1];
                accAndPreds[0] = Double.parseDouble(scan.next().trim().split(",")[0]);
                int correct = 0;
                String[] temp;
                for (int i = 0; i < train.numInstances(); i++) {
                    temp = scan.next().split(",");
                    accAndPreds[i + 1] = Double.parseDouble(temp[1]);
                    if (accAndPreds[i + 1] == Double.parseDouble(temp[0])) {
                        correct++;
                    }
                }
                // commented out for now as this breaks the new EE loocv thing we're doing for the competition. Basically, if we try and load for train-1 ins for test in loocv, the number of train instances doesn't match so the acc is slightly off. should be an edge case, but can leave this check out so long as we trust the code
//                if(((double)correct/train.numInstances())!=accAndPreds[0]){
//                    System.err.println(existing.getAbsolutePath());
//                    System.err.println(((double)correct/train.numInstances())+" "+accAndPreds[0]);
//                    throw new Exception("Attempted file loading, but accuracy doesn't match itself?!");
//                }
                return accAndPreds;
            } else {
                new File(this.outputDir + this.classifierIdentifier + "/Predictions/" + datasetName + "/").mkdirs();
            }
        }

//        write output
//        maybe a different version which looks for missing files and runs them?


        double bsfAcc = -1;
        int bsfParamId = -1;
        double[] bsfaccAndPreds = null;

        for (int paramId = 0; paramId < 100; paramId++) {
//            System.out.print(paramId+" ");
            accAndPreds = loocvAccAndPreds(train, paramId);
//            System.out.println(this.allowLoocv);
//            System.out.println(accAndPreds[0]);
            if (accAndPreds[0] > bsfAcc) {
                bsfAcc = accAndPreds[0];
                bsfParamId = paramId;
                bsfaccAndPreds = accAndPreds;
            }
            if (!this.allowLoocv) {
                paramId = 100;
            }
        }
//        System.out.println(this.classifierIdentifier+", bsfParamId "+bsfParamId);
        this.buildClassifier(train);
        if (this.allowLoocv) {
            this.setParamsFromParamId(train, bsfParamId);
        }
        if (fileWriting) {
            FileWriter out = new FileWriter(parsedFileName);
            out.append(this.classifierIdentifier + "," + datasetName + ",parsedTrain\n");
            out.append(bsfParamId + "\n");
            out.append(bsfAcc + "\n");
            for (int i = 1; i < bsfaccAndPreds.length; i++) {
                out.append(train.instance(i - 1).classValue() + "," + bsfaccAndPreds[i] + "\n");
            }
            out.close();
        }

        return bsfaccAndPreds;
    }

    DecimalFormat df = new DecimalFormat("##.###");

    public double[] loocv(Instances[] trainGroup) throws Exception {
        double[] accAndPreds = null;
        String parsedFileName = this.outputDir + this.classifierIdentifier + "/Predictions/" + datasetName + "/trainFold" + resampleId + ".csv";

        Instances concatenated = concatenate(trainGroup);


        if (fileWriting) {
            File existing = new File(parsedFileName);
            if (existing.exists()) {
//                throw new Exception("Parsed results already exist for this measure: "+ parsedFileName);
                Scanner scan = new Scanner(existing);
                scan.useDelimiter("\n");
                scan.next(); // skip header line
                int paramId = Integer.parseInt(scan.next().trim().split(",")[0]);
                if (this.allowLoocv) {
                    this.setParamsFromParamId(concatenated, paramId);
                }
                this.buildClassifier(trainGroup);
                accAndPreds = new double[trainGroup[0].numInstances() + 1];
                accAndPreds[0] = Double.parseDouble(scan.next().trim().split(",")[0]);
                int correct = 0;
                String[] temp;
                for (int i = 0; i < trainGroup[0].numInstances(); i++) {
                    temp = scan.next().split(",");
                    accAndPreds[i + 1] = Double.parseDouble(temp[1]);
                    if (accAndPreds[i + 1] == Double.parseDouble(temp[0])) {
                        correct++;
                    }
                }
                // commented out for now as this breaks the new EE loocv thing we're doing for the competition. Basically, if we try and load for train-1 ins for test in loocv, the number of train instances doesn't match so the acc is slightly off. should be an edge case, but can leave this check out so long as we trust the code
//                if(((double)correct/train.numInstances())!=accAndPreds[0]){
//                    System.err.println(existing.getAbsolutePath());
//                    System.err.println(((double)correct/train.numInstances())+" "+accAndPreds[0]);
//                    throw new Exception("Attempted file loading, but accuracy doesn't match itself?!");
//                }
                return accAndPreds;
            } else {
                new File(this.outputDir + this.classifierIdentifier + "/Predictions/" + datasetName + "/").mkdirs();
            }
        }

//        write output
//        maybe a different version which looks for missing files and runs them?


        double bsfAcc = -1;
        int bsfParamId = -1;
        double[] bsfaccAndPreds = null;

        for (int paramId = 0; paramId < 100; paramId++) {
//            System.out.print(paramId+" ");
            accAndPreds = loocvAccAndPreds(trainGroup, concatenated, paramId);
//            System.out.println(this.allowLoocv);
//            System.out.println(accAndPreds[0]);
            if (accAndPreds[0] > bsfAcc) {
                bsfAcc = accAndPreds[0];
                bsfParamId = paramId;
                bsfaccAndPreds = accAndPreds;
            }
            System.out.println("\t" + paramId + ": " + df.format(accAndPreds[0] * 100) + " (" + df.format(bsfAcc * 100) + ")");
            if (!this.allowLoocv) {
                paramId = 100;
            }
        }
//        System.out.println(this.classifierIdentifier+", bsfParamId "+bsfParamId);
        this.buildClassifier(trainGroup);
        if (this.allowLoocv) {
            this.setParamsFromParamId(concatenated, bsfParamId);
        }
        if (fileWriting) {
            FileWriter out = new FileWriter(parsedFileName);
            out.append(this.classifierIdentifier + "," + datasetName + ",parsedTrain\n");
            out.append(bsfParamId + "\n");
            out.append(bsfAcc + "\n");
            for (int i = 1; i < bsfaccAndPreds.length; i++) {
                out.append(trainGroup[0].instance(i - 1).classValue() + "," + bsfaccAndPreds[i] + "\n");
            }
            out.close();
        }

        return bsfaccAndPreds;
    }

    public double[] loocvAccAndPreds(Instances train, int paramId) throws Exception {
        if (this.allowLoocv) {
            this.setParamsFromParamId(train, paramId);
        }
        FileWriter out = null;
        String parsedFileName = this.outputDir + this.classifierIdentifier + "/Predictions/" + datasetName + "/trainFold" + resampleId + ".csv";
        String singleFileName = this.outputDir + this.classifierIdentifier + "/cv/" + datasetName + "/trainFold" + resampleId + "/pid" + paramId + ".csv";
        if (this.individualCvParamFileWriting) {
            if (new File(parsedFileName).exists()) {//|| new File(singleFileName).exists()){
                throw new Exception("Error: Full parsed training results already exist - " + parsedFileName);
            } else if (new File(singleFileName).exists()) {
                throw new Exception("Error: CV training results already exist for this pid - " + singleFileName);
            }
        }


        // else we already know what the params are, so don't need to set

        Instances trainLoocv;
        Instance testLoocv;

        int correct = 0;
        double pred, actual;

        double[] accAndPreds = new double[train.numInstances() + 1];
        for (int i = 0; i < train.numInstances(); i++) {
            trainLoocv = new Instances(train);
            testLoocv = trainLoocv.remove(i);
            actual = testLoocv.classValue();
            this.buildClassifier(trainLoocv);
            pred = this.classifyInstance(testLoocv);
            if (pred == actual) {
                correct++;
            }
            accAndPreds[i + 1] = pred;
        }
        accAndPreds[0] = (double) correct / train.numInstances();
//        System.out.println(accAndPreds[0]);

        if (individualCvParamFileWriting) {
            new File(this.outputDir + this.classifierIdentifier + "/cv/" + datasetName + "/trainFold" + resampleId + "/").mkdirs();
            out = new FileWriter(singleFileName);
            out.append(this.classifierIdentifier + "," + datasetName + ",cv\n");
            out.append(paramId + "\n");
            out.append(accAndPreds[0] + "\n");
            for (int i = 1; i < accAndPreds.length; i++) {
                out.append(train.instance(i - 1).classValue() + "," + accAndPreds[i] + "\n");
            }
            out.close();
        }

        return accAndPreds;
    }

    public double[] loocvAccAndPreds(Instances[] trainGroup, Instances concatenated, int paramId) throws Exception {

        FileWriter out = null;
        String parsedFileName = this.outputDir + this.classifierIdentifier + "/Predictions/" + datasetName + "/trainFold" + resampleId + ".csv";
        String singleFileName = this.outputDir + this.classifierIdentifier + "/cv/" + datasetName + "/trainFold" + resampleId + "/pid" + paramId + ".csv";
//        if(this.fileWriting){
//            if(new File(parsedFileName).exists()){//|| new File(singleFileName).exists()){
//                throw new Exception("Error: Full parsed training results already exist - "+parsedFileName);
//            }else if(new File(singleFileName).exists()){
//                throw new Exception("Error: CV training results already exist for this pid - "+singleFileName);
//            }
//        }

        if (this.allowLoocv) {
//            System.out.println("allowed");
            this.setParamsFromParamId(concatenated, paramId);
//            System.out.println(this.toString());
        }
        // else we already know what the params are, so don't need to set

        Instances[] trainLoocv;
        Instance[] testLoocv;

        int correct = 0;
        double pred, actual;

        double[] accAndPreds = new double[trainGroup[0].numInstances() + 1];
        for (int i = 0; i < trainGroup[0].numInstances(); i++) {
            trainLoocv = new Instances[trainGroup.length];
            testLoocv = new Instance[trainGroup.length];

            for (int d = 0; d < trainGroup.length; d++) {
                trainLoocv[d] = new Instances(trainGroup[d]);
                testLoocv[d] = trainLoocv[d].remove(i);
            }

//            trainLoocv = new Instances(train);
//            testLoocv = trainLoocv.remove(i);
            actual = testLoocv[0].classValue();
            this.buildClassifier(trainLoocv);
            pred = this.classifyInstanceMultivariate(testLoocv);
            if (pred == actual) {
                correct++;
            }
            accAndPreds[i + 1] = pred;
        }
        accAndPreds[0] = (double) correct / trainGroup[0].numInstances();
//        System.out.println(accAndPreds[0]);

//        if(fileWriting){
//            out = new FileWriter(singleFileName);
//            out.append(this.classifierIdentifier+","+datasetName+",cv\n");
//            out.append(paramId+"\n");
//            out.append(accAndPreds[0]+"\n");
//            for(int i = 1; i < accAndPreds.length;i++){
//                out.append(train.instance(i-1).classValue()+","+accAndPreds[i]+"\n");
//            }
//            out.close();
//        }

        return accAndPreds;
    }

    public void writeTrainTestOutput(String tscProblemDir, String datasetName, int resampleId, String outputResultsDir) throws Exception {

        // load in param id from training results
        File cvResults = new File(outputResultsDir + classifierIdentifier + "/Predictions/" + datasetName + "/trainFold" + resampleId + ".csv");
        if (!cvResults.exists()) {
            throw new Exception("Error loading file " + cvResults.getAbsolutePath());
        }
        Scanner scan = new Scanner(cvResults);
        scan.useDelimiter(System.lineSeparator());
        scan.next();
        int paramId = Integer.parseInt(scan.next().trim());
        this.setParamsFromParamId(train, paramId);

        // Now classifier is set up, make the associated files and do the test classification

        new File(outputResultsDir + classifierIdentifier + "/Predictions/" + datasetName + "/").mkdirs();
        StringBuilder headerInfo = new StringBuilder();

        headerInfo.append(classifierIdentifier).append(System.lineSeparator());
        headerInfo.append(this.getParamInformationString()).append(System.lineSeparator());

        Instances train = DatasetLoading.loadDataNullable(tscProblemDir + datasetName + "/" + datasetName + "_TRAIN");
        Instances test = DatasetLoading.loadDataNullable(tscProblemDir + datasetName + "/" + datasetName + "_TEST");

        if (resampleId != 0) {
            Instances[] temp = InstanceTools.resampleTrainAndTestInstances(train, test, resampleId);
            train = temp[0];
            test = temp[1];
        }

        this.buildClassifier(train);
        StringBuilder classificationInfo = new StringBuilder();
        int correct = 0;
        double pred, actual;
        for (int i = 0; i < test.numInstances(); i++) {
            actual = test.instance(i).classValue();
            pred = this.classifyInstance(test.instance(i));
            classificationInfo.append(actual).append(",").append(pred).append(System.lineSeparator());
            if (actual == pred) {
                correct++;
            }
        }

        FileWriter outWriter = new FileWriter(outputResultsDir + this.classifierIdentifier + "/Predictions/" + datasetName + "/testFold" + resampleId + ".csv");
        outWriter.append(headerInfo);
        outWriter.append(((double) correct / test.numInstances()) + System.lineSeparator());
        outWriter.append(classificationInfo);
        outWriter.close();


    }

//    public static void parseLOOCVResults(String tscProblemDir, String datasetName, int resampleId, String outputResultsDir, boolean tidyUp){
//        
//    }

    public abstract String getParamInformationString();

    public Instances getTrainingData() {
        return this.train;
    }


    public static Instances concatenate(Instances[] train) {
        // make a super arff for finding params that need stdev etc
        Instances temp = new Instances(train[0], 0);
        for (int i = 1; i < train.length; i++) {
            for (int j = 0; j < train[i].numAttributes() - 1; j++) {
                temp.insertAttributeAt(train[i].attribute(j), temp.numAttributes() - 1);
            }
        }

        int dataset, attFromData;

        for (int insId = 0; insId < train[0].numInstances(); insId++) {
            DenseInstance dense = new DenseInstance(temp.numAttributes());
            for (int attId = 0; attId < temp.numAttributes() - 1; attId++) {

                dataset = attId / (train[0].numAttributes() - 1);
                attFromData = attId % (train[0].numAttributes() - 1);
                dense.setValue(attId, train[dataset].instance(insId).value(attFromData));

            }
            dense.setValue(temp.numAttributes() - 1, train[0].instance(insId).classValue());
            temp.add(dense);
        }
        return temp;
    }

    /************************************************************************************************
     Support for FastEE
     @author Chang Wei Tan, Monash University (chang.tan@monash.edu)
     ************************************************************************************************/
    CandidateNN[][] candidateNNS; // a table to store nearest neighbours for each instance in the training set
    final int nParams = 100; // number of parameters to learn
    int[][][] classCounts;
    int maxWindow;
    SequenceStatsCache trainCache; // cache to store some pre-computed data on the training set

    private int bsfParamId;

    // classify instance with lower bounds
    public int classifyInstance(final Instances trainData, final Instance query, final int queryIndex, final SequenceStatsCache cache) {
        int[] classCounts = new int[train.numClasses()];

        double dist;

        Instance candidate = trainData.get(0);
        double bsfDistance = distance(query, candidate, Double.POSITIVE_INFINITY);
        classCounts[(int) candidate.classValue()]++;

        for (int candidateIndex = 1; candidateIndex < trainData.size(); candidateIndex++) {
            candidate = trainData.get(candidateIndex);
            dist = lowerBound(query, candidate, queryIndex, candidateIndex, bsfDistance, cache);
            if (dist <= bsfDistance) {
                dist = distance(query, candidate, bsfDistance);
                if (dist < bsfDistance) {
                    bsfDistance = dist;
                    classCounts = new int[trainData.numClasses()];
                    classCounts[(int) candidate.classValue()]++;
                } else if (dist == bsfDistance) {
                    classCounts[(int) candidate.classValue()]++;
                }
            }
        }

        int bsfClass = -1;
        double bsfCount = -1;
        for (int i = 0; i < classCounts.length; i++) {
            if (classCounts[i] > bsfCount) {
                bsfCount = classCounts[i];
                bsfClass = i;
            }
        }
        return bsfClass;
    }

    public abstract void initNNSTable(final Instances trainData, final SequenceStatsCache cache);

    public abstract void initApproxNNSTable(final Instances trainData, final SequenceStatsCache cache, final int nSamples);

    public double[] fastParameterSearch(final Instances train) throws Exception {
        double[] accAndPreds;
        this.maxWindow = train.numAttributes() - 1;
        this.trainCache = new SequenceStatsCache(train, train.numAttributes() - 1);

        initNNSTable(train, trainCache);

        double bsfAcc = -1;
        int bsfParamId = -1;
        double[] bsfaccAndPreds = null;

        if(debug)
            System.out.print("[1-NN] Fast Parameter Search for " + this.classifierIdentifier + ", training ");
        for (int paramId = 0; paramId < nParams; paramId++) {
            if(debug)
                System.out.print(".");
            accAndPreds = fastParameterAccAndPred(train, paramId, train.size());
            if (accAndPreds[0] > bsfAcc) {
                bsfAcc = accAndPreds[0];
                bsfParamId = paramId;
                bsfaccAndPreds = accAndPreds;
            }
        }
        if(debug)
            System.out.println();

        this.buildClassifier(train);
        this.setParamsFromParamId(train, bsfParamId);


        if (fileWriting) {
            String parsedFileName = this.outputDir + this.classifierIdentifier + "/FastEE_Predictions/" +
                    datasetName + "/trainFold" + resampleId + ".csv";

            FileWriter out = new FileWriter(parsedFileName);
            out.append(this.classifierIdentifier + "," + datasetName + ",parsedTrain\n");
            out.append(bsfParamId + "\n");
            out.append(bsfAcc + "\n");
            for (int i = 1; i < bsfaccAndPreds.length; i++) {
                out.append(train.instance(i - 1).classValue() + "," + bsfaccAndPreds[i] + "\n");
            }
            out.close();
        }

        return bsfaccAndPreds;
    }

    private double[] fastParameterAccAndPred(final Instances train, final int paramId, final int n) {
        this.setParamsFromParamId(train, paramId);
        int correct = 0;
        double pred, actual;

        final double[] accAndPreds = new double[n + 1];
        for (int i = 0; i < n; i++) {
            actual = train.get(i).classValue();
            pred = -1;
            double bsfCount = -1;
            for (int c = 0; c < classCounts[paramId][i].length; c++) {
                if (classCounts[paramId][i][c] > bsfCount) {
                    bsfCount = classCounts[paramId][i][c];
                    pred = c;
                }
            }
            if (pred == actual) {
                correct++;
            }
            accAndPreds[i + 1] = pred;
        }
        accAndPreds[0] = 1.0 * correct / n;

        return accAndPreds;
    }

    public double[] approxParameterSearch(final Instances train, final int nSamples) throws Exception {
        final long start = System.nanoTime();
        double[] accAndPreds;
        this.maxWindow = train.numAttributes() - 1;
        this.trainCache = new SequenceStatsCache(train, train.numAttributes() - 1);

        initApproxNNSTable(train, trainCache, nSamples);

        bsfParamId = -1;
        double bsfAcc = -1;
        double[] bsfaccAndPreds = null;
        if(debug)
            System.out.print("[1-NN] Approximate Parameter Search for " + this.classifierIdentifier + ", training ");

        for (int paramId = 0; paramId < nParams; paramId++) {
            if(debug)
                System.out.print(".");
            accAndPreds = approxParameterAccAndPred(train, paramId, nSamples);
            if (accAndPreds[0] > bsfAcc) {
                bsfAcc = accAndPreds[0];
                bsfParamId = paramId;
                bsfaccAndPreds = accAndPreds;
            }
        }
        if(debug)
            System.out.println();

        this.buildClassifier(train);
        this.setParamsFromParamId(train, bsfParamId);


        if (fileWriting) {
            String parsedFileName = this.outputDir + this.classifierIdentifier + "/ApproxEE_Predictions/" +
                    datasetName + "/trainFold" + resampleId + ".csv";

            FileWriter out = new FileWriter(parsedFileName);
            out.append(this.classifierIdentifier + "," + datasetName + ",parsedTrain\n");
            out.append(bsfParamId + "\n");
            out.append(bsfAcc + "\n");
            for (int i = 1; i < bsfaccAndPreds.length; i++) {
                out.append(train.instance(i - 1).classValue() + "," + bsfaccAndPreds[i] + "\n");
            }
            out.close();
        }

        return bsfaccAndPreds;
    }

    private double[] approxParameterAccAndPred(final Instances train, final int paramId, final int nSamples) {
        this.setParamsFromParamId(train, paramId);
        int correct = 0;
        double pred, actual;

        final double[] accAndPreds = new double[nSamples + 1];
        for (int i = 0; i < nSamples; i++) {
            actual = train.get(i).classValue();
            pred = train.get(candidateNNS[paramId][i].index).classValue();
            if (pred == actual) {
                correct++;
            }
            accAndPreds[i + 1] = pred;
        }
        accAndPreds[0] = 1.0 * correct / nSamples;

        return accAndPreds;
    }
}
