// Package goprocess introduces a Process abstraction that allows simple
// organization, and orchestration of work. It is much like a WaitGroup,
// and much like a context.Context, but also ensures safe **exactly-once**,
// and well-ordered teardown semantics.
package goprocess

import (
	"os"
	"os/signal"
)

// Process is the basic unit of work in goprocess. It defines a computation
// with a lifecycle:
// - running (before calling Close),
// - closing (after calling Close at least once),
// - closed (after Close returns, and all teardown has _completed_).
//
// More specifically, it fits this:
//
//   p := WithTeardown(tf) // new process is created, it is now running.
//   p.AddChild(q)         // can register children **before** Closing.
//   go p.Close()          // blocks until done running teardown func.
//   <-p.Closing()         // would now return true.
//   <-p.childrenDone()    // wait on all children to be done
//   p.teardown()          // runs the user's teardown function tf.
//   p.Close()             // now returns, with error teardown returned.
//   <-p.Closed()          // would now return true.
//
// Processes can be arranged in a process "tree", where children are
// automatically Closed if their parents are closed. (Note, it is actually
// a Process DAG, children may have multiple parents). A process may also
// optionally wait for another to fully Close before beginning to Close.
// This makes it easy to ensure order of operations and proper sequential
// teardown of resurces. For example:
//
//   p1 := goprocess.WithTeardown(func() error {
//     fmt.Println("closing 1")
//   })
//   p2 := goprocess.WithTeardown(func() error {
//     fmt.Println("closing 2")
//   })
//   p3 := goprocess.WithTeardown(func() error {
//     fmt.Println("closing 3")
//   })
//
//   p1.AddChild(p2)
//   p2.AddChild(p3)
//
//
//   go p1.Close()
//   go p2.Close()
//   go p3.Close()
//
//   // Output:
//   // closing 3
//   // closing 2
//   // closing 1
//
// Process is modelled after the UNIX processes group idea, and heavily
// informed by sync.WaitGroup and go.net/context.Context.
//
// In the function documentation of this interface, `p` always refers to
// the self Process.
type Process interface {

	// WaitFor makes p wait for q before exiting. Thus, p will _always_ close
	// _after_ q. Note well: a waiting cycle is deadlock.
	//
	// If q is already Closed, WaitFor calls p.Close()
	// If p is already Closing or Closed, WaitFor panics. This is the same thing
	// as calling Add(1) _after_ calling Done() on a wait group. Calling WaitFor
	// on an already-closed process is a programming error likely due to bad
	// synchronization
	WaitFor(q Process)

	// AddChildNoWait registers child as a "child" of Process. As in UNIX,
	// when parent is Closed, child is Closed -- child may Close beforehand.
	// This is the equivalent of calling:
	//
	//  go func(parent, child Process) {
	//    <-parent.Closing()
	//    child.Close()
	//  }(p, q)
	//
	// Note: the naming of functions is `AddChildNoWait` and `AddChild` (instead
	// of `AddChild` and `AddChildWaitFor`) because:
	// - it is the more common operation,
	// - explicitness is helpful in the less common case (no waiting), and
	// - usual "child" semantics imply parent Processes should wait for children.
	AddChildNoWait(q Process)

	// AddChild is the equivalent of calling:
	//  parent.AddChildNoWait(q)
	//  parent.WaitFor(q)
	AddChild(q Process)

	// Go is much like `go`, as it runs a function in a newly spawned goroutine.
	// The neat part of Process.Go is that the Process object you call it on will:
	//  * construct a child Process, and call AddChild(child) on it
	//  * spawn a goroutine, and call the given function
	//  * Close the child when the function exits.
	// This way, you can rest assured each goroutine you spawn has its very own
	// Process context, and that it will be closed when the function exits.
	// It is the function's responsibility to respect the Closing of its Process,
	// namely it should exit (return) when <-Closing() is ready. It is basically:
	//
	//   func (p Process) Go(f ProcessFunc) Process {
	//   	child := WithParent(p)
	//   	go func () {
	//   		f(child)
	//   		child.Close()
	//   	}()
	//   }
	//
	// It is useful to construct simple asynchronous workers, children of p.
	Go(f ProcessFunc) Process

	// Close ends the process. Close blocks until the process has completely
	// shut down, and any teardown has run _exactly once_. The returned error
	// is available indefinitely: calling Close twice returns the same error.
	// If the process has already been closed, Close returns immediately.
	Close() error

	// CloseAfterChildren calls Close _after_ its children have Closed
	// normally (i.e. it _does not_ attempt to close them).
	CloseAfterChildren() error

	// Closing is a signal to wait upon. The returned channel is closed
	// _after_ Close has been called at least once, but teardown may or may
	// not be done yet. The primary use case of Closing is for children who
	// need to know when a parent is shutting down, and therefore also shut
	// down.
	Closing() <-chan struct{}

	// Closed is a signal to wait upon. The returned channel is closed
	// _after_ Close has completed; teardown has finished. The primary use case
	// of Closed is waiting for a Process to Close without _causing_ the Close.
	Closed() <-chan struct{}
}

// TeardownFunc is a function used to cleanup state at the end of the
// lifecycle of a Process.
type TeardownFunc func() error

var nilTeardownFunc = func() error { return nil }

// ProcessFunc is a function that takes a process. Its main use case is goprocess.Go,
// which spawns a ProcessFunc in its own goroutine, and returns a corresponding
// Process object.
type ProcessFunc func(proc Process)

var nilProcessFunc = func(Process) {}

// Go is much like `go`: it runs a function in a newly spawned goroutine. The neat
// part of Go is that it provides Process object to communicate between the
// function and the outside world. Thus, callers can easily WaitFor, or Close the
// function. It is the function's responsibility to respect the Closing of its Process,
// namely it should exit (return) when <-Closing() is ready. It is simply:
//
//   func Go(f ProcessFunc) Process {
//     p := WithParent(Background())
//     p.Go(f)
//     return p
//   }
//
// Note that a naive implementation of Go like the following would not work:
//
//   func Go(f ProcessFunc) Process {
//     return Background().Go(f)
//   }
//
// This is because having the process you
func Go(f ProcessFunc) Process {
	// return GoChild(Background(), f)

	// we use two processes, one for communication, and
	// one for ensuring we wait on the function (unclosable from the outside).
	p := newProcess(nil)
	waitFor := newProcess(nil)
	p.WaitFor(waitFor) // prevent p from closing
	go func() {
		f(p)
		waitFor.Close() // allow p to close.
		p.Close()       // ensure p closes.
	}()
	return p
}

// GoChild is like Go, but it registers the returned Process as a child of parent,
// **before** spawning the goroutine, which ensures proper synchronization with parent.
// It is somewhat like
//
//   func GoChild(parent Process, f ProcessFunc) Process {
//     p := WithParent(parent)
//     p.Go(f)
//     return p
//   }
//
// And it is similar to the classic WaitGroup use case:
//
//   func WaitGroupGo(wg sync.WaitGroup, child func()) {
//     wg.Add(1)
//     go func() {
//       child()
//       wg.Done()
//     }()
//   }
//
func GoChild(parent Process, f ProcessFunc) Process {
	p := WithParent(parent)
	p.Go(f)
	return p
}

// Spawn is an alias of `Go`. In many contexts, Spawn is a
// well-known Process launching word, which fits our use case.
var Spawn = Go

// SpawnChild is an alias of `GoChild`. In many contexts, Spawn is a
// well-known Process launching word, which fits our use case.
var SpawnChild = GoChild

// WithTeardown constructs and returns a Process with a TeardownFunc.
// TeardownFunc tf will be called **exactly-once** when Process is
// Closing, after all Children have fully closed, and before p is Closed.
// In fact, Process p will not be Closed until tf runs and exits.
// See lifecycle in Process doc.
func WithTeardown(tf TeardownFunc) Process {
	if tf == nil {
		panic("nil tf TeardownFunc")
	}
	return newProcess(tf)
}

// WithParent constructs and returns a Process with a given parent.
func WithParent(parent Process) Process {
	if parent == nil {
		panic("nil parent Process")
	}
	q := newProcess(nil)
	parent.AddChild(q)
	return q
}

// WithSignals returns a Process that will Close() when any given signal fires.
// This is useful to bind Process trees to syscall.SIGTERM, SIGKILL, etc.
func WithSignals(sig ...os.Signal) Process {
	p := WithParent(Background())
	c := make(chan os.Signal)
	signal.Notify(c, sig...)
	go func() {
		<-c
		signal.Stop(c)
		p.Close()
	}()
	return p
}

// Background returns the "background" Process: a statically allocated
// process that can _never_ close. It also never enters Closing() state.
// Calling Background().Close() will hang indefinitely.
func Background() Process {
	return background
}

// background is the background process
var background = &unclosable{Process: newProcess(nil)}

// unclosable is a process that _cannot_ be closed. calling Close simply hangs.
type unclosable struct {
	Process
}

func (p *unclosable) Close() error {
	var hang chan struct{}
	<-hang // hang forever
	return nil
}
