package netid

import (
	"errors"

	"k8s.io/kubernetes/pkg/registry/service/allocator"
)

// Interface manages the allocation of netids out of a range.
// Interface should be threadsafe.
type Interface interface {
	Allocate(uint32) error
	AllocateNext() (uint32, error)
	Release(uint32) error
	Has(uint32) bool
}

var (
	ErrFull       = errors.New("range is full")
	ErrNotInRange = errors.New("provided netid is not in the valid range")
	ErrAllocated  = errors.New("provided netid is already allocated")
)

type Allocator struct {
	netIDRange *NetIDRange
	alloc      allocator.Interface
}

// Allocator implements allocator Interface
var _ Interface = &Allocator{}

// New creates a Allocator over a netid Range, calling allocatorFactory to construct the backing store.
func New(r *NetIDRange, allocatorFactory allocator.AllocatorFactory) *Allocator {
	return &Allocator{
		netIDRange: r,
		alloc:      allocatorFactory(int(r.Size), r.String()),
	}
}

// Helper that wraps New, for creating a range backed by an in-memory store.
func NewInMemory(r *NetIDRange) *Allocator {
	return New(r, func(max int, rangeSpec string) allocator.Interface {
		return allocator.NewAllocationMap(max, rangeSpec)
	})
}

// Free returns the count of netid left in the range.
func (r *Allocator) Free() int {
	return r.alloc.Free()
}

// Allocate attempts to reserve the provided netid. ErrNotInRange or
// ErrAllocated will be returned if the netid is not valid for this range
// or has already been reserved.
func (r *Allocator) Allocate(id uint32) error {
	ok, offset := r.netIDRange.Contains(id)
	if !ok {
		return ErrNotInRange
	}

	allocated, err := r.alloc.Allocate(int(offset))
	if err != nil {
		return err
	}
	if !allocated {
		return ErrAllocated
	}
	return nil
}

// AllocateNext reserves one of the netids from the pool. ErrFull may
// be returned if there are no netids left.
func (r *Allocator) AllocateNext() (uint32, error) {
	offset, ok, err := r.alloc.AllocateNext()
	if err != nil {
		return 0, err
	}
	if !ok {
		return 0, ErrFull
	}
	return r.netIDRange.Base + uint32(offset), nil
}

// Release releases the netid back to the pool. Releasing an
// unallocated netid or a netid out of the range is a no-op and
// returns no error.
func (r *Allocator) Release(id uint32) error {
	ok, offset := r.netIDRange.Contains(id)
	if !ok {
		return nil
	}
	return r.alloc.Release(int(offset))
}

// Has returns true if the provided netid is already allocated and a call
// to Allocate(netid) would fail with ErrAllocated.
func (r *Allocator) Has(id uint32) bool {
	ok, offset := r.netIDRange.Contains(id)
	if !ok {
		return false
	}
	return r.alloc.Has(int(offset))
}