package errgroup

import (
	"context"
	"sync"
)

type Group struct {
	ctx    context.Context
	cancel context.CancelFunc

	wg sync.WaitGroup

	firstErr chan error

	// Used to limit number of goroutines (workers) for group.
	workerLimCh chan struct{}
}

func WithContext(parent context.Context, workers int) (*Group, context.Context) {
	ctx, cancel := context.WithCancel(parent)
	return &Group{
		ctx:         ctx,
		cancel:      cancel,
		firstErr:    make(chan error, 1),
		workerLimCh: make(chan struct{}, workers),
	}, ctx
}

func (g *Group) Go(f func(ctx context.Context) error) {
	// Wait for worker to be freed.
	g.workerLimCh <- struct{}{}

	g.wg.Add(1)
	go func() {
		defer func() {
			g.wg.Done()

			<-g.workerLimCh // release worker
		}()

		if err := f(g.ctx); err != nil {
			select {
			case g.firstErr <- err:
				g.cancel()
			default: // got error from other goroutine
			}
		}
	}()
}

func (g *Group) Wait() error {
	g.wg.Wait()
	g.cancel()

	select {
	case err := <-g.firstErr:
		return err
	default:
		return nil
	}
}
