// Copyright (C) 2020 Michael J. Fromberger. All Rights Reserved.

package command

import (
	"flag"
	"fmt"
	"strings"
)

// Flags returns a function with the signature of the [C.SetFlags] callback,
// that calls bind(fs, v) for each v and the given flag set.
func Flags(bind func(*flag.FlagSet, any), vs ...any) func(*Env, *flag.FlagSet) {
	return func(_ *Env, fs *flag.FlagSet) {
		for _, v := range vs {
			bind(fs, v)
		}
	}
}

// usageLines parses and normalizes usage lines. The command name is stripped
// from the head of each line if it is present.
func (c *C) usageLines(flags HelpFlags) []string {
	var lines []string
	prefix := c.Name + " "
	for line := range strings.SplitSeq(c.Usage, "\n") {
		line = strings.TrimSpace(line)
		if line == "" {
			continue
		} else if line == c.Name {
			lines = append(lines, "")
		} else {
			lines = append(lines, strings.TrimPrefix(line, prefix))
		}
	}
	if len(lines) == 0 {
		var tag string
		if c.hasFlagsDefined(flags.wantPrivateFlags()) {
			tag = "[flags]"
		}
		if len(c.Commands) != 0 {
			tag = joinSpace(tag, "<command>")
		}
		if tag != "" {
			lines = append(lines, tag)
		}
		if hc := c.FindSubcommand("help"); hc != nil && hc.Runnable() {
			lines = append(lines, "help")
		}
	}
	return lines
}

func joinSpace(a, b string) string {
	if a == "" {
		return b
	} else if b == "" {
		return a
	}
	return a + " " + b
}

// indent returns text indented as specified; first is prepended to the first
// line, and prefix to all subsequent lines.
func indent(first, prefix, text string) string {
	return first + strings.ReplaceAll(text, "\n", "\n"+prefix)
}

// FailWithUsage is a run function that logs a usage message for the command
// and returns [ErrRequestHelp].
func FailWithUsage(env *Env) error {
	env.Command.HelpInfo(0).WriteUsage(env)
	return ErrRequestHelp
}

// splitFlags constructs two slices from args, the first containing all flags
// and their arguments matched by fs, the second containing all the other free
// arguments.
//
// The arguments for flags mentioned in fs are checked for presence, but are
// not parsed.  An error is reported if a flag lacks its argument.
// Flag-shaped strings NOT matched by fs are treated as free arguments.
// We do not at this point have enough information to know whether
// such a flag-candidate requires an argument, as that requires the FlagSet.
func splitFlags(fs *flag.FlagSet, args []string) (flags, free []string, _ error) {
	var wantArg bool
	for i, s := range args {
		// Case 1: The previous argument is a flag that needs a value.
		if wantArg {
			flags = append(flags, s)
			wantArg = false
			continue
		}

		// Some shortcuts to simplify processing below:
		if s == "-" {
			// Bare "-" is flag-shaped, but treated as a non-flag argument for parsing.
			free = append(free, s)
			continue
		} else if s == "--" {
			// Bare "--" is consumed by the flag parser as a signal to stop parsing.
			// Seeing it when we have not yet observed any other free arguments,
			// we give up looking for flags belonging to this set.
			if len(free) == 0 {
				flags = append(flags, s)
				free = append(free, args[i+1:]...)
				break
			}
			free = append(free, s)
			continue
		}

		// Case 2: Flag-shaped arguments (-x, --x).
		if rest, ok := strings.CutPrefix(s, "-"); ok {
			rest = strings.TrimPrefix(rest, "-") // accept -name or --name

			// Some flags may carry their own values (e.g., --name=value).
			// Otherwise, anything that isn't a Boolean flag requires an argument.
			name, _, ok := strings.Cut(rest, "=")
			if f := fs.Lookup(name); f != nil {
				// This is a flag belonging to this flag set.
				flags = append(flags, s)
				if !isBoolFlag(f) && !ok {
					wantArg = true
				}
			} else {
				// This may be a flag for a downstream flag set; for now
				// treat it as a free argument.
				free = append(free, s)
			}
			continue
		}

		// Case 3: Free arguments.
		free = append(free, s)
	}
	if wantArg {
		return nil, nil, fmt.Errorf("missing value for flag %q", flags[len(flags)-1])
	}
	return flags, free, nil
}

func isBoolFlag(f *flag.Flag) bool {
	v, ok := f.Value.(interface {
		IsBoolFlag() bool
	})
	return ok && v.IsBoolFlag()
}

func joinArgs(a, b []string) []string { return append(a, b...) }

// CInfo represents metadata about a command and its subcommands, in a format
// suitable for JSON encoding.
type CInfo struct {
	Name  string   `json:"name"`
	Usage []string `json:"usage,omitempty"`
	Help  string   `json:"help,omitzero"`

	Runnable bool       `json:"runnable"`
	Flags    []FlagInfo `json:"flags,omitempty"`
	Unlisted bool       `json:"unlisted,omitzero"`
	Commands []*CInfo   `json:"commands,omitempty"`
}

// FlagInfo represents metadata about a flag defined by a command, in a format
// suitable for JSON encoding.
type FlagInfo struct {
	Name          string `json:"name"`
	Usage         string `json:"usage"`
	DefaultString string `json:"defaultString,omitzero"`
	Private       bool   `json:"private,omitzero"`
}

// Info constructs a [CInfo] record for c and its subcommands.  The provided
// help flags determine the visibility of unlisted and private flags.
func (c *C) Info(flags HelpFlags) *CInfo {
	c.setFlags(c.NewEnv(nil), &c.Flags)
	out := &CInfo{
		Name:     c.Name,
		Usage:    c.usageLines(flags),
		Help:     c.Help,
		Runnable: c.Runnable(),
		Unlisted: c.Unlisted,
	}
	c.Flags.VisitAll(func(f *flag.Flag) {
		u, ok := strings.CutPrefix(f.Usage, flagPrivatePrefix)
		if ok && !flags.wantPrivateFlags() {
			return // skip
		}
		out.Flags = append(out.Flags, FlagInfo{
			Name:          f.Name,
			Usage:         u,
			DefaultString: f.DefValue,
			Private:       ok,
		})
	})
	if flags.wantCommands() {
		for _, sub := range c.Commands {
			if sub.Unlisted && !flags.wantUnlisted() {
				continue
			}
			out.Commands = append(out.Commands, sub.Info(flags))
		}
	}
	return out
}
