//go:build windows

package conpty

import (
	"errors"
	"fmt"
	"os"
	"sync"
	"unsafe"

	"github.com/Microsoft/hcsshim/internal/winapi"
	"golang.org/x/sys/windows"
)

var (
	errClosedConPty   = errors.New("pseudo console is closed")
	errNotInitialized = errors.New("pseudo console hasn't been initialized")
)

// Pty is a wrapper around a Windows PseudoConsole handle. Create a new instance by calling `Create()`.
type Pty struct {
	// handleLock guards hpc
	handleLock sync.RWMutex
	// hpc is the pseudo console handle
	hpc windows.Handle
	// inPipe and outPipe are our end of the pipes to read/write to the pseudo console.
	inPipe  *os.File
	outPipe *os.File
}

// Create returns a new `Pty` object. This object is not ready for IO until `UpdateProcThreadAttribute` is called and a process has been started.
func Create(width, height int16, flags uint32) (*Pty, error) {
	// First we need to make both ends of the conpty's pipes, two to get passed into a process to use as input/output, and two for us to keep to
	// make use of this data.
	ptyIn, inPipeOurs, err := os.Pipe()
	if err != nil {
		return nil, fmt.Errorf("failed to create pipes for pseudo console: %w", err)
	}

	outPipeOurs, ptyOut, err := os.Pipe()
	if err != nil {
		return nil, fmt.Errorf("failed to create pipes for pseudo console: %w", err)
	}

	var hpc windows.Handle
	coord := windows.Coord{X: width, Y: height}
	err = winapi.CreatePseudoConsole(coord, windows.Handle(ptyIn.Fd()), windows.Handle(ptyOut.Fd()), 0, &hpc)
	if err != nil {
		return nil, fmt.Errorf("failed to create pseudo console: %w", err)
	}

	// The pty's end of its pipes can be closed here without worry. They're duped into the conhost
	// that will be launched and will be released on a call to ClosePseudoConsole() (Close() on the Pty object).
	if err := ptyOut.Close(); err != nil {
		return nil, fmt.Errorf("failed to close pseudo console handle: %w", err)
	}
	if err := ptyIn.Close(); err != nil {
		return nil, fmt.Errorf("failed to close pseudo console handle: %w", err)
	}

	return &Pty{
		hpc:     hpc,
		inPipe:  inPipeOurs,
		outPipe: outPipeOurs,
	}, nil
}

// UpdateProcThreadAttribute updates the passed in attribute list to contain the entry necessary for use with
// CreateProcess.
func (c *Pty) UpdateProcThreadAttribute(attrList *windows.ProcThreadAttributeListContainer) error {
	c.handleLock.RLock()
	defer c.handleLock.RUnlock()

	if c.hpc == 0 {
		return errClosedConPty
	}

	if err := attrList.Update(
		winapi.PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE,
		unsafe.Pointer(c.hpc),
		unsafe.Sizeof(c.hpc),
	); err != nil {
		return fmt.Errorf("failed to update proc thread attributes for pseudo console: %w", err)
	}

	return nil
}

// Resize resizes the internal buffers of the pseudo console to the passed in size
func (c *Pty) Resize(width, height int16) error {
	c.handleLock.RLock()
	defer c.handleLock.RUnlock()

	if c.hpc == 0 {
		return errClosedConPty
	}

	coord := windows.Coord{X: width, Y: height}
	if err := winapi.ResizePseudoConsole(c.hpc, coord); err != nil {
		return fmt.Errorf("failed to resize pseudo console: %w", err)
	}
	return nil
}

// Close closes the pseudo-terminal and cleans up all attached resources
func (c *Pty) Close() error {
	c.handleLock.Lock()
	defer c.handleLock.Unlock()

	if c.hpc == 0 {
		return errClosedConPty
	}

	// Close the pseudo console, set the handle to 0 to invalidate this object and then close the side of the pipes that we own.
	winapi.ClosePseudoConsole(c.hpc)
	c.hpc = 0
	if err := c.inPipe.Close(); err != nil {
		return fmt.Errorf("failed to close pseudo console input pipe: %w", err)
	}
	if err := c.outPipe.Close(); err != nil {
		return fmt.Errorf("failed to close pseudo console output pipe: %w", err)
	}
	return nil
}

// OutPipe returns the output pipe of the pseudo console.
func (c *Pty) OutPipe() *os.File {
	return c.outPipe
}

// InPipe returns the input pipe of the pseudo console.
func (c *Pty) InPipe() *os.File {
	return c.inPipe
}

// Write writes the contents of `buf` to the pseudo console. Returns the number of bytes written and an error if there is one.
func (c *Pty) Write(buf []byte) (int, error) {
	if c.inPipe == nil {
		return 0, errNotInitialized
	}
	return c.inPipe.Write(buf)
}

// Read reads from the pseudo console into `buf`. Returns the number of bytes read and an error if there is one.
func (c *Pty) Read(buf []byte) (int, error) {
	if c.outPipe == nil {
		return 0, errNotInitialized
	}
	return c.outPipe.Read(buf)
}
