// Package lineprinter wraps a Print implementation to provide an io.WriteCloser.
package lineprinter

import (
	"bytes"
	"io"
	"sync"
)

// Print is a type that can hold fmt.Print and other implementations
// which match that signature.  For example, you can use:
//
//   trimmer := &lineprinter.Trimmer{WrappedPrint: logrus.StandardLogger().Debug}
//   linePrinter := &linePrinter{Print: trimmer.Print}
//
// to connect the line printer to logrus at the debug level.
type Print func(args ...interface{})

// LinePrinter is an io.WriteCloser that buffers written bytes.
// During each Write, newline-terminated lines are removed from the
// buffer and passed to Print.  On Close, any content remaining in the
// buffer is also passed to Print.
//
// One use-case is connecting a subprocess's standard streams to a
// logger:
//
//   linePrinter := &linePrinter{
//     Print: &Trimmer{WrappedPrint: logrus.StandardLogger().Debug}.Print,
//   }
//   defer linePrinter.Close()
//   cmd := exec.Command(...)
//   cmd.Stdout = linePrinter
//
// LinePrinter buffers the subcommand's byte stream and splits it into
// lines for the logger.  Sometimes we might have a partial line
// written to the buffer.  We don't want to push that partial line into
// the logs if the next read attempt will pull in the remainder of the
// line.  But we do want to push that partial line into the logs if there
// will never be a next read.  So LinePrinter.Write pushes any
// complete lines to the wrapped printer, and LinePrinter.Close pushes
// any remaining partial line.
type LinePrinter struct {
	buf   bytes.Buffer
	Print Print

	sync.Mutex
}

// Write writes len(p) bytes from p to an internal buffer.  Then it
// retrieves any newline-terminated lines from the internal buffer and
// prints them with lp.Print.  Partial lines are left in the internal
// buffer.
func (lp *LinePrinter) Write(p []byte) (int, error) {
	lp.Lock()
	defer lp.Unlock()
	n, err := lp.buf.Write(p)
	if err != nil {
		return n, err
	}

	for {
		line, err := lp.buf.ReadString(byte('\n'))
		if err == io.EOF {
			_, err = lp.buf.Write([]byte(line))
			return n, err
		} else if err != nil {
			return n, err
		}

		lp.Print(line)
	}
}

// Close prints anything that remains in the buffer.
func (lp *LinePrinter) Close() error {
	lp.Lock()
	defer lp.Unlock()
	line := lp.buf.String()
	if len(line) > 0 {
		lp.Print(line)
	}
	return nil
}
