// Package script provides helper functions to write scripts.
//
// Inspired by https://github.com/bitfield/script, with some improvements:
//
// * Output between streamed commands is a stream and not loaded to memory.
//
// * Better representation and handling of errors.
//
// * Proper incocation, usage and handling of stderr of custom commands.
//
// The script chain is represented by a
// (`Stream`) https://godoc.org/github.com/posener/script#Stream object. While each command in the
// stream is abstracted by the (`Command`) https://godoc.org/github.com/posener/script#Command
// struct. This library provides basic functionality, but can be extended freely.
package script

import (
	"io"
	"reflect"

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

// Stream is a chain of operations on a stream of bytes. The stdout of each operation in the stream
// feeds the following operation stdin. The stream object have different methods that allow
// manipulating it, most of them resemble well known linux commands.
//
// A custom modifier can be used with the `Through` or with the `Modify` functions.
//
// The stream object is created by some in this library or from any `io.Reader` using the `From`
// function. It can be dumped using some functions in this library, to a custom reader using the
// `To` method.
type Stream struct {
	// r is the output reader of this stream. If r also implements the `io.Closer` interface, it
	// will be closed when the stream is closed.
	r io.Reader
	// stage is the name of the current stage in the stream.
	stage string
	// parent points to the stage before the current stage in the stream.
	parent *Stream
	// err contains an error from the current stage in the stream.
	err error
}

// Read can be used to read from the stream.
func (s Stream) Read(b []byte) (int, error) {
	return s.r.Read(b)
}

// Close closes all the stages in the stream and return the errors that occurred in all of the
// stages.
func (s Stream) Close() error {
	var errors *multierror.Error
	for cur := &s; cur != nil; cur = cur.parent {
		if cur.err != nil {
			errors = multierror.Append(errors, cur.err)
		}
		if closer, ok := cur.r.(io.Closer); ok {
			if err := closer.Close(); err != nil {
				errors = multierror.Append(errors, err)
			}
		}
	}
	return errors.ErrorOrNil()
}

// From creates a stream from a reader.
func From(name string, r io.Reader) Stream {
	return Stream{stage: name, r: r}
}

// Through passes the current stream through a pipe. This function can be used to add custom
// commands that are not available in this library.
func (s Stream) Through(pipe Pipe) Stream {
	r, err := pipe.Pipe(s.r)
	if r == nil {
		panic("a command must contain a reader")
	}
	return Stream{
		stage:  pipe.Name(),
		r:      r,
		err:    err,
		parent: &s,
	}
}

// Pipe reads from a reader and returns another reader.
type Pipe interface {
	// Pipe gets a reader and returns another reader. A pipe may return an error and a reader
	// together.
	Pipe(stdin io.Reader) (io.Reader, error)
	// Name of pipe.
	Name() string
}

// PipeFn is a function that implements Pipe.
type PipeFn func(io.Reader) (io.Reader, error)

func (f PipeFn) Pipe(stdin io.Reader) (io.Reader, error) { return f(stdin) }

func (f PipeFn) Name() string { return reflect.TypeOf(f).Name() }

type readcloser struct {
	io.Reader
	io.Closer
}
