// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause

// This is a modified, simplified version of code from golang.org/x/time/rate.

// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Package rate provides a rate limiter.
package rate

import (
	"sync"
	"time"

	"tailscale.com/tstime/mono"
)

// Limit defines the maximum frequency of some events.
// Limit is represented as number of events per second.
// A zero Limit is invalid.
type Limit float64

// Every converts a minimum time interval between events to a Limit.
func Every(interval time.Duration) Limit {
	if interval <= 0 {
		panic("invalid interval")
	}
	return 1 / Limit(interval.Seconds())
}

// A Limiter controls how frequently events are allowed to happen.
// It implements a [token bucket] of a particular size b,
// initially full and refilled at rate r tokens per second.
// Informally, in any large enough time interval,
// the Limiter limits the rate to r tokens per second,
// with a maximum burst size of b events.
// Use NewLimiter to create non-zero Limiters.
//
// [token bucket]: https://en.wikipedia.org/wiki/Token_bucket
type Limiter struct {
	limit  Limit
	burst  float64
	mu     sync.Mutex // protects following fields
	tokens float64    // number of tokens currently in bucket
	last   mono.Time  // the last time the limiter's tokens field was updated
}

// NewLimiter returns a new Limiter that allows events up to rate r and permits
// bursts of at most b tokens.
func NewLimiter(r Limit, b int) *Limiter {
	if b < 1 {
		panic("bad burst, must be at least 1")
	}
	return &Limiter{limit: r, burst: float64(b)}
}

// Allow reports whether an event may happen now.
func (lim *Limiter) Allow() bool {
	return lim.allow(mono.Now())
}

func (lim *Limiter) allow(now mono.Time) bool {
	lim.mu.Lock()
	defer lim.mu.Unlock()

	// If time has moved backwards, look around awkwardly and pretend nothing happened.
	if now.Before(lim.last) {
		lim.last = now
	}

	// Calculate the new number of tokens available due to the passage of time.
	elapsed := now.Sub(lim.last)
	tokens := lim.tokens + float64(lim.limit)*elapsed.Seconds()
	if tokens > lim.burst {
		tokens = lim.burst
	}

	// Consume a token.
	tokens--

	// Update state.
	ok := tokens >= 0
	if ok {
		lim.last = now
		lim.tokens = tokens
	}
	return ok
}
