package retry

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

// ExponentialBackoff is a retry mechanism for retrying some calls.
type ExponentialBackoff struct {
	// InitialInterval is the initial time interval for backoff algorithm.
	InitialInterval time.Duration

	// MaxBackoffTime is the maximum time duration for backoff algorithm. It limits
	// the maximum sleep time.
	MaxBackoffTime time.Duration

	// Multiplier is a multiplier number of the backoff algorithm.
	Multiplier float64

	// MaxRetryCount is the maximum number of retry count.
	MaxRetryCount int

	// currentInterval tracks the current sleep time.
	currentInterval time.Duration
}

// NewExponentialBackoff creates a ExponentialBackoff with default values.
func NewExponentialBackoff(config ...Config) *ExponentialBackoff {
	cfg := configDefault(config...)
	return &ExponentialBackoff{
		InitialInterval: cfg.InitialInterval,
		MaxBackoffTime:  cfg.MaxBackoffTime,
		Multiplier:      cfg.Multiplier,
		MaxRetryCount:   cfg.MaxRetryCount,
		currentInterval: cfg.currentInterval,
	}
}

// Retry is the core logic of the retry mechanism. If the calling function returns
// nil as an error, then the Retry method is terminated with returning nil. Otherwise,
// if all function calls are returned error, then the method returns this error.
func (e *ExponentialBackoff) Retry(f func() error) error {
	if e.currentInterval <= 0 {
		e.currentInterval = e.InitialInterval
	}
	var err error
	for i := 0; i < e.MaxRetryCount; i++ {
		err = f()
		if err == nil {
			return nil
		}
		if i < e.MaxRetryCount-1 {
			next := e.next()
			time.Sleep(next)
		}
	}
	return err
}

// next calculates the next sleeping time interval.
func (e *ExponentialBackoff) next() time.Duration {
	// generate a random value between [0, 1000)
	n, err := rand.Int(rand.Reader, big.NewInt(1000))
	if err != nil {
		return e.MaxBackoffTime
	}
	t := e.currentInterval + (time.Duration(n.Int64()) * time.Millisecond)
	e.currentInterval = time.Duration(float64(e.currentInterval) * e.Multiplier)
	if t >= e.MaxBackoffTime {
		e.currentInterval = e.MaxBackoffTime
		return e.MaxBackoffTime
	}
	return t
}
