// Copyright (c) 2021, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

package syntax

import (
	
	
	
	
)

type QuoteError struct {
	ByteOffset int
	Message    string
}

func ( QuoteError) () string {
	return fmt.Sprintf("cannot quote character at byte %d: %s", .ByteOffset, .Message)
}

const (
	quoteErrNull  = "shell strings cannot contain null bytes"
	quoteErrPOSIX = "POSIX shell lacks escape sequences"
	quoteErrRange = "rune out of range"
	quoteErrMksh  = "mksh cannot escape codepoints above 16 bits"
)

// Quote returns a quoted version of the input string,
// so that the quoted version is expanded or interpreted
// as the original string in the given language variant.
//
// Quoting is necessary when using arbitrary literal strings
// as words in a shell script or command.
// Without quoting, one can run into syntax errors,
// as well as the possibility of running unintended code.
//
// An error is returned when a string cannot be quoted for a variant.
// For instance, POSIX lacks escape sequences for non-printable characters,
// and no language variant can represent a string containing null bytes.
// In such cases, the returned error type will be *QuoteError.
//
// The quoting strategy is chosen on a best-effort basis,
// to minimize the amount of extra bytes necessary.
//
// Some strings do not require any quoting and are returned unchanged.
// Those strings can be directly surrounded in single quotes as well.
func ( string,  LangVariant) (string, error) {
	if  == "" {
		// Special case; an empty string must always be quoted,
		// as otherwise it expands to zero fields.
		return "''", nil
	}
	 := false
	 := false
	 := 0
	for  := ; len() > 0; {
		,  := utf8.DecodeRuneInString()
		switch  {
		// Like regOps; token characters.
		case ';', '"', '\'', '(', ')', '$', '|', '&', '>', '<', '`',
			// Whitespace; might result in multiple fields.
			' ', '\t', '\r', '\n',
			// Escape sequences would be expanded.
			'\\',
			// Would start a comment unless quoted.
			'#',
			// Might result in brace expansion.
			'{',
			// Might result in tilde expansion.
			'~',
			// Might result in globbing.
			'*', '?', '[',
			// Might result in an assignment.
			'=':
			 = true
		case '\x00':
			return "", &QuoteError{ByteOffset: , Message: quoteErrNull}
		}
		if  == utf8.RuneError || !unicode.IsPrint() {
			if  == LangPOSIX {
				return "", &QuoteError{ByteOffset: , Message: quoteErrPOSIX}
			}
			 = true
		}
		 = [:]
		 += 
	}
	if ! && ! && !IsKeyword() {
		// Nothing to quote; avoid allocating.
		return , nil
	}

	// Single quotes are usually best,
	// as they don't require any escaping of characters.
	// If we have any invalid utf8 or non-printable runes,
	// use $'' so that we can escape them.
	// Note that we can't use double quotes for those.
	var  strings.Builder
	if  {
		.WriteString("$'")
		 := false
		 := 0
		for  := ; len() > 0; {
			 := false
			,  := utf8.DecodeRuneInString()
			switch {
			case  == '\'',  == '\\':
				.WriteByte('\\')
				.WriteRune()
			case unicode.IsPrint() &&  != utf8.RuneError:
				if  && isHex() {
					.WriteString("'$'")
				}
				.WriteRune()
			case  == '\a':
				.WriteString(`\a`)
			case  == '\b':
				.WriteString(`\b`)
			case  == '\f':
				.WriteString(`\f`)
			case  == '\n':
				.WriteString(`\n`)
			case  == '\r':
				.WriteString(`\r`)
			case  == '\t':
				.WriteString(`\t`)
			case  == '\v':
				.WriteString(`\v`)
			case  < utf8.RuneSelf,  == utf8.RuneError &&  == 1:
				// \xXX, fixed at two hexadecimal characters.
				fmt.Fprintf(&, "\\x%02x", [0])
				// Unfortunately, mksh allows \x to consume more hex characters.
				// Ensure that we don't allow it to read more than two.
				if  == LangMirBSDKorn {
					 = true
				}
			case  > utf8.MaxRune:
				// Not a valid Unicode code point?
				return "", &QuoteError{ByteOffset: , Message: quoteErrRange}
			case  == LangMirBSDKorn &&  > 0xFFFD:
				// From the CAVEATS section in R59's man page:
				//
				// mksh currently uses OPTU-16 internally, which is the same as
				// UTF-8 and CESU-8 with 0000..FFFD being valid codepoints.
				return "", &QuoteError{ByteOffset: , Message: quoteErrMksh}
			case  < 0x10000:
				// \uXXXX, fixed at four hexadecimal characters.
				fmt.Fprintf(&, "\\u%04x", )
			default:
				// \UXXXXXXXX, fixed at eight hexadecimal characters.
				fmt.Fprintf(&, "\\U%08x", )
			}
			 = [:]
			 = 
			 += 
		}
		.WriteString("'")
		return .String(), nil
	}

	// Single quotes without any need for escaping.
	if !strings.Contains(, "'") {
		return "'" +  + "'", nil
	}

	// The string contains single quotes,
	// so fall back to double quotes.
	.WriteByte('"')
	for ,  := range  {
		switch  {
		case '"', '\\', '`', '$':
			.WriteByte('\\')
		}
		.WriteRune()
	}
	.WriteByte('"')
	return .String(), nil
}

func isHex( rune) bool {
	return ( >= '0' &&  <= '9') ||
		( >= 'a' &&  <= 'f') ||
		( >= 'A' &&  <= 'F')
}