/**
 * Written by Gil Tene of Azul Systems, and released to the public domain,
 * as explained at http://creativecommons.org/publicdomain/zero/1.0/
 *
 * @author Gil Tene
 */

package org.HdrHistogram;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.nio.ByteBuffer;
import java.nio.LongBuffer;
import java.util.concurrent.atomic.AtomicLongArray;
import java.util.concurrent.atomic.AtomicLongFieldUpdater;
import java.util.zip.DataFormatException;

/**
 * <h3>An integer values High Dynamic Range (HDR) Histogram that supports safe concurrent recording operations.</h3>
 * A ConcurrentHistogram guarantees lossless recording of values into the histogram even when the
 * histogram is updated by multiple threads, and supports auto-resize and shift operations that may
 * result from or occur concurrently with other recording operations.
 * <p>
 * It is important to note that concurrent recording, auto-sizing, and value shifting are the only thread-safe
 * behaviors provided by {@link ConcurrentHistogram}, and that it is not otherwise synchronized. Specifically, {@link
 * ConcurrentHistogram} provides no implicit synchronization that would prevent the contents of the histogram
 * from changing during queries, iterations, copies, or addition operations on the histogram. Callers wishing to make
 * potentially concurrent, multi-threaded updates that would safely work in the presence of queries, copies, or
 * additions of histogram objects should either take care to externally synchronize and/or order their access,
 * use the {@link SynchronizedHistogram} variant, or (recommended) use {@link Recorder} or
 * {@link SingleWriterRecorder} which are intended for this purpose.
 * <p>
 * Auto-resizing: When constructed with no specified value range range (or when auto-resize is turned on with {@link
 * Histogram#setAutoResize}) a {@link Histogram} will auto-resize its dynamic range to include recorded values as
 * they are encountered. Note that recording calls that cause auto-resizing may take longer to execute, as resizing
 * incurs allocation and copying of internal data structures.
 * <p>
 * See package description for {@link org.HdrHistogram} for details.
 */

public class ConcurrentHistogram extends Histogram {

    static final AtomicLongFieldUpdater<ConcurrentHistogram> totalCountUpdater =
            AtomicLongFieldUpdater.newUpdater(ConcurrentHistogram.class, "totalCount");
    volatile long totalCount;

    volatile AtomicLongArrayWithNormalizingOffset activeCounts;
    volatile AtomicLongArrayWithNormalizingOffset inactiveCounts;
    transient WriterReaderPhaser wrp = new WriterReaderPhaser();

    @Override
    void setIntegerToDoubleValueConversionRatio(final double integerToDoubleValueConversionRatio) {
        try {
            wrp.readerLock();

            inactiveCounts.doubleToIntegerValueConversionRatio = 1.0 / integerToDoubleValueConversionRatio;

            // switch active and inactive:
            AtomicLongArrayWithNormalizingOffset tmp = activeCounts;
            activeCounts = inactiveCounts;
            inactiveCounts = tmp;

            wrp.flipPhase();

            inactiveCounts.doubleToIntegerValueConversionRatio = 1.0 / integerToDoubleValueConversionRatio;

            // switch active and inactive again:
            tmp = activeCounts;
            activeCounts = inactiveCounts;
            inactiveCounts = tmp;

            wrp.flipPhase();

            // At this point, both active and inactive have normalizingIndexOffset safely set,
            // and the switch in each was done without any writers using the wrong value in flight.

        } finally {
            wrp.readerUnlock();
        }
        super.setIntegerToDoubleValueConversionRatio(integerToDoubleValueConversionRatio);
    }

    @Override
    long getCountAtIndex(final int index) {
        try {
            wrp.readerLock();
            assert (countsArrayLength == activeCounts.length());
            assert (countsArrayLength == inactiveCounts.length());
            long activeCount = activeCounts.get(
                    normalizeIndex(index, activeCounts.getNormalizingIndexOffset(), activeCounts.length()));
            long inactiveCount = inactiveCounts.get(
                    normalizeIndex(index, inactiveCounts.getNormalizingIndexOffset(), inactiveCounts.length()));
            return activeCount + inactiveCount;
        } finally {
            wrp.readerUnlock();
        }
    }

    @Override
    long getCountAtNormalizedIndex(final int index) {
        try {
            wrp.readerLock();
            assert (countsArrayLength == activeCounts.length());
            assert (countsArrayLength == inactiveCounts.length());
            long activeCount = activeCounts.get(index);
            long inactiveCount = inactiveCounts.get(index);
            return activeCount + inactiveCount;
        } finally {
            wrp.readerUnlock();
        }
    }

    @Override
    void incrementCountAtIndex(final int index) {
        long criticalValue = wrp.writerCriticalSectionEnter();
        try {
            activeCounts.incrementAndGet(
                    normalizeIndex(index, activeCounts.getNormalizingIndexOffset(), activeCounts.length()));
        } finally {
            wrp.writerCriticalSectionExit(criticalValue);
        }
    }

    @Override
    void addToCountAtIndex(final int index, final long value) {
        long criticalValue = wrp.writerCriticalSectionEnter();
        try {
            activeCounts.addAndGet(
                    normalizeIndex(index, activeCounts.getNormalizingIndexOffset(), activeCounts.length()), value);
        } finally {
            wrp.writerCriticalSectionExit(criticalValue);
        }
    }

    @Override
    void setCountAtIndex(final int index, final long value) {
        try {
            wrp.readerLock();
            assert (countsArrayLength == activeCounts.length());
            assert (countsArrayLength == inactiveCounts.length());
            activeCounts.lazySet(
                    normalizeIndex(index, activeCounts.getNormalizingIndexOffset(), activeCounts.length()), value);
            inactiveCounts.lazySet(
                    normalizeIndex(index, inactiveCounts.getNormalizingIndexOffset(), inactiveCounts.length()), 0);
        } finally {
            wrp.readerUnlock();
        }
    }

    @Override
    void setCountAtNormalizedIndex(final int index, final long value) {
        try {
            wrp.readerLock();
            assert (countsArrayLength == activeCounts.length());
            assert (countsArrayLength == inactiveCounts.length());
            inactiveCounts.lazySet(index, value);
            activeCounts.lazySet(index, 0);
        } finally {
            wrp.readerUnlock();
        }
    }

    @Override
    void recordConvertedDoubleValue(final double value) {
        long criticalValue = wrp.writerCriticalSectionEnter();
        try {
            long integerValue = (long) (value * activeCounts.doubleToIntegerValueConversionRatio);
            int index = countsArrayIndex(integerValue);
            activeCounts.incrementAndGet(
                    normalizeIndex(index, activeCounts.getNormalizingIndexOffset(), activeCounts.length()));
            updateMinAndMax(integerValue);
            incrementTotalCount();
        } finally {
            wrp.writerCriticalSectionExit(criticalValue);
        }
    }

    @Override
    public void recordConvertedDoubleValueWithCount(final double value, final long count) throws ArrayIndexOutOfBoundsException {
        long criticalValue = wrp.writerCriticalSectionEnter();
        try {
            long integerValue = (long) (value * activeCounts.doubleToIntegerValueConversionRatio);
            int index = countsArrayIndex(integerValue);
            activeCounts.addAndGet(
                    normalizeIndex(index, activeCounts.getNormalizingIndexOffset(), activeCounts.length()), count);
            updateMinAndMax(integerValue);
            addToTotalCount(count);
        } finally {
            wrp.writerCriticalSectionExit(criticalValue);
        }
    }

    @Override
    int getNormalizingIndexOffset() {
        return activeCounts.getNormalizingIndexOffset();
    }

    @Override
    void setNormalizingIndexOffset(final int normalizingIndexOffset) {
        setNormalizingIndexOffset(normalizingIndexOffset, 0, false, getIntegerToDoubleValueConversionRatio());
    }

    private void setNormalizingIndexOffset(
            final int normalizingIndexOffset,
            final int shiftedAmount,
            final boolean lowestHalfBucketPopulated,
            final double newIntegerToDoubleValueConversionRatio) {
        try {
            wrp.readerLock();

            assert (countsArrayLength == activeCounts.length());
            assert (countsArrayLength == inactiveCounts.length());

            assert (activeCounts.getNormalizingIndexOffset() == inactiveCounts.getNormalizingIndexOffset());

            if (normalizingIndexOffset == activeCounts.getNormalizingIndexOffset()) {
                return; // Nothing to do.
            }

            // Save and clear the inactive 0 value count:
            int zeroIndex = normalizeIndex(0, inactiveCounts.getNormalizingIndexOffset(), inactiveCounts.length());
            long inactiveZeroValueCount = inactiveCounts.get(zeroIndex);
            inactiveCounts.lazySet(zeroIndex, 0);

            // Change the normalizingIndexOffset on the current inactiveCounts:
            inactiveCounts.setNormalizingIndexOffset(normalizingIndexOffset);

            // Handle the inactive lowest half bucket:
            if ((shiftedAmount > 0) && lowestHalfBucketPopulated) {
                shiftLowestInactiveHalfBucketContentsLeft(shiftedAmount, zeroIndex);
            }

            // Restore the inactive 0 value count:
            zeroIndex = normalizeIndex(0, inactiveCounts.getNormalizingIndexOffset(), inactiveCounts.length());
            inactiveCounts.lazySet(zeroIndex, inactiveZeroValueCount);

            inactiveCounts.doubleToIntegerValueConversionRatio = 1.0 / newIntegerToDoubleValueConversionRatio;

            // switch active and inactive:
            AtomicLongArrayWithNormalizingOffset tmp = activeCounts;
            activeCounts = inactiveCounts;
            inactiveCounts = tmp;

            wrp.flipPhase();

            // Save and clear the newly inactive 0 value count:
            zeroIndex = normalizeIndex(0, inactiveCounts.getNormalizingIndexOffset(), inactiveCounts.length());
            inactiveZeroValueCount = inactiveCounts.get(zeroIndex);
            inactiveCounts.lazySet(zeroIndex, 0);

            // Change the normalizingIndexOffset on the newly inactiveCounts:
            inactiveCounts.setNormalizingIndexOffset(normalizingIndexOffset);

            // Handle the newly inactive lowest half bucket:
            if ((shiftedAmount > 0) && lowestHalfBucketPopulated) {
                shiftLowestInactiveHalfBucketContentsLeft(shiftedAmount, zeroIndex);
            }

            // Restore the newly inactive 0 value count:
            zeroIndex = normalizeIndex(0, inactiveCounts.getNormalizingIndexOffset(), inactiveCounts.length());
            inactiveCounts.lazySet(zeroIndex, inactiveZeroValueCount);

            inactiveCounts.doubleToIntegerValueConversionRatio = 1.0 / newIntegerToDoubleValueConversionRatio;

            // switch active and inactive again:
            tmp = activeCounts;
            activeCounts = inactiveCounts;
            inactiveCounts = tmp;

            wrp.flipPhase();

            // At this point, both active and inactive have normalizingIndexOffset safely set,
            // and the switch in each was done without any writers using the wrong value in flight.

        } finally {
            wrp.readerUnlock();
        }
    }

    private void shiftLowestInactiveHalfBucketContentsLeft(final int shiftAmount, final int preShiftZeroIndex) {
        final int numberOfBinaryOrdersOfMagnitude = shiftAmount >> subBucketHalfCountMagnitude;

        // The lowest inactive half-bucket (not including the 0 value) is special: unlike all other half
        // buckets, the lowest half bucket values cannot be scaled by simply changing the
        // normalizing offset. Instead, they must be individually re-recorded at the new
        // scale, and cleared from the current one.
        //
        // We know that all half buckets "below" the current lowest one are full of 0s, because
        // we would have overflowed otherwise. So we need to shift the values in the current
        // lowest half bucket into that range (including the current lowest half bucket itself).
        // Iterating up from the lowermost non-zero "from slot" and copying values to the newly
        // scaled "to slot" (and then zeroing the "from slot"), will work in a single pass,
        // because the scale "to slot" index will always be a lower index than its or any
        // preceding non-scaled "from slot" index:
        //
        // (Note that we specifically avoid slot 0, as it is directly handled in the outer case)

        for (int fromIndex = 1; fromIndex < subBucketHalfCount; fromIndex++) {
            long toValue = valueFromIndex(fromIndex) << numberOfBinaryOrdersOfMagnitude;
            int toIndex = countsArrayIndex(toValue);
            int normalizedToIndex =
                    normalizeIndex(toIndex, inactiveCounts.getNormalizingIndexOffset(), inactiveCounts.length());
            long countAtFromIndex = inactiveCounts.get(fromIndex + preShiftZeroIndex);
            inactiveCounts.lazySet(normalizedToIndex, countAtFromIndex);
            inactiveCounts.lazySet(fromIndex + preShiftZeroIndex, 0);
        }

        // Note that the above loop only creates O(N) work for histograms that have values in
        // the lowest half-bucket (excluding the 0 value). Histograms that never have values
        // there (e.g. all integer value histograms used as internal storage in DoubleHistograms)
        // will never loop, and their shifts will remain O(1).
    }

    @Override
    void shiftNormalizingIndexByOffset(final int offsetToAdd,
                                       final boolean lowestHalfBucketPopulated,
                                       final double newIntegerToDoubleValueConversionRatio) {
        try {
            wrp.readerLock();
            assert (countsArrayLength == activeCounts.length());
            assert (countsArrayLength == inactiveCounts.length());
            int newNormalizingIndexOffset = getNormalizingIndexOffset() + offsetToAdd;
            setNormalizingIndexOffset(newNormalizingIndexOffset,
                    offsetToAdd,
                    lowestHalfBucketPopulated,
                    newIntegerToDoubleValueConversionRatio
                    );
        } finally {
            wrp.readerUnlock();
        }
    }

    @Override
    void resize(final long newHighestTrackableValue) {
        try {
            wrp.readerLock();

            assert (countsArrayLength == activeCounts.length());
            assert (countsArrayLength == inactiveCounts.length());

            int newArrayLength = determineArrayLengthNeeded(newHighestTrackableValue);
            int countsDelta = newArrayLength - countsArrayLength;

            if (countsDelta <= 0) {
                // This resize need was already covered by a concurrent resize op.
                return;
            }

            // Allocate both counts arrays here, so if one allocation fails, neither will "take":
            AtomicLongArrayWithNormalizingOffset newInactiveCounts1 =
                    new AtomicLongArrayWithNormalizingOffset(
                            newArrayLength,
                            inactiveCounts.getNormalizingIndexOffset()
                    );
            AtomicLongArrayWithNormalizingOffset newInactiveCounts2 =
                    new AtomicLongArrayWithNormalizingOffset(
                            newArrayLength,
                            activeCounts.getNormalizingIndexOffset()
                    );


            // Resize the current inactiveCounts:
            AtomicLongArrayWithNormalizingOffset oldInactiveCounts = inactiveCounts;
            inactiveCounts = newInactiveCounts1;

            // Copy inactive contents to newly sized inactiveCounts:
            copyInactiveCountsContentsOnResize(oldInactiveCounts, countsDelta);

            // switch active and inactive:
            AtomicLongArrayWithNormalizingOffset tmp = activeCounts;
            activeCounts = inactiveCounts;
            inactiveCounts = tmp;

            wrp.flipPhase();

            // Resize the newly inactiveCounts:
            oldInactiveCounts = inactiveCounts;
            inactiveCounts = newInactiveCounts2;

            // Copy inactive contents to newly sized inactiveCounts:
            copyInactiveCountsContentsOnResize(oldInactiveCounts, countsDelta);

            // switch active and inactive again:
            tmp = activeCounts;
            activeCounts = inactiveCounts;
            inactiveCounts = tmp;

            wrp.flipPhase();

            // At this point, both active and inactive have been safely resized,
            // and the switch in each was done without any writers modifying it in flight.

            // We resized things. We can now make the historam establish size accordingly for future recordings:
            establishSize(newHighestTrackableValue);

            assert (countsArrayLength == activeCounts.length());
            assert (countsArrayLength == inactiveCounts.length());

        } finally {
            wrp.readerUnlock();
        }
    }

    private void copyInactiveCountsContentsOnResize(
            AtomicLongArrayWithNormalizingOffset oldInactiveCounts, int countsDelta) {
        int oldNormalizedZeroIndex =
                normalizeIndex(0,
                        oldInactiveCounts.getNormalizingIndexOffset(),
                        oldInactiveCounts.length());

        // Copy old inactive contents to (current) newly sized inactiveCounts:
        for (int i = 0; i < oldInactiveCounts.length(); i++) {
            inactiveCounts.lazySet(i, oldInactiveCounts.get(i));
        }
        if (oldNormalizedZeroIndex != 0) {
            // We need to shift the stuff from the zero index and up to the end of the array:
            int newNormalizedZeroIndex = oldNormalizedZeroIndex + countsDelta;
            int lengthToCopy = (inactiveCounts.length() - countsDelta) - oldNormalizedZeroIndex;
            int src, dst;
            for (src = oldNormalizedZeroIndex, dst = newNormalizedZeroIndex;
                 src < oldNormalizedZeroIndex + lengthToCopy;
                 src++, dst++) {
                inactiveCounts.lazySet(dst, oldInactiveCounts.get(src));
            }
            for (dst = oldNormalizedZeroIndex; dst < newNormalizedZeroIndex; dst++) {
                inactiveCounts.lazySet(dst, 0);
            }
        }
    }


    @Override
    public void setAutoResize(final boolean autoResize) {
        this.autoResize = true;
    }

    @Override
    void clearCounts() {
        try {
            wrp.readerLock();
            assert (countsArrayLength == activeCounts.length());
            assert (countsArrayLength == inactiveCounts.length());
            for (int i = 0; i < activeCounts.length(); i++) {
                activeCounts.lazySet(i, 0);
                inactiveCounts.lazySet(i, 0);
            }
            totalCountUpdater.set(this, 0);
        } finally {
            wrp.readerUnlock();
        }
    }

    @Override
    public ConcurrentHistogram copy() {
        ConcurrentHistogram copy = new ConcurrentHistogram(this);
        copy.add(this);
        return copy;
    }

    @Override
    public ConcurrentHistogram copyCorrectedForCoordinatedOmission(final long expectedIntervalBetweenValueSamples) {
        ConcurrentHistogram toHistogram = new ConcurrentHistogram(this);
        toHistogram.addWhileCorrectingForCoordinatedOmission(this, expectedIntervalBetweenValueSamples);
        return toHistogram;
    }

    @Override
    public long getTotalCount() {
        return totalCountUpdater.get(this);
    }

    @Override
    void setTotalCount(final long totalCount) {
        totalCountUpdater.set(this, totalCount);
    }

    @Override
    void incrementTotalCount() {
        totalCountUpdater.incrementAndGet(this);
    }

    @Override
    void addToTotalCount(final long value) {
        totalCountUpdater.addAndGet(this, value);
    }

    @Override
    int _getEstimatedFootprintInBytes() {
        return (512 + (2 * 8 * activeCounts.length()));
    }

    /**
     * Construct an auto-resizing ConcurrentHistogram with a lowest discernible value of 1 and an auto-adjusting
     * highestTrackableValue. Can auto-resize up to track values up to (Long.MAX_VALUE / 2).
     *
     * @param numberOfSignificantValueDigits Specifies the precision to use. This is the number of significant
     *                                       decimal digits to which the histogram will maintain value resolution
     *                                       and separation. Must be a non-negative integer between 0 and 5.
     */
    public ConcurrentHistogram(final int numberOfSignificantValueDigits) {
        this(1, 2, numberOfSignificantValueDigits);
        setAutoResize(true);
    }

    /**
     * Construct a ConcurrentHistogram given the Highest value to be tracked and a number of significant decimal digits. The
     * histogram will be constructed to implicitly track (distinguish from 0) values as low as 1.
     *
     * @param highestTrackableValue The highest value to be tracked by the histogram. Must be a positive
     *                              integer that is {@literal >=} 2.
     * @param numberOfSignificantValueDigits Specifies the precision to use. This is the number of significant
     *                                       decimal digits to which the histogram will maintain value resolution
     *                                       and separation. Must be a non-negative integer between 0 and 5.
     */
    public ConcurrentHistogram(final long highestTrackableValue, final int numberOfSignificantValueDigits) {
        this(1, highestTrackableValue, numberOfSignificantValueDigits);
    }

    /**
     * Construct a ConcurrentHistogram given the Lowest and Highest values to be tracked and a number of significant
     * decimal digits. Providing a lowestDiscernibleValue is useful is situations where the units used
     * for the histogram's values are much smaller that the minimal accuracy required. E.g. when tracking
     * time values stated in nanosecond units, where the minimal accuracy required is a microsecond, the
     * proper value for lowestDiscernibleValue would be 1000.
     *
     * @param lowestDiscernibleValue The lowest value that can be tracked (distinguished from 0) by the histogram.
     *                               Must be a positive integer that is {@literal >=} 1. May be internally rounded
     *                               down to nearest power of 2.
     * @param highestTrackableValue The highest value to be tracked by the histogram. Must be a positive
     *                              integer that is {@literal >=} (2 * lowestDiscernibleValue).
     * @param numberOfSignificantValueDigits Specifies the precision to use. This is the number of significant
     *                                       decimal digits to which the histogram will maintain value resolution
     *                                       and separation. Must be a non-negative integer between 0 and 5.
     */
    public ConcurrentHistogram(final long lowestDiscernibleValue, final long highestTrackableValue,
                               final int numberOfSignificantValueDigits) {
        super(lowestDiscernibleValue, highestTrackableValue, numberOfSignificantValueDigits, false);
        activeCounts = new AtomicLongArrayWithNormalizingOffset(countsArrayLength, 0);
        inactiveCounts = new AtomicLongArrayWithNormalizingOffset(countsArrayLength, 0);
        wordSizeInBytes = 8;
    }

    /**
     * Construct a histogram with the same range settings as a given source histogram,
     * duplicating the source's start/end timestamps (but NOT it's contents)
     * @param source The source histogram to duplicate
     */
    public ConcurrentHistogram(final AbstractHistogram source) {
        super(source, false);
        activeCounts = new AtomicLongArrayWithNormalizingOffset(countsArrayLength, 0);
        inactiveCounts = new AtomicLongArrayWithNormalizingOffset(countsArrayLength, 0);
        wordSizeInBytes = 8;
    }

    /**
     * Construct a new histogram by decoding it from a ByteBuffer.
     * @param buffer The buffer to decode from
     * @param minBarForHighestTrackableValue Force highestTrackableValue to be set at least this high
     * @return The newly constructed histogram
     */
    public static ConcurrentHistogram decodeFromByteBuffer(final ByteBuffer buffer,
                                                           final long minBarForHighestTrackableValue) {
        return (ConcurrentHistogram) decodeFromByteBuffer(buffer, ConcurrentHistogram.class,
                minBarForHighestTrackableValue);
    }

    /**
     * Construct a new histogram by decoding it from a compressed form in a ByteBuffer.
     * @param buffer The buffer to decode from
     * @param minBarForHighestTrackableValue Force highestTrackableValue to be set at least this high
     * @return The newly constructed histogram
     * @throws java.util.zip.DataFormatException on error parsing/decompressing the buffer
     */
    public static ConcurrentHistogram decodeFromCompressedByteBuffer(final ByteBuffer buffer,
                                                                     final long minBarForHighestTrackableValue) throws DataFormatException {
        return (ConcurrentHistogram) decodeFromCompressedByteBuffer(buffer, ConcurrentHistogram.class,
                minBarForHighestTrackableValue);
    }

    private void readObject(final ObjectInputStream o)
            throws IOException, ClassNotFoundException {
        o.defaultReadObject();
        wrp = new WriterReaderPhaser();
    }

    @Override
    synchronized void fillCountsArrayFromBuffer(final ByteBuffer buffer, final int length) {
        LongBuffer logbuffer = buffer.asLongBuffer();
        for (int i = 0; i < length; i++) {
            inactiveCounts.lazySet(i, logbuffer.get());
            activeCounts.lazySet(i, 0);
        }
    }

    @Override
    synchronized void fillBufferFromCountsArray(final ByteBuffer buffer) {
        try {
            wrp.readerLock();
            super.fillBufferFromCountsArray(buffer);
        } finally {
            wrp.readerUnlock();
        }
    }

    static class AtomicLongArrayWithNormalizingOffset extends AtomicLongArray {

        private int normalizingIndexOffset;
        private double doubleToIntegerValueConversionRatio;

        AtomicLongArrayWithNormalizingOffset(int length, int normalizingIndexOffset) {
            super(length);
            this.normalizingIndexOffset = normalizingIndexOffset;
        }

        public int getNormalizingIndexOffset() {
            return normalizingIndexOffset;
        }

        public void setNormalizingIndexOffset(int normalizingIndexOffset) {
            this.normalizingIndexOffset = normalizingIndexOffset;
        }
    }
}
