package ipldutil

import (
	"context"
	"errors"
	"io"

	"github.com/ipld/go-ipld-prime"
	"github.com/ipld/go-ipld-prime/traversal"
	"github.com/ipld/go-ipld-prime/traversal/selector"
)

var defaultVisitor traversal.AdvVisitFn = func(traversal.Progress, ipld.Node, traversal.VisitReason) error { return nil }

// ContextCancelError is a sentinel that indicates the passed in context
// was cancelled
type ContextCancelError struct{}

func (cp ContextCancelError) Error() string {
	return "Context cancelled"
}

// TraversalBuilder defines parameters for an iterative traversal
type TraversalBuilder struct {
	Root     ipld.Link
	Selector ipld.Node
	Visitor  traversal.AdvVisitFn
	Chooser  traversal.LinkTargetNodePrototypeChooser
}

// Traverser is an interface for performing a selector traversal that operates iteratively --
// it stops and waits for a manual load every time a block boundary is encountered
type Traverser interface {
	// IsComplete returns the completion state (boolean) and if so, the final error result from IPLD
	IsComplete() (bool, error)
	// Current request returns the current link waiting to be loaded
	CurrentRequest() (ipld.Link, ipld.LinkContext)
	// Advance advances the traversal successfully by supplying the given reader as the result of the next IPLD load
	Advance(reader io.Reader) error
	// Error errors the traversal by returning the given error as the result of the next IPLD load
	Error(err error)
	// Shutdown cancels the traversal
	Shutdown(ctx context.Context)
}

type state struct {
	isDone         bool
	completionErr  error
	currentLink    ipld.Link
	currentContext ipld.LinkContext
}

type nextResponse struct {
	input io.Reader
	err   error
}

// Start initiates the traversal (run in a go routine because the regular
// selector traversal expects a call back)
func (tb TraversalBuilder) Start(parentCtx context.Context) Traverser {
	ctx, cancel := context.WithCancel(parentCtx)
	t := &traverser{
		parentCtx:    parentCtx,
		ctx:          ctx,
		cancel:       cancel,
		root:         tb.Root,
		selector:     tb.Selector,
		visitor:      defaultVisitor,
		chooser:      defaultChooser,
		awaitRequest: make(chan struct{}, 1),
		stateChan:    make(chan state, 1),
		responses:    make(chan nextResponse),
		stopped:      make(chan struct{}),
	}
	if tb.Visitor != nil {
		t.visitor = tb.Visitor
	}
	if tb.Chooser != nil {
		t.chooser = tb.Chooser
	}
	t.start()
	return t
}

// traverser is a class to perform a selector traversal that stops every time a new block is loaded
// and waits for manual input (in the form of advance or error)
type traverser struct {
	parentCtx      context.Context
	ctx            context.Context
	cancel         func()
	root           ipld.Link
	selector       ipld.Node
	visitor        traversal.AdvVisitFn
	chooser        traversal.LinkTargetNodePrototypeChooser
	currentLink    ipld.Link
	currentContext ipld.LinkContext
	isDone         bool
	completionErr  error
	awaitRequest   chan struct{}
	stateChan      chan state
	responses      chan nextResponse
	stopped        chan struct{}
}

func (t *traverser) checkState() {
	select {
	case <-t.awaitRequest:
		select {
		case <-t.ctx.Done():
			t.isDone = true
			t.completionErr = ContextCancelError{}
		case newState := <-t.stateChan:
			t.isDone = newState.isDone
			t.completionErr = newState.completionErr
			t.currentLink = newState.currentLink
			t.currentContext = newState.currentContext
		}
	default:
	}
}

func (t *traverser) writeDone(err error) {
	select {
	case <-t.ctx.Done():
	case t.stateChan <- state{true, err, nil, ipld.LinkContext{}}:
	}
}

func (t *traverser) start() {
	select {
	case <-t.ctx.Done():
		return
	case t.awaitRequest <- struct{}{}:
	}
	go func() {
		defer close(t.stopped)
		loader := func(lnk ipld.Link, lnkCtx ipld.LinkContext) (io.Reader, error) {
			select {
			case <-t.ctx.Done():
				return nil, ContextCancelError{}
			case t.stateChan <- state{false, nil, lnk, lnkCtx}:
			}
			select {
			case <-t.ctx.Done():
				return nil, ContextCancelError{}
			case response := <-t.responses:
				return response.input, response.err
			}
		}
		ns, err := t.chooser(t.root, ipld.LinkContext{})
		if err != nil {
			t.writeDone(err)
			return
		}
		nb := ns.NewBuilder()
		err = t.root.Load(t.ctx, ipld.LinkContext{}, nb, loader)
		if err != nil {
			t.writeDone(err)
			return
		}
		nd := nb.Build()

		sel, err := selector.ParseSelector(t.selector)
		if err != nil {
			t.writeDone(err)
			return
		}
		err = traversal.Progress{
			Cfg: &traversal.Config{
				Ctx:                            t.ctx,
				LinkLoader:                     loader,
				LinkTargetNodePrototypeChooser: t.chooser,
			},
		}.WalkAdv(nd, sel, t.visitor)
		t.writeDone(err)
	}()
}

func (t *traverser) Shutdown(ctx context.Context) {
	t.cancel()
	select {
	case <-ctx.Done():
	case <-t.stopped:
	}
}

// IsComplete returns true if a traversal is complete
func (t *traverser) IsComplete() (bool, error) {
	t.checkState()
	return t.isDone, t.completionErr
}

// CurrentRequest returns the current block load waiting to be fulfilled in order
// to advance further
func (t *traverser) CurrentRequest() (ipld.Link, ipld.LinkContext) {
	t.checkState()
	return t.currentLink, t.currentContext
}

// Advance advances the traversal with an io.Reader for the next requested block
func (t *traverser) Advance(reader io.Reader) error {
	isComplete, _ := t.IsComplete()
	if isComplete {
		return errors.New("cannot advance when done")
	}
	select {
	case <-t.ctx.Done():
		return ContextCancelError{}
	case t.awaitRequest <- struct{}{}:
	}
	select {
	case <-t.ctx.Done():
		return ContextCancelError{}
	case t.responses <- nextResponse{reader, nil}:
	}
	return nil
}

// Error aborts the traversal with an error
func (t *traverser) Error(err error) {
	isComplete, _ := t.IsComplete()
	if isComplete {
		return
	}
	select {
	case <-t.ctx.Done():
		return
	case t.awaitRequest <- struct{}{}:
	}
	select {
	case <-t.ctx.Done():
	case t.responses <- nextResponse{nil, err}:
	}
}
