// This code has been inspired from https://github.com/google/shlex
//
// Copyright 2012 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// The following changes are published under the MIT license:
//
//  * Internal variables renamed
//  * Recording of the original argument start position in the string
//  * Reformatting of some code parts.
//
// SPDX-License-Identifier: MIT
// Copyright (c) 2023 Robin Jarry

package opt

// runeClass is the type of a UTF-8 character classification: A quote, space,
// escape.
type runeClass int

const (
	other runeClass = iota
	space
	doubleQuote
	singleQuote
	backslash
)

var runeClasses = map[rune]runeClass{
	' ':  space,
	'\t': space,
	'\r': space,
	'\n': space,
	'"':  doubleQuote,
	'\'': singleQuote,
	'\\': backslash,
}

// the internal state used by the lexer state machine
type lexerState int

// Lexer state machine states
const (
	// no runes have been seen
	start lexerState = iota
	// processing regular runes in a word
	inWord
	// we have just consumed an escape rune; the next rune is literal
	escaping
	// we have just consumed an escape rune within a quoted string
	escapingQuoted
	// we are within a quoted string that supports escaping ("...")
	inDoubleQuote
	// we are within a string that does not support escaping ('...')
	inSingleQuote
)

// Each argument info contains the start offset of the raw argument in the
// command line (including shell escapes, quotes, etc.), and its "unquoted"
// value after interpreting shell quotes and escapes.
type argInfo struct {
	start    int
	end      int
	unquoted string
}

// Parse a raw command line and return a list of argument info structs
func lexCmdline(raw []rune) []argInfo {
	var state lexerState
	var unquoted []rune
	var argstart int
	var infos []argInfo

	state = start

	for i, char := range raw {
		class := runeClasses[char]

		switch state {
		case start:
			// no runes read yet
			switch class {
			case space:
				break
			case doubleQuote:
				state = inDoubleQuote
			case singleQuote:
				state = inSingleQuote
			case backslash:
				state = escaping
			default:
				// start a new word
				unquoted = []rune{char}
				state = inWord
			}
			argstart = i
		case inWord:
			switch class {
			case space:
				infos = append(infos, argInfo{
					start:    argstart,
					end:      i,
					unquoted: string(unquoted),
				})
				unquoted = nil
				state = start
			case doubleQuote:
				state = inDoubleQuote
			case singleQuote:
				state = inSingleQuote
			case backslash:
				state = escaping
			default:
				unquoted = append(unquoted, char)
			}
		case escaping:
			// the rune after an escape character
			state = inWord
			unquoted = append(unquoted, char)
		case escapingQuoted:
			// the next rune after an escape character, in double quotes
			state = inDoubleQuote
			unquoted = append(unquoted, char)
		case inDoubleQuote:
			switch class {
			case doubleQuote:
				state = inWord
			case backslash:
				state = escapingQuoted
			default:
				unquoted = append(unquoted, char)
			}
		case inSingleQuote:
			switch class {
			case singleQuote:
				state = inWord
			default:
				unquoted = append(unquoted, char)
			}
		}
	}

	if unquoted != nil {
		infos = append(infos, argInfo{
			start:    argstart,
			end:      len(raw),
			unquoted: string(unquoted),
		})
	}

	return infos
}
