package interp

import (
	"bytes"
	"fmt"
	"io"
	"strings"

	"mvdan.cc/sh/v3/syntax"
)

// tracer prints expressions like a shell would do if its
// options '-o' is set to either 'xtrace' or its shorthand, '-x'.
type tracer struct {
	buf       bytes.Buffer
	printer   *syntax.Printer
	output    io.Writer
	needsPlus bool
}

func (r *Runner) tracer() *tracer {
	if !r.opts[optXTrace] {
		return nil
	}

	return &tracer{
		printer:   syntax.NewPrinter(),
		output:    r.stderr,
		needsPlus: true,
	}
}

// string writes s to tracer.buf if tracer is non-nil,
// prepending "+" if tracer.needsPlus is true.
func (t *tracer) string(s string) {
	if t == nil {
		return
	}

	if t.needsPlus {
		t.buf.WriteString("+ ")
	}
	t.needsPlus = false
	t.buf.WriteString(s)
}

func (t *tracer) stringf(f string, a ...any) {
	if t == nil {
		return
	}

	t.string(fmt.Sprintf(f, a...))
}

// expr prints x to tracer.buf if tracer is non-nil,
// prepending "+" if tracer.isFirstPrint is true.
func (t *tracer) expr(x syntax.Node) {
	if t == nil {
		return
	}

	if t.needsPlus {
		t.buf.WriteString("+ ")
	}
	t.needsPlus = false
	if err := t.printer.Print(&t.buf, x); err != nil {
		panic(err)
	}
}

// flush writes the contents of tracer.buf to the tracer.stdout.
func (t *tracer) flush() {
	if t == nil {
		return
	}

	t.output.Write(t.buf.Bytes())
	t.buf.Reset()
}

// newLineFlush is like flush, but with extra new line before tracer.buf gets flushed.
func (t *tracer) newLineFlush() {
	if t == nil {
		return
	}

	t.buf.WriteString("\n")
	t.flush()
	// reset state
	t.needsPlus = true
}

// call prints a command and its arguments with varying formats depending on the cmd type,
// for example, built-in command's arguments are printed enclosed in single quotes,
// otherwise, call defaults to printing with double quotes.
func (t *tracer) call(cmd string, args ...string) {
	if t == nil {
		return
	}

	s := strings.Join(args, " ")
	if strings.TrimSpace(s) == "" {
		// fields may be empty for function () {} declarations
		t.string(cmd)
	} else if isBuiltin(cmd) {
		if cmd == "set" {
			// TODO: only first occurrence of set is not printed, succeeding calls are printed
			return
		}

		qs, err := syntax.Quote(s, syntax.LangBash)
		if err != nil { // should never happen
			panic(err)
		}
		t.stringf("%s %s", cmd, qs)
	} else {
		t.stringf("%s %s", cmd, s)
	}
}
