// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package flag

import (
	"bytes"
	"flag"
	"fmt"
	"io"
	"io/ioutil"
	"strings"

	"github.com/posener/complete"
)

// Sets is a group of flag sets.
type Sets struct {
	// unionSet is the set that is the union of all other sets. This
	// has ALL flags defined on it and is the set that is parsed. But
	// we maintain the other list of sets so that we can generate proper help.
	unionSet *flag.FlagSet

	// flagSets is the list of sets that we have. We don't parse these
	// directly but use them for help generation and autocompletion.
	flagSets []*Set

	// completions is our set of autocompletion handlers. This is also
	// the union of all available flags similar to unionSet.
	completions complete.Flags
}

// NewSets creates a new flag sets.
func NewSets() *Sets {
	unionSet := flag.NewFlagSet("", flag.ContinueOnError)

	// Errors and usage are expected to be controlled externally by
	// checking on the result of Parse.
	unionSet.Usage = func() {}
	unionSet.SetOutput(ioutil.Discard)

	return &Sets{
		unionSet:    unionSet,
		completions: complete.Flags{},
	}
}

// NewSet creates a new single flag set. A set should be created for
// any grouping of flags, for example "Common Options", "Auth Options", etc.
func (f *Sets) NewSet(name string) *Set {
	flagSet := NewSet(name)

	// The union and completions are pointers to our own values
	flagSet.unionSet = f.unionSet
	flagSet.completions = f.completions

	// Keep track of it for help generation
	f.flagSets = append(f.flagSets, flagSet)
	return flagSet
}

// AddSet adds a single flag set
func (f *Sets) AddSet(set *Set) {
	newSet := f.NewSet(set.Name())
	set.VisitVars(func(fl *VarFlag) {
		newSet.VarFlag(fl)
	})
}

// Completions returns the completions for this flag set.
func (f *Sets) Completions() complete.Flags {
	return f.completions
}

// Parse parses the given flags, returning any errors.
func (f *Sets) Parse(args []string) error {
	return f.unionSet.Parse(args)
}

// Parsed reports whether the command-line flags have been parsed.
func (f *Sets) Parsed() bool {
	return f.unionSet.Parsed()
}

// Args returns the remaining args after parsing.
func (f *Sets) Args() []string {
	return f.unionSet.Args()
}

// Visit visits the flags in lexicographical order, calling fn for each. It
// visits only those flags that have been set.
func (f *Sets) Visit(fn func(*flag.Flag)) {
	f.unionSet.Visit(fn)
}

// Help builds custom help for this command, grouping by flag set.
func (fs *Sets) Help() string {
	var out bytes.Buffer

	for _, set := range fs.flagSets {
		printFlagTitle(&out, set.name+":")
		set.VisitAll(func(f *flag.Flag) {
			// Skip any hidden flags
			if v, ok := f.Value.(FlagVisibility); ok && v.Hidden() {
				return
			}
			printFlagDetail(&out, f)
		})
	}

	return strings.TrimRight(out.String(), "\n")
}

// Help builds custom help for this command, grouping by flag set.
func (fs *Sets) VisitSets(fn func(name string, set *Set)) {
	for _, set := range fs.flagSets {
		fn(set.name, set)
	}
}

// Set is a grouped wrapper around a real flag set and a grouped flag set.
type Set struct {
	name        string
	flagSet     *flag.FlagSet
	unionSet    *flag.FlagSet
	completions complete.Flags

	vars []*VarFlag
}

// NewSet creates a new flag set.
func NewSet(name string) *Set {
	return &Set{
		name:    name,
		flagSet: flag.NewFlagSet(name, flag.ContinueOnError),
	}
}

// Name returns the name of this flag set.
func (f *Set) Name() string {
	return f.name
}

func (f *Set) Visit(fn func(*flag.Flag)) {
	f.flagSet.Visit(fn)
}

func (f *Set) VisitAll(fn func(*flag.Flag)) {
	f.flagSet.VisitAll(fn)
}

func (f *Set) VisitVars(fn func(*VarFlag)) {
	for _, v := range f.vars {
		fn(v)
	}
}

// printFlagTitle prints a consistently-formatted title to the given writer.
func printFlagTitle(w io.Writer, s string) {
	fmt.Fprintf(w, "%s\n\n", s)
}

// printFlagDetail prints a single flag to the given writer.
func printFlagDetail(w io.Writer, f *flag.Flag) {
	// Check if the flag is hidden - do not print any flag detail or help output
	// if it is hidden.
	if h, ok := f.Value.(FlagVisibility); ok && h.Hidden() {
		return
	}

	// Check for a detailed example
	example := ""
	if t, ok := f.Value.(FlagExample); ok {
		example = t.Example()
	}

	if example != "" {
		fmt.Fprintf(w, "  -%s=<%s>\n", f.Name, example)
	} else {
		fmt.Fprintf(w, "  -%s\n", f.Name)
	}

	usage := reRemoveWhitespace.ReplaceAllString(f.Usage, " ")
	indented := wrapAtLengthWithPadding(usage, 6)
	fmt.Fprintf(w, "%s\n\n", indented)
}
