// Package getopt implements a command-line argument parser.
//
// It tries to cover all common styles of option syntaxes, and provides context
// information when given a partial input. It is mainly useful for writing
// completion engines and wrapper programs.
//
// If you are looking for an option parser for your go program, consider using
// the flag package in the standard library instead.
package getopt

//go:generate stringer -type=Config,Arity,ContextType -output=string.go

import (
	"fmt"
	"strings"

	"src.elv.sh/pkg/diag"
)

// Config configurates the parsing behavior.
type Config uint

const (
	// Stop parsing options after "--".
	StopAfterDoubleDash Config = 1 << iota
	// Stop parsing options before the first non-option argument.
	StopBeforeFirstNonOption
	// Allow long options to start with "-", and disallow short options.
	// Replicates the behavior of getopt_long_only and the flag package.
	LongOnly

	// Config to replicate the behavior of GNU's getopt_long.
	GNU = StopAfterDoubleDash
	// Config to replicate the behavior of BSD's getopt_long.
	BSD = StopAfterDoubleDash | StopBeforeFirstNonOption
)

// Tests whether a configuration has all specified flags set.
func (c Config) has(bits Config) bool { return c&bits == bits }

// OptionSpec is a command-line option.
type OptionSpec struct {
	// Short option. Set to 0 for long-only.
	Short rune
	// Long option. Set to "" for short-only.
	Long string
	// Whether the option takes an argument, and whether it is required.
	Arity Arity
}

// Arity indicates whether an option takes an argument, and whether it is
// required.
type Arity uint

const (
	// The option takes no argument.
	NoArgument Arity = iota
	// The option requires an argument. The argument can come either directly
	// after a short option (-oarg), after a long option followed by an equal
	// sign (--long=arg), or as a separate argument after the option (-o arg,
	// --long arg).
	RequiredArgument
	// The option takes an optional argument. The argument can come either
	// directly after a short option (-oarg) or after a long option followed by
	// an equal sign (--long=arg).
	OptionalArgument
)

// Option represents a parsed option.
type Option struct {
	Spec     *OptionSpec
	Unknown  bool
	Long     bool
	Argument string
}

// Context describes the context of the last argument.
type Context struct {
	// The nature of the context.
	Type ContextType
	// Current option, with a likely incomplete Argument. Non-nil when Type is
	// OptionArgument.
	Option *Option
	// Current partial long option name or argument. Non-empty when Type is
	// LongOption or Argument.
	Text string
}

// ContextType encodes how the last argument can be completed.
type ContextType uint

const (
	// OptionOrArgument indicates that the last element may be either a new
	// option or a new argument. Returned when it is an empty string.
	OptionOrArgument ContextType = iota
	// AnyOption indicates that the last element must be new option, short or
	// long. Returned when it is "-".
	AnyOption
	// LongOption indicates that the last element is a long option (but not its
	// argument). The partial name of the long option is stored in Context.Text.
	LongOption
	// ChainShortOption indicates that a new short option may be chained.
	// Returned when the last element consists of a chain of options that take
	// no arguments.
	ChainShortOption
	// OptionArgument indicates that the last element list must be an argument
	// to an option. The option in question is stored in Context.Option.
	OptionArgument
	// Argument indicates that the last element is a non-option argument. The
	// partial argument is stored in Context.Text.
	Argument
)

// Parse parses an argument list. It returns the parsed options, the non-option
// arguments, and any error.
func Parse(args []string, specs []*OptionSpec, cfg Config) ([]*Option, []string, error) {
	opts, nonOptArgs, opt, _ := parse(args, specs, cfg)
	var err error
	if opt != nil {
		err = fmt.Errorf("missing argument for %s", optionPart(opt))
	}
	for _, opt := range opts {
		if opt.Unknown {
			err = diag.Errors(err, fmt.Errorf("unknown option %s", optionPart(opt)))
		}
	}
	return opts, nonOptArgs, err
}

func optionPart(opt *Option) string {
	if opt.Long {
		return "--" + opt.Spec.Long
	}
	return "-" + string(opt.Spec.Short)
}

// Complete parses an argument list for completion. It returns the parsed
// options, the non-option arguments, and the context of the last argument. It
// tolerates unknown options, assuming that they take optional arguments.
func Complete(args []string, specs []*OptionSpec, cfg Config) ([]*Option, []string, Context) {
	opts, nonOptArgs, opt, stopOpt := parse(args[:len(args)-1], specs, cfg)

	arg := args[len(args)-1]
	var ctx Context
	switch {
	case opt != nil:
		opt.Argument = arg
		ctx = Context{Type: OptionArgument, Option: opt}
	case stopOpt:
		ctx = Context{Type: Argument, Text: arg}
	case arg == "":
		ctx = Context{Type: OptionOrArgument}
	case arg == "-":
		ctx = Context{Type: AnyOption}
	case strings.HasPrefix(arg, "--"):
		if !strings.ContainsRune(arg, '=') {
			ctx = Context{Type: LongOption, Text: arg[2:]}
		} else {
			newopt, _ := parseLong(arg[2:], specs)
			ctx = Context{Type: OptionArgument, Option: newopt}
		}
	case strings.HasPrefix(arg, "-"):
		if cfg.has(LongOnly) {
			if !strings.ContainsRune(arg, '=') {
				ctx = Context{Type: LongOption, Text: arg[1:]}
			} else {
				newopt, _ := parseLong(arg[1:], specs)
				ctx = Context{Type: OptionArgument, Option: newopt}
			}
		} else {
			newopts, _ := parseShort(arg[1:], specs)
			if newopts[len(newopts)-1].Spec.Arity == NoArgument {
				opts = append(opts, newopts...)
				ctx = Context{Type: ChainShortOption}
			} else {
				opts = append(opts, newopts[:len(newopts)-1]...)
				ctx = Context{Type: OptionArgument, Option: newopts[len(newopts)-1]}
			}
		}
	default:
		ctx = Context{Type: Argument, Text: arg}
	}
	return opts, nonOptArgs, ctx
}

func parse(args []string, spec []*OptionSpec, cfg Config) ([]*Option, []string, *Option, bool) {
	var (
		opts       []*Option
		nonOptArgs []string
		// Non-nil only when the last argument was an option with required
		// argument, but the argument has not been seen.
		opt *Option
		// Whether option parsing has been stopped. The condition is controlled
		// by the StopAfterDoubleDash and StopBeforeFirstNonOption bits in cfg.
		stopOpt bool
	)
	for _, arg := range args {
		switch {
		case opt != nil:
			opt.Argument = arg
			opts = append(opts, opt)
			opt = nil
		case stopOpt:
			nonOptArgs = append(nonOptArgs, arg)
		case cfg.has(StopAfterDoubleDash) && arg == "--":
			stopOpt = true
		case strings.HasPrefix(arg, "--") && arg != "--":
			newopt, needArg := parseLong(arg[2:], spec)
			if needArg {
				opt = newopt
			} else {
				opts = append(opts, newopt)
			}
		case strings.HasPrefix(arg, "-") && arg != "--" && arg != "-":
			if cfg.has(LongOnly) {
				newopt, needArg := parseLong(arg[1:], spec)
				if needArg {
					opt = newopt
				} else {
					opts = append(opts, newopt)
				}
			} else {
				newopts, needArg := parseShort(arg[1:], spec)
				if needArg {
					opts = append(opts, newopts[:len(newopts)-1]...)
					opt = newopts[len(newopts)-1]
				} else {
					opts = append(opts, newopts...)
				}
			}
		default:
			nonOptArgs = append(nonOptArgs, arg)
			if cfg.has(StopBeforeFirstNonOption) {
				stopOpt = true
			}
		}
	}
	return opts, nonOptArgs, opt, stopOpt
}

// Parses short options, without the leading dash. Returns the parsed options
// and whether an argument is still to be seen.
func parseShort(s string, specs []*OptionSpec) ([]*Option, bool) {
	var opts []*Option
	var needArg bool
	for i, r := range s {
		opt := findShort(r, specs)
		if opt != nil {
			if opt.Arity == NoArgument {
				opts = append(opts, &Option{Spec: opt})
				continue
			} else {
				parsed := &Option{Spec: opt, Argument: s[i+len(string(r)):]}
				opts = append(opts, parsed)
				needArg = parsed.Argument == "" && opt.Arity == RequiredArgument
				break
			}
		}
		// Unknown option, treat as taking an optional argument
		parsed := &Option{
			Spec: &OptionSpec{r, "", OptionalArgument}, Unknown: true,
			Argument: s[i+len(string(r)):]}
		opts = append(opts, parsed)
		break
	}
	return opts, needArg
}

func findShort(r rune, specs []*OptionSpec) *OptionSpec {
	for _, opt := range specs {
		if r == opt.Short {
			return opt
		}
	}
	return nil
}

// Parses a long option, without the leading dashes. Returns the parsed option
// and whether an argument is still to be seen.
func parseLong(s string, specs []*OptionSpec) (*Option, bool) {
	eq := strings.IndexRune(s, '=')
	for _, opt := range specs {
		if s == opt.Long {
			return &Option{Spec: opt, Long: true}, opt.Arity == RequiredArgument
		} else if eq != -1 && s[:eq] == opt.Long {
			return &Option{Spec: opt, Long: true, Argument: s[eq+1:]}, false
		}
	}
	// Unknown option, treat as taking an optional argument
	if eq == -1 {
		return &Option{
			Spec: &OptionSpec{0, s, OptionalArgument}, Unknown: true, Long: true}, false
	}
	return &Option{
		Spec: &OptionSpec{0, s[:eq], OptionalArgument}, Unknown: true,
		Long: true, Argument: s[eq+1:]}, false
}
