/*
 *
 *  Copyright 2016 Vladimir Bukhtoyarov
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *          http://www.apache.org/licenses/LICENSE-2.0
 *
 *   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.
 */

package com.github.rollingmetrics.histogram;

import com.codahale.metrics.Reservoir;
import com.codahale.metrics.Snapshot;
import com.github.rollingmetrics.histogram.accumulator.Accumulator;
import org.HdrHistogram.Histogram;
import org.HdrHistogram.HistogramIterationValue;

import java.io.*;
import java.util.Arrays;
import java.util.Optional;
import java.util.function.Function;

import static java.nio.charset.StandardCharsets.UTF_8;

/**
 * A {@link com.codahale.metrics.Reservoir} implementation backed by {@link org.HdrHistogram.Recorder}
 *
 * This class is not the part of metrics-core-hdr public API and should not be used by user directly.
 *
 * @see HdrBuilder
 */
class HdrReservoir implements Reservoir {

    private final Accumulator accumulator;
    private final Function<Histogram, Snapshot> snapshotTaker;
    private final long highestTrackableValue;
    private final OverflowResolver overflowResolver;
    private final long expectedIntervalBetweenValueSamples;

    HdrReservoir(Accumulator accumulator, Optional<double[]> predefinedPercentiles, Optional<Long> highestTrackableValue, Optional<OverflowResolver> overflowResolver, Optional<Long> expectedIntervalBetweenValueSamples) {
        this.accumulator = accumulator;
        this.highestTrackableValue = highestTrackableValue.orElse(Long.MAX_VALUE);
        this.overflowResolver = overflowResolver.orElse(null);
        this.expectedIntervalBetweenValueSamples = expectedIntervalBetweenValueSamples.orElse(0L);

        if (predefinedPercentiles.isPresent()) {
            double[] percentiles = predefinedPercentiles.get();
            snapshotTaker = histogram -> takeSmartSnapshot(percentiles, histogram);
        } else {
            snapshotTaker = HdrReservoir::takeFullSnapshot;
        }
    }

    @Override
    public int size() {
        throw new UnsupportedOperationException("You should not use this method https://github.com/dropwizard/metrics/issues/874");
    }

    @Override
    public void update(long value) {
        if (value > highestTrackableValue) {
            switch (overflowResolver) {
                case SKIP: return;
                case PASS_THRU: break;
                case REDUCE_TO_HIGHEST_TRACKABLE: value = highestTrackableValue;
            }
        }
        accumulator.recordSingleValueWithExpectedInterval(value, expectedIntervalBetweenValueSamples);
    }

    @Override
    public Snapshot getSnapshot() {
        return accumulator.getSnapshot(snapshotTaker);
    }

    /**
     * Provide a (conservatively high) estimate of the Reservoir's total footprint in bytes
     *
     * @return a (conservatively high) estimate of the Reservoir's total footprint in bytes
     */
    public int getEstimatedFootprintInBytes() {
        return accumulator.getEstimatedFootprintInBytes();
    }

    static Snapshot takeSmartSnapshot(final double[] predefinedQuantiles, Histogram histogram) {
        final long max = histogram.getMaxValue();
        final long min = histogram.getMinValue();
        final double mean = histogram.getMean();
        final double median = histogram.getValueAtPercentile(50.0);
        final double stdDeviation = histogram.getStdDeviation();

        final double[] values = new double[predefinedQuantiles.length];
        for (int i = 0; i < predefinedQuantiles.length; i++) {
            double quantile = predefinedQuantiles[i];
            double percentile = quantile * 100.0;
            values[i] = histogram.getValueAtPercentile(percentile);
        }

        return createSmartSnapshot(predefinedQuantiles, max, min, mean, median, stdDeviation, values);
    }

    static Snapshot createSmartSnapshot(final double[] predefinedQuantiles, final long max, final long min, final double mean, final double median, final double stdDeviation, final double[] values) {
        return new Snapshot() {
            @Override
            public double getValue(double quantile) {
                for (int i = 0; i < predefinedQuantiles.length; i++) {
                    if (quantile <= predefinedQuantiles[i]) {
                        return values[i];
                    }
                }
                return max;
            }

            @Override
            public long[] getValues() {
                long[] toReturn = new long[values.length];
                for (int i = 0; i < values.length; i++) {
                    toReturn[i] = (long) values[i];
                }
                return toReturn;
            }

            @Override
            public int size() {
                return values.length;
            }

            public double getMedian() {
                return median;
            }

            @Override
            public long getMax() {
                return max;
            }

            @Override
            public double getMean() {
                return mean;
            }

            @Override
            public long getMin() {
                return min;
            }

            @Override
            public double getStdDev() {
                return stdDeviation;
            }

            @Override
            public void dump(OutputStream output) {
                try (PrintWriter p = new PrintWriter(new OutputStreamWriter(output, UTF_8))) {
                    for (double value : values) {
                        p.printf("%f%n", value);
                    }
                }
            }

            @Override
            public String toString() {
                StringBuilder distribution = new StringBuilder();
                for(int i = 0; i < predefinedQuantiles.length; i++) {
                    distribution.append(predefinedQuantiles[i] * 100).append("%:").append(values[i]).append("; ");
                }
                return "SmartSnapshot{" +
                        "max=" + max +
                        ", min=" + min +
                        ", mean=" + mean +
                        ", stdDeviation=" + stdDeviation +
                        ", distribution=" + distribution +
                        '}';
            }
        };
    }

    private static Snapshot takeFullSnapshot(final Histogram histogram) {
        return new Snapshot() {
            @Override
            public double getValue(double quantile) {
                double percentile = quantile * 100.0;
                return histogram.getValueAtPercentile(percentile);
            }

            @Override
            public long[] getValues() {
                long[] values = new long[1024];
                int i = 0;
                for (HistogramIterationValue value : histogram.recordedValues()) {
                    values[i] = value.getValueIteratedTo();
                    i++;
                    if (i == values.length) {
                        values = Arrays.copyOf(values, values.length * 2);
                    }
                }
                return Arrays.copyOf(values, i);
            }

            @Override
            public int size() {
                return (int) histogram.getTotalCount();
            }

            @Override
            public long getMax() {
                return histogram.getMaxValue();
            }

            @Override
            public double getMean() {
                return histogram.getMean();
            }

            @Override
            public long getMin() {
                return histogram.getMinValue();
            }

            @Override
            public double getStdDev() {
                return histogram.getStdDeviation();
            }

            @Override
            public void dump(OutputStream output) {
                try (PrintWriter p = new PrintWriter(new OutputStreamWriter(output, UTF_8))) {
                    for (HistogramIterationValue value : histogram.recordedValues()) {
                        for (int j = 0; j < value.getCountAddedInThisIterationStep(); j++) {
                            p.printf("%d%n", value.getValueIteratedTo());
                        }
                    }
                }
            }

            @Override
            public String toString() {
                ByteArrayOutputStream stream = new ByteArrayOutputStream();
                PrintStream printStream;
                printStream = new PrintStream(stream, true);
                histogram.outputPercentileDistribution(printStream, 1.0);
                String distributionAsString = new String(stream.toByteArray());
                return "FullSnapshot{" + distributionAsString + "}";
            }
        };
    }

    @Override
    public String toString() {
        return "HdrReservoir{" +
                "highestTrackableValue=" + highestTrackableValue +
                ", overflowResolver=" + overflowResolver +
                ", expectedIntervalBetweenValueSamples=" + expectedIntervalBetweenValueSamples +
                "\n accumulator=" + accumulator +
                '}';
    }

}