// Copyright 2017 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Package cmdflag handles flag processing common to several go tools.
package cmdflag

import (
	"flag"
	"fmt"
	"os"
	"strconv"
	"strings"

	"cmd/go/internal/base"
)

// The flag handling part of go commands such as test is large and distracting.
// We can't use the standard flag package because some of the flags from
// our command line are for us, and some are for the binary we're running,
// and some are for both.

// Defn defines a flag we know about.
type Defn struct {
	Name       string     // Name on command line.
	BoolVar    *bool      // If it's a boolean flag, this points to it.
	Value      flag.Value // The flag.Value represented.
	PassToTest bool       // Pass to the test binary? Used only by go test.
	Present    bool       // Flag has been seen.
}

// IsBool reports whether v is a bool flag.
func IsBool(v flag.Value) bool {
	vv, ok := v.(interface {
		IsBoolFlag() bool
	})
	if ok {
		return vv.IsBoolFlag()
	}
	return false
}

// SetBool sets the addressed boolean to the value.
func SetBool(cmd string, flag *bool, value string) {
	x, err := strconv.ParseBool(value)
	if err != nil {
		SyntaxError(cmd, "illegal bool flag value "+value)
	}
	*flag = x
}

// SetInt sets the addressed integer to the value.
func SetInt(cmd string, flag *int, value string) {
	x, err := strconv.Atoi(value)
	if err != nil {
		SyntaxError(cmd, "illegal int flag value "+value)
	}
	*flag = x
}

// SyntaxError reports an argument syntax error and exits the program.
func SyntaxError(cmd, msg string) {
	fmt.Fprintf(os.Stderr, "go %s: %s\n", cmd, msg)
	if cmd == "test" {
		fmt.Fprintf(os.Stderr, `run "go help %s" or "go help testflag" for more information`+"\n", cmd)
	} else {
		fmt.Fprintf(os.Stderr, `run "go help %s" for more information`+"\n", cmd)
	}
	os.Exit(2)
}

// AddKnownFlags registers the flags in defns with base.AddKnownFlag.
func AddKnownFlags(cmd string, defns []*Defn) {
	for _, f := range defns {
		base.AddKnownFlag(f.Name)
		base.AddKnownFlag(cmd + "." + f.Name)
	}
}

// Parse sees if argument i is present in the definitions and if so,
// returns its definition, value, and whether it consumed an extra word.
// If the flag begins (cmd+".") it is ignored for the purpose of this function.
func Parse(cmd string, defns []*Defn, args []string, i int) (f *Defn, value string, extra bool) {
	arg := args[i]
	if strings.HasPrefix(arg, "--") { // reduce two minuses to one
		arg = arg[1:]
	}
	switch arg {
	case "-?", "-h", "-help":
		base.Usage()
	}
	if arg == "" || arg[0] != '-' {
		return
	}
	name := arg[1:]
	// If there's already a prefix such as "test.", drop it for now.
	name = strings.TrimPrefix(name, cmd+".")
	equals := strings.Index(name, "=")
	if equals >= 0 {
		value = name[equals+1:]
		name = name[:equals]
	}
	for _, f = range defns {
		if name == f.Name {
			// Booleans are special because they have modes -x, -x=true, -x=false.
			if f.BoolVar != nil || IsBool(f.Value) {
				if equals < 0 { // Otherwise, it's been set and will be verified in SetBool.
					value = "true"
				} else {
					// verify it parses
					SetBool(cmd, new(bool), value)
				}
			} else { // Non-booleans must have a value.
				extra = equals < 0
				if extra {
					if i+1 >= len(args) {
						SyntaxError(cmd, "missing argument for flag "+f.Name)
					}
					value = args[i+1]
				}
			}
			if f.Present {
				SyntaxError(cmd, f.Name+" flag may be set only once")
			}
			f.Present = true
			return
		}
	}
	f = nil
	return
}

// FindGOFLAGS extracts and returns the flags matching defns from GOFLAGS.
// Ideally the caller would mention that the flags were from GOFLAGS
// when reporting errors, but that's too hard for now.
func FindGOFLAGS(defns []*Defn) []string {
	var flags []string
	for _, flag := range base.GOFLAGS() {
		// Flags returned by base.GOFLAGS are well-formed, one of:
		//	-x
		//	--x
		//	-x=value
		//	--x=value
		if strings.HasPrefix(flag, "--") {
			flag = flag[1:]
		}
		name := flag[1:]
		if i := strings.Index(name, "="); i >= 0 {
			name = name[:i]
		}
		for _, f := range defns {
			if name == f.Name {
				flags = append(flags, flag)
				break
			}
		}
	}
	return flags
}
