package context

import (
	"context"
	"errors"
	"math"
	"time"
)

type BackoffStrategy string

const (
	BackoffStrategyNone        = "none"
	BackoffStrategyConstant    = "constant"
	BackoffStrategyLinear      = "linear"
	BackoffStrategyExponential = "exponential"
)

var DefaultRetryParams = RetryParams{Strategy: BackoffStrategyNone}

// RetryParams holds parameters applied to retries
type RetryParams struct {
	// Strategy is the backoff strategy to applies between retries
	Strategy BackoffStrategy

	// MaxTries is the maximum number of times to retry request before giving up
	MaxTries int

	// Period is
	// - for none strategy: no delay
	// - for constant strategy: the delay interval between retries
	// - for linear strategy: interval between retries = Period * retries
	// - for exponential strategy: interval between retries = Period * retries^2
	Period time.Duration
}

// BackoffFor tries will return the time duration that should be used for this
// current try count.
// `tries` is assumed to be the number of times the caller has already retried.
func (r *RetryParams) BackoffFor(tries int) time.Duration {
	switch r.Strategy {
	case BackoffStrategyConstant:
		return r.Period
	case BackoffStrategyLinear:
		return r.Period * time.Duration(tries)
	case BackoffStrategyExponential:
		exp := math.Exp2(float64(tries))
		return r.Period * time.Duration(exp)
	case BackoffStrategyNone:
		fallthrough // default
	default:
		return r.Period
	}
}

// Backoff is a blocking call to wait for the correct amount of time for the retry.
// `tries` is assumed to be the number of times the caller has already retried.
func (r *RetryParams) Backoff(ctx context.Context, tries int) error {
	if tries > r.MaxTries {
		return errors.New("too many retries")
	}
	ticker := time.NewTicker(r.BackoffFor(tries))
	select {
	case <-ctx.Done():
		ticker.Stop()
		return errors.New("context has been cancelled")
	case <-ticker.C:
		ticker.Stop()
	}
	return nil
}
