package patterns

import (
	"context"
	"errors"
	"sync"
)

var (
	ErrRunnableAlreadyStarted = errors.New("already started")
	ErrRunnableNotStarted     = errors.New("already started")
)

// Runnable encapsulates a common pattern of a process that can start and stop, where it must be stopped before in can
// start and vice versa. This pattern is suitable for long-running objects such as servers.
type Runnable interface {
	Start() error
	StartWithContext(ctx context.Context) error
	Stop() error
	Context() context.Context
}

// runnableImpl implements a Runnable.
type runnableImpl struct {
	mu     sync.Mutex
	ctx    context.Context
	cancel context.CancelFunc
}

// NewRunnable returns a new instance of Runnable.
func NewRunnable() Runnable {
	return &runnableImpl{}
}

// Start starts the object in a background context if it has not started yet. Otherwise, ErrRunnableAlreadyStarted is returned.
func (r *runnableImpl) Start() error {
	return r.StartWithContext(context.Background())
}

// StartWithContext starts the object in a given context if it has not started yet. Otherwise, ErrRunnableAlreadyStarted is returned.
func (r *runnableImpl) StartWithContext(ctx context.Context) (err error) {
	r.mu.Lock()
	defer func() {
		if err != nil && r.cancel != nil {
			r.cancel()
			r.ctx = nil
			r.cancel = nil
		}

		r.mu.Unlock()
	}()

	if r.ctx != nil {
		return ErrRunnableAlreadyStarted
	}

	cctx, cancel := context.WithCancel(ctx)
	r.ctx, r.cancel = cctx, cancel

	return err
}

// Stop stops the object if it already starts. Otherwise, ErrRunnableNotStarted is returned.
func (r *runnableImpl) Stop() error {
	r.mu.Lock()
	defer r.mu.Unlock()

	if r.ctx == nil {
		return ErrRunnableNotStarted
	}

	r.cancel()
	r.ctx = nil
	r.cancel = nil

	return nil
}

// Context returns the current context in which the object is running.
func (r *runnableImpl) Context() context.Context {
	return r.ctx
}
