// Copyright 2012-present Oliver Eilhard. All rights reserved.
// Use of this source code is governed by a MIT-license.
// See http://olivere.mit-license.org/license.txt for details.

package backoff

import (
	"math"
	"math/rand"
	"sync"
	"sync/atomic"
	"time"
)

// Backoff is an interface for different types of backoff algorithms.
type Backoff interface {
	Next() time.Duration
	Reset()
}

// Stop is used as a signal to indicate that no more retries should be made.
const Stop time.Duration = -1

// -- Simple Backoff --

// SimpleBackoff takes a list of fixed values for backoff intervals.
// Each call to Next returns the next value from that fixed list.
// After each value is returned, subsequent calls to Next will only return
// the last element. The caller may specify if the values are "jittered".
type SimpleBackoff struct {
	sync.Mutex
	ticks  []int
	index  int
	jitter bool
	stop   bool
}

// NewSimpleBackoff creates a SimpleBackoff algorithm with the specified
// list of fixed intervals in milliseconds.
func NewSimpleBackoff(ticks ...int) *SimpleBackoff {
	return &SimpleBackoff{
		ticks:  ticks,
		index:  0,
		jitter: false,
		stop:   false,
	}
}

// Jitter, when set, randomizes to return a value of [0.5*value .. 1.5*value].
func (b *SimpleBackoff) Jitter(doJitter bool) *SimpleBackoff {
	b.Lock()
	defer b.Unlock()
	b.jitter = doJitter
	return b
}

// SendStop, when enables, makes Next to return Stop once
// the list of values is exhausted.
func (b *SimpleBackoff) SendStop(doStop bool) *SimpleBackoff {
	b.Lock()
	defer b.Unlock()
	b.stop = doStop
	return b
}

// Next returns the next wait interval.
func (b *SimpleBackoff) Next() time.Duration {
	b.Lock()
	defer b.Unlock()

	i := b.index
	if i >= len(b.ticks) {
		if b.stop {
			return Stop
		}
		i = len(b.ticks) - 1
		b.index = i
	} else {
		b.index++
	}

	ms := b.ticks[i]
	if b.jitter {
		ms = jitter(ms)
	}
	return time.Duration(ms) * time.Millisecond
}

// Reset resets SimpleBackoff.
func (b *SimpleBackoff) Reset() {
	b.Lock()
	b.index = 0
	b.Unlock()
}

// jitter randomizes the interval to return a value of [0.5*millis .. 1.5*millis].
func jitter(millis int) int {
	if millis <= 0 {
		return 0
	}
	return millis/2 + rand.Intn(millis)
}

// -- Exponential --

// ExponentialBackoff implements the simple exponential backoff described by
// Douglas Thain at http://dthain.blogspot.de/2009/02/exponential-backoff-in-distributed.html.
type ExponentialBackoff struct {
	sync.Mutex
	t    float64 // initial timeout (in msec)
	f    float64 // exponential factor (e.g. 2)
	m    float64 // maximum timeout (in msec)
	n    int64   // number of retries
	stop bool    // indicates whether Next should send "Stop" whan max timeout is reached
}

// NewExponentialBackoff returns a ExponentialBackoff backoff policy.
// Use initialTimeout to set the first/minimal interval
// and maxTimeout to set the maximum wait interval.
func NewExponentialBackoff(initialTimeout, maxTimeout time.Duration) *ExponentialBackoff {
	return &ExponentialBackoff{
		t:    float64(int64(initialTimeout / time.Millisecond)),
		f:    2.0,
		m:    float64(int64(maxTimeout / time.Millisecond)),
		n:    0,
		stop: false,
	}
}

// SendStop, when enables, makes Next to return Stop once
// the maximum timeout is reached.
func (b *ExponentialBackoff) SendStop(doStop bool) *ExponentialBackoff {
	b.Lock()
	defer b.Unlock()
	b.stop = doStop
	return b
}

// Next returns the next wait interval.
func (t *ExponentialBackoff) Next() time.Duration {
	t.Lock()
	defer t.Unlock()

	n := float64(atomic.AddInt64(&t.n, 1))
	r := 1.0 + rand.Float64() // random number in [1..2]
	m := math.Min(r*t.t*math.Pow(t.f, n), t.m)
	if t.stop && m >= t.m {
		return Stop
	}
	d := time.Duration(int64(m)) * time.Millisecond
	return d
}

// Reset resets the backoff policy so that it can be reused.
func (t *ExponentialBackoff) Reset() {
	t.Lock()
	t.n = 0
	t.Unlock()
}
