/*
 *  ******************************************************************************
 *  *
 *  *
 *  * This program and the accompanying materials are made available under the
 *  * terms of the Apache License, Version 2.0 which is available at
 *  * https://www.apache.org/licenses/LICENSE-2.0.
 *  *
 *  *  See the NOTICE file distributed with this work for additional
 *  *  information regarding copyright ownership.
 *  * Unless required by applicable law or agreed to in writing, software
 *  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 *  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 *  * License for the specific language governing permissions and limitations
 *  * under the License.
 *  *
 *  * SPDX-License-Identifier: Apache-2.0
 *  *****************************************************************************
 */

package org.eclipse.deeplearning4j.nd4j.linalg.dataset;


import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;

import org.nd4j.common.tests.tags.NativeTag;
import org.nd4j.common.tests.tags.TagNames;
import org.nd4j.linalg.BaseNd4jTestWithBackends;
import org.nd4j.linalg.api.ndarray.INDArray;
import org.nd4j.linalg.dataset.DataSet;
import org.nd4j.linalg.dataset.api.iterator.DataSetIterator;
import org.nd4j.linalg.dataset.api.iterator.TestDataSetIterator;
import org.nd4j.linalg.dataset.api.preprocessor.DataNormalization;
import org.nd4j.linalg.dataset.api.preprocessor.NormalizerMinMaxScaler;
import org.nd4j.linalg.factory.Nd4j;
import org.nd4j.linalg.factory.Nd4jBackend;
import org.nd4j.linalg.ops.transforms.Transforms;

import static org.junit.jupiter.api.Assertions.*;


@Tag(TagNames.NDARRAY_ETL)
@NativeTag
@Tag(TagNames.FILE_IO)
public class NormalizerMinMaxScalerTest extends BaseNd4jTestWithBackends {


    @ParameterizedTest
    @MethodSource("org.nd4j.linalg.BaseNd4jTestWithBackends#configs")
    public void testBruteForce(Nd4jBackend backend) {
        //X_std = (X - X.min(axis=0)) / (X.max(axis=0) - X.min(axis=0))
        //X_scaled = X_std * (max - min) + min
        // Dataset features are scaled consecutive natural numbers
        int nSamples = 500;
        int x = 4, y = 2, z = 3;

        INDArray featureX = Nd4j.linspace(1, nSamples, nSamples).reshape(nSamples, 1);
        INDArray featureY = featureX.mul(y);
        INDArray featureZ = featureX.mul(z);
        featureX.muli(x);
        INDArray featureSet = Nd4j.concat(1, featureX, featureY, featureZ);
        INDArray labelSet = Nd4j.zeros(nSamples, 1);
        DataSet sampleDataSet = new DataSet(featureSet, labelSet);

        //expected min and max
        INDArray theoreticalMin = Nd4j.create(new double[] {x, y, z}, new long[]{1,3});
        INDArray theoreticalMax = Nd4j.create(new double[] {nSamples * x, nSamples * y, nSamples * z}, new long[]{1,3});
        INDArray theoreticalRange = theoreticalMax.sub(theoreticalMin);

        NormalizerMinMaxScaler myNormalizer = new NormalizerMinMaxScaler();
        myNormalizer.fit(sampleDataSet);

        INDArray minDataSet = myNormalizer.getMin();
        INDArray maxDataSet = myNormalizer.getMax();
        INDArray minDiff = minDataSet.sub(theoreticalMin).max();
        INDArray maxDiff = maxDataSet.sub(theoreticalMax).max();
        assertEquals(minDiff.getDouble(0), 0.0, 0.000000001);
        assertEquals(maxDiff.max().getDouble(0), 0.0, 0.000000001);

        // SAME TEST WITH THE ITERATOR
        int bSize = 1;
        DataSetIterator sampleIter = new TestDataSetIterator(sampleDataSet, bSize);
        myNormalizer.fit(sampleIter);
        minDataSet = myNormalizer.getMin();
        maxDataSet = myNormalizer.getMax();
        assertEquals(minDataSet.sub(theoreticalMin).max(1).getDouble(0), 0.0, 0.000000001);
        assertEquals(maxDataSet.sub(theoreticalMax).max(1).getDouble(0), 0.0, 0.000000001);

        sampleIter.setPreProcessor(myNormalizer);
        INDArray actual, expected, delta;
        int i = 1;
        while (sampleIter.hasNext()) {
            expected = theoreticalMin.mul(i - 1).div(theoreticalRange);
            actual = sampleIter.next().getFeatures();
            delta = Transforms.abs(actual.sub(expected));
            assertTrue(delta.max(1).getDouble(0) < 0.0001);
            i++;
        }

    }

    @ParameterizedTest
    @MethodSource("org.nd4j.linalg.BaseNd4jTestWithBackends#configs")
    public void testRevert(Nd4jBackend backend) {
        double tolerancePerc = 1; // 1% of correct value
        int nSamples = 500;
        int nFeatures = 3;

        Nd4j.getRandom().setSeed(12345);
        INDArray featureSet = Nd4j.rand(nSamples, nFeatures);
        INDArray labelSet = Nd4j.zeros(nSamples, 1);
        DataSet sampleDataSet = new DataSet(featureSet, labelSet);

        NormalizerMinMaxScaler myNormalizer = new NormalizerMinMaxScaler();
        DataSet transformed = sampleDataSet.copy();

        myNormalizer.fit(sampleDataSet);
        myNormalizer.transform(transformed);
        myNormalizer.revert(transformed);
        INDArray delta = Transforms.abs(transformed.getFeatures().sub(sampleDataSet.getFeatures()))
                .div(sampleDataSet.getFeatures());
        double maxdeltaPerc = delta.max(0, 1).mul(100).getDouble(0);
        System.out.println("Delta: " + maxdeltaPerc);
        assertTrue(maxdeltaPerc < tolerancePerc);

    }

    @ParameterizedTest
    @MethodSource("org.nd4j.linalg.BaseNd4jTestWithBackends#configs")
    public void testGivenMaxMin(Nd4jBackend backend) {
        double tolerancePerc = 1; // 1% of correct value
        int nSamples = 500;
        int nFeatures = 3;

        Nd4j.getRandom().setSeed(12345);
        INDArray featureSet = Nd4j.rand(nSamples, nFeatures);
        INDArray labelSet = Nd4j.zeros(nSamples, 1);
        DataSet sampleDataSet = new DataSet(featureSet, labelSet);

        double givenMin = -1;
        double givenMax = 1;
        NormalizerMinMaxScaler myNormalizer = new NormalizerMinMaxScaler(givenMin, givenMax);
        DataSet transformed = sampleDataSet.copy();

        myNormalizer.fit(sampleDataSet);
        myNormalizer.transform(transformed);

        myNormalizer.revert(transformed);
        INDArray delta = Transforms.abs(transformed.getFeatures().sub(sampleDataSet.getFeatures()))
                .div(sampleDataSet.getFeatures());
        double maxdeltaPerc = delta.max(0, 1).mul(100).getDouble(0);
        System.out.println("Delta: " + maxdeltaPerc);
        assertTrue(maxdeltaPerc < tolerancePerc);
    }

    @ParameterizedTest
    @MethodSource("org.nd4j.linalg.BaseNd4jTestWithBackends#configs")
    public void testGivenMaxMinConstant(Nd4jBackend backend) {
        double tolerancePerc = 1; // 1% of correct value
        int nSamples = 500;
        int nFeatures = 3;

        INDArray featureSet = Nd4j.rand(nSamples, nFeatures).mul(0.1).add(10);
        INDArray labelSet = Nd4j.zeros(nSamples, 1);
        DataSet sampleDataSet = new DataSet(featureSet, labelSet);

        double givenMin = -1000;
        double givenMax = 1000;
        DataNormalization myNormalizer = new NormalizerMinMaxScaler(givenMin, givenMax);
        DataSet transformed = sampleDataSet.copy();

        myNormalizer.fit(sampleDataSet);
        myNormalizer.transform(transformed);

        //feature set is basically all 10s -> should transform to the min
        INDArray expected = Nd4j.ones(nSamples, nFeatures).mul(givenMin);
        INDArray delta = Transforms.abs(transformed.getFeatures().sub(expected)).div(expected);
        double maxdeltaPerc = delta.max(0, 1).mul(100).getDouble(0);
        assertTrue(maxdeltaPerc < tolerancePerc);
    }

    @ParameterizedTest
    @MethodSource("org.nd4j.linalg.BaseNd4jTestWithBackends#configs")
    public void testConstant(Nd4jBackend backend) {
        double tolerancePerc = 0.01; // 0.01% of correct value
        int nSamples = 500;
        int nFeatures = 3;

        INDArray featureSet = Nd4j.zeros(nSamples, nFeatures).add(100);
        INDArray labelSet = Nd4j.zeros(nSamples, 1);
        DataSet sampleDataSet = new DataSet(featureSet, labelSet);

        NormalizerMinMaxScaler myNormalizer = new NormalizerMinMaxScaler();
        myNormalizer.fit(sampleDataSet);
        myNormalizer.transform(sampleDataSet);
        assertFalse(Double.isNaN(sampleDataSet.getFeatures().min(0, 1).getDouble(0)));
        assertEquals(sampleDataSet.getFeatures().sumNumber().doubleValue(), 0, 0.00001);
        myNormalizer.revert(sampleDataSet);
        assertFalse(Double.isNaN(sampleDataSet.getFeatures().min(0, 1).getDouble(0)));
        assertEquals(sampleDataSet.getFeatures().sumNumber().doubleValue(), 100 * nFeatures * nSamples, 0.00001);
    }

    @Override
    public char ordering() {
        return 'c';
    }
}

