package net.ranides.assira.math;

import lombok.Data;

import net.ranides.assira.collection.lookups.Lookup;
import net.ranides.assira.collection.lookups.OpenLookup;
import net.ranides.assira.collection.query.CQuery;
import net.ranides.assira.generic.CompareUtils;

import java.util.DoubleSummaryStatistics;
import java.util.IntSummaryStatistics;
import java.util.List;

/**
 * Rudimentary implementation of statistic measures: mode, standard deviation
 */
public class MathStats {

    /**
     * Traverses whole input and establishes mode of data.
     *
     * @param data input
     * @param <T> input
     * @return mode stats
     */
    public static <T> Mode<T> mode(CQuery<T> data) {
        return new Mode<>(data);
    }

    /**
     * Traverses whole input and calculates average, variance and deviation
     * @param data input
     * @return standard deviation stats
     */
    public static DoubleDeviationSummary deviation(CQuery<? extends Number> data) {
        return new DoubleDeviationSummary(data);
    }

    /**
     * Traverses whole input and calculates average, variance and deviation
     * @param data input
     * @return standard deviation stats
     */
    public static IntDeviationSummary deviation(int... data) {
        return new IntDeviationSummary(CQuery.from().array(data).map(Integer.class));
    }

    /**
     * Basic statistics about most popular element
     *
     * @param <T> type of element
     */
    @Data
    public static class Mode<T> {

        /**
         * value of most popular element
         */
        private final T value;

        /**
         * number of occurrences of element inside data
         */
        private final int count;

        /**
         * number of elements in data
         */
        private final int domain;

        private Mode(CQuery<T> data) {
            List<T> list = data.list();

            this.domain = list.size();

            if(list.get(0) instanceof Comparable<?>) {
                list.sort(CompareUtils.comparator());
                T lastV = null;
                T currV = null;
                int lastC = 0;
                int currC = 0;

                for(T v : list) {
                    if(v == currV) {
                        currC++;
                    } else {
                        if(currC > lastC) {
                            lastV = currV;
                            lastC = currC;
                        }
                        currC = 1;
                        currV = v;
                    }
                }
                if(currC > lastC) {
                    lastV = currV;
                    lastC = currC;
                }
                this.value = lastV;
                this.count = lastC;

            } else {
                Lookup<T> lookup = new OpenLookup<>();
                T lastV = null;
                int lastC = 0;
                for(T v : list) {
                    int c = lookup.inc(v);
                    if(c > lastC) {
                        lastC = c;
                        lastV = v;
                    }
                }
                this.value = lastV;
                this.count = lastC;
            }

        }
    }

    /**
     * Basic statistics about classical average
     */
    @Data
    public static class DoubleDeviationSummary extends DoubleSummaryStatistics {

        /**
         * classic variance
         */
        private final double variance;

        /**
         * classic standard deviation
         */
        private final double deviation;

        private DoubleDeviationSummary(CQuery<? extends Number> data) {
            combine(data.stream().mapToDouble(Number::doubleValue).summaryStatistics());

            double avg = getAverage();
            DoubleSummaryStatistics statVar = data.stream()
                    .mapToDouble(Number::doubleValue)
                    .map(x -> MathUtils.square(avg - x))
                    .summaryStatistics();

            this.variance = statVar.getAverage();
            this.deviation = Math.sqrt(statVar.getAverage());
        }
    }

    /**
     * Basic statistics about classical average
     */
    @Data
    public static class IntDeviationSummary extends IntSummaryStatistics {

        /**
         * classic variance
         */
        private final double variance;

        /**
         * classic standard deviation
         */
        private final double deviation;

        private IntDeviationSummary(CQuery<? extends Number> data) {
            combine(data.stream().mapToInt(Number::intValue).summaryStatistics());

            double avg = getAverage();
            DoubleSummaryStatistics statVar = data.stream()
                .mapToInt(Number::intValue)
                .mapToDouble(x -> MathUtils.square(avg - x))
                .summaryStatistics();

            this.variance = statVar.getAverage();
            this.deviation = Math.sqrt(statVar.getAverage());
        }
    }
}
