package io.airlift.stats;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.base.Ticker;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterators;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import com.google.common.collect.Ordering;
import com.google.common.collect.PeekingIterator;
import com.google.common.util.concurrent.AtomicDouble;
import io.airlift.slice.BasicSliceInput;
import io.airlift.slice.DynamicSliceOutput;
import io.airlift.slice.SizeOf;
import io.airlift.slice.Slice;
import io.airlift.slice.SliceInput;
import io.airlift.slice.SliceOutput;
import org.openjdk.jol.info.ClassLayout;

import javax.annotation.concurrent.NotThreadSafe;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static io.airlift.stats.QuantileDigest.MiddleFunction.DEFAULT;
import static java.lang.String.format;

/**
 * Implements http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.132.7343, a data structure
 * for approximating quantiles by trading off error with memory requirements.
 *

 * The size of the digest is adjusted dynamically to achieve the error bound and requires
 * O(log2(U) / maxError) space, where U is the number of bits needed to represent the
 * domain of the values added to the digest. The error is defined as the discrepancy between the
 * real rank of the value returned in a quantile query and the rank corresponding to the queried
 * quantile.
 *

 * Thus, for a query for quantile q that returns value v, the error is
 * |rank(v) - q * N| / N, where N is the number of elements added to the digest and rank(v) is the
 * real rank of v
 *

 * This class also supports exponential decay. The implementation is based on the ideas laid out
 * in http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.159.3978
 */
@NotThreadSafe
public class QuantileDigest
{
    private static final int MAX_BITS = 64;
    private static final int QUANTILE_DIGEST_SIZE = ClassLayout.parseClass(QuantileDigest.class).instanceSize();

    // needs to be such that Math.exp(alpha * seconds) does not grow too big
    static final long RESCALE_THRESHOLD_SECONDS = 50;
    static final double ZERO_WEIGHT_THRESHOLD = 1e-5;

    private static final int INITIAL_CAPACITY = 1;

    private final double maxError;
    private final Ticker ticker;
    private final double alpha;
    private long landmarkInSeconds;

    private double weightedCount;
    private long max = Long.MIN_VALUE;
    private long min = Long.MAX_VALUE;

    private int root = -1;
    private int nextNode;
    private double[] counts;
    private byte[] levels;
    private long[] values;

    private int[] lefts;
    private int[] rights;

    // We use lefts[] to store a linked list of free slots.
    // freeIndex points to the first available slot
    private int freeCount;
    private int firstFree = -1;

    private enum TraversalOrder
    {
        FORWARD, REVERSE
    }

    /**
     *
Create a QuantileDigest with a maximum error guarantee of "maxError" and no decay.
     *
     * @param maxError the max error tolerance
     */
    public QuantileDigest(double maxError)
    {
        this(maxError, 0.0);
    }

    /**
     *
Create a QuantileDigest with a maximum error guarantee of "maxError" and exponential decay
     * with factor "alpha".

     *
     * @param maxError the max error tolerance
     * @param alpha the exponential decay factor
     */
    public QuantileDigest(double maxError, double alpha)
    {
        this(maxError, alpha, alpha == 0.0 ? noOpTicker() : Ticker.systemTicker());
    }

    @VisibleForTesting
    QuantileDigest(double maxError, double alpha, Ticker ticker)
    {
        checkArgument(maxError >= 0 && maxError <= 1, "maxError must be in range [0, 1]");
        checkArgument(alpha >= 0 && alpha < 1, "alpha must be in range [0, 1)");

        this.maxError = maxError;
        this.alpha = alpha;
        this.ticker = ticker;

        landmarkInSeconds = TimeUnit.NANOSECONDS.toSeconds(ticker.read());

        counts = new double[INITIAL_CAPACITY];
        levels = new byte[INITIAL_CAPACITY];
        values = new long[INITIAL_CAPACITY];

        lefts = new int[INITIAL_CAPACITY];
        rights = new int[INITIAL_CAPACITY];

        Arrays.fill(lefts, -1);
        Arrays.fill(rights, -1);
    }

    public QuantileDigest(QuantileDigest other)
    {
        this.maxError = other.maxError;
        this.alpha = other.alpha;
        this.ticker = alpha == 0.0 ? noOpTicker() : Ticker.systemTicker();

        this.landmarkInSeconds = other.landmarkInSeconds;
        this.weightedCount = other.weightedCount;

        this.max = other.max;
        this.min = other.min;
        this.root = other.root;
        this.nextNode = other.nextNode;
        this.counts = other.counts.clone();
        this.levels = other.levels.clone();
        this.values = other.values.clone();
        this.lefts = other.lefts.clone();
        this.rights = other.rights.clone();
        this.freeCount = other.freeCount;
        this.firstFree = other.firstFree;
    }

    public QuantileDigest(Slice serialized)
    {
        SliceInput input = new BasicSliceInput(serialized);

        byte format = input.readByte();
        checkArgument(format == 0, "Invalid format");
        maxError = input.readDouble();
        alpha = input.readDouble();

        if (alpha == 0.0) {
            ticker = noOpTicker();
        }
        else {
            ticker = Ticker.systemTicker();
        }
        landmarkInSeconds = input.readLong();

        min = input.readLong();
        max = input.readLong();
        int nodeCount = input.readInt();

        // non-zero-nodes < 3 * k, and k <= log2(domain-size) / max-error
        // To be conservative, assume all non-zero-nodes can be leaves and we need a complete tree => total-nodes <= 2 * non-zero-nodes
        // => total-nodes <= 2 * 3 * log2(domain-size) / max-error
        int numberOfLevels = MAX_BITS - Long.numberOfLeadingZeros(min ^ max) + 1;
        double k = 3 * numberOfLevels / maxError;
        checkArgument(nodeCount <= 2 * k, "Too many nodes in deserialized tree. Possible corruption");

        counts = new double[nodeCount];
        levels = new byte[nodeCount];
        values = new long[nodeCount];

        int[] stack = new int[(Integer.highestOneBit(nodeCount - 1) << 1) + 1]; // log2 ceiling
        int top = -1;

        // The nodes are organized in a left-to-right post-order sequence, so
        // we rebuild the tree (left/right links) bottom up
        lefts = new int[nodeCount];
        rights = new int[nodeCount];
        for (int node = 0; node < nodeCount; node++) {
            byte nodeStructure = input.readByte();
            boolean hasRight = (nodeStructure & Flags.HAS_RIGHT) != 0;
            boolean hasLeft = (nodeStructure & Flags.HAS_LEFT) != 0;
            byte level = (byte) ((nodeStructure >>> 2) & 0b111111);

            // Branch node levels are serialized as 0-indexed to save a bit, therefore if this is not a leaf node then
            // add back one to the level.
            if (hasLeft || hasRight) {
                level++;
            }
            levels[node] = level;

            if (hasRight) {
                rights[node] = stack[top--];
            }
            else {
                rights[node] = -1;
            }

            if (hasLeft) {
                lefts[node] = stack[top--];
            }
            else {
                lefts[node] = -1;
            }

            stack[++top] = node;

            double count = input.readDouble();
            weightedCount += count;
            counts[node] = count;
            values[node] = input.readLong();
        }
        checkArgument(nodeCount == 0 || top == 0, "Tree is corrupted. Expected a single root node");
        root = nodeCount - 1; // last node in post-order

        nextNode = nodeCount;
    }

    public double getMaxError()
    {
        return maxError;
    }

    public double getAlpha()
    {
        return alpha;
    }

    public void add(long value)
    {
        add(value, 1);
    }

    /**
     * Adds a value to this digest. The value must be {@code >= 0}
     */
    public void add(long value, long count)
    {
        checkArgument(count > 0, "count must be > 0");

        boolean needsCompression = false;
        double weight = count;
        if (alpha > 0.0) {
            long nowInSeconds = TimeUnit.NANOSECONDS.toSeconds(ticker.read());
            if (nowInSeconds - landmarkInSeconds >= RESCALE_THRESHOLD_SECONDS) {
                rescale(nowInSeconds);
                needsCompression = true; // rescale affects weights globally, so force compression
            }

            weight = weight(nowInSeconds) * count;
        }

        max = Math.max(max, value);
        min = Math.min(min, value);

        double previousCount = weightedCount;
        insert(longToBits(value), weight);

        // When totalCount crosses the next multiple of k (compression factor), the compression
        // equation changes for every node in the tree, so we need to compress globally.
        // Otherwise, only node along the insertion path are affected -- TODO: implement this.
        int compressionFactor = calculateCompressionFactor();
        if (needsCompression || ((long) previousCount) / compressionFactor != ((long) weightedCount) / compressionFactor) {
            compress();
        }
    }

    public void merge(QuantileDigest other)
    {
        rescaleToCommonLandmark(this, other);

        // 1. merge other into this (don't modify other)
        root = merge(root, other, other.root);

        max = Math.max(max, other.max);
        min = Math.min(min, other.min);

        // 2. compress to remove unnecessary nodes
        compress();
    }

    /**
     * Get a lower bound on the quantiles for the given proportions. A returned q quantile is guaranteed to be within
     * the q - maxError and q quantiles.
     *

     * The input list of quantile proportions must be sorted in increasing order, and each value must be in the range [0, 1]
     */
    public List<Long> getQuantilesLowerBound(List<Double> quantiles)
    {
        checkArgument(Ordering.natural().isOrdered(quantiles), "quantiles must be sorted in increasing order");
        for (double quantile : quantiles) {
            checkArgument(quantile >= 0 && quantile <= 1, "quantile must be between [0,1]");
        }

        List<Double> reversedQuantiles = ImmutableList.copyOf(quantiles).reverse();

        ImmutableList.Builder<Long> builder = ImmutableList.builder();
        PeekingIterator<Double> iterator = Iterators.peekingIterator(reversedQuantiles.iterator());

        postOrderTraversal(root, new Callback()
        {
            private double sum;

            @Override
            public boolean process(int node)
            {
                sum += counts[node];

                while (iterator.hasNext() && sum > (1.0 - iterator.peek()) * weightedCount) {
                    iterator.next();

                    // we know the min value ever seen, so cap the percentile to provide better error
                    // bounds in this case
                    long value = Math.max(lowerBound(node), min);

                    builder.add(value);
                }

                return iterator.hasNext();
            }
        }, TraversalOrder.REVERSE);

        // we finished the traversal without consuming all quantiles. This means the remaining quantiles
        // correspond to the max known value
        while (iterator.hasNext()) {
            builder.add(min);
            iterator.next();
        }

        return builder.build().reverse();
    }

    /**
     * Get an upper bound on the quantiles for the given proportions. A returned q quantile is guaranteed to be within
     * the q and q + maxError quantiles.
     *

     * The input list of quantile proportions must be sorted in increasing order, and each value must be in the range [0, 1]
     */
    public List<Long> getQuantilesUpperBound(List<Double> quantiles)
    {
        checkArgument(Ordering.natural().isOrdered(quantiles), "quantiles must be sorted in increasing order");
        for (double quantile : quantiles) {
            checkArgument(quantile >= 0 && quantile <= 1, "quantile must be between [0,1]");
        }

        final ImmutableList.Builder<Long> builder = ImmutableList.builder();
        final PeekingIterator<Double> iterator = Iterators.peekingIterator(quantiles.iterator());

        postOrderTraversal(root, new Callback()
        {
            private double sum;

            public boolean process(int node)
            {
                sum += counts[node];

                while (iterator.hasNext() && sum > iterator.peek() * weightedCount) {
                    iterator.next();

                    // we know the max value ever seen, so cap the percentile to provide better error
                    // bounds in this case
                    long value = Math.min(upperBound(node), max);

                    builder.add(value);
                }

                return iterator.hasNext();
            }
        });

        // we finished the traversal without consuming all quantiles. This means the remaining quantiles
        // correspond to the max known value
        while (iterator.hasNext()) {
            builder.add(max);
            iterator.next();
        }

        return builder.build();
    }

    public List<Long> getQuantiles(List<Double> quantiles)
    {
        return getQuantilesUpperBound(quantiles);
    }

    /**
     * Gets the value at the specified quantile +/- maxError. The quantile must be in the range [0, 1]
     */
    public long getQuantile(double quantile)
    {
        return getQuantiles(ImmutableList.of(quantile)).get(0);
    }

    public long getQuantileLowerBound(double quantile)
    {
        return getQuantilesLowerBound(ImmutableList.of(quantile)).get(0);
    }

    public long getQuantileUpperBound(double quantile)
    {
        return getQuantilesUpperBound(ImmutableList.of(quantile)).get(0);
    }

    /**
     * Number (decayed) of elements added to this quantile digest
     */
    public double getCount()
    {
        return weightedCount / weight(TimeUnit.NANOSECONDS.toSeconds(ticker.read()));
    }

    /*
     * Get the exponentially-decayed approximate counts of values in multiple buckets. The elements in
     * the provided list denote the upper bound each of the buckets and must be sorted in ascending
     * order.
     *
     * The approximate count in each bucket is guaranteed to be within 2 * totalCount * maxError of
     * the real count.
     */
    public List<Bucket> getHistogram(List<Long> bucketUpperBounds)
    {
        return getHistogram(bucketUpperBounds, DEFAULT);
    }

    // A separate lambda is provided to allow one to override how the middle between two quantiles buckets
    // is calculated.
    public List<Bucket> getHistogram(List<Long> bucketUpperBounds, MiddleFunction middleFunction)
    {
        checkArgument(Ordering.natural().isOrdered(bucketUpperBounds), "buckets must be sorted in increasing order");

        ImmutableList.Builder<Bucket> builder = ImmutableList.builder();
        PeekingIterator<Long> iterator = Iterators.peekingIterator(bucketUpperBounds.iterator());

        HistogramBuilderStateHolder holder = new HistogramBuilderStateHolder();

        double normalizationFactor = weight(TimeUnit.NANOSECONDS.toSeconds(ticker.read()));

        postOrderTraversal(root, node -> {
            while (iterator.hasNext() && iterator.peek() <= upperBound(node)) {
                double bucketCount = holder.sum - holder.lastSum;

                Bucket bucket = new Bucket(bucketCount / normalizationFactor, holder.bucketWeightedSum / bucketCount);

                builder.add(bucket);
                holder.lastSum = holder.sum;
                holder.bucketWeightedSum = 0;
                iterator.next();
            }

            holder.bucketWeightedSum += middleFunction.middle(lowerBound(node), upperBound(node)) * counts[node];
            holder.sum += counts[node];
            return iterator.hasNext();
        });

        while (iterator.hasNext()) {
            double bucketCount = holder.sum - holder.lastSum;
            Bucket bucket = new Bucket(bucketCount / normalizationFactor, holder.bucketWeightedSum / bucketCount);

            builder.add(bucket);

            iterator.next();
        }

        return builder.build();
    }

    private static final class HistogramBuilderStateHolder
    {
        double sum;
        double lastSum;
        // for computing weighed average of values in bucket
        double bucketWeightedSum;
    }

    public long getMin()
    {
        final AtomicLong chosen = new AtomicLong(min);
        postOrderTraversal(root, node -> {
            if (counts[node] >= ZERO_WEIGHT_THRESHOLD) {
                chosen.set(lowerBound(node));
                return false;
            }
            return true;
        }, TraversalOrder.FORWARD);

        return Math.max(min, chosen.get());
    }

    public long getMax()
    {
        final AtomicLong chosen = new AtomicLong(max);
        postOrderTraversal(root, node -> {
            if (counts[node] >= ZERO_WEIGHT_THRESHOLD) {
                chosen.set(upperBound(node));
                return false;
            }
            return true;
        }, TraversalOrder.REVERSE);

        return Math.min(max, chosen.get());
    }

    public int estimatedInMemorySizeInBytes()
    {
        return (int) (QUANTILE_DIGEST_SIZE +
                SizeOf.sizeOf(counts) +
                SizeOf.sizeOf(levels) +
                SizeOf.sizeOf(values) +
                SizeOf.sizeOf(lefts) +
                SizeOf.sizeOf(rights));
    }

    public int estimatedSerializedSizeInBytes()
    {
        int nodeSize = SizeOf.SIZE_OF_LONG + // counts
                SizeOf.SIZE_OF_BYTE + // levels and left/right flags
                SizeOf.SIZE_OF_LONG; // values

        return SizeOf.SIZE_OF_BYTE + // format
                SizeOf.SIZE_OF_DOUBLE + // maxError
                SizeOf.SIZE_OF_DOUBLE + // alpha
                SizeOf.SIZE_OF_LONG + // landmarkInSeconds
                SizeOf.SIZE_OF_LONG + // min
                SizeOf.SIZE_OF_LONG + // max
                SizeOf.SIZE_OF_INT + // node count
                getNodeCount() * nodeSize;
    }

    public Slice serialize()
    {
        compress();

        SliceOutput output = new DynamicSliceOutput(estimatedSerializedSizeInBytes());

        output.writeByte(Flags.FORMAT);
        output.writeDouble(maxError);
        output.writeDouble(alpha);
        output.writeLong(landmarkInSeconds);
        output.writeLong(min);
        output.writeLong(max);
        output.writeInt(getNodeCount());

        int[] nodes = new int[getNodeCount()];
        postOrderTraversal(root, new Callback()
        {
            int index;

            @Override
            public boolean process(int node)
            {
                nodes[index++] = node;
                return true;
            }
        });

        for (int node : nodes) {
            // The max value for a level is 64.  Non-leaf nodes are decremented by 1
            // to save a bit (so max serialized value is 63 (111111, 6 bits needed)).
            // This is shifted 2 bits to give space for left/right child flags.
            byte nodeStructure = (byte) (Math.max(levels[node] - 1, 0) << 2);
            if (lefts[node] != -1) {
                nodeStructure |= Flags.HAS_LEFT;
            }
            if (rights[node] != -1) {
                nodeStructure |= Flags.HAS_RIGHT;
            }
            output.writeByte(nodeStructure);
            output.writeDouble(counts[node]);
            output.writeLong(values[node]);
        }

        return output.slice();
    }

    @VisibleForTesting
    int getNodeCount()
    {
        return nextNode - freeCount;
    }

    @VisibleForTesting
    void compress()
    {
        double bound = Math.floor(weightedCount / calculateCompressionFactor());

        postOrderTraversal(root, node -> {
            // if children's weights are 0 remove them and shift the weight to their parent
            int left = lefts[node];
            int right = rights[node];

            if (left == -1 && right == -1) {
                // leaf, nothing to do
                return true;
            }

            double leftCount = (left == -1) ? 0.0 : counts[left];
            double rightCount = (right == -1) ? 0.0 : counts[right];

            boolean shouldCompress = (counts[node] + leftCount + rightCount) < bound;

            if (left != -1 && (shouldCompress || leftCount < ZERO_WEIGHT_THRESHOLD)) {
                lefts[node] = tryRemove(left);
                counts[node] += leftCount;
            }

            if (right != -1 && (shouldCompress || rightCount < ZERO_WEIGHT_THRESHOLD)) {
                rights[node] = tryRemove(right);
                counts[node] += rightCount;
            }

            return true;
        });

        // root's count may have decayed to ~0
        if (root != -1 && counts[root] < ZERO_WEIGHT_THRESHOLD) {
            root = tryRemove(root);
        }
    }

    private double weight(long timestamp)
    {
        return Math.exp(alpha * (timestamp - landmarkInSeconds));
    }

    private void rescale(long newLandmarkInSeconds)
    {
        // rescale the weights based on a new landmark to avoid numerical overflow issues
        double factor = Math.exp(-alpha * (newLandmarkInSeconds - landmarkInSeconds));
        weightedCount *= factor;
        for (int i = 0; i < nextNode; i++) {
            counts[i] *= factor;
        }
        landmarkInSeconds = newLandmarkInSeconds;
    }

    private int calculateCompressionFactor()
    {
        if (root == -1) {
            return 1;
        }

        return Math.max((int) ((levels[root] + 1) / maxError), 1);
    }

    private void insert(long value, double count)
    {
        if (count < ZERO_WEIGHT_THRESHOLD) {
            return;
        }

        long lastBranch = 0;
        int parent = -1;
        int current = root;

        while (true) {
            if (current == -1) {
                setChild(parent, lastBranch, createLeaf(value, count));
                return;
            }

            long currentValue = values[current];
            byte currentLevel = levels[current];
            if (!inSameSubtree(value, currentValue, currentLevel)) {
                // if value and node.value are not in the same branch given node's level,
                // insert a parent above them at the point at which branches diverge
                setChild(parent, lastBranch, makeSiblings(current, createLeaf(value, count)));
                return;
            }

            if (currentLevel == 0 && currentValue == value) {
                // found the node
                counts[current] += count;
                weightedCount += count;
                return;
            }

            // we're on the correct branch of the tree and we haven't reached a leaf, so keep going down
            long branch = value & getBranchMask(currentLevel);

            parent = current;
            lastBranch = branch;

            if (branch == 0) {
                current = lefts[current];
            }
            else {
                current = rights[current];
            }
        }
    }

    private void setChild(int parent, long branch, int child)
    {
        if (parent == -1) {
            root = child;
        }
        else if (branch == 0) {
            lefts[parent] = child;
        }
        else {
            rights[parent] = child;
        }
    }

    private int makeSiblings(int first, int second)
    {
        long firstValue = values[first];
        long secondValue = values[second];

        int parentLevel = MAX_BITS - Long.numberOfLeadingZeros(firstValue ^ secondValue);
        int parent = createNode(firstValue, parentLevel, 0);

        // the branch is given by the bit at the level one below parent
        long branch = firstValue & getBranchMask(levels[parent]);

        if (branch == 0) {
            lefts[parent] = first;
            rights[parent] = second;
        }
        else {
            lefts[parent] = second;
            rights[parent] = first;
        }

        return parent;
    }

    private int createLeaf(long value, double count)
    {
        return createNode(value, 0, count);
    }

    private int createNode(long value, int level, double count)
    {
        int node = popFree();

        if (node == -1) {
            if (nextNode == counts.length) {
                // try to double the array, but don't allocate too much to avoid going over the upper bound of nodes
                // by a large margin (hence, the heuristic to not allocate more than k / 5 nodes)
                int newSize = counts.length + Math.min(counts.length, calculateCompressionFactor() / 5 + 1);
                counts = Arrays.copyOf(counts, newSize);
                levels = Arrays.copyOf(levels, newSize);
                values = Arrays.copyOf(values, newSize);

                lefts = Arrays.copyOf(lefts, newSize);
                rights = Arrays.copyOf(rights, newSize);
            }

            node = nextNode;
            nextNode++;
        }

        weightedCount += count;

        values[node] = value;
        levels[node] = (byte) level;
        counts[node] = count;

        lefts[node] = -1;
        rights[node] = -1;

        return node;
    }

    private int merge(int node, QuantileDigest other, int otherNode)
    {
        if (otherNode == -1) {
            return node;
        }
        else if (node == -1) {
            return copyRecursive(other, otherNode);
        }
        else if (!inSameSubtree(values[node], other.values[otherNode], Math.max(levels[node], other.levels[otherNode]))) {
            return makeSiblings(node, copyRecursive(other, otherNode));
        }
        else if (levels[node] > other.levels[otherNode]) {
            long branch = other.values[otherNode] & getBranchMask(levels[node]);

            if (branch == 0) {
                // variable needed because the array may be re-allocated during merge()
                int left = merge(lefts[node], other, otherNode);
                lefts[node] = left;
            }
            else {
                // variable needed because the array may be re-allocated during merge()
                int right = merge(rights[node], other, otherNode);
                rights[node] = right;
            }
            return node;
        }
        else if (levels[node] < other.levels[otherNode]) {
            long branch = values[node] & getBranchMask(other.levels[otherNode]);

            // variables needed because the arrays may be re-allocated during merge()
            int left;
            int right;
            if (branch == 0) {
                left = merge(node, other, other.lefts[otherNode]);
                right = copyRecursive(other, other.rights[otherNode]);
            }
            else {
                left = copyRecursive(other, other.lefts[otherNode]);
                right = merge(node, other, other.rights[otherNode]);
            }

            int result = createNode(other.values[otherNode], other.levels[otherNode], other.counts[otherNode]);
            lefts[result] = left;
            rights[result] = right;

            return result;
        }

        // else, they must be at the same level and on the same path, so just bump the counts
        weightedCount += other.counts[otherNode];
        counts[node] += other.counts[otherNode];

        // variables needed because the arrays may be re-allocated during merge()
        int left = merge(lefts[node], other, other.lefts[otherNode]);
        int right = merge(rights[node], other, other.rights[otherNode]);
        lefts[node] = left;
        rights[node] = right;

        return node;
    }

    private static boolean inSameSubtree(long bitsA, long bitsB, int level)
    {
        return level == MAX_BITS || (bitsA >>> level) == (bitsB >>> level);
    }

    private int copyRecursive(QuantileDigest other, int otherNode)
    {
        if (otherNode == -1) {
            return otherNode;
        }

        int node = createNode(other.values[otherNode], other.levels[otherNode], other.counts[otherNode]);

        if (other.lefts[otherNode] != -1) {
            // variable needed because the array may be re-allocated during merge()
            int left = copyRecursive(other, other.lefts[otherNode]);
            lefts[node] = left;
        }

        if (other.rights[otherNode] != -1) {
            // variable needed because the array may be re-allocated during merge()
            int right = copyRecursive(other, other.rights[otherNode]);
            rights[node] = right;
        }

        return node;
    }

    /**
     * Remove the node if possible or set its count to 0 if it has children and
     * it needs to be kept around
     */
    private int tryRemove(int node)
    {
        checkArgument(node != -1, "node is -1");

        int left = lefts[node];
        int right = rights[node];

        if (left == -1 && right == -1) {
            // leaf, just remove it
            remove(node);
            return -1;
        }

        if (left != -1 && right != -1) {
            // node has both children so we can't physically remove it
            counts[node] = 0;
            return node;
        }

        // node has a single child, so remove it and return the child
        remove(node);
        if (left != -1) {
            return left;
        }
        else {
            return right;
        }
    }

    private void remove(int node)
    {
        if (node == nextNode - 1) {
            // if we're removing the last node, no need to add it to the free list
            nextNode--;
        }
        else {
            pushFree(node);
        }

        if (node == root) {
            root = -1;
        }
    }

    private void pushFree(int node)
    {
        lefts[node] = firstFree;
        firstFree = node;
        freeCount++;
    }

    private int popFree()
    {
        int node = firstFree;

        if (node == -1) {
            return node;
        }

        firstFree = lefts[firstFree];
        freeCount--;

        return node;
    }

    private void postOrderTraversal(int node, Callback callback)
    {
        postOrderTraversal(node, callback, TraversalOrder.FORWARD);
    }

    private void postOrderTraversal(int node, Callback callback, TraversalOrder order)
    {
        if (order == TraversalOrder.FORWARD) {
            postOrderTraversal(node, callback, lefts, rights);
        }
        else {
            postOrderTraversal(node, callback, rights, lefts);
        }
    }

    private boolean postOrderTraversal(int node, Callback callback, int[] lefts, int[] rights)
    {
        if (node == -1) {
            return false;
        }

        int first = lefts[node];
        int second = rights[node];

        if (first != -1 && !postOrderTraversal(first, callback, lefts, rights)) {
            return false;
        }

        if (second != -1 && !postOrderTraversal(second, callback, lefts, rights)) {
            return false;
        }

        return callback.process(node);
    }

    /**
     * Computes the maximum error of the current digest
     */
    public double getConfidenceFactor()
    {
        return computeMaxPathWeight(root) * 1.0 / weightedCount;
    }

    @VisibleForTesting
    boolean equivalent(QuantileDigest other)
    {
        return (getNodeCount() == other.getNodeCount() &&
                min == other.min &&
                max == other.max &&
                weightedCount == other.weightedCount &&
                alpha == other.alpha);
    }

    private void rescaleToCommonLandmark(QuantileDigest one, QuantileDigest two)
    {
        long nowInSeconds = TimeUnit.NANOSECONDS.toSeconds(ticker.read());

        // 1. rescale this and other to common landmark
        long targetLandmark = Math.max(one.landmarkInSeconds, two.landmarkInSeconds);

        if (nowInSeconds - targetLandmark >= RESCALE_THRESHOLD_SECONDS) {
            targetLandmark = nowInSeconds;
        }

        if (targetLandmark != one.landmarkInSeconds) {
            one.rescale(targetLandmark);
        }

        if (targetLandmark != two.landmarkInSeconds) {
            two.rescale(targetLandmark);
        }
    }

    /**
     * Computes the max "weight" of any path starting at node and ending at a leaf in the
     * hypothetical complete tree. The weight is the sum of counts in the ancestors of a given node
     */
    private double computeMaxPathWeight(int node)
    {
        if (node == -1 || levels[node] == 0) {
            return 0;
        }

        double leftMaxWeight = computeMaxPathWeight(lefts[node]);
        double rightMaxWeight = computeMaxPathWeight(rights[node]);

        return Math.max(leftMaxWeight, rightMaxWeight) + counts[node];
    }

    @VisibleForTesting
    void validate()
    {
        AtomicDouble sum = new AtomicDouble();
        AtomicInteger nodeCount = new AtomicInteger();

        Set<Integer> freeSlots = computeFreeList();
        checkState(freeSlots.size() == freeCount, "Free count (%s) doesn't match actual free slots: %s", freeCount, freeSlots.size());

        if (root != -1) {
            validateStructure(root, freeSlots);

            postOrderTraversal(root, node -> {
                sum.addAndGet(counts[node]);
                nodeCount.incrementAndGet();
                return true;
            });
        }

        checkState(Math.abs(sum.get() - weightedCount) < ZERO_WEIGHT_THRESHOLD,
                "Computed weight (%s) doesn't match summary (%s)", sum.get(),
                weightedCount);

        checkState(nodeCount.get() == getNodeCount(),
                "Actual node count (%s) doesn't match summary (%s)",
                nodeCount.get(), getNodeCount());
    }

    private void validateStructure(int node, Set<Integer> freeNodes)
    {
        checkState(levels[node] >= 0);

        checkState(!freeNodes.contains(node), "Node is in list of free slots: %s", node);
        if (lefts[node] != -1) {
            validateBranchStructure(node, lefts[node], rights[node], true);
            validateStructure(lefts[node], freeNodes);
        }

        if (rights[node] != -1) {
            validateBranchStructure(node, rights[node], lefts[node], false);
            validateStructure(rights[node], freeNodes);
        }
    }

    private void validateBranchStructure(int parent, int child, int otherChild, boolean isLeft)
    {
        checkState(levels[child] < levels[parent], "Child level (%s) should be smaller than parent level (%s)", levels[child], levels[parent]);

        long branch = values[child] & (1L << (levels[parent] - 1));
        checkState(branch == 0 && isLeft || branch != 0 && !isLeft, "Value of child node is inconsistent with its branch");

        Preconditions.checkState(counts[parent] > 0 ||
                        counts[child] > 0 || otherChild != -1,
                "Found a linear chain of zero-weight nodes");
    }

    private Set<Integer> computeFreeList()
    {
        Set<Integer> freeSlots = new HashSet<>();
        int index = firstFree;
        while (index != -1) {
            freeSlots.add(index);
            index = lefts[index];
        }
        return freeSlots;
    }

    public String toGraphviz()
    {
        StringBuilder builder = new StringBuilder();

        builder.append("digraph QuantileDigest {\n")
                .append("\tgraph [ordering=\"out\"];");

        final List<Integer> nodes = new ArrayList<>();
        postOrderTraversal(root, node -> {
            nodes.add(node);
            return true;
        });

        Multimap<Byte, Integer> nodesByLevel = Multimaps.index(nodes, input -> levels[input]);

        for (Map.Entry<Byte, Collection<Integer>> entry : nodesByLevel.asMap().entrySet()) {
            builder.append("\tsubgraph level_" + entry.getKey() + " {\n")
                    .append("\t\trank = same;\n");

            for (int node : entry.getValue()) {
                if (levels[node] == 0) {
                    builder.append(String.format("\t\t%s [label=\"%s:[%s]@%s\\n%s\", shape=rect, style=filled,color=%s];\n",
                            idFor(node),
                            node,
                            lowerBound(node),
                            levels[node],
                            counts[node],
                            counts[node] > 0 ? "salmon2" : "white"));
                }
                else {
                    builder.append(String.format("\t\t%s [label=\"%s:[%s..%s]@%s\\n%s\", shape=rect, style=filled,color=%s];\n",
                            idFor(node),
                            node,
                            lowerBound(node),
                            upperBound(node),
                            levels[node],
                            counts[node],
                            counts[node] > 0 ? "salmon2" : "white"));
                }
            }
            builder.append("\t}\n");
        }

        for (int node : nodes) {
            if (lefts[node] != -1) {
                builder.append(format("\t%s -> %s [style=\"%s\"];\n",
                        idFor(node),
                        idFor(lefts[node]),
                        levels[node] - levels[lefts[node]] == 1 ? "solid" : "dotted"));
            }
            if (rights[node] != -1) {
                builder.append(format("\t%s -> %s [style=\"%s\"];\n",
                        idFor(node),
                        idFor(rights[node]),
                        levels[node] - levels[rights[node]] == 1 ? "solid" : "dotted"));
            }
        }

        builder.append("}\n");

        return builder.toString();
    }

    private static String idFor(int node)
    {
        return String.format("node_%x", node);
    }

    /**
     * Convert a java long (two's complement representation) to a 64-bit lexicographically-sortable binary
     */
    private static long longToBits(long value)
    {
        return value ^ 0x8000_0000_0000_0000L;
    }

    /**
     * Convert a 64-bit lexicographically-sortable binary to a java long (two's complement representation)
     */
    private static long bitsToLong(long bits)
    {
        return bits ^ 0x8000_0000_0000_0000L;
    }

    private long getBranchMask(byte level)
    {
        return (1L << (level - 1));
    }

    private long upperBound(int node)
    {
        // set all lsb below level to 1 (we're looking for the highest value of the range covered by this node)
        long mask = 0;

        if (levels[node] > 0) { // need to special case when level == 0 because (value >> 64 really means value >> (64 % 64))
            mask = 0xFFFF_FFFF_FFFF_FFFFL >>> (MAX_BITS - levels[node]);
        }
        return bitsToLong(values[node] | mask);
    }

    private long lowerBound(int node)
    {
        // set all lsb below level to 0 (we're looking for the lowest value of the range covered by this node)
        long mask = 0;

        if (levels[node] > 0) { // need to special case when level == 0 because (value >> 64 really means value >> (64 % 64))
            mask = 0xFFFF_FFFF_FFFF_FFFFL >>> (MAX_BITS - levels[node]);
        }

        return bitsToLong(values[node] & (~mask));
    }

    private long middle(int node)
    {
        long lower = lowerBound(node);
        long upper = upperBound(node);

        return lower + (upper - lower) / 2;
    }

    private static Ticker noOpTicker()
    {
        return new Ticker()
        {
            @Override
            public long read()
            {
                return 0;
            }
        };
    }

    public static class Bucket
    {
        private double count;
        private double mean;

        public Bucket(double count, double mean)
        {
            this.count = count;
            this.mean = mean;
        }

        public double getCount()
        {
            return count;
        }

        public double getMean()
        {
            return mean;
        }

        @Override
        public boolean equals(Object o)
        {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }

            final Bucket bucket = (Bucket) o;

            if (Double.compare(bucket.count, count) != 0) {
                return false;
            }
            if (Double.compare(bucket.mean, mean) != 0) {
                return false;
            }

            return true;
        }

        @Override
        public int hashCode()
        {
            int result;
            long temp;
            temp = count != +0.0d ? Double.doubleToLongBits(count) : 0L;
            result = (int) (temp ^ (temp >>> 32));
            temp = mean != +0.0d ? Double.doubleToLongBits(mean) : 0L;
            result = 31 * result + (int) (temp ^ (temp >>> 32));
            return result;
        }

        public String toString()
        {
            return String.format("[count: %f, mean: %f]", count, mean);
        }
    }

    private interface Callback
    {
        /**
         * @param node the node to process
         * @return true if processing should continue
         */
        boolean process(int node);
    }

    private static class Flags
    {
        public static final int HAS_LEFT = 1 << 0;
        public static final int HAS_RIGHT = 1 << 1;
        public static final byte FORMAT = 0; // Currently there is just one format
    }

    public interface MiddleFunction
    {
        MiddleFunction DEFAULT = (lowerBound, upperBound) -> lowerBound + (upperBound - lowerBound) / 2.0;

        double middle(long lowerBound, long upperBound);
    }
}


package io.airlift.concurrent;

import com.google.common.util.concurrent.ThreadFactoryBuilder;

import java.util.concurrent.ThreadFactory;

import static java.util.concurrent.Executors.defaultThreadFactory;

public final class Threads
{
    private Threads() {}

    /**
     * Creates a {@link ThreadFactory} that creates named threads
     * using the specified naming format.
     *
     * @param nameFormat a {@link String#format(String, Object...)}-compatible
     * format string, to which a string will be supplied as the single
     * parameter. This string will be unique to this instance of the
     * ThreadFactory and will be assigned sequentially.
     * @return the created ThreadFactory
     */
    public static ThreadFactory threadsNamed(String nameFormat)
    {
        return new ThreadFactoryBuilder()
                .setNameFormat(nameFormat)
                .setThreadFactory(new ContextClassLoaderThreadFactory(Thread.currentThread().getContextClassLoader(), defaultThreadFactory()))
                .build();
    }

    /**
     * Creates a {@link ThreadFactory} that creates named daemon threads.
     * using the specified naming format.
     *
     * @param nameFormat see {@link #threadsNamed(String)}
     * @return the created ThreadFactory
     */
    public static ThreadFactory daemonThreadsNamed(String nameFormat)
    {
        return new ThreadFactoryBuilder()
                .setNameFormat(nameFormat)
                .setDaemon(true)
                .setThreadFactory(new ContextClassLoaderThreadFactory(Thread.currentThread().getContextClassLoader(), defaultThreadFactory()))
                .build();
    }

    private static class ContextClassLoaderThreadFactory
            implements ThreadFactory
    {
        private final ClassLoader classLoader;
        private final ThreadFactory delegate;

        public ContextClassLoaderThreadFactory(ClassLoader classLoader, ThreadFactory delegate)
        {
            this.classLoader = classLoader;
            this.delegate = delegate;
        }

        @Override
        public Thread newThread(Runnable runnable)
        {
            Thread thread = delegate.newThread(runnable);
            thread.setContextClassLoader(classLoader);
            return thread;
        }
    }
}


/*
 * Copyright 2010 Proofpoint, Inc.
 *
 * 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 io.airlift.stats;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.annotations.Beta;
import io.airlift.stats.DecayCounter.DecayCounterSnapshot;
import org.weakref.jmx.Managed;
import org.weakref.jmx.Nested;

import java.util.concurrent.atomic.AtomicLong;

import static java.util.Objects.requireNonNull;

@Beta
public class CounterStat
{
    private final AtomicLong count = new AtomicLong(0);
    private final DecayCounter oneMinute = new DecayCounter(ExponentialDecay.oneMinute());
    private final DecayCounter fiveMinute = new DecayCounter(ExponentialDecay.fiveMinutes());
    private final DecayCounter fifteenMinute = new DecayCounter(ExponentialDecay.fifteenMinutes());

    public void update(long count)
    {
        oneMinute.add(count);
        fiveMinute.add(count);
        fifteenMinute.add(count);
        this.count.addAndGet(count);
    }

    public void merge(CounterStat counterStat)
    {
        requireNonNull(counterStat, "counterStat is null");
        oneMinute.merge(counterStat.getOneMinute());
        fiveMinute.merge(counterStat.getFiveMinute());
        fifteenMinute.merge(counterStat.getFifteenMinute());
        count.addAndGet(counterStat.getTotalCount());
    }

    @Managed
    public void reset()
    {
        oneMinute.reset();
        fiveMinute.reset();
        fifteenMinute.reset();
        count.set(0);
    }

    /**
     * This is a hack to work around limitations in Jmxutils.
     */
    @Deprecated
    public void resetTo(CounterStat counterStat)
    {
        oneMinute.resetTo(counterStat.getOneMinute());
        fiveMinute.resetTo(counterStat.getFiveMinute());
        fifteenMinute.resetTo(counterStat.getFifteenMinute());
        count.set(counterStat.getTotalCount());
    }

    @Managed
    public long getTotalCount()
    {
        return count.get();
    }

    @Managed
    @Nested
    public DecayCounter getOneMinute()
    {
        return oneMinute;
    }

    @Managed
    @Nested
    public DecayCounter getFiveMinute()
    {
        return fiveMinute;
    }

    @Managed
    @Nested
    public DecayCounter getFifteenMinute()
    {
        return fifteenMinute;
    }

    public CounterStatSnapshot snapshot()
    {
        return new CounterStatSnapshot(getTotalCount(), getOneMinute().snapshot(), getFiveMinute().snapshot(), getFifteenMinute().snapshot());
    }

    public static class CounterStatSnapshot
    {
        private final long totalCount;
        private final DecayCounterSnapshot oneMinute;
        private final DecayCounterSnapshot fiveMinute;
        private final DecayCounterSnapshot fifteenMinute;

        @JsonCreator
        public CounterStatSnapshot(@JsonProperty("totalCount") long totalCount,
                @JsonProperty("oneMinute") DecayCounterSnapshot oneMinute,
                @JsonProperty("fiveMinute") DecayCounterSnapshot fiveMinute,
                @JsonProperty("fifteenMinute") DecayCounterSnapshot fifteenMinute)
        {
            this.totalCount = totalCount;
            this.oneMinute = oneMinute;
            this.fiveMinute = fiveMinute;
            this.fifteenMinute = fifteenMinute;
        }

        @JsonProperty
        public long getTotalCount()
        {
            return totalCount;
        }

        @JsonProperty
        public DecayCounterSnapshot getOneMinute()
        {
            return oneMinute;
        }

        @JsonProperty
        public DecayCounterSnapshot getFiveMinute()
        {
            return fiveMinute;
        }

        @JsonProperty
        public DecayCounterSnapshot getFifteenMinute()
        {
            return fifteenMinute;
        }
    }
}


/*
 * 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 io.airlift.stats.cardinality;

import com.google.common.annotations.VisibleForTesting;
import io.airlift.slice.Murmur3Hash128;
import io.airlift.slice.Slice;
import org.openjdk.jol.info.ClassLayout;

import static com.google.common.base.Preconditions.checkArgument;
import static io.airlift.stats.cardinality.Utils.indexBitLength;

public class HyperLogLog
{
    private static final int INSTANCE_SIZE = ClassLayout.parseClass(HyperLogLog.class).instanceSize();
    private static final int MAX_NUMBER_OF_BUCKETS = 65536;
    private HllInstance instance;

    private HyperLogLog(HllInstance instance)
    {
        this.instance = instance;
    }

    public static HyperLogLog newInstance(int numberOfBuckets)
    {
        checkArgument(numberOfBuckets <= MAX_NUMBER_OF_BUCKETS, "numberOfBuckets must be <= %s, actual: %s", MAX_NUMBER_OF_BUCKETS, numberOfBuckets);

        return new HyperLogLog(new SparseHll(indexBitLength(numberOfBuckets)));
    }

    public static HyperLogLog newInstance(Slice serialized)
    {
        checkArgument(serialized.getByte(0) != Format.SPARSE_V1.getTag(), "Sparse v1 encoding no longer supported");

        if (SparseHll.canDeserialize(serialized)) {
            return new HyperLogLog(new SparseHll(serialized));
        }
        else if (DenseHll.canDeserialize(serialized)) {
            return new HyperLogLog(new DenseHll(serialized));
        }

        throw new IllegalArgumentException("Cannot deserialize HyperLogLog");
    }

    public void add(long value)
    {
        addHash(Murmur3Hash128.hash64(value));
    }

    public void add(Slice value)
    {
        addHash(Murmur3Hash128.hash64(value));
    }

    /**
     * Adds a value that has already been hashed to the set of values tracked by this HyperLogLog instance.
     *
     * @param hash The hash should be the 64 least significant bits of the murmur3_128 hash of the value.
     * For example: io.airlift.slice.Murmur3.hash64(value).
     */
    public void addHash(long hash)
    {
        instance.insertHash(hash);

        if (instance instanceof SparseHll) {
            instance = makeDenseIfNecessary((SparseHll) instance);
        }
    }

    public void mergeWith(HyperLogLog other)
    {
        if (instance instanceof SparseHll && other.instance instanceof SparseHll) {
            ((SparseHll) instance).mergeWith((SparseHll) other.instance);
            instance = makeDenseIfNecessary((SparseHll) instance);
        }
        else {
            DenseHll dense = instance.toDense();
            dense.mergeWith(other.instance.toDense());

            instance = dense;
        }
    }

    public long cardinality()
    {
        return instance.cardinality();
    }

    public int estimatedInMemorySize()
    {
        return instance.estimatedInMemorySize() + INSTANCE_SIZE;
    }

    public int estimatedSerializedSize()
    {
        return instance.estimatedSerializedSize();
    }

    public Slice serialize()
    {
        return instance.serialize();
    }

    public void makeDense()
    {
        instance = instance.toDense();
    }

    @VisibleForTesting
    void verify()
    {
        instance.verify();
    }

    private static HllInstance makeDenseIfNecessary(SparseHll instance)
    {
        if (instance.estimatedInMemorySize() > DenseHll.estimatedInMemorySize(instance.getIndexBitLength())) {
            return instance.toDense();
        }

        return instance;
    }
}


/*
 * Copyright 2010 Proofpoint, Inc.
 *
 * 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 io.airlift.configuration;

import com.google.common.collect.ImmutableSortedMap;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
import java.util.Properties;
import java.util.TreeMap;

import static com.google.common.collect.Maps.fromProperties;

public final class ConfigurationLoader
{
    private ConfigurationLoader() {}

    public static Map<String, String> loadProperties()
            throws IOException
    {
        Map<String, String> result = new TreeMap<>();
        String configFile = System.getProperty("config");
        if (configFile != null) {
            result.putAll(loadPropertiesFrom(configFile));
        }

        result.putAll(getSystemProperties());

        return ImmutableSortedMap.copyOf(result);
    }

    /**
     * Loads properties from the given file
     *
     * @param path file path
     * @return properties
     * @throws IOException errors
     */
    public static Map<String, String> loadPropertiesFrom(String path)
            throws IOException
    {
        Properties properties = new Properties();
        try (InputStream inputStream = new FileInputStream(path)) {
            properties.load(inputStream);
        }

        return fromProperties(properties);
    }

    public static Map<String, String> getSystemProperties()
    {
        return fromProperties(System.getProperties());
    }
}


package io.airlift.configuration;

import com.google.common.reflect.TypeParameter;
import com.google.common.reflect.TypeToken;
import com.google.inject.Binder;
import com.google.inject.Key;
import com.google.inject.TypeLiteral;
import com.google.inject.multibindings.Multibinder;

import java.lang.annotation.Annotation;
import java.util.Optional;

import static com.google.inject.multibindings.Multibinder.newSetBinder;
import static java.util.Objects.requireNonNull;

public class ConfigBinder
{
    public static ConfigBinder configBinder(Binder binder)
    {
        return new ConfigBinder(new GuiceConfigBinder(binder));
    }

    static ConfigBinder configBinder(ConfigurationFactory configurationFactory, Optional<Object> bindingSource)
    {
        return new ConfigBinder(new CallbackConfigBinder(configurationFactory, bindingSource));
    }

    public interface InternalConfigBinder
    {
        <T> void bind(ConfigurationBinding<T> configurationBinding);

        <T> void bindConfigDefaults(ConfigDefaultsHolder<T> configDefaultsHolder);

        void bindConfigurationBindingListener(ConfigurationBindingListener configurationBindingListener);
    }

    private static final class GuiceConfigBinder
            implements InternalConfigBinder
    {
        private final Binder binder;
        private Multibinder<ConfigurationBindingListenerHolder> listenerMultibinder;

        public GuiceConfigBinder(Binder binder)
        {
            this.binder = requireNonNull(binder, "binder is null").skipSources(getClass(), ConfigBinder.class);
            this.listenerMultibinder = newSetBinder(binder, ConfigurationBindingListenerHolder.class);
        }

        @Override
        public <T> void bind(ConfigurationBinding<T> configurationBinding)
        {
            Key<T> key = configurationBinding.getKey();
            binder.bind(key).toProvider(new ConfigurationProvider<>(configurationBinding));
            createConfigDefaultsBinder(key);
        }

        @Override
        public <T> void bindConfigDefaults(ConfigDefaultsHolder<T> configDefaultsHolder)
        {
            createConfigDefaultsBinder(configDefaultsHolder.getConfigKey()).addBinding().toInstance(configDefaultsHolder);
        }

        @Override
        public void bindConfigurationBindingListener(ConfigurationBindingListener configurationBindingListener)
        {
            listenerMultibinder.addBinding().toInstance(new ConfigurationBindingListenerHolder(configurationBindingListener));
        }

        private <T> Multibinder<ConfigDefaultsHolder<T>> createConfigDefaultsBinder(Key<T> key)
        {
            TypeLiteral<ConfigDefaultsHolder<T>> typeLiteral = getTypeLiteral(key);

            if (key.getAnnotationType() == null) {
                return newSetBinder(binder, typeLiteral);
            }
            if (key.hasAttributes()) {
                return newSetBinder(binder, typeLiteral, key.getAnnotation());
            }
            return newSetBinder(binder, typeLiteral, key.getAnnotationType());
        }

        @SuppressWarnings("unchecked")
        private static <T> TypeLiteral<ConfigDefaultsHolder<T>> getTypeLiteral(Key<T> key)
        {
            TypeToken<T> typeToken = (TypeToken<T>) TypeToken.of(key.getTypeLiteral().getType());
            return (TypeLiteral<ConfigDefaultsHolder<T>>)
                    TypeLiteral.get(new TypeToken<ConfigDefaultsHolder<T>>() {}
                            .where(new TypeParameter<T>() {}, typeToken)
                            .getType());
        }
    }

    private static final class CallbackConfigBinder
            implements InternalConfigBinder
    {
        private final ConfigurationFactory configurationFactory;
        private final Optional<Object> bindingSource;

        public CallbackConfigBinder(ConfigurationFactory configurationFactory, Optional<Object> bindingSource)
        {
            this.configurationFactory = requireNonNull(configurationFactory, "configurationFactory is null");
            this.bindingSource = requireNonNull(bindingSource, "bindingSource is null");
        }

        @Override
        public <T> void bind(ConfigurationBinding<T> configurationBinding)
        {
            configurationFactory.registerConfigurationProvider(new ConfigurationProvider<>(configurationBinding), bindingSource);
        }

        @Override
        public <T> void bindConfigDefaults(ConfigDefaultsHolder<T> configDefaultsHolder)
        {
            configurationFactory.registerConfigDefaults(configDefaultsHolder);
        }

        @Override
        public void bindConfigurationBindingListener(ConfigurationBindingListener configurationBindingListener)
        {
            configurationFactory.addConfigurationBindingListener(configurationBindingListener);
        }
    }

    private final InternalConfigBinder binder;

    private ConfigBinder(InternalConfigBinder binder)
    {
        this.binder = requireNonNull(binder, "binder is null");
    }

    public <T> void bindConfig(Class<T> configClass)
    {
        requireNonNull(configClass, "configClass is null");

        bindConfig(Key.get(configClass), configClass, null);
    }

    public <T> void bindConfig(Class<T> configClass, Annotation annotation)
    {
        requireNonNull(configClass, "configClass is null");
        requireNonNull(annotation, "annotation is null");

        bindConfig(Key.get(configClass, annotation), configClass, null);
    }

    public <T> void bindConfig(Class<T> configClass, Class<? extends Annotation> annotation)
    {
        requireNonNull(configClass, "configClass is null");
        requireNonNull(annotation, "annotation is null");

        bindConfig(Key.get(configClass, annotation), configClass, null);
    }

    public <T> void bindConfig(Class<T> configClass, String prefix)
    {
        requireNonNull(configClass, "configClass is null");

        bindConfig(Key.get(configClass), configClass, prefix);
    }

    public <T> void bindConfig(Class<T> configClass, Annotation annotation, String prefix)
    {
        requireNonNull(configClass, "configClass is null");
        requireNonNull(annotation, "annotation is null");

        bindConfig(Key.get(configClass, annotation), configClass, prefix);
    }

    public <T> void bindConfig(Class<T> configClass, Class<? extends Annotation> annotation, String prefix)
    {
        requireNonNull(configClass, "configClass is null");
        requireNonNull(annotation, "annotation is null");

        bindConfig(Key.get(configClass, annotation), configClass, prefix);
    }

    public <T> void bindConfig(Key<T> key, Class<T> configClass, String prefix)
    {
        binder.bind(new ConfigurationBinding<>(key, configClass, Optional.ofNullable(prefix)));
    }

    public <T> void bindConfigDefaults(Class<T> configClass, ConfigDefaults<T> configDefaults)
    {
        requireNonNull(configClass, "configClass is null");
        requireNonNull(configDefaults, "configDefaults is null");

        bindConfigDefaults(Key.get(configClass), configDefaults);
    }

    public <T> void bindConfigDefaults(Class<T> configClass, Annotation annotation, ConfigDefaults<T> configDefaults)
    {
        requireNonNull(configClass, "configClass is null");
        requireNonNull(annotation, "annotation is null");
        requireNonNull(configDefaults, "configDefaults is null");

        bindConfigDefaults(Key.get(configClass, annotation), configDefaults);
    }

    public <T> void bindConfigDefaults(Class<T> configClass, Class<? extends Annotation> annotation, ConfigDefaults<T> configDefaults)
    {
        requireNonNull(configClass, "configClass is null");
        requireNonNull(annotation, "annotation is null");
        requireNonNull(configDefaults, "configDefaults is null");

        bindConfigDefaults(Key.get(configClass, annotation), configDefaults);
    }

    public <T> void bindConfigDefaults(Key<T> key, ConfigDefaults<T> configDefaults)
    {
        binder.bindConfigDefaults(new ConfigDefaultsHolder<>(key, configDefaults));
    }

    /**
     * Binds default values for all the instances of given config class for the current binder
     */
    public <T> void bindConfigGlobalDefaults(Class<T> configClass, ConfigDefaults<T> configDefaults)
    {
        Key<T> key = Key.get(configClass, GlobalDefaults.class);
        binder.bindConfigDefaults(new ConfigDefaultsHolder<>(key, configDefaults));
    }

    /**
     * Binds a configuration binding listener that can create additional config bindings.
     */
    public void bindConfigurationBindingListener(ConfigurationBindingListener configurationBindingListener)
    {
        binder.bindConfigurationBindingListener(configurationBindingListener);
    }
}


/*
 * Copyright 2010 PowerMock original author or authors.
 *
 * 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 org.awaitility.core;

/**
 * This is an internal class and should NOT be used outside of Awaitility! Use it at own risk (please don't :)).
 */
public class CheckedExceptionRethrower {

    /**
     *
safeRethrow.

     *
     * @param t   a {@link java.lang.Throwable} object.
     * @param <T> a T object.
     * @return a T object.
     */
    public static <T> T safeRethrow(Throwable t) {
        CheckedExceptionRethrower.<RuntimeException>safeRethrow0(t);
        return null;
    }

    @SuppressWarnings("unchecked")
    private static <T extends Throwable> void safeRethrow0(Throwable t) throws T {
        throw (T) t;
    }

}


package org.awaitility.core;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;

/**
 * Please don't use this class directly, it's for internal purposes only and will be moved/renamed without notice!
 */
public class InternalExecutorServiceFactory {

    /**
     * @return A executor service that executes tasks in the caller's thread
     */
    public static ExecutorService sameThreadExecutorService() {
        return new SameThreadExecutorService();
    }

    /**
     * Creates an executor service that creates thread's based on the thread supplier
     * @param threadSupplier The thread supplier
     * @return An executor service that will use the thread created by the threadSupplier when polling
     */
    public static ExecutorService create(final Function<Runnable, Thread> threadSupplier) {
        if (threadSupplier == null) {
            throw new IllegalArgumentException("Condition evaluation thread supplier cannot be null");
        }
        return create(new BiFunction<Runnable, String, Thread>() {
            @Override
            public Thread apply(Runnable runnable, String __) {
                return threadSupplier.apply(runnable);
            }
        }, null);
    }

    /**
     * Creates an executor services that creates thread's based on the thread supplier.
     * The first argument to the threadSupplier is the Runnable (the condition that will be evaluated).
     * The second argument is the default thread name generated by Awaitility.
     * @param threadSupplier The thread supplier
     * @return An executor service that will use the thread created by the threadSupplier when polling
     */
    public static ExecutorService create(final BiFunction<Runnable, String, Thread> threadSupplier, final String alias) {
        if (threadSupplier == null) {
            throw new IllegalArgumentException("Condition evaluation thread supplier cannot be null");
        }
        return Executors.newSingleThreadExecutor(new ThreadFactory() {
            public Thread newThread(Runnable r) {
                final String threadName = generateDefaultThreadName(alias);
                return threadSupplier.apply(r, threadName);
            }
        });
    }

    private static String generateDefaultThreadName(String alias) {
        return alias == null ? "awaitility-thread" : "awaitility[" + alias + "]";
    }
}


/*
 * Copyright 2015 the original author or authors.
 *
 * 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 org.awaitility.pollinterval;

import org.awaitility.Duration;

import java.util.concurrent.TimeUnit;

/**
 * Generates a non-linear poll interval based on the fibonacci sequence: [0, 1, 1, 2, 3, 5, 8, 13, ..]
 */
public class FibonacciPollInterval implements PollInterval {

    private static final int DEFAULT_OFFSET = 0;

    private final TimeUnit timeUnit;
    private final int offset;

    /**
     * Create an instance of the {@link FibonacciPollInterval} with the time unit of {@link java.util.concurrent.TimeUnit#MILLISECONDS}.
     */
    public FibonacciPollInterval() {
        this(TimeUnit.MILLISECONDS);
    }

    /**
     * Create an instance of the {@link FibonacciPollInterval} with the supplied time unit starting with offset equal to {@value #DEFAULT_OFFSET}.
     *
     * @param timeUnit The time unit
     * @see FibonacciPollInterval#FibonacciPollInterval(int, TimeUnit)
     */
    public FibonacciPollInterval(TimeUnit timeUnit) {
        this(DEFAULT_OFFSET, timeUnit);
    }

    /**
     * Create an instance of the {@link FibonacciPollInterval} with a supplied time unit.
     *
     * @param offset   The fibonacci offset. For example if offset is 5 and poll count is 1 then the returned duration will be 8 (since  fib(6) is equal to 8).
     *                 Default offset is {@value #DEFAULT_OFFSET}. You can set the offset to <tt>-1</tt> if you want the first value to be fib(0) (i.e. pollCount - offset).
     *                 By default the first value will be fib(1).
     * @param timeUnit The time unit
     */
    public FibonacciPollInterval(int offset, TimeUnit timeUnit) {
        if (offset <= -1) {
            throw new IllegalArgumentException("Offset must be greater than or equal to -1");
        }
        if (timeUnit == null) {
            throw new IllegalArgumentException("Time unit cannot be null");
        }
        this.offset = offset;
        this.timeUnit = timeUnit;
    }

    /**
     * Generate the next duration
     *
     * @param pollCount        The number of times the condition has been polled (evaluated)
     * @param previousDuration The duration of the previously returned poll interval
     * @return The next duration in the fibonacci sequence.
     */
    public Duration next(int pollCount, Duration previousDuration) {
        return new Duration(fibonacci(offset + pollCount), timeUnit);
    }

    /**
     * Syntactic sugar for new FibonacciPollInterval()
     *
     * @return A new instance of {@link FibonacciPollInterval}.
     */
    public static FibonacciPollInterval fibonacci() {
        return new FibonacciPollInterval();
    }

    /**
     * Syntactic sugar for new FibonacciPollInterval(timeUnit)
     *
     * @param timeUnit The time unit
     * @return A new instance of {@link FibonacciPollInterval}.
     */
    public static FibonacciPollInterval fibonacci(TimeUnit timeUnit) {
        return new FibonacciPollInterval(timeUnit);
    }

    /**
     * Syntactic sugar for new FibonacciPollInterval(offset, timeUnit)
     *
     * @param offset   The fibonacci offset. For example if offset is 5 and poll count is 1 then the returned duration will be 8 (since  fib(6) is equal to 8).
     *                 Default offset is {@value #DEFAULT_OFFSET}.
     * @param timeUnit The time unit
     * @return A new instance of {@link FibonacciPollInterval}.
     */
    public static FibonacciPollInterval fibonacci(int offset, TimeUnit timeUnit) {
        return new FibonacciPollInterval(offset, timeUnit);
    }

    /**
     * Syntactic sugar
     *
     * @return The same of instance of {@link FibonacciPollInterval}
     */
    public FibonacciPollInterval with() {
        return this;
    }

    /**
     * Syntactic sugar
     *
     * @return The same of instance of {@link FibonacciPollInterval}
     */
    public FibonacciPollInterval and() {
        return this;
    }

    /**
     * Create a new {@link FibonacciPollInterval} with the same offset but with a different time unit
     *
     * @return The same of instance of {@link FibonacciPollInterval}
     */
    public FibonacciPollInterval timeUnit(TimeUnit unit) {
        return new FibonacciPollInterval(offset, unit);
    }

    /**
     * Create a new {@link FibonacciPollInterval} with the same time unit but with a different offset
     *
     * @return The same of instance of {@link FibonacciPollInterval}
     */
    public FibonacciPollInterval offset(int offset) {
        return new FibonacciPollInterval(offset, timeUnit);
    }

    /**
     * Generate the value of the fibonacci sequence for number.
     *
     * @param value The value
     * @return the fibonacci number
     */
    protected int fibonacci(int value) {
        return fib(value, 1, 0);
    }

    // Tail recursive implementation of fibonacci
    private int fib(int value, int current, int previous) {
        if (value == 0) {
            return previous;
        } else if (value == 1) {
            return current;
        }
        return fib(value - 1, current + previous, current);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof FibonacciPollInterval)) return false;

        FibonacciPollInterval that = (FibonacciPollInterval) o;

        return offset == that.offset && timeUnit == that.timeUnit;
    }

    @Override
    public int hashCode() {
        int result = timeUnit.hashCode();
        result = 31 * result + offset;
        return result;
    }

    @Override
    public String toString() {
        return "FibonacciPollInterval{" +
                "offset=" + offset +
                ", timeUnit=" + timeUnit +
                '}';
    }
}


/*
 * Copyright 2010 the original author or authors.
 *
 * 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 org.awaitility.reflect;

import org.awaitility.reflect.exception.FieldNotFoundException;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;

/**
 *
FieldAnnotationMatcherStrategy class.

 *
 */
public class FieldAnnotationMatcherStrategy extends FieldMatcherStrategy {

    final Class<? extends Annotation> annotation;

    /**
     *
Constructor for FieldAnnotationMatcherStrategy.

     *
     * @param annotation a {@link java.lang.Class} object.
     */
    public FieldAnnotationMatcherStrategy(Class<? extends Annotation> annotation) {
        if (annotation == null) {
            throw new IllegalArgumentException("You must specify an annotation.");
        }
        this.annotation = annotation;
    }

    /** {@inheritDoc} */
    @Override
    public boolean matches(Field field) {
        return field.isAnnotationPresent(annotation);
    }

    /** {@inheritDoc} */
    @Override
    public void notFound(Class<?> type, boolean isInstanceField) throws FieldNotFoundException {
        throw new FieldNotFoundException("No field with annotation of type \"" + annotation.getName()
                + "\" could be found in the class hierarchy of " + type.getName() + ".");
    }

    /** {@inheritDoc} */
    @Override
    public String toString() {
        return "annotation " + annotation.getName();
    }
}


/*
 * Copyright 2010 the original author or authors.
 *
 * 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 org.awaitility;

import java.util.concurrent.TimeUnit;

import static java.util.concurrent.TimeUnit.*;

/**
 *
Duration class.

 */
@SuppressWarnings("Duplicates")
public class Duration implements Comparable<Duration> {

    /**
     * Constant FOREVER
     */
    public static final Duration FOREVER = new Duration(Long.MAX_VALUE, DAYS);

    /**
     * Constant ZERO
     */
    public static final Duration ZERO = new Duration(0, MILLISECONDS);

    /**
     * Constant ONE_MILLISECOND
     *
     * @since 1.7.0
     */
    public static final Duration ONE_MILLISECOND = new Duration(1, MILLISECONDS);

    /**
     * Constant ONE_HUNDRED_MILLISECONDS
     */
    public static final Duration ONE_HUNDRED_MILLISECONDS = new Duration(100, MILLISECONDS);
    /**
     * Constant TWO_HUNDRED_MILLISECONDS
     */
    public static final Duration TWO_HUNDRED_MILLISECONDS = new Duration(200, MILLISECONDS);
    /**
     * Constant FIVE_HUNDRED_MILLISECONDS
     */
    public static final Duration FIVE_HUNDRED_MILLISECONDS = new Duration(500, MILLISECONDS);
    /**
     * Constant ONE_SECOND
     */
    public static final Duration ONE_SECOND = new Duration(1, SECONDS);
    /**
     * Constant TWO_SECONDS
     */
    public static final Duration TWO_SECONDS = new Duration(2, SECONDS);
    /**
     * Constant FIVE_SECONDS
     */
    public static final Duration FIVE_SECONDS = new Duration(5, SECONDS);
    /**
     * Constant TEN_SECONDS
     */
    public static final Duration TEN_SECONDS = new Duration(10, SECONDS);
    /**
     * Constant ONE_MINUTE
     */
    public static final Duration ONE_MINUTE = new Duration(60, SECONDS);
    /**
     * Constant TWO_MINUTES
     */
    public static final Duration TWO_MINUTES = new Duration(120, SECONDS);
    /**
     * Constant FIVE_MINUTES
     */
    public static final Duration FIVE_MINUTES = new Duration(300, SECONDS);
    /**
     * Constant TEN_MINUTES
     */
    public static final Duration TEN_MINUTES = new Duration(600, SECONDS);

    private static final int NONE = -1;

    private final long value;
    private final TimeUnit unit;

    private Duration() {
        this.value = NONE;
        this.unit = null;
    }

    /**
     *
Constructor for Duration.

     *
     * @param value a long.
     * @param unit  a {@link java.util.concurrent.TimeUnit} object.
     */
    public Duration(long value, TimeUnit unit) {
        if (value < 0) {
            throw new IllegalArgumentException("value must be >= 0, was " + value);
        }
        if (unit == null) {
            throw new IllegalArgumentException("TimeUnit cannot be null");
        }
        this.value = value;
        this.unit = unit;
    }

    /**
     *
getTimeUnit.

     *
     * @return a {@link java.util.concurrent.TimeUnit} object.
     */
    public TimeUnit getTimeUnit() {
        return unit;
    }

    /**
     *
getTimeUnitAsString.

     *
     * @return a {@link java.lang.String} object.
     */
    public String getTimeUnitAsString() {
        return unit == null ? "<not defined>" : unit.toString().toLowerCase();
    }

    /**
     *
isForever.

     *
     * @return a boolean.
     */
    public boolean isForever() {
        return this == FOREVER;
    }

    /**
     *
isZero

     *
     * @return a boolean.
     */
    public boolean isZero() {
        return this.equals(ZERO);
    }

    /**
     *
Getter for the field value.

     *
     * @return a long.
     */
    public long getValue() {
        return value;
    }

    /**
     * Get the value in milliseconds.
     *
     * @return a long.
     */
    public long getValueInMS() {
        if (value == NONE) {
            return value;
        }
        return MILLISECONDS.convert(value, unit);
    }

    /**
     * Get the value in nanoseconds.
     *
     * @return a long.
     */
    public long getValueInNS() {
        if (value == NONE) {
            return value;
        }
        return NANOSECONDS.convert(value, unit);
    }

    /**
     * Add a amount (with the same time unit as the current duration)
     *
     * @param amount The amount to add
     * @return A new amount
     */
    public Duration plus(long amount) {
        return new Plus().apply(this, unit == null ? FOREVER : new Duration(amount, unit));
    }

    /**
     * Add a amount with the given time unit. For example:
     *

     * new Duration(2, SECONDS).plus(4, MILLISECONDS)
     *

     * will return a Duration of 2004 {@link TimeUnit#MILLISECONDS}.
     *

     *

     * new Duration(2, SECONDS).plus(1, MINUTES)
     *

     * will return a Duration of 62 {@link TimeUnit#SECONDS}.
     *
     * @param amount   The amount to add
     * @param timeUnit The time unit to add
     * @return A new amount
     */
    public Duration plus(long amount, TimeUnit timeUnit) {
        if (timeUnit == null) {
            throw new IllegalArgumentException("Time unit cannot be null");
        }

        return new Plus().apply(this, new Duration(amount, timeUnit));
    }

    /**
     * Add a duration with the given time unit. For example:
     *

     * new Duration(2, SECONDS).plus(Duration.FIVE_HUNDRED_MILLISECONDS)
     *

     * will return a Duration of 2500 {@link TimeUnit#MILLISECONDS}.
     *

     *
     * @param duration The duration to add
     * @return A new duration
     */
    public Duration plus(Duration duration) {
        return new Plus().apply(this, duration);
    }

    /**
     * Multiply this amount with the given amount
     *
     * @param amount The amount
     * @return A new amount
     */
    public Duration multiply(long amount) {
        return new Multiply().apply(this, unit == null ? FOREVER : new Duration(amount, unit));
    }

    /**
     * Divide this duration amount with the given amount
     *
     * @param amount The amount
     * @return A new amount
     */
    public Duration divide(long amount) {
        return new Divide().apply(this, unit == null ? FOREVER : new Duration(amount, unit));
    }

    /**
     * Subtract an amount (with the same time unit as the current amount)
     *
     * @param amount The amount to add
     * @return A new amount
     */
    public Duration minus(long amount) {
        return new Minus().apply(this, unit == null ? FOREVER : new Duration(amount, unit));
    }

    /**
     * Subtract an amount with the given time unit. For example:
     *

     * new Duration(2, SECONDS).minus(4, MILLISECONDS)
     *

     * will return a Duration of 1996 {@link TimeUnit#MILLISECONDS}.
     *

     *

     * new Duration(2, MINUTES).minus(1, SECONDS)
     *

     * will return a Duration of 119 {@link TimeUnit#SECONDS}.
     *
     * @param amount   The amount to add
     * @param timeUnit The time unit to add
     * @return A new amount
     */
    public Duration minus(long amount, TimeUnit timeUnit) {
        if (timeUnit == null) {
            throw new IllegalArgumentException("Time unit cannot be null");
        }

        return new Minus().apply(this, new Duration(amount, timeUnit));
    }

    /**
     * Add a duration with the given time unit. For example:
     *

     * new Duration(2, SECONDS).plus(Duration.FIVE_HUNDRED_MILLISECONDS)
     *

     * will return a Duration of 2500 {@link TimeUnit#MILLISECONDS}.
     *

     *
     * @param duration The duration to add
     * @return A new duration
     */
    public Duration minus(Duration duration) {
        return new Minus().apply(this, duration);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Duration duration = (Duration) o;

        return getValueInMS() == duration.getValueInMS();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int hashCode() {
        int result = (int) (value ^ (value >>> 32));
        result = 31 * result + (unit != null ? unit.hashCode() : 0);
        return result;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String toString() {
        return "Duration{" +
                "unit=" + unit +
                ", value=" + value +
                '}';
    }

    public int compareTo(Duration other) {
        if (other == null) return 1;
        long x = getValueInMS();
        long y = other.getValueInMS();
        return (x < y) ? -1 : ((x == y) ? 0 : 1);
    }

    private static abstract class BiFunction {
        public final Duration apply(Duration lhs, Duration rhs) {
            if (lhs == null || rhs == null) {
                throw new IllegalArgumentException("Duration cannot be null");
            }

            Duration specialDuration = handleSpecialCases(lhs, rhs);
            if (specialDuration != null) {
                return specialDuration;
            }

            final Duration newDuration;
            if (lhs.getTimeUnit().ordinal() > rhs.getTimeUnit().ordinal()) {
                long lhsConverted = rhs.getTimeUnit().convert(lhs.getValue(), lhs.getTimeUnit());
                newDuration = new Duration(apply(lhsConverted, rhs.getValue()), rhs.getTimeUnit());
            } else if (lhs.getTimeUnit().ordinal() < rhs.getTimeUnit().ordinal()) {
                long rhsConverted = lhs.getTimeUnit().convert(rhs.getValue(), rhs.getTimeUnit());
                newDuration = new Duration(apply(lhs.getValue(), rhsConverted), lhs.getTimeUnit());
            } else {
                // Same time unit
                newDuration = new Duration(apply(lhs.getValue(), rhs.getValue()), lhs.getTimeUnit());
            }

            return newDuration;
        }

        protected abstract Duration handleSpecialCases(Duration lhs, Duration rhs);

        abstract long apply(long operand1, long operand2);
    }

    private static class Plus extends BiFunction {
        protected Duration handleSpecialCases(Duration lhs, Duration rhs) {
            if (ZERO.equals(rhs)) {
                return lhs;
            } else if (ZERO.equals(lhs)) {
                return rhs;
            } else if (lhs == FOREVER || rhs == FOREVER) {
                return FOREVER;
            }
            return null;
        }

        long apply(long operand1, long operand2) {
            return operand1 + operand2;
        }
    }

    private static class Minus extends BiFunction {
        protected Duration handleSpecialCases(Duration lhs, Duration rhs) {
            if (!lhs.isZero() && rhs.isZero()) {
                return lhs;
            } else if (lhs == FOREVER) {
                return FOREVER;
            } else if (rhs == FOREVER) {
                return ZERO;
            } else if (FOREVER.equals(rhs)) {
                return ZERO;
            }
            return null;
        }

        long apply(long operand1, long operand2) {
            return operand1 - operand2;
        }
    }

    private static class Multiply extends BiFunction {
        @Override
        protected Duration handleSpecialCases(Duration lhs, Duration rhs) {
            if (lhs.isZero() || rhs.isZero()) {
                return ZERO;
            } else if (lhs == FOREVER || rhs == FOREVER) {
                return FOREVER;
            }
            return null;
        }

        long apply(long operand1, long operand2) {
            return operand1 * operand2;
        }
    }

    private static class Divide extends BiFunction {
        protected Duration handleSpecialCases(Duration lhs, Duration rhs) {
            if (lhs == FOREVER) {
                return FOREVER;
            } else if (rhs == FOREVER) {
                throw new IllegalArgumentException("Cannot divide by infinity");
            } else if (ZERO.equals(lhs)) {
                return ZERO;
            }
            return null;
        }

        long apply(long operand1, long operand2) {
            return operand1 / operand2;
        }
    }
}


/*
 * Copyright 2008 the original author or authors.
 *
 * 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 org.awaitility.reflect;

import org.awaitility.reflect.exception.FieldNotFoundException;
import org.awaitility.reflect.exception.TooManyFieldsFoundException;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;

/**
 * Various utilities for accessing internals of a class. Basically a simplified
 * reflection utility. Copied with permission from PowerMock project.
 */
public class WhiteboxImpl {

    /**
     * Get the value of a field using reflection. This method will iterate
     * through the entire class hierarchy and return the value of the first
     * field named <tt>fieldName</tt>. If you want to get a specific field value
     * at specific place in the class hierarchy please refer to
     *
     * @param <T>       the generic type
     * @param object    the object to modify
     * @param fieldName the name of the field
     * @return the internal state.
     */
    @SuppressWarnings("unchecked")
    public static <T> T getInternalState(Object object, String fieldName) {
        Field foundField = findFieldInHierarchy(object, fieldName);
        try {
            return (T) foundField.get(object);
        } catch (IllegalAccessException e) {
            throw new RuntimeException("Internal error: Failed to get field in method getInternalState.", e);
        }
    }

    /**
     * Find field in hierarchy.
     *
     * @param object    the object
     * @param fieldName the field name
     * @return the field
     */
    private static Field findFieldInHierarchy(Object object, String fieldName) {
        return findFieldInHierarchy(object, new FieldNameMatcherStrategy(fieldName));
    }

    /**
     * Find field in hierarchy.
     *
     * @param object   the object
     * @param strategy the strategy
     * @return the field
     */
    private static Field findFieldInHierarchy(Object object, FieldMatcherStrategy strategy) {
        assertObjectInGetInternalStateIsNotNull(object);
        return findSingleFieldUsingStrategy(strategy, object, true, getType(object));
    }

    /**
     * Assert object in get internal state is not null.
     *
     * @param object the object
     */
    private static void assertObjectInGetInternalStateIsNotNull(Object object) {
        if (object == null) {
            throw new IllegalArgumentException("The object containing the field cannot be null");
        }
    }

    /**
     * Find single field using strategy.
     *
     * @param strategy       the strategy
     * @param object         the object
     * @param checkHierarchy the check hierarchy
     * @param startClass     the start class
     * @return the field
     */
    private static Field findSingleFieldUsingStrategy(FieldMatcherStrategy strategy, Object object,
                                                      boolean checkHierarchy, Class<?> startClass) {
        assertObjectInGetInternalStateIsNotNull(object);
        Field foundField = null;
        final Class<?> originalStartClass = startClass;
        while (startClass != null) {
            final Field[] declaredFields = startClass.getDeclaredFields();
            for (Field field : declaredFields) {
                if (strategy.matches(field) && hasFieldProperModifier(object, field)) {
                    if (foundField != null) {
                        throw new TooManyFieldsFoundException("Two or more fields matching " + strategy + ".");
                    }
                    foundField = field;
                }
            }
            if (foundField != null) {
                break;
            } else if (!checkHierarchy) {
                break;
            }
            startClass = startClass.getSuperclass();
        }
        if (foundField == null) {
            strategy.notFound(originalStartClass, !isClass(object));
            return null;
        }
        foundField.setAccessible(true);
        return foundField;
    }

    /**
     * Checks for field proper modifier.
     *
     * @param object the object
     * @param field  the field
     * @return true, if successful
     */
    private static boolean hasFieldProperModifier(Object object, Field field) {
        return ((object instanceof Class<?> && Modifier.isStatic(field.getModifiers())) || !(object instanceof Class<?> || Modifier
                .isStatic(field.getModifiers())));
    }

    /**
     * Get the value of a field using reflection. This method will traverse the
     * super class hierarchy until the first field of type <tt>fieldType</tt> is
     * found. The value of this field will be returned.
     *
     * @param <T>       the generic type
     * @param object    the object to modify
     * @param fieldType the type of the field
     * @return the internal state
     */
    @SuppressWarnings("unchecked")
    public static <T> T getInternalState(Object object, Class<T> fieldType) {
        Field foundField = findFieldInHierarchy(object, new AssignableToFieldTypeMatcherStrategy(fieldType));
        try {
            return (T) foundField.get(object);
        } catch (IllegalAccessException e) {
            throw new RuntimeException("Internal error: Failed to get field in method getInternalState.", e);
        }
    }

    /**
     * Throw exception if field was not found.
     *
     * @param type      the type
     * @param fieldName the field name
     * @param field     the field
     */
    public static void throwExceptionIfFieldWasNotFound(Class<?> type, String fieldName, Field field) {
        if (field == null) {
            throw new FieldNotFoundException("No field was found with name '" + fieldName + "' in class "
                    + type.getName() + ".");
        }
    }

    /**
     * Gets the type.
     *
     * @param object the object
     * @return The type of the of an object.
     */
    public static Class<?> getType(Object object) {
        Class<?> type = null;
        if (isClass(object)) {
            type = (Class<?>) object;
        } else if (object != null) {
            type = object.getClass();
        }
        return type;
    }

    /**
     * Get field annotated with a particular annotation. This method traverses
     * the class hierarchy when checking for the annotation.
     *
     * @param object         The object to look for annotations. Note that if're you're
     *                       passing an object only instance fields are checked, passing a
     *                       class will only check static fields.
     * @param annotationType The annotation types to look for
     * @return A set of all fields containing the particular annotation(s).
     * @since 1.3
     */
    public static Field getFieldAnnotatedWith(Object object, Class<? extends Annotation> annotationType) {
        return findSingleFieldUsingStrategy(new FieldAnnotationMatcherStrategy(annotationType), object, true,
                getType(object));
    }

    /**
     * Checks if is class.
     *
     * @param argument the argument
     * @return a boolean.
     */
    public static boolean isClass(Object argument) {
        return argument instanceof Class<?>;
    }

    /**
     *
getByNameAndType.

     *
     * @param object a {@link java.lang.Object} object.
     * @param fieldName a {@link java.lang.String} object.
     * @param expectedFieldType a {@link java.lang.Class} object.
     * @param <T> a T object.
     * @return a T object.
     */
    @SuppressWarnings("unchecked")
    public static <T> T getByNameAndType(Object object, String fieldName, Class<T> expectedFieldType) {
        Field foundField = findSingleFieldUsingStrategy(new FieldNameAndTypeMatcherStrategy(fieldName,
                expectedFieldType), object, true, getType(object));
        try {
            return (T) foundField.get(object);
        } catch (IllegalAccessException e) {
            throw new RuntimeException("Internal error: Failed to get field in method getInternalState.", e);
        }
    }
}


/*
 * Copyright 2010 the original author or authors.
 *
 * 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 org.awaitility.core;

import org.hamcrest.Matcher;

import java.util.LinkedList;
import java.util.List;

/**
 * The Class HamcrestToStringFilter.
 */
class HamcrestToStringFilter {
	private static final List<String> wordsToRemove = new LinkedList<String>();
	static {
		wordsToRemove.add("not not ");
		wordsToRemove.add("is ");
	}

	/**
	 * Filter words from the matcher.toString() so it looks nicer
	 * when printed out. E.g. "not not" is removed and "is" are removed.
	 *
	 * @param matcher
	 *            the matcher
	 * @return A filtered version of the {@link Matcher#toString()}.
	 */
	static String filter(Matcher<?> matcher) {
		String matcherToString = matcher.toString();
		for (String wordToRemove : wordsToRemove) {
			matcherToString = matcherToString.replaceAll(wordToRemove, "");
		}
		return matcherToString;
	}
}