package script

import (
	"fmt"
	"io"
	"io/ioutil"
	"os/exec"

	"github.com/hashicorp/go-multierror"
)

// Exec executes a command and returns a stream of the stdout of the command.
func Exec(cmd string, args ...string) Stream {
	return Stdin().Through(exe{cmd: cmd, args: args})
}

// ExecHandleStderr executes a command, returns a stream of the stdout of the command and enable
// collecting the stderr of the command.
//
// If the stderr is nil, it will be ignored.
//
// For example, collecting the stderr to memory can be done by providing a `&bytes.Buffer` as
// `stderr`. Writing it to stderr can be done by providing `os.Stderr` as `stderr`. Logging it
// to a file can be done by providing an `os.File` as the `stderr`.
func ExecHandleStderr(stderr io.Writer, cmd string, args ...string) Stream {
	return Stdin().Through(exe{cmd: cmd, args: args, stderr: stderr})
}

// Exec executes a command and returns a stream of the stdout of the command.
func (s Stream) Exec(cmd string, args ...string) Stream {
	return s.Through(exe{cmd: cmd, args: args})
}

// ExecHandleStderr executes a command, returns a stream of the stdout of the command and enable
// collecting the stderr of the command.
//
// If the stderr is nil, it will be ignored.
func (s Stream) ExecHandleStderr(stderr io.Writer, cmd string, args ...string) Stream {
	return s.Through(exe{cmd: cmd, args: args, stderr: stderr})
}

type exe struct {
	cmd    string
	args   []string
	stderr io.Writer
}

func (e exe) Name() string {
	return fmt.Sprintf("exec(%v, %+v)", e.cmd, e.args)
}

func (e exe) Pipe(stdin io.Reader) (io.Reader, error) {
	cmd := exec.Command(e.cmd, e.args...)
	var errors *multierror.Error

	// Pipe previous stdin if available.
	if stdin != nil {
		cmd.Stdin = stdin
	}

	// Pipe stdout to the current command output.
	cmdOut, err := cmd.StdoutPipe()
	if err != nil {
		errors = multierror.Append(errors, fmt.Errorf("pipe stdout: %v", err))
	}

	if e.stderr == nil {
		e.stderr = ioutil.Discard
	}
	cmd.Stderr = e.stderr

	// start the process
	err = cmd.Start()
	if err != nil {
		errors = multierror.Append(errors, fmt.Errorf("start process: %v", err))
	}
	return readcloser{
		Reader: cmdOut,
		Closer: closerFn(func() error { return cmd.Wait() }),
	}, errors.ErrorOrNil()
}

type closerFn func() error

func (f closerFn) Close() error { return f() }
