// Package jitter provides timers with built-in jitter
package jitter

import (
	"container/heap"
	"sync"
	"time"
)

var (
	timers timerHeap
	mu     sync.Mutex
	reset  = make(chan struct{}, 0)
)

type timer struct {
	i int // heap index

	when   int64
	period time.Duration
	f      func(interface{})
	arg    interface{}
}

func addtimer(t *timer) {
	mu.Lock()
	heap.Push(&timers, t)
	atHead := t == timers[0]
	mu.Unlock()
	if atHead {
		// new deadline
		reset <- struct{}{}
	}
}

func deltimer(t *timer) bool {
	// induce panic before grabbing the lock
	_ = t.f

	mu.Lock()
	if t.i < 0 {
		mu.Unlock()
		return false
	}
	heap.Remove(&timers, t.i)
	mu.Unlock()
	return true
}

func init() {
	go proc()
}

func proc() {
	var (
		delta time.Duration
		now   int64
		t     *time.Timer
	)

	t = time.NewTimer(0)
	for {
		select {
		case <-reset:
			mu.Lock()
			if len(timers) == 0 {
				mu.Unlock()
				continue
			}
			when := timers[0].when
			mu.Unlock()

			now = time.Now().UnixNano()
			delta = time.Duration(when - now)
			if delta < 0 {
				delta = 0
			}
			t.Reset(delta)
		case <-t.C:
			now = time.Now().UnixNano()

			mu.Lock()
			for {
				// give someone else a chance (but without having to be too
				// careful about flow control)
				mu.Unlock()
				mu.Lock()

				if len(timers) == 0 {
					delta = -1
					break
				}
				t := timers[0]
				delta = time.Duration(t.when - now)
				if delta > 0 {
					break
				}
				if t.period > 0 {
					t.when += int64(t.period * (1 + -delta/t.period))
					heap.Fix(&timers, t.i)
				} else {
					heap.Pop(&timers)
				}
				t.f(t.arg)
			}
			mu.Unlock()

			if delta > 0 {
				t.Reset(delta)
			}
		}
	}
}
