// Package interp is the GoAWK interpreter.
//
// For basic usage, use the Exec function. For more complicated use
// cases and configuration options, first use the parser package to
// parse the AWK source, and then use ExecProgram to execute it with
// a specific configuration.
//
// If you need to re-run the same parsed program repeatedly on different
// inputs or with different variables, use New to instantiate an Interpreter
// and then call the Interpreter.Execute method as many times as you need.
package interp

import (
	"bufio"
	"bytes"
	"context"
	"errors"
	"fmt"
	"io"
	"math"
	"math/rand"
	"os"
	"regexp"
	"runtime"
	"strconv"
	"strings"
	"unicode/utf8"

	"github.com/benhoyt/goawk/internal/ast"
	"github.com/benhoyt/goawk/internal/compiler"
	"github.com/benhoyt/goawk/internal/resolver"
	"github.com/benhoyt/goawk/parser"
)

var (
	errExit     = errors.New("exit")
	errBreak    = errors.New("break")
	errNext     = errors.New("next")
	errNextfile = errors.New("nextfile")

	errCSVSeparator = errors.New("invalid CSV field separator or comment delimiter")

	varRegex = regexp.MustCompile(`^([_a-zA-Z][_a-zA-Z0-9]*)=(.*)`)

	defaultShellCommand = getDefaultShellCommand()
)

// Error (actually *Error) is returned by Exec and Eval functions on
// interpreter error, for example FS being set to an invalid regex.
type Error struct {
	message string
}

func (e *Error) Error() string {
	return e.message
}

func newError(format string, args ...any) error {
	return &Error{fmt.Sprintf(format, args...)}
}

type returnValue struct {
	Value value
}

func (r returnValue) Error() string {
	return "<return " + r.Value.str("%.6g") + ">"
}

type interp struct {
	// Input/output
	output        io.Writer
	errorOutput   io.Writer
	scanner       *bufio.Scanner
	scanners      map[string]*bufio.Scanner
	stdin         io.Reader
	filenameIndex int
	hadFiles      bool
	input         io.Reader
	inputBuffer   []byte
	inputStreams  map[string]inputStream
	outputStreams map[string]outputStream
	noExec        bool
	noFileWrites  bool
	noFileReads   bool
	shellCommand  []string
	csvOutput     *bufio.Writer
	noArgVars     bool
	splitBuffer   []byte

	// Scalars, arrays, and function state
	globals       []value
	stack         []value
	sp            int
	frame         []value
	arrays        []map[string]value
	localArrays   [][]int
	callDepth     int
	nativeFuncs   []nativeFunc
	scalarIndexes map[string]int
	arrayIndexes  map[string]int

	// File, line, and field handling
	filename        value
	line            string
	lineIsTrueStr   bool
	lineNum         int
	fileLineNum     int
	fields          []string
	fieldsIsTrueStr []bool
	numFields       int
	haveFields      bool
	fieldNames      []string
	fieldIndexes    map[string]int
	reparseCSV      bool

	// Built-in variables
	argc             int
	convertFormat    string
	outputFormat     string
	fieldSep         string
	fieldSepRegex    *regexp.Regexp
	recordSep        string
	recordSepRegex   *regexp.Regexp
	recordTerminator string
	outputFieldSep   string
	outputRecordSep  string
	subscriptSep     string
	matchLength      int
	matchStart       int
	inputMode        IOMode
	csvInputConfig   CSVInputConfig
	outputMode       IOMode
	csvOutputConfig  CSVOutputConfig

	savedFieldSep      string
	savedFieldSepRegex *regexp.Regexp

	// Parsed program, compiled functions and constants
	program   *parser.Program
	functions []compiler.Function
	nums      []float64
	strs      []string
	regexes   []*regexp.Regexp

	// Context support (for Interpreter.ExecuteContext)
	checkCtx bool
	ctx      context.Context
	ctxDone  <-chan struct{}
	ctxOps   int

	// Misc pieces of state
	random            *rand.Rand
	randSeed          float64
	exitStatus        int
	regexCache        map[string]*regexp.Regexp
	formatCache       map[string]cachedFormat
	csvJoinFieldsBuf  bytes.Buffer
	chars             bool
	newlineOutputCRLF bool
}

// Various const configuration. Could make these part of Config if
// we wanted to, but no need for now.
const (
	maxCachedRegexes = 100
	maxCachedFormats = 100
	maxRecordLength  = 10 * 1024 * 1024 // 10MB seems like plenty
	maxFieldIndex    = 1000000
	maxCallDepth     = 1000
	initialStackSize = 100
	outputBufSize    = 64 * 1024
	inputBufSize     = 64 * 1024
)

// NewlineMode specifies how newline characters are handled when reading or
// writing text.
type NewlineMode int

const (
	// SmartNewlineMode uses the newline mode most appropriate for the
	// operating system: raw on Unix/Linux, crlf on Windows.
	SmartNewlineMode NewlineMode = 0

	// RawNewlineMode does no newline translation.
	RawNewlineMode NewlineMode = 1

	// CRLFNewlineMode forces the use of CRLF ("\r\n") newlines on output,
	// regardless of the input format.
	CRLFNewlineMode NewlineMode = 2
)

// Config defines the interpreter configuration for ExecProgram.
type Config struct {
	// Standard input reader (defaults to os.Stdin)
	Stdin io.Reader

	// Writer for normal output (defaults to a buffered version of os.Stdout).
	// If you need to write to stdout but want control over the buffer size or
	// allocation, wrap os.Stdout yourself and set Output to that.
	Output io.Writer

	// Writer for non-fatal error messages (defaults to os.Stderr)
	Error io.Writer

	// The name of the executable (accessible via ARGV[0])
	Argv0 string

	// Input arguments (usually filenames): empty slice means read
	// only from Stdin, and a filename of "-" means read from Stdin
	// instead of a real file.
	//
	// Arguments of the form "var=value" are treated as variable
	// assignments.
	Args []string

	// Set to true to disable "var=value" assignments in Args.
	NoArgVars bool

	// List of name-value pairs for variables to set before executing
	// the program (useful for setting FS and other built-in
	// variables, for example []string{"FS", ",", "OFS", ","}).
	Vars []string

	// Map of named Go functions to allow calling from AWK. You need
	// to pass this same map to the parser.ParseProgram config.
	//
	// Functions can have any number of parameters, and variadic
	// functions are supported. Functions can have no return values,
	// one return value, or two return values (result, error). In the
	// two-value case, if the function returns a non-nil error,
	// program execution will stop and ExecProgram will return that
	// error.
	//
	// Apart from the error return value, the types supported are
	// bool, integer and floating point types (excluding complex),
	// and string types (string or []byte).
	//
	// It's not an error to call a Go function from AWK with fewer
	// arguments than it has parameters in Go. In this case, the zero
	// value will be used for any additional parameters. However, it
	// is a parse error to call a non-variadic function from AWK with
	// more arguments than it has parameters in Go.
	//
	// Functions defined with the "function" keyword in AWK code
	// take precedence over functions in Funcs.
	Funcs map[string]any

	// Set one or more of these to true to prevent unsafe behaviours,
	// useful when executing untrusted scripts:
	//
	// * NoExec prevents system calls via system() or pipe operator
	// * NoFileWrites prevents writing to files via '>' or '>>'
	// * NoFileReads prevents reading from files via getline or the
	//   filenames in Args
	NoExec       bool
	NoFileWrites bool
	NoFileReads  bool

	// Exec args used to run system shell. Typically, this will
	// be {"/bin/sh", "-c"}
	ShellCommand []string

	// List of name-value pairs to be assigned to the ENVIRON special
	// array, for example []string{"USER", "bob", "HOME", "/home/bob"}.
	// If nil (the default), values from os.Environ() are used.
	//
	// If the script doesn't need environment variables, set Environ to a
	// non-nil empty slice, []string{}.
	Environ []string

	// Mode for parsing input fields and record: default is to use normal FS
	// and RS behaviour. If set to CSVMode or TSVMode, FS and RS are ignored,
	// and input records are parsed as comma-separated values or tab-separated
	// values, respectively. Parsing is done as per RFC 4180 and the
	// "encoding/csv" package, but FieldsPerRecord is not supported,
	// LazyQuotes is always on, and TrimLeadingSpace is always off.
	//
	// You can also enable CSV or TSV input mode by setting INPUTMODE to "csv"
	// or "tsv" in Vars or in the BEGIN block (those override this setting).
	//
	// For further documentation about GoAWK's CSV support, see the full docs
	// in "../docs/csv.md".
	InputMode IOMode

	// Additional options if InputMode is CSVMode or TSVMode. The zero value
	// is valid, specifying a separator of ',' in CSVMode and '\t' in TSVMode.
	//
	// You can also specify these options by setting INPUTMODE in the BEGIN
	// block, for example, to use '|' as the field separator, '#' as the
	// comment character, and enable header row parsing:
	//
	//     BEGIN { INPUTMODE="csv separator=| comment=# header" }
	CSVInput CSVInputConfig

	// Mode for print output: default is to use normal OFS and ORS
	// behaviour. If set to CSVMode or TSVMode, the "print" statement with one
	// or more arguments outputs fields using CSV or TSV formatting,
	// respectively. Output is written as per RFC 4180 and the "encoding/csv"
	// package.
	//
	// You can also enable CSV or TSV output mode by setting OUTPUTMODE to
	// "csv" or "tsv" in Vars or in the BEGIN block (those override this
	// setting).
	OutputMode IOMode

	// Additional options if OutputMode is CSVMode or TSVMode. The zero value
	// is valid, specifying a separator of ',' in CSVMode and '\t' in TSVMode.
	//
	// You can also specify these options by setting OUTPUTMODE in the BEGIN
	// block, for example, to use '|' as the output field separator:
	//
	//     BEGIN { OUTPUTMODE="csv separator=|" }
	CSVOutput CSVOutputConfig

	// Set to true to count using Unicode chars instead of bytes for
	// index(), length(), match(), substr(), and printf %c.
	Chars bool

	// NewlineOutput specifies how newline characters are handled when writing
	// output. The default is "smart", meaning no translation on Linux/Unix
	// and CRLF translation on Windows.
	NewlineOutput NewlineMode
}

// IOMode specifies the input parsing or print output mode.
type IOMode int

const (
	// DefaultMode uses normal AWK field and record separators: FS and RS for
	// input, OFS and ORS for print output.
	DefaultMode IOMode = 0

	// CSVMode uses comma-separated value mode for input or output.
	CSVMode IOMode = 1

	// TSVMode uses tab-separated value mode for input or output.
	TSVMode IOMode = 2
)

// CSVInputConfig holds additional configuration for when InputMode is CSVMode
// or TSVMode.
type CSVInputConfig struct {
	// Input field separator character. If this is zero, it defaults to ','
	// when InputMode is CSVMode and '\t' when InputMode is TSVMode.
	Separator rune

	// If nonzero, specifies that lines beginning with this character (and no
	// leading whitespace) should be ignored as comments.
	Comment rune

	// If true, parse the first row in each input file as a header row (that
	// is, a list of field names), and enable the @"field" syntax to get a
	// field by name as well as the FIELDS special array.
	Header bool
}

// CSVOutputConfig holds additional configuration for when OutputMode is
// CSVMode or TSVMode.
type CSVOutputConfig struct {
	// Output field separator character. If this is zero, it defaults to ','
	// when OutputMode is CSVMode and '\t' when OutputMode is TSVMode.
	Separator rune
}

// ExecProgram executes the parsed program using the given interpreter
// config, returning the exit status code of the program. Error is nil
// on successful execution of the program, even if the program returns
// a non-zero status code.
//
// As of GoAWK version v1.16.0, a nil config is valid and will use the
// defaults (zero values). However, it may be simpler to use Exec in that
// case.
func ExecProgram(program *parser.Program, config *Config) (int, error) {
	p := newInterp(program)
	err := p.setExecuteConfig(config)
	if err != nil {
		return 0, err
	}
	return p.executeAll()
}

func newInterp(program *parser.Program) *interp {
	p := &interp{
		program:   program,
		functions: program.Compiled.Functions,
		nums:      program.Compiled.Nums,
		strs:      program.Compiled.Strs,
		regexes:   program.Compiled.Regexes,
	}

	// Allocate memory for variables and virtual machine stack
	p.scalarIndexes = make(map[string]int)
	p.arrayIndexes = make(map[string]int)
	program.IterVars("", func(name string, info resolver.VarInfo) {
		if info.Type == resolver.Array {
			p.arrayIndexes[name] = info.Index
		} else {
			p.scalarIndexes[name] = info.Index
		}
	})
	p.globals = make([]value, len(p.scalarIndexes))
	p.stack = make([]value, initialStackSize)
	p.arrays = make([]map[string]value, len(p.arrayIndexes), len(p.arrayIndexes)+initialStackSize)
	for i := 0; i < len(p.arrayIndexes); i++ {
		p.arrays[i] = make(map[string]value)
	}

	// Initialize defaults
	p.regexCache = make(map[string]*regexp.Regexp, 10)
	p.formatCache = make(map[string]cachedFormat, 10)
	p.randSeed = 1.0
	seed := math.Float64bits(p.randSeed)
	p.random = rand.New(rand.NewSource(int64(seed)))
	p.convertFormat = "%.6g"
	p.outputFormat = "%.6g"
	p.fieldSep = " "
	p.savedFieldSep = " "
	p.recordSep = "\n"
	p.outputFieldSep = " "
	p.outputRecordSep = "\n"
	p.subscriptSep = "\x1c"

	p.inputStreams = make(map[string]inputStream)
	p.outputStreams = make(map[string]outputStream)
	p.scanners = make(map[string]*bufio.Scanner)

	return p
}

func (p *interp) setExecuteConfig(config *Config) error {
	if config == nil {
		config = &Config{}
	}
	if len(config.Vars)%2 != 0 {
		return newError("length of config.Vars must be a multiple of 2, not %d", len(config.Vars))
	}
	if len(config.Environ)%2 != 0 {
		return newError("length of config.Environ must be a multiple of 2, not %d", len(config.Environ))
	}

	// Set up I/O mode config (Vars will override)
	p.inputMode = config.InputMode
	p.csvInputConfig = config.CSVInput
	switch p.inputMode {
	case CSVMode:
		if p.csvInputConfig.Separator == 0 {
			p.csvInputConfig.Separator = ','
		}
	case TSVMode:
		if p.csvInputConfig.Separator == 0 {
			p.csvInputConfig.Separator = '\t'
		}
	case DefaultMode:
		if p.csvInputConfig != (CSVInputConfig{}) {
			return newError("input mode configuration not valid in default input mode")
		}
	}
	p.outputMode = config.OutputMode
	p.csvOutputConfig = config.CSVOutput
	switch p.outputMode {
	case CSVMode:
		if p.csvOutputConfig.Separator == 0 {
			p.csvOutputConfig.Separator = ','
		}
	case TSVMode:
		if p.csvOutputConfig.Separator == 0 {
			p.csvOutputConfig.Separator = '\t'
		}
	case DefaultMode:
		if p.csvOutputConfig != (CSVOutputConfig{}) {
			return newError("output mode configuration not valid in default output mode")
		}
	}

	// Set up ARGV and other variables from config
	argvIndex := p.arrayIndexes["ARGV"]
	p.setArrayValue(resolver.Global, argvIndex, "0", str(config.Argv0))
	p.argc = len(config.Args) + 1
	for i, arg := range config.Args {
		p.setArrayValue(resolver.Global, argvIndex, strconv.Itoa(i+1), numStr(arg))
	}
	p.noArgVars = config.NoArgVars
	p.filenameIndex = 1
	p.hadFiles = false
	for i := 0; i < len(config.Vars); i += 2 {
		err := p.setVarByName(config.Vars[i], config.Vars[i+1])
		if err != nil {
			return err
		}
	}
	p.chars = config.Chars

	// After Vars has been handled, validate CSV configuration.
	err := validateCSVInputConfig(p.inputMode, p.csvInputConfig)
	if err != nil {
		return err
	}
	err = validateCSVOutputConfig(p.outputMode, p.csvOutputConfig)
	if err != nil {
		return err
	}

	// Set up ENVIRON from config or environment variables
	environIndex := p.arrayIndexes["ENVIRON"]
	if config.Environ != nil {
		for i := 0; i < len(config.Environ); i += 2 {
			p.setArrayValue(resolver.Global, environIndex, config.Environ[i], numStr(config.Environ[i+1]))
		}
	} else {
		for _, kv := range os.Environ() {
			key, val, ok := strings.Cut(kv, "=")
			if ok {
				p.setArrayValue(resolver.Global, environIndex, key, numStr(val))
			}
		}
	}

	// Set up system shell command
	if len(config.ShellCommand) != 0 {
		p.shellCommand = config.ShellCommand
	} else {
		p.shellCommand = defaultShellCommand
	}

	// Set up I/O structures
	p.noExec = config.NoExec
	p.noFileWrites = config.NoFileWrites
	p.noFileReads = config.NoFileReads
	p.stdin = config.Stdin
	if p.stdin == nil {
		p.stdin = os.Stdin
	}
	p.output = config.Output
	if p.output == nil {
		p.output = bufio.NewWriterSize(os.Stdout, outputBufSize)
	}
	p.errorOutput = config.Error
	if p.errorOutput == nil {
		p.errorOutput = os.Stderr
	}

	// Initialize native Go functions
	if p.nativeFuncs == nil {
		err := p.initNativeFuncs(config.Funcs)
		if err != nil {
			return err
		}
	}

	switch config.NewlineOutput {
	case SmartNewlineMode:
		p.newlineOutputCRLF = (runtime.GOOS == "windows")
	case RawNewlineMode:
		p.newlineOutputCRLF = false
	case CRLFNewlineMode:
		p.newlineOutputCRLF = true
	default:
		return fmt.Errorf("invalid newline output mode %d", config.NewlineOutput)
	}

	return nil
}

func validateCSVInputConfig(mode IOMode, config CSVInputConfig) error {
	if mode != CSVMode && mode != TSVMode {
		return nil
	}
	if config.Separator == config.Comment || !validCSVSeparator(config.Separator) ||
		config.Comment != 0 && !validCSVSeparator(config.Comment) {
		return errCSVSeparator
	}
	return nil
}

func validateCSVOutputConfig(mode IOMode, config CSVOutputConfig) error {
	if mode != CSVMode && mode != TSVMode {
		return nil
	}
	if !validCSVSeparator(config.Separator) {
		return errCSVSeparator
	}
	return nil
}

func validCSVSeparator(r rune) bool {
	return r != 0 && r != '"' && r != '\r' && r != '\n' && utf8.ValidRune(r) && r != utf8.RuneError
}

func (p *interp) executeAll() (int, error) {
	defer p.closeAll()

	// Execute the program: BEGIN, then pattern/actions, then END
	err := p.execute(p.program.Compiled.Begin)
	if err != nil && err != errExit {
		if p.checkCtx {
			ctxErr := p.checkContextNow()
			if ctxErr != nil {
				return 0, ctxErr
			}
		}
		return 0, err
	}
	if len(p.program.Compiled.Actions) == 0 && len(p.program.Compiled.End) == 0 {
		return p.exitStatus, nil // only BEGIN specified, don't process input
	}
	if err != errExit {
		err = p.execActions(p.program.Compiled.Actions)
		if err != nil && err != errExit {
			if p.checkCtx {
				ctxErr := p.checkContextNow()
				if ctxErr != nil {
					return 0, ctxErr
				}
			}
			return 0, err
		}
	}
	err = p.execute(p.program.Compiled.End)
	if err != nil && err != errExit {
		if p.checkCtx {
			ctxErr := p.checkContextNow()
			if ctxErr != nil {
				return 0, ctxErr
			}
		}
		return 0, err
	}
	return p.exitStatus, nil
}

// Exec provides a simple way to parse and execute an AWK program
// with the given field separator. Exec reads input from the given
// reader (nil means use os.Stdin) and writes output to stdout (nil
// means use a buffered version of os.Stdout).
func Exec(source, fieldSep string, input io.Reader, output io.Writer) error {
	prog, err := parser.ParseProgram([]byte(source), nil)
	if err != nil {
		return err
	}
	config := &Config{
		Stdin:  input,
		Output: output,
		Error:  io.Discard,
		Vars:   []string{"FS", fieldSep},
	}
	_, err = ExecProgram(prog, config)
	return err
}

// Execute pattern-action blocks (may be multiple)
func (p *interp) execActions(actions []compiler.Action) error {
	var inRange []bool
lineLoop:
	for {
		// Read and setup next line of input
		line, err := p.nextLine()
		if err == io.EOF {
			break
		}
		if err != nil {
			return err
		}
		p.setLine(line, false)
		p.reparseCSV = false

		// Execute all the pattern-action blocks for each line
		for i, action := range actions {
			// First determine whether the pattern matches
			matched := false
			switch len(action.Pattern) {
			case 0:
				// No pattern is equivalent to pattern evaluating to true
				matched = true
			case 1:
				// Single boolean pattern
				err := p.execute(action.Pattern[0])
				if err != nil {
					return err
				}
				matched = p.pop().boolean()
			case 2:
				// Range pattern (matches between start and stop lines)
				if inRange == nil {
					inRange = make([]bool, len(actions))
				}
				if !inRange[i] {
					err := p.execute(action.Pattern[0])
					if err != nil {
						return err
					}
					inRange[i] = p.pop().boolean()
				}
				matched = inRange[i]
				if inRange[i] {
					err := p.execute(action.Pattern[1])
					if err != nil {
						return err
					}
					inRange[i] = !p.pop().boolean()
				}
			}
			if !matched {
				continue
			}

			// No action is equivalent to { print $0 }
			if len(action.Body) == 0 {
				err := p.printLine(p.output, p.line)
				if err != nil {
					return err
				}
				continue
			}

			// Execute the body statements
			err := p.execute(action.Body)
			switch {
			case err == errNext:
				// "next" statement skips straight to next line
				continue lineLoop
			case err == errNextfile:
				// Tell nextLine to move on to next file
				p.scanner = nil
				continue lineLoop
			case err != nil:
				return err
			}
		}
	}
	return nil
}

// Get a special variable by index
func (p *interp) getSpecial(index int) value {
	switch index {
	case ast.V_NF:
		p.ensureFields()
		return num(float64(p.numFields))
	case ast.V_NR:
		return num(float64(p.lineNum))
	case ast.V_RLENGTH:
		return num(float64(p.matchLength))
	case ast.V_RSTART:
		return num(float64(p.matchStart))
	case ast.V_FNR:
		return num(float64(p.fileLineNum))
	case ast.V_ARGC:
		return num(float64(p.argc))
	case ast.V_CONVFMT:
		return str(p.convertFormat)
	case ast.V_FILENAME:
		return p.filename
	case ast.V_FS:
		return str(p.fieldSep)
	case ast.V_OFMT:
		return str(p.outputFormat)
	case ast.V_OFS:
		return str(p.outputFieldSep)
	case ast.V_ORS:
		return str(p.outputRecordSep)
	case ast.V_RS:
		return str(p.recordSep)
	case ast.V_RT:
		return str(p.recordTerminator)
	case ast.V_SUBSEP:
		return str(p.subscriptSep)
	case ast.V_INPUTMODE:
		return str(inputModeString(p.inputMode, p.csvInputConfig))
	case ast.V_OUTPUTMODE:
		return str(outputModeString(p.outputMode, p.csvOutputConfig))
	default:
		panic(fmt.Sprintf("unexpected special variable index: %d", index))
	}
}

// Set a variable by name (specials and globals only)
func (p *interp) setVarByName(name, value string) error {
	index := ast.SpecialVarIndex(name)
	if index > 0 {
		return p.setSpecial(index, numStr(value))
	}
	index, ok := p.scalarIndexes[name]
	if ok {
		p.globals[index] = numStr(value)
		return nil
	}
	// Ignore variables that aren't defined in program
	return nil
}

// Set special variable by index to given value
func (p *interp) setSpecial(index int, v value) error {
	switch index {
	case ast.V_NF:
		numFields := int(v.num())
		if numFields < 0 {
			return newError("NF set to negative value: %d", numFields)
		}
		if numFields > maxFieldIndex {
			return newError("NF set too large: %d", numFields)
		}
		p.ensureFields()
		p.numFields = numFields
		if p.numFields < len(p.fields) {
			p.fields = p.fields[:p.numFields]
			p.fieldsIsTrueStr = p.fieldsIsTrueStr[:p.numFields]
		}
		for i := len(p.fields); i < p.numFields; i++ {
			p.fields = append(p.fields, "")
			p.fieldsIsTrueStr = append(p.fieldsIsTrueStr, false)
		}
		p.line = p.joinFields(p.fields)
		p.lineIsTrueStr = true
	case ast.V_NR:
		p.lineNum = int(v.num())
	case ast.V_RLENGTH:
		p.matchLength = int(v.num())
	case ast.V_RSTART:
		p.matchStart = int(v.num())
	case ast.V_FNR:
		p.fileLineNum = int(v.num())
	case ast.V_ARGC:
		argc := int(v.num())
		if argc > maxFieldIndex {
			return newError("ARGC set too large: %d", argc)
		}
		p.argc = argc
	case ast.V_CONVFMT:
		p.convertFormat = p.toString(v)
	case ast.V_FILENAME:
		p.filename = v
	case ast.V_FS:
		p.fieldSep = p.toString(v)
		if utf8.RuneCountInString(p.fieldSep) > 1 { // compare to interp.ensureFields
			re, err := regexp.Compile(compiler.AddRegexFlags(p.fieldSep))
			if err != nil {
				return newError("invalid regex %q: %s", p.fieldSep, err)
			}
			re.Longest() // other awks use leftmost-longest matching
			p.fieldSepRegex = re
		}
	case ast.V_OFMT:
		p.outputFormat = p.toString(v)
	case ast.V_OFS:
		p.outputFieldSep = p.toString(v)
	case ast.V_ORS:
		p.outputRecordSep = p.toString(v)
	case ast.V_RS:
		p.recordSep = p.toString(v)
		switch { // compare to interp.newScanner
		case len(p.recordSep) <= 1:
			// Simple cases use specialized splitters, not regex
		case utf8.RuneCountInString(p.recordSep) == 1:
			// Multi-byte unicode char falls back to regex splitter
			sep := regexp.QuoteMeta(p.recordSep) // not strictly necessary as no multi-byte chars are regex meta chars
			p.recordSepRegex = regexp.MustCompile(sep)
			p.recordSepRegex.Longest() // other awks use leftmost-longest matching
		default:
			re, err := regexp.Compile(compiler.AddRegexFlags(p.recordSep))
			if err != nil {
				return newError("invalid regex %q: %s", p.recordSep, err)
			}
			re.Longest() // other awks use leftmost-longest matching
			p.recordSepRegex = re
		}
	case ast.V_RT:
		p.recordTerminator = p.toString(v)
	case ast.V_SUBSEP:
		p.subscriptSep = p.toString(v)
	case ast.V_INPUTMODE:
		var err error
		p.inputMode, p.csvInputConfig, err = parseInputMode(p.toString(v))
		if err != nil {
			return err
		}
		err = validateCSVInputConfig(p.inputMode, p.csvInputConfig)
		if err != nil {
			return err
		}
	case ast.V_OUTPUTMODE:
		var err error
		p.outputMode, p.csvOutputConfig, err = parseOutputMode(p.toString(v))
		if err != nil {
			return err
		}
		err = validateCSVOutputConfig(p.outputMode, p.csvOutputConfig)
		if err != nil {
			return err
		}
	default:
		panic(fmt.Sprintf("unexpected special variable index: %d", index))
	}
	return nil
}

// Determine the index of given array into the p.arrays slice. Global
// arrays are just at p.arrays[index], local arrays have to be looked
// up indirectly.
func (p *interp) arrayIndex(scope resolver.Scope, index int) int {
	if scope == resolver.Global {
		return index
	} else {
		return p.localArrays[len(p.localArrays)-1][index]
	}
}

// Return array with given scope and index.
func (p *interp) array(scope resolver.Scope, index int) map[string]value {
	return p.arrays[p.arrayIndex(scope, index)]
}

// Return local array with given index.
func (p *interp) localArray(index int) map[string]value {
	return p.arrays[p.localArrays[len(p.localArrays)-1][index]]
}

// Set a value in given array by key (index)
func (p *interp) setArrayValue(scope resolver.Scope, arrayIndex int, index string, v value) {
	array := p.array(scope, arrayIndex)
	array[index] = v
}

// Get the value of given numbered field, equivalent to "$index"
func (p *interp) getField(index int) value {
	if index == 0 {
		if p.lineIsTrueStr {
			return str(p.line)
		} else {
			return numStr(p.line)
		}
	}
	p.ensureFields()
	if index < 1 {
		index = len(p.fields) + 1 + index
		if index < 1 {
			return str("")
		}
	}
	if index > len(p.fields) {
		return str("")
	}
	if p.fieldsIsTrueStr[index-1] {
		return str(p.fields[index-1])
	} else {
		return numStr(p.fields[index-1])
	}
}

// Get the value of a field by name (for CSV/TSV mode), as in @"name".
func (p *interp) getFieldByName(name string) (value, error) {
	if p.fieldIndexes == nil {
		// Lazily create map of field names to indexes.
		if p.fieldNames == nil {
			return null(), newError(`no field names for @; use -H or add "header" to INPUTMODE, and use "getline" first if in BEGIN`)
		}
		p.fieldIndexes = make(map[string]int, len(p.fieldNames))
		for i, n := range p.fieldNames {
			p.fieldIndexes[n] = i + 1
		}
	}
	index := p.fieldIndexes[name]
	if index == 0 {
		return str(""), nil
	}
	return p.getField(index), nil
}

// Sets a single field, equivalent to "$index = value"
func (p *interp) setField(index int, value string) error {
	if index == 0 {
		p.setLine(value, true)
		return nil
	}
	if index > maxFieldIndex {
		return newError("field index too large: %d", index)
	}
	// If there aren't enough fields, add empty string fields in between
	p.ensureFields()
	if index < 1 {
		index = len(p.fields) + 1 + index
		if index < 1 {
			return nil
		}
	}
	for i := len(p.fields); i < index; i++ {
		p.fields = append(p.fields, "")
		p.fieldsIsTrueStr = append(p.fieldsIsTrueStr, true)
	}
	p.fields[index-1] = value
	p.fieldsIsTrueStr[index-1] = true
	p.numFields = len(p.fields)
	p.line = p.joinFields(p.fields)
	p.lineIsTrueStr = true
	return nil
}

func (p *interp) joinFields(fields []string) string {
	switch p.outputMode {
	case CSVMode, TSVMode:
		p.csvJoinFieldsBuf.Reset()
		_ = p.writeCSV(&p.csvJoinFieldsBuf, fields)
		line := p.csvJoinFieldsBuf.Bytes()
		line = line[:len(line)-lenNewline(line)]
		return string(line)
	default:
		return strings.Join(fields, p.outputFieldSep)
	}
}

// Convert value to string using current CONVFMT
func (p *interp) toString(v value) string {
	return v.str(p.convertFormat)
}

// Compile regex string (or fetch from regex cache)
func (p *interp) compileRegex(regex string) (*regexp.Regexp, error) {
	if re, ok := p.regexCache[regex]; ok {
		return re, nil
	}
	re, err := regexp.Compile(compiler.AddRegexFlags(regex))
	if err != nil {
		return nil, newError("invalid regex %q: %s", regex, err)
	}
	re.Longest() // other awks use leftmost-longest matching
	// Dumb, non-LRU cache: just cache the first N regexes
	if len(p.regexCache) < maxCachedRegexes {
		p.regexCache[regex] = re
	}
	return re, nil
}

func getDefaultShellCommand() []string {
	executable := "/bin/sh"
	if runtime.GOOS == "windows" {
		executable = "sh"
	}
	return []string{executable, "-c"}
}

func inputModeString(mode IOMode, csvConfig CSVInputConfig) string {
	var s string
	var defaultSep rune
	switch mode {
	case CSVMode:
		s = "csv"
		defaultSep = ','
	case TSVMode:
		s = "tsv"
		defaultSep = '\t'
	case DefaultMode:
		return ""
	}
	if csvConfig.Separator != defaultSep {
		s += " separator=" + string([]rune{csvConfig.Separator})
	}
	if csvConfig.Comment != 0 {
		s += " comment=" + string([]rune{csvConfig.Comment})
	}
	if csvConfig.Header {
		s += " header"
	}
	return s
}

func parseInputMode(s string) (mode IOMode, csvConfig CSVInputConfig, err error) {
	fields := strings.Fields(s)
	if len(fields) == 0 {
		return DefaultMode, CSVInputConfig{}, nil
	}
	switch fields[0] {
	case "csv":
		mode = CSVMode
		csvConfig.Separator = ','
	case "tsv":
		mode = TSVMode
		csvConfig.Separator = '\t'
	default:
		return DefaultMode, CSVInputConfig{}, newError("invalid input mode %q", fields[0])
	}
	for _, field := range fields[1:] {
		key, val, _ := strings.Cut(field, "=")
		switch key {
		case "separator":
			r, n := utf8.DecodeRuneInString(val)
			if n == 0 || n < len(val) {
				return DefaultMode, CSVInputConfig{}, newError("invalid CSV/TSV separator %q", val)
			}
			csvConfig.Separator = r
		case "comment":
			r, n := utf8.DecodeRuneInString(val)
			if n == 0 || n < len(val) {
				return DefaultMode, CSVInputConfig{}, newError("invalid CSV/TSV comment character %q", val)
			}
			csvConfig.Comment = r
		case "header":
			if val != "" && val != "true" && val != "false" {
				return DefaultMode, CSVInputConfig{}, newError("invalid header value %q", val)
			}
			csvConfig.Header = val == "" || val == "true"
		default:
			return DefaultMode, CSVInputConfig{}, newError("invalid input mode key %q", key)
		}
	}
	return mode, csvConfig, nil
}

func outputModeString(mode IOMode, csvConfig CSVOutputConfig) string {
	var s string
	var defaultSep rune
	switch mode {
	case CSVMode:
		s = "csv"
		defaultSep = ','
	case TSVMode:
		s = "tsv"
		defaultSep = '\t'
	case DefaultMode:
		return ""
	}
	if csvConfig.Separator != defaultSep {
		s += " separator=" + string([]rune{csvConfig.Separator})
	}
	return s
}

func parseOutputMode(s string) (mode IOMode, csvConfig CSVOutputConfig, err error) {
	fields := strings.Fields(s)
	if len(fields) == 0 {
		return DefaultMode, CSVOutputConfig{}, nil
	}
	switch fields[0] {
	case "csv":
		mode = CSVMode
		csvConfig.Separator = ','
	case "tsv":
		mode = TSVMode
		csvConfig.Separator = '\t'
	default:
		return DefaultMode, CSVOutputConfig{}, newError("invalid output mode %q", fields[0])
	}
	for _, field := range fields[1:] {
		key, val, _ := strings.Cut(field, "=")
		switch key {
		case "separator":
			r, n := utf8.DecodeRuneInString(val)
			if n == 0 || n < len(val) {
				return DefaultMode, CSVOutputConfig{}, newError("invalid CSV/TSV separator %q", val)
			}
			csvConfig.Separator = r
		default:
			return DefaultMode, CSVOutputConfig{}, newError("invalid output mode key %q", key)
		}
	}
	return mode, csvConfig, nil
}
