/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.apache.cassandra.utils;

import java.util.concurrent.ConcurrentNavigableMap;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;

/**
 * Concurrent rate computation over a sliding time window.
 *
 * Currently not used in the Cassandra 4.0 code base. If you decide to use it, please check CASSANDRA-16713.
 * There still might be a bug, flaky test to be fixed before using it again.
 */
public class SlidingTimeRate
{
    private final ConcurrentSkipListMap<Long, AtomicInteger> counters = new ConcurrentSkipListMap<>();
    private final AtomicLong lastCounterTimestamp = new AtomicLong(0);
    private final ReadWriteLock pruneLock = new ReentrantReadWriteLock();
    private final long sizeInMillis;
    private final long precisionInMillis;
    private final TimeSource timeSource;

    /**
     * Creates a sliding rate whose time window is of the given size, with the given precision and time unit.
     * <p>
     * The precision defines how accurate the rate computation is, as it will be computed over window size +/-
     * precision.
     * </p>
     */
    public SlidingTimeRate(TimeSource timeSource, long size, long precision, TimeUnit unit)
    {
        Preconditions.checkArgument(size > precision, "Size should be greater than precision.");
        Preconditions.checkArgument(TimeUnit.MILLISECONDS.convert(precision, unit) >= 1, "Precision must be greater than or equal to 1 millisecond.");
        this.sizeInMillis = TimeUnit.MILLISECONDS.convert(size, unit);
        this.precisionInMillis = TimeUnit.MILLISECONDS.convert(precision, unit);
        this.timeSource = timeSource;
    }

    /**
     * Updates the rate.
     */
    public void update(int delta)
    {
        pruneLock.readLock().lock();
        try
        {
            while (true)
            {
                long now = timeSource.currentTimeMillis();
                long lastTimestamp = lastCounterTimestamp.get();
                boolean isWithinPrecisionRange = (now - lastTimestamp) < precisionInMillis;
                AtomicInteger lastCounter = counters.get(lastTimestamp);
                // If there's a valid counter for the current last timestamp, and we're in the precision range,
                // update such counter:
                if (lastCounter != null && isWithinPrecisionRange)
                {
                    lastCounter.addAndGet(delta);

                    break;
                }
                // Else if there's no counter or we're past the precision range, try to create a new counter,
                // but only the thread updating the last timestamp will create a new counter:
                else if (lastCounterTimestamp.compareAndSet(lastTimestamp, now))
                {
                    AtomicInteger existing = counters.putIfAbsent(now, new AtomicInteger(delta));
                    if (existing != null)
                    {
                        existing.addAndGet(delta);
                    }

                    break;
                }
            }
        }
        finally
        {
            pruneLock.readLock().unlock();
        }
    }

    /**
     * Gets the current rate in the given time unit from the beginning of the time window to the
     * provided point in time ago.
     */
    public double get(long toAgo, TimeUnit unit)
    {
        pruneLock.readLock().lock();
        try
        {
            long toAgoInMillis = TimeUnit.MILLISECONDS.convert(toAgo, unit);
            Preconditions.checkArgument(toAgoInMillis < sizeInMillis, "Cannot get rate in the past!");

            long now = timeSource.currentTimeMillis();
            long sum = 0;
            ConcurrentNavigableMap<Long, AtomicInteger> tailCounters = counters
                    .tailMap(now - sizeInMillis, true)
                    .headMap(now - toAgoInMillis, true);
            for (AtomicInteger i : tailCounters.values())
            {
                sum += i.get();
            }

            double rateInMillis = sum == 0
                                  ? sum
                                  : sum / (double) Math.max(1000, (now - toAgoInMillis) - tailCounters.firstKey());
            double multiplier = TimeUnit.MILLISECONDS.convert(1, unit);
            return rateInMillis * multiplier;
        }
        finally
        {
            pruneLock.readLock().unlock();
        }
    }

    /**
     * Gets the current rate in the given time unit.
     */
    public double get(TimeUnit unit)
    {
        return get(0, unit);
    }

    /**
     * Prunes the time window of old unused updates.
     */
    public void prune()
    {
        pruneLock.writeLock().lock();
        try
        {
            long now = timeSource.currentTimeMillis();
            counters.headMap(now - sizeInMillis, false).clear();
        }
        finally
        {
            pruneLock.writeLock().unlock();
        }
    }

    @VisibleForTesting
    public int size()
    {
        return counters.values().stream().reduce(new AtomicInteger(), (v1, v2) -> {
            v1.addAndGet(v2.get());
            return v1;
        }).get();
    }
}
