// +build darwin freebsd dragonfly netbsd openbsd linux
package reuseport

import (
	"sync"
	"sync/atomic"
	"syscall"
	"time"
)

// checker is a struct to gather the availability check fields + funcs.
// we use atomic ints because this is potentially a really hot function call.
type checkerT struct {
	avail int32      // atomic int managed by set/isAvailable()
	check int32      // atomic int managed by has/checked()
	mu    sync.Mutex // synchonizes the actual check
}

// the static location of the vars.
var checker checkerT

func (c *checkerT) isAvailable() bool {
	return atomic.LoadInt32(&c.avail) != 0
}

func (c *checkerT) setIsAvailable(b bool) {
	if b {
		atomic.StoreInt32(&c.avail, 1)
	} else {
		atomic.StoreInt32(&c.avail, 0)
	}
}

func (c *checkerT) hasChecked() bool {
	return atomic.LoadInt32(&c.check) != 0
}

func (c *checkerT) setHasChecked(b bool) {
	if b {
		atomic.StoreInt32(&c.check, 1)
	} else {
		atomic.StoreInt32(&c.check, 0)
	}
}

// Available returns whether or not SO_REUSEPORT is available in the OS.
// It does so by attepting to open a tcp listener, setting the option, and
// checking ENOPROTOOPT on error. After checking, the decision is cached
// for the rest of the process run.
func available() bool {
	if checker.hasChecked() {
		return checker.isAvailable()
	}

	// synchronize, only one should check
	checker.mu.Lock()
	defer checker.mu.Unlock()

	// we blocked. someone may have been gotten this.
	if checker.hasChecked() {
		return checker.isAvailable()
	}

	// there may be fluke reasons to fail to add a listener.
	// so we give it 5 shots. if not, give up and call it not avail.
	for i := 0; i < 5; i++ {
		// try to listen at tcp port 0.
		l, err := listenStream("tcp", "127.0.0.1:0")
		if err == nil {
			// no error? available.
			checker.setIsAvailable(true)
			checker.setHasChecked(true)
			l.Close() // Go back to the Shadow!
			return true
		}

		if errno, ok := err.(syscall.Errno); ok {
			if errno == syscall.ENOPROTOOPT {
				break // :( that's all folks.
			}
		}

		// not an errno? or not ENOPROTOOPT? retry.
		<-time.After(20 * time.Millisecond) // wait a bit
	}

	checker.setIsAvailable(false)
	checker.setHasChecked(true)
	return false
}
