/*
 * 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/>.
 */
 
/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package tsml.classifiers.legacy.elastic_ensemble.distance_functions;

import tsml.classifiers.legacy.elastic_ensemble.fast_elastic_ensemble.WarpingPathResults;
import weka.core.EuclideanDistance;
import weka.core.Instance;
import weka.core.Instances;
import weka.core.neighboursearch.PerformanceStats;

/**
 * @author sjx07ngu
 */


public class LCSSDistance extends EuclideanDistance {

    private double epsilon;
    private int delta;

    public LCSSDistance(int delta, double epsilon) {
        this.m_DontNormalize = true;
        this.delta = delta;
        this.epsilon = epsilon;
    }

    public double distance(Instance first, Instance second, double cutOffValue, PerformanceStats stats) {
        return distance(first, second);
    }

    /**
     * distance method that converts instances to arrays of doubles
     *
     * @param first       instance 1
     * @param second      instance 2
     * @param cutOffValue used for early abandon
     * @return distance between instances
     */
    @Override
    public double distance(Instance first, Instance second, double cutOffValue) {
//        if(this.distanceCount % 10000000 == 0){
//            System.out.println("New Instance: "+this.distanceCount);
//        }
//        this.distanceCount++;
        //remove class index from first instance if there is one
        int firtClassIndex = first.classIndex();
        double[] arr1;
        if (firtClassIndex > 0) {
            arr1 = new double[first.numAttributes() - 1];
            for (int i = 0, j = 0; i < first.numAttributes(); i++) {
                if (i != firtClassIndex) {
                    arr1[j] = first.value(i);
                    j++;
                }
            }
        } else {
            arr1 = first.toDoubleArray();
        }

        //remove class index from second instance if there is one
        int secondClassIndex = second.classIndex();
        double[] arr2;
        if (secondClassIndex > 0) {
            arr2 = new double[second.numAttributes() - 1];
            for (int i = 0, j = 0; i < second.numAttributes(); i++) {
                if (i != secondClassIndex) {
                    arr2[j] = second.value(i);
                    j++;
                }
            }
        } else {
            arr2 = second.toDoubleArray();
        }

        return distance(arr1, arr2, cutOffValue);
    }

    /**
     * calculates the distance between two instances (been converted to arrays)
     *
     * @param first       instance 1 as array
     * @param second      instance 2 as array
     * @param cutOffValue used for early abandon
     * @return distance between instances
     */
    public double distance(double[] first, double[] second, double cutOffValue) {
        return distance(first, second);
    }

    public double distance(double[] first, double[] second) {

        double[] a = first;
        double[] b = second;
        int m = first.length;
        int n = second.length;

        int[][] lcss = new int[m + 1][n + 1];
        int[][] lastX = new int[m + 1][n + 1];
        int[][] lastY = new int[m + 1][n + 1];


        for (int i = 0; i < m; i++) {
            for (int j = i - delta; j <= i + delta; j++) {
//                System.out.println("here");
                if (j < 0 || j >= n) {
                    //do nothing
                } else if (b[j] + this.epsilon >= a[i] && b[j] - epsilon <= a[i]) {
                    lcss[i + 1][j + 1] = lcss[i][j] + 1;
                    lastX[i + 1][j + 1] = i;
                    lastY[i + 1][j + 1] = j;
                } else if (lcss[i][j + 1] > lcss[i + 1][j]) {
                    lcss[i + 1][j + 1] = lcss[i][j + 1];
                    lastX[i + 1][j + 1] = i;
                    lastY[i + 1][j + 1] = j + 1;
                } else {
                    lcss[i + 1][j + 1] = lcss[i + 1][j];
                    lastX[i + 1][j + 1] = i + 1;
                    lastY[i + 1][j + 1] = j;
                }
            }
        }

        int max = -1;
        for (int i = 1; i < lcss[lcss.length - 1].length; i++) {
            if (lcss[lcss.length - 1][i] > max) {
                max = lcss[lcss.length - 1][i];
            }
        }
        return 1 - ((double) max / m);
    }

    public static double stdv_s(double[] input) {

        double sumx = 0;
        double sumx2 = 0;


        for (int j = 0; j < input.length; j++) {
            sumx += input[j];
            sumx2 += input[j] * input[j];
        }

        int n = input.length;
        double mean = sumx / n;
        double standardDev = Math.sqrt(sumx2 / (n - 1) - mean * mean);

        return standardDev;

    }

    public static double stdv_p(Instances input) {

        double sumx = 0;
        double sumx2 = 0;
        double[] ins2array;
        for (int i = 0; i < input.numInstances(); i++) {
            ins2array = input.instance(i).toDoubleArray();
            for (int j = 0; j < ins2array.length - 1; j++) {//-1 to avoid classVal
                sumx += ins2array[j];
                sumx2 += ins2array[j] * ins2array[j];
            }
        }
        int n = input.numInstances() * (input.numAttributes() - 1);
        double mean = sumx / n;
        return Math.sqrt(sumx2 / (n) - mean * mean);

    }

    public static int[] getInclusive10(int min, int max) {
        int[] output = new int[10];

        double diff = (double) (max - min) / 9;
        double[] doubleOut = new double[10];
        doubleOut[0] = min;
        output[0] = min;
        for (int i = 1; i < 9; i++) {
            doubleOut[i] = doubleOut[i - 1] + diff;
            output[i] = (int) Math.round(doubleOut[i]);
        }
        output[9] = max; // to make sure max isn't omitted due to double imprecision
        return output;
    }

    public static double[] getInclusive10(double min, double max) {
        double[] output = new double[10];
        double diff = (double) (max - min) / 9;
        output[0] = min;
        for (int i = 1; i < 9; i++) {
            output[i] = output[i - 1] + diff;
        }
        output[9] = max;

        return output;
    }

    /************************************************************************************************
     Support for FastEE
     ************************************************************************************************/

    public static WarpingPathResults distanceExt(final Instance first, final Instance second, final double epsilon, final int delta) {
        final int m = first.numAttributes() - 1;
        final int n = second.numAttributes() - 1;
        int i, j, absIJ;
        final int[][] matrixD = new int[m + 1][n + 1];
        final int[][] minDelta = new int[m + 1][n + 1];

        for (i = 0; i < m; i++) {
            for (j = i - delta; j <= i + delta; j++) {
                if (j < 0) {
                    j = -1;
                } else if (j >= n) {
                    j = i + delta;
                } else if (second.value(j) + epsilon >= first.value(i) &&
                        second.value(j) - epsilon <= first.value(i)) {
                    absIJ = Math.abs(i - j);
                    matrixD[i + 1][j + 1] = matrixD[i][j] + 1;
                    minDelta[i + 1][j + 1] = Math.max(absIJ, minDelta[i][j]);
                } else if (delta == 0) {
                    matrixD[i + 1][j + 1] = matrixD[i][j];
                    minDelta[i + 1][j + 1] = 0;
                } else if (matrixD[i][j + 1] > matrixD[i + 1][j]) {
                    matrixD[i + 1][j + 1] = matrixD[i][j + 1];
                    minDelta[i + 1][j + 1] = minDelta[i][j + 1];
                } else {
                    matrixD[i + 1][j + 1] = matrixD[i + 1][j];
                    minDelta[i + 1][j + 1] = minDelta[i + 1][j];
                }
            }
        }

        int max = -1, maxR = -1;
        for (i = 1; i < m + 1; i++) {
            if (matrixD[m][i] > max) {
                max = matrixD[m][i];
                maxR = minDelta[m][i];
            }
        }
        WarpingPathResults resExt = new WarpingPathResults();
        resExt.distance = 1.0 - 1.0 * matrixD[m][n] / m;
        resExt.distanceFromDiagonal = maxR;
        return resExt;
    }

}
