package libcontainerd

import (
	"context"
	"io"
	"net"
	"sync"

	winio "github.com/Microsoft/go-winio"
	"github.com/containerd/containerd/cio"
	"github.com/pkg/errors"
)

type winpipe struct {
	sync.Mutex

	ctx      context.Context
	listener net.Listener
	readyCh  chan struct{}
	readyErr error

	client net.Conn
}

func newWinpipe(ctx context.Context, pipe string) (*winpipe, error) {
	l, err := winio.ListenPipe(pipe, nil)
	if err != nil {
		return nil, errors.Wrapf(err, "%q pipe creation failed", pipe)
	}
	wp := &winpipe{
		ctx:      ctx,
		listener: l,
		readyCh:  make(chan struct{}),
	}
	go func() {
		go func() {
			defer close(wp.readyCh)
			defer wp.listener.Close()
			c, err := wp.listener.Accept()
			if err != nil {
				wp.Lock()
				if wp.readyErr == nil {
					wp.readyErr = err
				}
				wp.Unlock()
				return
			}
			wp.client = c
		}()

		select {
		case <-wp.readyCh:
		case <-ctx.Done():
			wp.Lock()
			if wp.readyErr == nil {
				wp.listener.Close()
				wp.readyErr = ctx.Err()
			}
			wp.Unlock()
		}
	}()

	return wp, nil
}

func (wp *winpipe) Read(b []byte) (int, error) {
	select {
	case <-wp.ctx.Done():
		return 0, wp.ctx.Err()
	case <-wp.readyCh:
		return wp.client.Read(b)
	}
}

func (wp *winpipe) Write(b []byte) (int, error) {
	select {
	case <-wp.ctx.Done():
		return 0, wp.ctx.Err()
	case <-wp.readyCh:
		return wp.client.Write(b)
	}
}

func (wp *winpipe) Close() error {
	select {
	case <-wp.readyCh:
		return wp.client.Close()
	default:
		return nil
	}
}

func newIOPipe(fifos *cio.FIFOSet) (*IOPipe, error) {
	var (
		err         error
		ctx, cancel = context.WithCancel(context.Background())
		p           io.ReadWriteCloser
		iop         = &IOPipe{
			Terminal: fifos.Terminal,
			cancel:   cancel,
			config: cio.Config{
				Terminal: fifos.Terminal,
				Stdin:    fifos.In,
				Stdout:   fifos.Out,
				Stderr:   fifos.Err,
			},
		}
	)
	defer func() {
		if err != nil {
			cancel()
			iop.Close()
		}
	}()

	if fifos.In != "" {
		if p, err = newWinpipe(ctx, fifos.In); err != nil {
			return nil, err
		}
		iop.Stdin = p
	}

	if fifos.Out != "" {
		if p, err = newWinpipe(ctx, fifos.Out); err != nil {
			return nil, err
		}
		iop.Stdout = p
	}

	if fifos.Err != "" {
		if p, err = newWinpipe(ctx, fifos.Err); err != nil {
			return nil, err
		}
		iop.Stderr = p
	}

	return iop, nil
}
