package strutil

import (
	"bytes"
	"errors"
	"regexp"
	"strings"
	"unicode/utf8"
)

var (
	errUnterminatedSingleQuote = errors.New("unterminated single-quoted string")
	errUnterminatedDoubleQuote = errors.New("unterminated double-quoted string")
	errUnterminatedEscape      = errors.New("unterminated backslash-escape")
)

var (
	splitChars        = " \n\t"
	singleChar        = '\''
	doubleChar        = '"'
	escapeChar        = '\\'
	doubleEscapeChars = "$`\"\n\\"
)

// NewlineMatcher is a regular expression matching all newlines or returned newlines.
var NewlineMatcher = regexp.MustCompile(`\r\n`)

// Split splits a string according to /bin/sh's word-splitting rules. It
// supports backslash-escapes, single-quotes, and double-quotes. Notably it does
// not support the $” style of quoting. It also doesn't attempt to perform any
// other sort of expansion, including brace expansion, shell expansion, or
// pathname expansion.
//
// If the given input has an unterminated quoted string or ends in a
// backslash-escape, one of UnterminatedSingleQuoteError,
// UnterminatedDoubleQuoteError, or UnterminatedEscapeError is returned.
func Split(input string) (words []string, err error) {
	var buf bytes.Buffer
	words = make([]string, 0)

	for len(input) > 0 {
		// skip any splitChars at the start
		c, l := utf8.DecodeRuneInString(input)
		if strings.ContainsRune(splitChars, c) {
			input = input[l:]
			continue
		} else if c == escapeChar {
			// Look ahead for escaped newline so we can skip over it
			next := input[l:]
			if len(next) == 0 {
				err = errUnterminatedEscape
				return words, err
			}

			c2, l2 := utf8.DecodeRuneInString(next)
			if c2 == '\n' {
				input = next[l2:]
				continue
			}
		}

		var word string

		word, input, err = splitWord(input, &buf)
		if err != nil {
			return words, err
		}

		words = append(words, word)
	}

	return words, err
}

func splitWord(input string, buf *bytes.Buffer) (word string, remainder string, err error) {
	buf.Reset()

raw:
	{
		cur := input
		for len(cur) > 0 {
			c, l := utf8.DecodeRuneInString(cur)
			cur = cur[l:]
			switch {
			case c == singleChar:
				buf.WriteString(input[0 : len(input)-len(cur)-l])
				input = cur
				goto single
			case c == doubleChar:
				buf.WriteString(input[0 : len(input)-len(cur)-l])
				input = cur
				goto double
			case c == escapeChar:
				buf.WriteString(input[0 : len(input)-len(cur)-l])
				input = cur
				goto escape
			case strings.ContainsRune(splitChars, c):
				buf.WriteString(input[0 : len(input)-len(cur)-l])
				return buf.String(), cur, nil
			}
		}
		if len(input) > 0 {
			buf.WriteString(input)
			input = ""
		}
		goto done
	}

escape:
	{
		if len(input) == 0 {
			return "", "", errUnterminatedEscape
		}
		c, l := utf8.DecodeRuneInString(input)
		// A backslash-escaped newline is elided from the output entirely.
		if c != '\n' {
			buf.WriteString(input[:l])
		}
		input = input[l:]
	}

	goto raw

single:
	{
		i := strings.IndexRune(input, singleChar)
		if i == -1 {
			return "", "", errUnterminatedSingleQuote
		}
		buf.WriteString(input[0:i])
		input = input[i+1:]
		goto raw
	}

double:
	{
		cur := input
		for len(cur) > 0 {
			c, l := utf8.DecodeRuneInString(cur)
			cur = cur[l:]
			switch c {
			case doubleChar:
				buf.WriteString(input[0 : len(input)-len(cur)-l])
				input = cur
				goto raw
			case escapeChar:
				// bash only supports certain escapes in double-quoted strings
				c2, l2 := utf8.DecodeRuneInString(cur)
				cur = cur[l2:]
				if strings.ContainsRune(doubleEscapeChars, c2) {
					buf.WriteString(input[0 : len(input)-len(cur)-l-l2])
					// A newline is special: skip the backslash entirely (write nothing).
					if c2 != '\n' {
						buf.WriteRune(c2)
					}
					input = cur
				}
			}
		}
		return "", "", errUnterminatedDoubleQuote
	}

done:
	return buf.String(), input, nil
}
