// Package retry provides the most advanced interruptible mechanism
// to perform actions repetitively until successful.
package retry

import (
	"context"
	"sync/atomic"
)

// Retry takes action and performs it, repetitively, until successful.
// When it is done it releases resources associated with the Breaker.
//
// Optionally, strategies may be passed that assess whether or not an attempt
// should be made.
func Retry(
	breaker BreakCloser,
	action func(attempt uint) error,
	strategies ...func(attempt uint, err error) bool,
) error {
	err := retry(breaker, action, strategies...)
	breaker.Close()
	return err
}

// Try takes action and performs it, repetitively, until successful.
//
// Optionally, strategies may be passed that assess whether or not an attempt
// should be made.
func Try(
	breaker Breaker,
	action func(attempt uint) error,
	strategies ...func(attempt uint, err error) bool,
) error {
	return retry(breaker, action, strategies...)
}

// TryContext takes action and performs it, repetitively, until successful.
// It uses the Context as a Breaker to prevent unnecessary action execution.
//
// Optionally, strategies may be passed that assess whether or not an attempt
// should be made.
func TryContext(
	ctx context.Context,
	action func(ctx context.Context, attempt uint) error,
	strategies ...func(attempt uint, err error) bool,
) error {
	cascade, cancel := context.WithCancel(ctx)
	err := retry(ctx, currying(cascade, action), strategies...)
	cancel()
	return err
}

func currying(ctx context.Context, action func(context.Context, uint) error) func(uint) error {
	return func(attempt uint) error { return action(ctx, attempt) }
}

func retry(
	breaker Breaker,
	action func(attempt uint) error,
	strategies ...func(attempt uint, err error) bool,
) error {
	var interrupted uint32
	done := make(chan result, 1)

	go func(breaker *uint32) {
		var err error

		defer func() {
			done <- result{err, recover()}
			close(done)
		}()

		for attempt := uint(0); shouldAttempt(breaker, attempt, err, strategies...); attempt++ {
			err = action(attempt)
		}
	}(&interrupted)

	select {
	case <-breaker.Done():
		atomic.CompareAndSwapUint32(&interrupted, 0, 1)
		return Interrupted
	case err := <-done:
		if _, is := IsRecovered(err); is {
			return err
			// TODO:v5 throw origin
			// panic(origin)
		}
		return err.error
	}
}

// shouldAttempt evaluates the provided strategies with the given attempt to
// determine if the Retry loop should make another attempt.
func shouldAttempt(breaker *uint32, attempt uint, err error, strategies ...func(uint, error) bool) bool {
	should := attempt == 0 || err != nil

	for i, repeat := 0, len(strategies); should && i < repeat; i++ {
		should = should && strategies[i](attempt, err)
	}

	return should && !atomic.CompareAndSwapUint32(breaker, 1, 0)
}