package de.ivu.elect.business.auffaelligkeit.entity;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import org.apache.commons.math3.exception.DimensionMismatchException;
import org.apache.commons.math3.stat.inference.TestUtils;

public enum BenfordStatistischerTestArt {
  CHI_QUADRAT_ANPASSUNGSTEST {
    @Override
    public double berechneP_value(double[] expected, long[] observed) {
      return TestUtils.chiSquareTest(expected, observed);
    }
  },


  G_TEST {
    @Override
    public double berechneP_value(double[] expected, long[] observed) {
      double[] expectedCopy = Arrays.copyOf(expected, expected.length);
      long[] observedCopy = Arrays.copyOf(observed, observed.length);

      makeAllObservedCountsStrictlyPositive (expectedCopy, observedCopy);

      return TestUtils.gTest(expectedCopy, observedCopy);
    }
  },

  FLAECHEN_RATIO {
    class Point {
      double x, y;
      Point(double x, double y) {
        this.x = x;
        this.y = y;
      }
    }class Segment {
      Point p1, p2;
      double slope, constant;

      Segment(Point p1, Point p2) {
        if (p1.x <= p2.x) {
          this.p1 = p1;
          this.p2 = p2;
        } else {
          this.p1 = p2;
          this.p2 = p1;
        }
        this.slope = (p2.y - p1.y) / (p2.x - p1.x);
        this.constant = p2.y - this.slope * p2.x;
      }

      Point getIntersectionPointWithAnotherSegment(Segment that) {
        double m1 = this.slope;
        double m2 = that.slope;
        if (m1 == m2) {
          return null;
        }
        double x = (that.constant - this.constant)/(m1 - m2);
        if (x < p1.x || x > p2.x) {
          return null;
        }
        double y = m1 * x + this.constant;
        return new Point(x, y);
      }

      double getAreaUnderSegment() {
        // Angenommen, dass die Linie ueber die x-Achse liegt.
        return slope / 2 * (p2.x * p2.x - p1.x * p1.x) + constant * (p2.x - p1.x);
      }

      double getAreaBetweenTwoSegments(Segment that) {
        // Angenommen, dass die Linien das gleiche Interval teilen.
        if (this.p1.y < that.p1.y) {
          return that.getAreaBetweenTwoSegments(this);
        }
        Point intersectionPoint = getIntersectionPointWithAnotherSegment(that);
        if (intersectionPoint == null) {
          intersectionPoint = this.p2;
        }
        double areaBeforeIntersection = (this.slope - that.slope) / 2 * (intersectionPoint.x * intersectionPoint.x - p1.x * p1.x) + (this.constant - that.constant) * (intersectionPoint.x - p1.x);
        double areaAfterIntersection = (that.slope - this.slope) / 2 * (p2.x * p2.x - intersectionPoint.x * intersectionPoint.x) + (that.constant - this.constant) * (p2.x - intersectionPoint.x);
        return areaBeforeIntersection + areaAfterIntersection;
      }
    }

    @Override
    public double berechneP_value(double[] expected, long[] observed) {
      double areaBetweenExpectedAndObserved = 0;
      double areaUnderExpected = 0;
      assert expected.length == observed.length;
      for (int i = 0; i < expected.length - 1; i++) {
        Segment expectedSegment = new Segment(new Point(i, expected[i]), new Point(i + 1, expected[i + 1]));
        Segment observedSegment = new Segment(new Point(i, observed[i]), new Point(i + 1, observed[i + 1]));
        areaBetweenExpectedAndObserved += expectedSegment.getAreaBetweenTwoSegments(observedSegment);
        areaUnderExpected += expectedSegment.getAreaUnderSegment();
      }
      if (areaUnderExpected == 0) {
        return 1d;
      }
      return areaBetweenExpectedAndObserved / areaUnderExpected;
    }
  },


  KOLMOGOROV_SMIRNOV_TEST {
    @Override
    public double berechneP_value(double[] expectedCounts, long[] observedCounts) {
      List<Double> expectedPoints = new ArrayList<>();
      List<Double> observedPoints = new ArrayList<>();

      for (int i = 0; i < expectedCounts.length;i++) {
        long expectedCount = Math.round(expectedCounts[i]);
        expectedPoints.addAll(Collections.nCopies((int)expectedCount, (double)i));
        observedPoints.addAll(Collections.nCopies((int)observedCounts[i], (double)i));
      }

      if (observedPoints.size() < 2 || expectedPoints.size() < 2) {
        // Sample is too small
        return 1;
      }

      double[] expectedAsArray = expectedPoints.stream().mapToDouble(Double::doubleValue).toArray();
      double[] observedAsArray = observedPoints.stream().mapToDouble(Double::doubleValue).toArray();

      return TestUtils.kolmogorovSmirnovTest(expectedAsArray, observedAsArray);
    }
  };

  void normiereWahrscheinlichkeiten(double[] wahrscheinlichkeiten) {
      double summe = Arrays.stream(wahrscheinlichkeiten).sum();

      if (summe == 0) {
        return;
      }

      for (double wahrscheinlichkeit: wahrscheinlichkeiten) {
        wahrscheinlichkeit /= summe;
      }
  }

  void makeAllObservedCountsStrictlyPositive(double[] expected, long[] observed) {
    if (expected.length != observed.length) {
      throw new DimensionMismatchException(expected.length, observed.length);
    }

    for (int i = 0; i<observed.length;i++) {
      if (observed[i] == 0) {
        observed[i]++;
      }
    }
  }

  public abstract double berechneP_value (double[] expected, long[] observed);
}
