/**
 * 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.pulsar.common.util;

import static com.google.common.base.Preconditions.checkArgument;

import com.google.common.base.MoreObjects;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * A Rate Limiter that distributes permits at a configurable rate. Each {@link #acquire()} blocks if necessary until a
 * permit is available, and then takes it. Each {@link #tryAcquire()} tries to acquire permits from available permits,
 * it returns true if it succeed else returns false. Rate limiter release configured permits at every configured rate
 * time, so, on next ticket new fresh permits will be available.
 *
 * <p>For example: if RateLimiter is configured to release 10 permits at every 1 second then RateLimiter will allow to
 * acquire 10 permits at any time with in that 1 second.
 *
 * <p>Comparison with other RateLimiter such as {@link com.google.common.util.concurrent.RateLimiter}
 * <ul>
 * <li><b>Per second rate-limiting:</b> Per second rate-limiting not satisfied by Guava-RateLimiter</li>
 * <li><b>Guava RateLimiter:</b> For X permits: it releases X/1000 permits every msec. therefore,
 * for permits=2/sec =&gt; it release 1st permit on first 500msec and 2nd permit on next 500ms. therefore,
 * if 2 request comes with in 500msec duration then 2nd request fails to acquire permit
 * though we have configured 2 permits/second.</li>
 * <li><b>RateLimiter:</b> it releases X permits every second. so, in above usecase:
 * if 2 requests comes at the same time then both will acquire the permit.</li>
 * <li><b>Faster: </b>RateLimiter is light-weight and faster than Guava-RateLimiter</li>
 * </ul>
 */
public class RateLimiter implements AutoCloseable{

    private final ScheduledExecutorService executorService;
    private long rateTime;
    private TimeUnit timeUnit;
    private final boolean externalExecutor;
    private ScheduledFuture<?> renewTask;
    private long permits;
    private long acquiredPermits;
    private boolean isClosed;

    public RateLimiter(final long permits, final long rateTime, final TimeUnit timeUnit) {
        this(null, permits, rateTime, timeUnit);
    }

    public RateLimiter(final ScheduledExecutorService service, final long permits, final long rateTime,
            final TimeUnit timeUnit) {
        checkArgument(permits > 0, "rate must be > 0");
        checkArgument(rateTime > 0, "Renew permit time must be > 0");

        this.rateTime = rateTime;
        this.timeUnit = timeUnit;
        this.permits = permits;

        if (service != null) {
            this.executorService = service;
            this.externalExecutor = true;
        } else {
            final ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1);
            executor.setContinueExistingPeriodicTasksAfterShutdownPolicy(false);
            executor.setExecuteExistingDelayedTasksAfterShutdownPolicy(false);
            this.executorService = executor;
            this.externalExecutor = false;
        }

    }

    @Override
    public synchronized void close() {
        if (!isClosed) {
            if (!externalExecutor) {
                executorService.shutdownNow();
            }
            if (renewTask != null) {
                renewTask.cancel(false);
            }
            isClosed = true;
        }
    }

    public synchronized boolean isClosed() {
        return isClosed;
    }

    /**
     * Acquires the given number of permits from this {@code RateLimiter}, blocking until the request be granted.
     *
     * <p>This method is equivalent to {@code acquire(1)}.
     */
    public synchronized void acquire() throws InterruptedException {
        acquire(1);
    }

    /**
     * Acquires the given number of permits from this {@code RateLimiter}, blocking until the request be granted.
     *
     * @param acquirePermit
     *            the number of permits to acquire
     */
    public synchronized void acquire(long acquirePermit) throws InterruptedException {
        checkArgument(!isClosed(), "Rate limiter is already shutdown");
        checkArgument(acquirePermit <= this.permits,
                "acquiring permits must be less or equal than initialized rate =" + this.permits);

        // lazy init and start task only once application start using it
        if (renewTask == null) {
            renewTask = createTask();
        }

        boolean canAcquire = false;
        do {
            canAcquire = acquirePermit < 0 || acquiredPermits < this.permits;
            if (!canAcquire) {
                wait();
            } else {
                acquiredPermits += acquirePermit;
            }
        } while (!canAcquire);
    }

    /**
     * Acquires permits from this {@link RateLimiter} if it can be acquired immediately without delay.
     *
     * <p>This method is equivalent to {@code tryAcquire(1)}.
     *
     * @return {@code true} if the permits were acquired, {@code false} otherwise
     */
    public synchronized boolean tryAcquire() {
        return tryAcquire(1);
    }

    /**
     * Acquires permits from this {@link RateLimiter} if it can be acquired immediately without delay.
     *
     * @param acquirePermit
     *            the number of permits to acquire
     * @return {@code true} if the permits were acquired, {@code false} otherwise
     */
    public synchronized boolean tryAcquire(long acquirePermit) {
        checkArgument(!isClosed(), "Rate limiter is already shutdown");
        // lazy init and start task only once application start using it
        if (renewTask == null) {
            renewTask = createTask();
        }

        // acquired-permits can't be larger than the rate
        if (acquirePermit > this.permits) {
            acquiredPermits = this.permits;
            return false;
        }
        boolean canAcquire = acquirePermit < 0 || acquiredPermits < this.permits;
        if (canAcquire) {
            acquiredPermits += acquirePermit;
        }
        return canAcquire;
    }

    /**
     * Return available permits for this {@link RateLimiter}.
     *
     * @return returns 0 if permits is not available
     */
    public synchronized long getAvailablePermits() {
        return Math.max(0, this.permits - this.acquiredPermits);
    }

    /**
     * Resets new rate by configuring new value for permits per configured rate-period.
     *
     * @param permits
     */
    public synchronized void setRate(long permits) {
        this.permits = permits;
    }

    /**
     * Resets new rate with new permits and rate-time.
     *
     * @param permits
     * @param rateTime
     * @param timeUnit
     */
    public synchronized void setRate(long permits, long rateTime, TimeUnit timeUnit) {
        if (renewTask != null) {
            renewTask.cancel(false);
        }
        this.permits = permits;
        this.rateTime = rateTime;
        this.timeUnit = timeUnit;
        this.renewTask = createTask();
    }

    /**
     * Returns configured permit rate per pre-configured rate-period.
     *
     * @return rate
     */
    public synchronized long getRate() {
        return this.permits;
    }

    public synchronized long getRateTime() {
        return this.rateTime;
    }

    public synchronized TimeUnit getRateTimeUnit() {
        return this.timeUnit;
    }

    protected ScheduledFuture<?> createTask() {
        return executorService.scheduleAtFixedRate(this::renew, this.rateTime, this.rateTime, this.timeUnit);
    }

    synchronized void renew() {
        acquiredPermits = 0;
        notifyAll();
    }

    @Override
    public String toString() {
        return MoreObjects.toStringHelper(this).add("rateTime", rateTime).add("permits", permits)
                .add("acquiredPermits", acquiredPermits).toString();
    }

}
