package dap

import (
	"bufio"
	"context"
	"io"
	"sync"

	"github.com/google/go-dap"
	"github.com/pkg/errors"
	"golang.org/x/sync/errgroup"
)

type Conn interface {
	SendMsg(m dap.Message) error
	RecvMsg(ctx context.Context) (dap.Message, error)
	io.Closer
}

type conn struct {
	recvCh <-chan dap.Message
	sendCh chan<- dap.Message

	ctx    context.Context
	cancel context.CancelCauseFunc

	eg   *errgroup.Group
	once sync.Once
}

func NewConn(rd io.Reader, wr io.Writer) Conn {
	recvCh := make(chan dap.Message, 100)
	sendCh := make(chan dap.Message, 100)
	errCh := make(chan error, 1)

	// Reader input may never close so this is an orphaned goroutine.
	// It's ok if it does actually close but not necessary for the
	// proper functioning of this connection.
	//
	// The reason this might not close is because stdin close is controlled
	// by the OS and can't be closed from within the program.
	go func() {
		defer close(errCh)
		defer close(recvCh)

		rd := bufio.NewReader(rd)
		for {
			m, err := dap.ReadProtocolMessage(rd)
			if err != nil {
				if !errors.Is(err, io.EOF) {
					// TODO: not actually using this yet
					errCh <- err
				}
				return
			}
			recvCh <- m
		}
	}()

	eg, _ := errgroup.WithContext(context.Background())
	eg.Go(func() error {
		for m := range sendCh {
			if err := dap.WriteProtocolMessage(wr, m); err != nil {
				return err
			}
		}
		return nil
	})

	ctx, cancel := context.WithCancelCause(context.Background())
	return &conn{
		recvCh: recvCh,
		sendCh: sendCh,
		ctx:    ctx,
		cancel: cancel,
		eg:     eg,
	}
}

func (c *conn) SendMsg(m dap.Message) error {
	select {
	case c.sendCh <- m:
		return nil
	default:
		return errors.New("send channel full")
	}
}

func (c *conn) RecvMsg(ctx context.Context) (dap.Message, error) {
	select {
	case m, ok := <-c.recvCh:
		if !ok {
			return nil, io.EOF
		}
		return m, nil
	case <-ctx.Done():
		return nil, context.Cause(ctx)
	case <-c.ctx.Done():
		return nil, c.ctx.Err()
	}
}

func (c *conn) Close() error {
	c.cancel(context.Canceled)
	c.once.Do(func() {
		close(c.sendCh)
	})
	return c.eg.Wait()
}
