package ticker

import (
	"fmt"
	"sync"
)

// IterationHandler is a callback of a certain block advance.
type IterationHandler func()

// IterationsTicker represents a fixed tick number block timer.
//
// It can tick the blocks and perform certain actions
// on block time intervals.
type IterationsTicker struct {
	m sync.Mutex

	curr   uint64
	period uint64

	times uint64

	h IterationHandler
}

// NewIterationsTicker creates a new IterationsTicker.
//
// It guaranties that a handler would be called the
// specified amount of times in the specified amount
// of blocks. After the last meaningful Tick, IterationsTicker
// becomes no-op timer.
//
// Returns an error only if times is greater than totalBlocks.
func NewIterationsTicker(totalBlocks uint64, times uint64, h IterationHandler) (*IterationsTicker, error) {
	period := totalBlocks / times

	if period == 0 {
		return nil, fmt.Errorf("impossible to tick %d times in %d blocks",
			times, totalBlocks,
		)
	}

	var curr uint64

	// try to make handler calls as rare as possible
	if totalBlocks%times != 0 {
		extraBlocks := (period+1)*times - totalBlocks

		if period >= extraBlocks {
			curr = extraBlocks + (period-extraBlocks)/2
			period++
		}
	}

	return &IterationsTicker{
		curr:   curr,
		period: period,
		times:  times,
		h:      h,
	}, nil
}

// Tick ticks one block in the IterationsTicker.
//
// Returns `false` if the timer has finished its operations
// and there will be no more handler calls.
// Calling Tick after the returned `false` is safe, no-op
// and also returns `false`.
func (ft *IterationsTicker) Tick() bool {
	ft.m.Lock()
	defer ft.m.Unlock()

	if ft.times == 0 {
		return false
	}

	ft.curr++

	if ft.curr%ft.period == 0 {
		ft.h()

		ft.times--

		if ft.times == 0 {
			return false
		}
	}

	return true
}