// Package perp contains tools for efficient sharing of resources.
package perp

import (
	"context"
	"sync"
	"sync/atomic"
)

// A Pool mediates access to a set of shared resources.
type Pool struct {
	// accessed atomically
	requestCount int32

	pool sync.Pool
}

// NewPoolSize creates a new sharded resource Pool with the specified size.
// Functions passed to Do will be called with i less than size and no less
// than zero, in the half-open interval [0, size).
func NewPoolSize(size int) *Pool {
	entries := make([]*entry, size)

	for i := 0; i < size; i++ {
		e := &entry{idx: i, ch: make(chan struct{}, 1)}
		e.ch <- struct{}{}
		entries[i] = e
	}

	p := &Pool{}
	p.pool.New = func() interface{} {
		req := atomic.AddInt32(&p.requestCount, 1)
		i := int(uint32(req) % uint32(size))
		e := entries[i]
		return e
	}

	return p
}

// pick returns a shard entry. The Pool makes an effort to return an entry
// corresponding to the local thread.
//
// This is different from usual usage of sync.Pool. Instead of using sync.Pool
// to hold an elastic set of available resources, this code uses it to share
// coordination points for a fixed set of resources. Since sync.Pool uses a
// separate cache for each processor, we're roughly able to generate an
// identifier for each processor and to use that for more efficient handoff of
// the limited resource.
//
// To use Go runtime terms, it's aiming for one associated with the current P.
func (p *Pool) pick() *entry {
	// Grab an entry out of the Pool's entries slice
	e := p.pool.Get().(*entry)
	// Immediately put the entry back "into" the sync.Pool, so other runs of
	// this function that are scheduled on the same P (*runtime.p) are likely
	// to choose the same one.
	p.pool.Put(e)
	return e
}

// Do runs the provided function after acquiring permission to use one of the
// Pool's resources. While the provided function is running, it has exclusive
// use of the resource at index i. Do will return without calling the function
// when ctx is canceled.
func (p *Pool) Do(ctx context.Context, fn func(i int)) {
	e := p.pick()

	select {
	case <-ctx.Done():
	case <-e.ch:
		defer func() { e.ch <- struct{}{} }()
		fn(e.idx)
	}
}

// An entry is a coordination point where goroutines can hand off a shared
// resource.
type entry struct {
	idx int           // resource index
	ch  chan struct{} // semaphore controlling resource handoff
}
