package h2mux

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

// IdleTimer is a type of Timer designed for managing heartbeats on an idle connection.
// The timer ticks on an interval with added jitter to avoid accidental synchronisation
// between two endpoints. It tracks the number of retries/ticks since the connection was
// last marked active.
//
// The methods of IdleTimer must not be called while a goroutine is reading from C.
type IdleTimer struct {
	// The channel on which ticks are delivered.
	C <-chan time.Time

	// A timer used to measure idle connection time. Reset after sending data.
	idleTimer *time.Timer
	// The maximum length of time a connection is idle before sending a ping.
	idleDuration time.Duration
	// A pseudorandom source used to add jitter to the idle duration.
	randomSource *rand.Rand
	// The maximum number of retries allowed.
	maxRetries uint64
	// The number of retries since the connection was last marked active.
	retries uint64
	// A lock to prevent race condition while checking retries
	stateLock sync.RWMutex
}

func NewIdleTimer(idleDuration time.Duration, maxRetries uint64) *IdleTimer {
	t := &IdleTimer{
		idleTimer:    time.NewTimer(idleDuration),
		idleDuration: idleDuration,
		randomSource: rand.New(rand.NewSource(time.Now().Unix())),
		maxRetries:   maxRetries,
	}
	t.C = t.idleTimer.C
	return t
}

// Retry should be called when retrying the idle timeout. If the maximum number of retries
// has been met, returns false.
// After calling this function and sending a heartbeat, call ResetTimer. Since sending the
// heartbeat could be a blocking operation, we resetting the timer after the write completes
// to avoid it expiring during the write.
func (t *IdleTimer) Retry() bool {
	t.stateLock.Lock()
	defer t.stateLock.Unlock()
	if t.retries >= t.maxRetries {
		return false
	}
	t.retries++
	return true
}

func (t *IdleTimer) RetryCount() uint64 {
	t.stateLock.RLock()
	defer t.stateLock.RUnlock()
	return t.retries
}

// MarkActive resets the idle connection timer and suppresses any outstanding idle events.
func (t *IdleTimer) MarkActive() {
	if !t.idleTimer.Stop() {
		// eat the timer event to prevent spurious pings
		<-t.idleTimer.C
	}
	t.stateLock.Lock()
	t.retries = 0
	t.stateLock.Unlock()
	t.ResetTimer()
}

// Reset the idle timer according to the configured duration, with some added jitter.
func (t *IdleTimer) ResetTimer() {
	jitter := time.Duration(t.randomSource.Int63n(int64(t.idleDuration)))
	t.idleTimer.Reset(t.idleDuration + jitter)
}