// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package tasklifecycle

const (
	gateClosed = false
	gateOpened = true
)

// Gate is used by the Coordinator to block or allow tasks from running.
//
// It provides a channel that taskRunners listens on to determine when they are
// allowed to run. The Gate has an infinite loop that is either feeding this
// channel (therefore allowing listeners to proceed) or not doing anything
// (causing listeners to block an wait).
//
// The Coordinator uses the Gate Open() and Close() methods to control this
// producer loop.
type Gate struct {
	sendCh     chan struct{}
	updateCh   chan bool
	shutdownCh <-chan struct{}
}

// NewGate returns a new Gate that is initially closed. The Gate should not be
// used after the shutdownCh is closed.
func NewGate(shutdownCh <-chan struct{}) *Gate {
	g := &Gate{
		sendCh:     make(chan struct{}),
		updateCh:   make(chan bool),
		shutdownCh: shutdownCh,
	}
	go g.run(gateClosed)

	return g
}

// WaitCh returns a channel that the listener must block on before starting its
// task.
//
// Callers must also check the state of the shutdownCh used to create the Gate
// to avoid blocking indefinitely.
func (g *Gate) WaitCh() <-chan struct{} {
	return g.sendCh
}

// Open is used to allow listeners to proceed.
// If the gate shutdownCh channel is closed, this method is a no-op so callers
// should check its state.
func (g *Gate) Open() {
	select {
	case <-g.shutdownCh:
	case g.updateCh <- gateOpened:
	}
}

// Close is used to block listeners from proceeding.
// if the gate shutdownch channel is closed, this method is a no-op so callers
// should check its state.
func (g *Gate) Close() {
	select {
	case <-g.shutdownCh:
	case g.updateCh <- gateClosed:
	}
}

// run starts the infinite loop that feeds the channel if the Gate is opened.
func (g *Gate) run(initState bool) {
	isOpen := initState
	for {
		if isOpen {
			select {
			// Feed channel if the gate is open.
			case g.sendCh <- struct{}{}:
			case <-g.shutdownCh:
				return
			case isOpen = <-g.updateCh:
				continue
			}
		} else {
			select {
			case <-g.shutdownCh:
				return
			case isOpen = <-g.updateCh:
				continue
			}
		}
	}
}
