package cz.it4i.qcmp.utilities;

import cz.it4i.qcmp.U16;
import cz.it4i.qcmp.data.Range;

import java.util.ArrayList;


public class Utils {

    public static Range<Integer> singlePlaneRange(final int index) {
        return new Range<>(index, index + 1);
    }

    public static double calculatePsnr(final double mse, final int signalMax) {
        final double psnr = 10.0 * Math.log10((Math.pow(signalMax, 2) / mse));
        return psnr;
    }

    public static <T> boolean arrayContains(final T[] array, final T element) {
        for (int i = 0; i < array.length; i++) {
            if (array[i].equals(element))
                return true;
        }
        return false;
    }

    public static double arrayListMax(final ArrayList<Double> array) {
        double max = Double.MIN_VALUE;
        for (final double val : array) {
            if (val > max) {
                max = val;
            }
        }
        return max;
    }

    public static boolean arrayContainsToIndex(final int[] array, final int toIndex, final int element) {
        for (int i = 0; i < toIndex; i++) {
            if (array[i] == element)
                return true;
        }
        return false;
    }


    public static double arrayListSum(final ArrayList<Double> array) {
        double sum = 0.0;
        for (final double val : array) {
            sum += val;
        }
        return sum;
    }


    public static int[] getDifference(final int[] original, final int[] transformed) {
        assert (original.length == transformed.length) : "Array lengths doesn't match";
        final int[] difference = new int[original.length];
        differenceToArray(original, transformed, difference);
        return difference;
    }

    public static void differenceToArray(final int[] original, final int[] transformed, final int[] result) {
        assert (original.length == transformed.length) : "Array lengths doesn't match";
        assert (original.length == result.length) : "Array lengths doesn't match with result array.";
        for (int i = 0; i < original.length; i++) {
            result[i] = (original[i] - transformed[i]);
        }
    }

    public static int[] asAbsoluteValues(final int[] values) {
        final int[] absValues = new int[values.length];
        for (int i = 0; i < values.length; i++) {
            absValues[i] = Math.abs(values[i]);
        }
        return absValues;
    }

    public static void applyAbsFunction(final int[] values) {
        for (int i = 0; i < values.length; i++) {
            values[i] = Math.abs(values[i]);
        }
    }


    public static MinMaxResult<Integer> getMinAndMax(final int[] data) {
        int min = Integer.MAX_VALUE;
        int max = Integer.MIN_VALUE;

        for (int i = 0; i < data.length; i++) {
            if (data[i] < min) {
                min = data[i];
            }
            if (data[i] > max) {
                max = data[i];
            }
        }
        return new MinMaxResult<Integer>(min, max);
    }

    /**
     * Calculate individual pixel values frequencies.
     * Same result as of histogram of bin width = 1
     *
     * @param data Pixel data.
     * @return Pixel value frequencies.
     */
    public static double[] calculatePixelFrequencies(final int[] data) {
        final double[] frequencies = new double[U16.Max + 1];
        for (final int pixelValue : data) {
            ++frequencies[pixelValue];
        }
        return frequencies;
    }

    public static double log2(final double v) {
        return (Math.log(v) / Math.log(2));
    }

    public static double calculateEntropy(final int[] pixelData) {
        final double pixelCount = pixelData.length;
        final double[] pixelFrequencies = Utils.calculatePixelFrequencies(pixelData);
        final double[] pixelProbabilities = new double[pixelFrequencies.length];

        for (int i = 0; i < pixelFrequencies.length; i++) {
            pixelProbabilities[i] = pixelFrequencies[i] / pixelCount;
        }

        double entropy = 0.0;

        for (final double pixelProbability : pixelProbabilities) {
            if (pixelProbability > 0.0) {
                entropy += pixelProbability * log2(pixelProbability);
            }
        }
        return (-entropy);
    }

    public static double calculateMse(final int[] difference) {
        double sum = 0.0;
        for (final int val : difference) {
            sum += Math.pow(val, 2);
        }
        final double mse = (sum / (double) difference.length);
        return mse;
    }

    /**
     * Calculate the euclidean distance between two vectors.
     *
     * @param v1 First vector.
     * @param v2 Second vector.
     * @return Euclidean distance.
     */
    public static double calculateEuclideanDistance(final int[] v1, final int[] v2) {
        assert (v1.length == v2.length);
        double sum = 0.0;
        for (int i = 0; i < v1.length; i++) {
            sum += Math.pow(((double) v1[i] - (double) v2[i]), 2);
        }
        return Math.sqrt(sum);
    }

    /**
     * Returns the product of the arguments,
     * throwing an exception if the result overflows an {@code int}.
     *
     * @param numbers values to multiply
     * @return the result
     * @throws ArithmeticException if the result overflows an int
     */
    public static int multiplyExact(final int... numbers) throws ArithmeticException {
        int result = numbers[0];
        for (int i = 1; i < numbers.length; i++) {
            result = Math.multiplyExact(result, numbers[i]);
        }
        return result;
    }

    /**
     * Returns the product of the arguments,
     * throwing an exception if the result overflows an {@code int}.
     *
     * @param numbers values to multiply
     * @return the result
     * @throws ArithmeticException if the result overflows an int
     */
    public static long multiplyExact(final long... numbers) throws ArithmeticException {
        long result = numbers[0];
        for (int i = 1; i < numbers.length; i++) {
            result = Math.multiplyExact(result, numbers[i]);
        }
        return result;
    }
}