package interp

// I/O streams are interfaces which allow file redirects and command pipelines to be treated
// equivalently.

import (
	"bufio"
	"errors"
	"io"
	"os/exec"
	"syscall"
)

const (
	notClosedExitCode = -127
)

var (
	errDoubleClose = errors.New("close: stream already closed")
)

// firstError returns the first non-nil error or nil if all errors are nil.
func firstError(errs ...error) error {
	for _, err := range errs {
		if err != nil {
			return err
		}
	}
	return nil
}

// Close the cmd and convert the error result into the result returned from goawk builtin functions.
// A nil error is returned if that error describes a non-zero exit status or an unhandled signal.
// Any other type of error returns -1 and err.
//
// The result mimicks gawk for expected child process errors:
// 1. Returns the exit status of the child process and nil error on normal process exit.
// 2. Returns 256 + signal on unhandled signal exit.
// 3. Returns 512 + signal on unhandled signal exit which caused a core dump.
func waitExitCode(cmd *exec.Cmd) (int, error) {
	err := cmd.Wait()
	if err == nil {
		return 0, nil
	}
	ee, ok := err.(*exec.ExitError)
	if !ok {
		// Wait() returned an io error.
		return -1, err
	}
	status, ok := ee.ProcessState.Sys().(syscall.WaitStatus)
	if !ok {
		// Maybe not all platforms support WaitStatus?
		return -1, err
	}
	switch {
	case status.CoreDump():
		return 512 + int(status.Signal()), nil
	case status.Signaled():
		return 256 + int(status.Signal()), nil
	case status.Exited():
		return status.ExitStatus(), nil
	default:
		return -1, err
	}
}

type inputStream interface {
	io.ReadCloser
	ExitCode() int
}

type outputStream interface {
	io.WriteCloser
	Flush() error
	ExitCode() int
}

type outFileStream struct {
	*bufio.Writer
	closer   io.Closer
	exitCode int
	closed   bool
}

func newOutFileStream(wc io.WriteCloser, size int) outputStream {
	b := bufio.NewWriterSize(wc, size)
	return &outFileStream{b, wc, notClosedExitCode, false}
}

func (s *outFileStream) Close() error {
	if s.closed {
		return errDoubleClose
	}
	s.closed = true
	flushErr := s.Writer.Flush()
	closeErr := s.closer.Close()
	if err := firstError(flushErr, closeErr); err != nil {
		s.exitCode = -1
		return err
	}
	s.exitCode = 0
	return nil
}

func (s *outFileStream) ExitCode() int {
	return s.exitCode
}

type outCmdStream struct {
	*bufio.Writer
	closer   io.Closer
	cmd      *exec.Cmd
	exitCode int
	closed   bool
}

func newOutCmdStream(cmd *exec.Cmd) (outputStream, error) {
	w, err := cmd.StdinPipe()
	if err != nil {
		return nil, newError("error connecting to stdin pipe: %v", err)
	}
	err = cmd.Start()
	if err != nil {
		_ = w.Close()
		return nil, err
	}
	out := &outCmdStream{bufio.NewWriterSize(w, outputBufSize), w, cmd, notClosedExitCode, false}
	return out, nil
}

func (s *outCmdStream) Close() error {
	if s.closed {
		return errDoubleClose
	}
	s.closed = true
	flushErr := s.Writer.Flush()
	closeErr := s.closer.Close()
	var waitErr error
	s.exitCode, waitErr = waitExitCode(s.cmd)
	return firstError(waitErr, flushErr, closeErr)
}

func (s *outCmdStream) ExitCode() int {
	return s.exitCode
}

// An outNullStream allows writes to not do anything while fulfilling the outputStream interface.
type outNullStream struct {
	io.Writer
	closed bool
}

func newOutNullStream() outputStream { return &outNullStream{io.Discard, false} }
func (s outNullStream) Flush() error { return nil }
func (s *outNullStream) Close() error {
	if s.closed {
		return errDoubleClose
	}
	s.closed = true
	return nil
}
func (s outNullStream) ExitCode() int { return -1 }

type inFileStream struct {
	io.ReadCloser
	exitCode int
	closed   bool
}

func newInFileStream(rc io.ReadCloser) inputStream {
	return &inFileStream{rc, notClosedExitCode, false}
}

func (s *inFileStream) Close() error {
	if s.closed {
		return errDoubleClose
	}
	s.closed = true
	if err := s.ReadCloser.Close(); err != nil {
		s.exitCode = -1
		return err
	}
	s.exitCode = 0
	return nil
}

func (s *inFileStream) ExitCode() int {
	return s.exitCode
}

type inCmdStream struct {
	io.ReadCloser
	cmd      *exec.Cmd
	exitCode int
	closed   bool
}

func newInCmdStream(cmd *exec.Cmd) (inputStream, error) {
	r, err := cmd.StdoutPipe()
	if err != nil {
		return nil, newError("error connecting to stdout pipe: %v", err)
	}
	err = cmd.Start()
	if err != nil {
		_ = r.Close()
		return nil, err
	}
	return &inCmdStream{r, cmd, notClosedExitCode, false}, nil
}

func (s *inCmdStream) Close() error {
	if s.closed {
		return errDoubleClose
	}
	s.closed = true
	closeErr := s.ReadCloser.Close()
	var waitErr error
	s.exitCode, waitErr = waitExitCode(s.cmd)
	return firstError(waitErr, closeErr)
}

func (s *inCmdStream) ExitCode() int {
	return s.exitCode
}
