/*
 * 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 com.datastax.driver.core.policies;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicLongArray;
import java.util.concurrent.atomic.AtomicReference;

/** A "rolling" count over a 1-minute sliding window. */
class RollingCount {
  // Divide the minute into 5-second intervals
  private static final long INTERVAL_SIZE = TimeUnit.SECONDS.toNanos(5);

  // A circular buffer containing the counts over the 12 previous intervals. Their sum is the count
  // we're looking for.
  // If we're at t = 61s this would span [0,60[
  private final AtomicLongArray previousIntervals = new AtomicLongArray(12);
  // The interval we're currently recording events for. It hasn't completed yet, so it's not
  // included in the count.
  // If we're at t = 61s this would span [60,65[
  // Note that we don't expect very high concurrency on RollingCount (it's used to count errors), so
  // AtomicLong is
  // good enough here.
  private final AtomicLong currentInterval = new AtomicLong();
  // Other mutable state, grouped in an object for atomic updates
  private final AtomicReference<State> state;
  private final Clock clock;

  RollingCount(Clock clock) {
    this.state = new AtomicReference<State>(new State(clock.nanoTime()));
    this.clock = clock;
  }

  void increment() {
    add(1);
  }

  void add(long amount) {
    tickIfNecessary();
    currentInterval.addAndGet(amount);
  }

  long get() {
    tickIfNecessary();
    return state.get().totalCount;
  }

  private void tickIfNecessary() {
    State oldState = state.get();
    long newTick = clock.nanoTime();
    long age = newTick - oldState.lastTick;
    if (age >= INTERVAL_SIZE) {
      long currentCount = currentInterval.get();

      long newIntervalStartTick = newTick - age % INTERVAL_SIZE;
      long elapsedIntervals = Math.min(age / INTERVAL_SIZE, 12);
      int newOffset = (int) ((oldState.offset + elapsedIntervals) % 12);

      long newTotal;
      if (elapsedIntervals == 12) {
        // We wrapped around the circular buffer, all our values are stale
        // Don't mutate previousIntervals yet because this part of the code is still multi-threaded.
        newTotal = 0;
      } else {
        // Add the current interval that just completed
        newTotal = oldState.totalCount + currentCount;
        // Subtract all elapsed intervals: they're either idle ones, or the one at the old offset
        // that we're
        // about to replace
        for (int i = 1; i <= elapsedIntervals; i++) {
          newTotal -= previousIntervals.get((newOffset + 12 - i) % 12);
        }
      }

      State newState = new State(newIntervalStartTick, newOffset, newTotal);
      if (state.compareAndSet(oldState, newState)) {
        // Only one thread gets here, so we can now:
        // - reset the current count (don't use reset because other threads might already have
        // started updating
        // it)
        currentInterval.addAndGet(-currentCount);
        // - store the interval that just completed (or clear it if we wrapped)
        previousIntervals.set(oldState.offset, elapsedIntervals < 12 ? currentCount : 0);
        // - clear any idle interval
        for (int i = 1; i < elapsedIntervals; i++) {
          previousIntervals.set((newOffset + 12 - i) % 12, 0);
        }
      }
    }
  }

  static class State {
    final long lastTick; // last time the state was modified
    final int offset; // the offset that the current interval will replace once it's complete
    final long totalCount; // cache of sum(previousIntervals)

    State(long lastTick) {
      this(lastTick, 0, 0);
    }

    State(long lastTick, int offset, long totalCount) {
      this.lastTick = lastTick;
      this.offset = offset;
      this.totalCount = totalCount;
    }
  }
}
