package agent

// Implements Backoff Interface for use with github.com/cenkalti/backoff

import (
	"math/rand"
	"time"

	log "github.com/Sirupsen/logrus"
	"github.com/cenkalti/backoff"
)

// ExponentialBackoff implements backoff.Backoff
type ExponentialBackoff struct {
	InitialInterval time.Duration
	Multiplier      float64
	MaxInterval     time.Duration
	// After MaxElapsedTime the ExponentialBackOff stops.
	// It never stops if MaxElapsedTime == 0.
	MaxElapsedTime time.Duration
	Clock          Clock

	currentInterval time.Duration
	startTime       time.Time
}

// Clock implements backoff.Clock
type Clock interface {
	Now() time.Time
}

type systemClock struct{}

func (t systemClock) Now() time.Time {
	return time.Now()
}

// SystemClock implements Clock interface that uses time.Now().
var SystemClock = systemClock{}

// NewExponentialBackoff returns an ExponentialBackoff with defaults set
func NewExponentialBackoff() *ExponentialBackoff {
	b := &ExponentialBackoff{
		InitialInterval: defaultInitialInterval * time.Second,
		Multiplier:      defaultBackoffMultiplier,
		MaxElapsedTime:  defaultMaxWaitTime * time.Second,
		MaxInterval:     defaultMaxWaitTime * time.Second,
		Clock:           SystemClock,
	}
	b.Reset()
	return b
}

// Reset to initial state
func (b *ExponentialBackoff) Reset() {
	b.currentInterval = b.InitialInterval
	b.startTime = b.Clock.Now()
}

// NextBackOff returns the duration to wait before retrying the operation
func (b *ExponentialBackoff) NextBackOff() time.Duration {
	if b.MaxElapsedTime != 0 && b.GetElapsedTime() >= b.MaxElapsedTime {
		return backoff.Stop
	}
	b.incrementCurrentInterval()
	return getRandomValueFromInterval(b.currentInterval, b.GetElapsedTime(), b.MaxElapsedTime)
}

// GetElapsedTime returns time elapsed since starting backoff
func (b *ExponentialBackoff) GetElapsedTime() time.Duration {
	return b.Clock.Now().Sub(b.startTime)
}

func (b *ExponentialBackoff) incrementCurrentInterval() {
	// Check for overflow, if overflow is detected set the current interval to the max interval.
	if float64(b.currentInterval) >= float64(b.MaxInterval)/b.Multiplier {
		b.currentInterval = b.MaxInterval
	} else {
		b.currentInterval = time.Duration(float64(b.currentInterval) * b.Multiplier)
	}
}

// Returns a random value from the following interval: [0, 2^n] where n is number of attempts
func getRandomValueFromInterval(currentInterval time.Duration, elapsedTime time.Duration, maxElapsedTime time.Duration) time.Duration {
	rV := time.Duration(rand.Int63n(int64(currentInterval)))
	if rV+elapsedTime > maxElapsedTime+1 {
		rV = maxElapsedTime - elapsedTime + 1
	}
	log.Infof("trying again in %f seconds...", rV.Seconds())
	return rV
}

func (a *Agent) newExponentialBackoff() *ExponentialBackoff {
	b := NewExponentialBackoff()
	b.Multiplier = float64(a.BackoffMultiplier)
	b.InitialInterval = time.Duration(a.BackoffMultiplier) * time.Second
	b.MaxInterval = time.Duration(a.MaxWaitTime) * time.Second
	b.MaxElapsedTime = time.Duration(a.MaxWaitTime) * time.Second
	return b
}
