package leafnodes

import (
	"fmt"
	"reflect"
	"time"

	"github.com/onsi/ginkgo/internal/codelocation"
	"github.com/onsi/ginkgo/internal/failer"
	"github.com/onsi/ginkgo/types"
)

type runner struct {
	isAsync          bool
	asyncFunc        func(chan<- interface{})
	syncFunc         func()
	codeLocation     types.CodeLocation
	timeoutThreshold time.Duration
	nodeType         types.SpecComponentType
	componentIndex   int
	failer           *failer.Failer
}

func newRunner(body interface{}, codeLocation types.CodeLocation, timeout time.Duration, failer *failer.Failer, nodeType types.SpecComponentType, componentIndex int) *runner {
	bodyType := reflect.TypeOf(body)
	if bodyType.Kind() != reflect.Func {
		panic(fmt.Sprintf("Expected a function but got something else at %v", codeLocation))
	}

	runner := &runner{
		codeLocation:     codeLocation,
		timeoutThreshold: timeout,
		failer:           failer,
		nodeType:         nodeType,
		componentIndex:   componentIndex,
	}

	switch bodyType.NumIn() {
	case 0:
		runner.syncFunc = body.(func())
		return runner
	case 1:
		if !(bodyType.In(0).Kind() == reflect.Chan && bodyType.In(0).Elem().Kind() == reflect.Interface) {
			panic(fmt.Sprintf("Must pass a Done channel to function at %v", codeLocation))
		}

		wrappedBody := func(done chan<- interface{}) {
			bodyValue := reflect.ValueOf(body)
			bodyValue.Call([]reflect.Value{reflect.ValueOf(done)})
		}

		runner.isAsync = true
		runner.asyncFunc = wrappedBody
		return runner
	}

	panic(fmt.Sprintf("Too many arguments to function at %v", codeLocation))
}

func (r *runner) run() (outcome types.SpecState, failure types.SpecFailure) {
	if r.isAsync {
		return r.runAsync()
	} else {
		return r.runSync()
	}
}

func (r *runner) runAsync() (outcome types.SpecState, failure types.SpecFailure) {
	done := make(chan interface{}, 1)

	go func() {
		finished := false

		defer func() {
			if e := recover(); e != nil || !finished {
				r.failer.Panic(codelocation.New(2), e)
				select {
				case <-done:
					break
				default:
					close(done)
				}
			}
		}()

		r.asyncFunc(done)
		finished = true
	}()

	// If this goroutine gets no CPU time before the select block,
	// the <-done case may complete even if the test took longer than the timeoutThreshold.
	// This can cause flaky behaviour, but we haven't seen it in the wild.
	select {
	case <-done:
	case <-time.After(r.timeoutThreshold):
		r.failer.Timeout(r.codeLocation)
	}

	failure, outcome = r.failer.Drain(r.nodeType, r.componentIndex, r.codeLocation)
	return
}
func (r *runner) runSync() (outcome types.SpecState, failure types.SpecFailure) {
	finished := false

	defer func() {
		if e := recover(); e != nil || !finished {
			r.failer.Panic(codelocation.New(2), e)
		}

		failure, outcome = r.failer.Drain(r.nodeType, r.componentIndex, r.codeLocation)
	}()

	r.syncFunc()
	finished = true

	return
}
