package sink

import (
	cryptorand "crypto/rand"
	"math"
	"math/big"
	mathrand "math/rand"
	"time"
)

const jitter = 0.25

// randDuration returns a random time.Duration between min and max. It
// uses crypto/rand if possible, but uses math/rand's global rand if
// it encounters an error.
func randDuration(min, max time.Duration) time.Duration {
	if min > max {
		min, max = max, min
	}
	if min == max {
		return min
	}

	r, err := cryptorand.Int(cryptorand.Reader, big.NewInt(int64(max-min)))
	if err != nil {
		// fall back to math/rand only if we must
		r := mathrand.Int63n(int64(max - min))
		return time.Duration(r + int64(min))
	}
	return time.Duration(r.Int64() + int64(min))
}

// backoff returns the amount of time to sleep before the i-th attempt (zero-indexed).
func backoff(i int, unit, max time.Duration) time.Duration {
	if i == 0 {
		return 0
	}
	var dur time.Duration
	if wouldExceedMax(i, unit, max) {
		dur = max
	} else {
		dur = time.Duration(math.Pow(2, float64(i-1))) * unit
	}
	jitterBy := time.Duration(jitter * float64(dur))
	return randDuration(dur-jitterBy, dur+jitterBy)
}

// to avoid overflows, check if (2**i-1) * unit > max by working in
// log space: check if i-1 > log2(max / unit)
func wouldExceedMax(i int, unit, max time.Duration) bool {
	return (i - 1) > int(math.Log2(float64(max)/float64(unit)))
}
