package alert

import (
	"sync"
	"sync/atomic"
)

// Notice defines fields required to alert sub-systems of a change of state so a
// routine can re-check in memory data
type Notice struct {
	// Channel to wait for an alert on.
	forAlert chan struct{}
	// Lets the updater functions know if there are any routines waiting for an
	// alert.
	sema uint32
	// After closing the forAlert channel this will notify when all the routines
	// that have waited, have completed their checks.
	wg sync.WaitGroup
	// Segregated lock only for waiting routines, so as this does not interfere
	// with the main calling lock, this acts as a rolling gate.
	m sync.Mutex
}

// Alert establishes a state change on the required struct.
func (n *Notice) Alert() {
	// CompareAndSwap is used to swap from 1 -> 2 so we don't keep actuating
	// the opposing compare and swap in method wait. This function can return
	// freely when an alert operation is in process.
	if !atomic.CompareAndSwapUint32(&n.sema, 1, 2) {
		// Return if no waiting routines or currently alerting.
		return
	}
	go n.actuate()
}

// Actuate lock in a different routine, as alerting is a second order priority
// compared to updating and releasing calling routine.
func (n *Notice) actuate() {
	n.m.Lock()
	// Closing; alerts many waiting routines.
	close(n.forAlert)
	// Wait for waiting routines to receive alert and return.
	n.wg.Wait()
	atomic.SwapUint32(&n.sema, 0) // Swap back to neutral state.
	n.m.Unlock()
}

// Wait pauses calling routine until change of state has been established via
// notice method Alert. Kick allows for cancellation of waiting or when the
// caller has been shut down, if this is not needed it can be set to nil. This
// returns a channel so strategies can cleanly wait on a select statement case.
func (n *Notice) Wait(kick <-chan struct{}) <-chan bool {
	reply := make(chan bool)
	n.m.Lock()
	n.wg.Add(1)
	if atomic.CompareAndSwapUint32(&n.sema, 0, 1) {
		n.forAlert = make(chan struct{})
	}
	go n.hold(reply, kick)
	n.m.Unlock()
	return reply
}

// hold waits on either channel in the event that the routine has
// finished/cancelled or an alert from an update has occurred.
func (n *Notice) hold(ch chan<- bool, kick <-chan struct{}) {
	select {
	// In a select statement, if by chance there is no receiver or its late,
	// we can still close and return, limiting dead-lock potential.
	case <-n.forAlert: // Main waiting channel from alert
		select {
		case ch <- false:
		default:
		}
	case <-kick: // This can be nil.
		select {
		case ch <- true:
		default:
		}
	}
	n.wg.Done()
	close(ch)
}
