// Copyright (C) MongoDB, Inc. 2014-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0

// Package failpoint implements triggers for custom debugging behavior.
package failpoint

import (
	"context"
	"fmt"
	"sync"
)

// pauseSignal coordinates a single pause: reached is closed by wait to
// signal that the paused code has arrived at its pause point, and signal is
// closed by signal to release it. mu guards the two closed flags so that
// closing either channel more than once is a safe no-op rather than a panic.
type pauseSignal struct {
	mu          sync.Mutex
	reachedOnce sync.Once
	reachedCh   chan struct{}
	signalOnce  sync.Once
	signalCh    chan struct{}
}

func newPauseSignal() *pauseSignal {
	return &pauseSignal{
		reachedCh: make(chan struct{}),
		signalCh:  make(chan struct{}),
	}
}

// wait closes reachedCh (so a concurrent call to reached returns) and then
// blocks until signal is called.
func (p *pauseSignal) wait(ctx context.Context) error {
	p.mu.Lock()
	p.reachedOnce.Do(func() {
		close(p.reachedCh)
	})
	p.mu.Unlock()

	select {
	case <-ctx.Done():
		return fmt.Errorf("context canceled while waiting for signal to be called: %w", ctx.Err())
	case <-p.signalCh:
	}

	return nil
}

// reached blocks until wait has been called.
func (p *pauseSignal) reached(ctx context.Context) error {
	select {
	case <-ctx.Done():
		return fmt.Errorf("context canceled while waiting for wait to be called: %w", ctx.Err())
	case <-p.reachedCh:
	}

	return nil
}

// signal releases a call blocked in wait. Safe to call more than once.
func (p *pauseSignal) signal() {
	p.mu.Lock()
	defer p.mu.Unlock()
	p.signalOnce.Do(func() {
		close(p.signalCh)
	})
}

// Failpoint represents one named failpoint's runtime state — for failpoints
// that need to pause code until externally resumed, the signaling to
// coordinate that pause.
type Failpoint struct {
	pause *pauseSignal
}

func newFailpoint() *Failpoint {
	return &Failpoint{
		pause: newPauseSignal(),
	}
}

// Wait signals that the caller has reached this pause point (so a concurrent
// call to Reached returns) and then blocks until Signal is called.
func (fp *Failpoint) Wait(ctx context.Context) error {
	return fp.pause.wait(ctx)
}

// Reached blocks until Wait has been called.
func (fp *Failpoint) Reached(ctx context.Context) error {
	return fp.pause.reached(ctx)
}

// Signal releases a call blocked in Wait. Safe to call more than once.
func (fp *Failpoint) Signal() {
	fp.pause.signal()
}

// Manager tracks which failpoints are currently enabled.
type Manager interface {
	// Parse enables a comma-separated list of failpoint names, as passed to
	// --failpoints. It returns an error if arg contains an unknown name.
	Parse(arg string) error
	// Reset disables every failpoint.
	Reset()
	// Get returns the Failpoint registered under name, and true, if it is
	// currently enabled.
	Get(name Name) (*Failpoint, bool)
}

// DefaultManager is set by manager.go's or noop_manager.go's init function,
// depending on whether this binary was built with the failpoints build tag.
// Callers use it directly, e.g. failpoint.DefaultManager.Get(failpoint.Foo).
var DefaultManager Manager
