package cz.senslog.analytics.domain;

import java.time.OffsetDateTime;
import java.util.Objects;
import java.util.stream.DoubleStream;

public class DoubleStatistics implements TimeSeriesDatasource {

    private long count;
    private double sum;
    private double sumCompensation;
    private double simpleSum;
    private double min = 1.0D / 0.0;
    private double max = -1.0D / 0.0;
    private final OffsetDateTime timestamp;

    private final long analyticGroupId;

    public static DoubleStatistics init(long sourceId, OffsetDateTime timestamp) {
        return new DoubleStatistics(sourceId, timestamp);
    }

    private DoubleStatistics(long analyticGroupId, OffsetDateTime timestamp) {
        this.analyticGroupId = analyticGroupId;
        this.timestamp = timestamp;
    }

    public DoubleStatistics(DoubleStatistics statistics) {
        this(statistics.analyticGroupId, statistics.timestamp);
    }

    public DoubleStatistics(long analyticGroupId, DoubleStatistics statistics, OffsetDateTime timestamp) {
        this(analyticGroupId, statistics.count, statistics.min, statistics.max, statistics.sum, timestamp);
    }

    public DoubleStatistics(long analyticGroupId, long count, double min, double max, double sum, OffsetDateTime timestamp) {
        this.analyticGroupId = analyticGroupId;
        this.timestamp = timestamp;
        if (count < 0L) {
            throw new IllegalArgumentException("Negative count value");
        } else {
            if (count > 0L) {
                if (min > max) {
                    throw new IllegalArgumentException("Minimum greater than maximum");
                }

                long nCount = DoubleStream.of(min, max, sum).filter(Double::isNaN).count();
                if (nCount > 0L && nCount < 3L) {
                    throw new IllegalArgumentException("Some, not all, of the minimum, maximum, or sum is NaN");
                }

                this.count = count;
                this.sum = sum;
                this.simpleSum = sum;
                this.sumCompensation = 0.0D;
                this.min = min;
                this.max = max;
            }
        }
    }

    private void sumWithCompensation(double value) {
        double tmp = value - this.sumCompensation;
        double velVel = this.sum + tmp;
        this.sumCompensation = velVel - this.sum - tmp;
        this.sum = velVel;
    }

    public final long count() {
        return this.count;
    }

    public final double sum() {
        double tmp = this.sum + this.sumCompensation;
        return Double.isNaN(tmp) && Double.isInfinite(this.simpleSum) ? this.simpleSum : tmp;
    }

    public final double min() {
        return this.min;
    }

    public final double max() {
        return this.max;
    }

    public final double average() {
        return this.count() > 0L ? this.sum() / (double)this.count() : 0.0D;
    }

    public OffsetDateTime timestamp() {
        return timestamp;
    }

    private boolean acceptSource(long sourceId) {
        return sourceId == datasourceId();
    }

    public boolean accept(DoubleStatistics other) {
        if (acceptSource(other.datasourceId())) {
            this.count += other.count;
            this.simpleSum += other.simpleSum;
            this.sumWithCompensation(other.sum);
            this.sumWithCompensation(other.sumCompensation);
            this.min = Math.min(this.min, other.min);
            this.max = Math.max(this.max, other.max);
            return true;
        }
        return false;
    }

    public boolean accept(long sourceId, double value) {
        if (acceptSource(sourceId)) {
            ++this.count;
            this.simpleSum += value;
            this.sumWithCompensation(value);
            this.min = Math.min(this.min, value);
            this.max = Math.max(this.max, value);
            return true;
        }
        return false;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        DoubleStatistics that = (DoubleStatistics) o;
        return datasourceId() == that.datasourceId() &&
                min() == that.min() &&
                max() == that.max() &&
                sum() == that.sum() &&
                average() == that.average() &&
                count() == that.count();
    }

    @Override
    public int hashCode() {
        return Objects.hash(datasourceId(), min(), max(), average(), sum(), count());
    }

    @Override
    public long datasourceId() {
        return analyticGroupId;
    }
}
