package completers

import (
	"errors"
	"fmt"
	"strings"

	"github.com/jessevdk/go-flags"

	"github.com/maxlandon/readline"
)

// CommandCompleter - A completer using a github.com/jessevdk/go-flags Command Parser, in order
// to build completions for commands, arguments, options and their arguments as well.
// This completer needs to be instantiated with its constructor, in order to ensure the parser is not nil.
type CommandCompleter struct {
	parser *flags.Parser
}

// NewCommandCompleter - Instantiate a new tab completer using a github.com/jessevdk/go-flags Command Parser.
func NewCommandCompleter(parser *flags.Parser) (completer *CommandCompleter, err error) {
	if parser == nil {
		return nil, errors.New("command completer was instantiated with a nil parser")
	}
	return &CommandCompleter{parser: parser}, nil
}

// TabCompleter - A default tab completer working with a github.com/jessevdk/go-flags parser.
func (c *CommandCompleter) TabCompleter(line []rune, pos int, dtc readline.DelayedTabContext) (lastWord string, completions []*readline.CompletionGroup) {

	// Format and sanitize input
	// @args     => All items of the input line
	// @last     => The last word detected in input line as []rune
	// @lastWord => The last word detected in input as string
	args, last, lastWord := formatInput(line)

	// Detect base command automatically
	var command = c.detectedCommand(args)

	// Propose commands
	if noCommandOrEmpty(args, last, command) {
		return c.completeMenuCommands(lastWord, pos)
	}

	// Check environment variables
	if envVarAsked(args, lastWord) {
		completeEnvironmentVariables(lastWord)
	}

	// Base command has been identified
	if commandFound(command) {
		// Check environment variables again
		if envVarAsked(args, lastWord) {
			return completeEnvironmentVariables(lastWord)
		}

		// If options are asked for root command, return commpletions.
		if len(command.Groups()) > 0 {
			for _, grp := range command.Groups() {
				if opt, yes := optionArgRequired(args, last, grp); yes {
					return completeOptionArguments(command, opt, lastWord)
				}
			}
		}

		// Then propose subcommands. We don't return from here, otherwise it always skips the next steps.
		if hasSubCommands(command, args) {
			completions = completeSubCommands(args, lastWord, command)
		}

		// Handle subcommand if found (maybe we should rewrite this function and use it also for base command)
		if sub, ok := subCommandFound(lastWord, args, command); ok {
			return handleSubCommand(line, pos, sub)
		}

		// If user asks for completions with "-" / "--", show command options.
		// We ask this here, after having ensured there is no subcommand invoked.
		// This prevails over command arguments, even if they are required.
		if commandOptionsAsked(args, lastWord, command) {
			return completeCommandOptions(args, lastWord, command)
		}

		// Propose argument completion before anything, and if needed
		if arg, yes := commandArgumentRequired(lastWord, args, command); yes {
			return completeCommandArguments(command, arg, lastWord)
		}

	}

	return
}

// [ Main Completion Functions ] -----------------------------------------------------------------------------------------------------------------

// completeMenuCommands - Selects all commands available in a given context and returns them as suggestions
// Many categories, all from command parsers.
func (c *CommandCompleter) completeMenuCommands(lastWord string, pos int) (prefix string, completions []*readline.CompletionGroup) {

	prefix = lastWord // We only return the PREFIX for readline to correctly show suggestions.

	// Check their namespace (which should be their "group" (like utils, core, Jobs, etc))
	for _, cmd := range c.parser.Commands() {
		// If command matches readline input
		if strings.HasPrefix(cmd.Name, lastWord) {
			// Check command group: add to existing group if found
			var found bool
			for _, grp := range completions {
				if grp.Name == cmd.Aliases[0] {
					found = true
					grp.Suggestions = append(grp.Suggestions, cmd.Name)
					grp.Descriptions[cmd.Name] = readline.Dim(cmd.ShortDescription)
				}
			}
			// Add a new group if not found
			if !found {
				grp := &readline.CompletionGroup{
					Name:        cmd.Aliases[0],
					Suggestions: []string{cmd.Name},
					Descriptions: map[string]string{
						cmd.Name: readline.Dim(cmd.ShortDescription),
					},
				}
				completions = append(completions, grp)
			}
		}
	}

	// Make adjustments to the CompletionGroup list: set maxlength depending on items, check descriptions, etc.
	for _, grp := range completions {
		// If the length of suggestions is too long and we have
		// many groups, use grid display.
		if len(completions) >= 10 && len(grp.Suggestions) >= 7 {
			grp.DisplayType = readline.TabDisplayGrid
		} else {
			// By default, we use a map of command to descriptions
			grp.DisplayType = readline.TabDisplayList
		}
	}

	return
}

// completeSubCommands - Takes subcommands and gives them as suggestions
// One category, from one source (a parent command).
func completeSubCommands(args []string, lastWord string, command *flags.Command) (completions []*readline.CompletionGroup) {

	group := &readline.CompletionGroup{
		Name:         command.Name,
		Suggestions:  []string{},
		Descriptions: map[string]string{},
		DisplayType:  readline.TabDisplayList,
	}

	for _, sub := range command.Commands() {
		if strings.HasPrefix(sub.Name, lastWord) {
			group.Suggestions = append(group.Suggestions, sub.Name)
			group.Descriptions[sub.Name] = readline.DIM + sub.ShortDescription + readline.RESET
		}
	}

	completions = append(completions, group)

	return
}

// handleSubCommand - Handles completion for subcommand options and arguments, + any option value related completion
// Many categories, from many sources: this function calls the same functions as the ones previously called for completing its parent command.
func handleSubCommand(line []rune, pos int, command *flags.Command) (lastWord string, completions []*readline.CompletionGroup) {

	args, last, lastWord := formatInput(line)

	// Check environment variables
	if envVarAsked(args, lastWord) {
		completeEnvironmentVariables(lastWord)
	}

	// Check argument options
	if len(command.Groups()) > 0 {
		for _, grp := range command.Groups() {
			if opt, yes := optionArgRequired(args, last, grp); yes {
				return completeOptionArguments(command, opt, lastWord)
			}
		}
	}

	// If user asks for completions with "-" or "--". This must take precedence on arguments.
	if subCommandOptionsAsked(args, lastWord, command) {
		return completeCommandOptions(args, lastWord, command)
	}

	// If command has non-filled arguments, propose them first
	if arg, yes := commandArgumentRequired(lastWord, args, command); yes {
		return completeCommandArguments(command, arg, lastWord)
	}

	return
}

// completeCommandOptions - Yields completion for options of a command, with various decorators
// Many categories, from one source (a command)
func completeCommandOptions(args []string, lastWord string, cmd *flags.Command) (prefix string, completions []*readline.CompletionGroup) {

	prefix = lastWord // We only return the PREFIX for readline to correctly show suggestions.

	// Get all (root) option groups.
	groups := cmd.Groups()

	// Append command options not gathered in groups
	groups = append(groups, cmd.Group)

	// For each group, build completions
	for _, grp := range groups {

		_, comp := completeOptionGroup(lastWord, grp, "")

		// No need to add empty groups, will screw the completion system.
		if len(comp.Suggestions) > 0 {
			completions = append(completions, comp)
		}
	}

	// Do the same for global options, which are not part of any group "per-se"
	_, gcomp := completeOptionGroup(lastWord, cmd.Group, "global options")
	if len(gcomp.Suggestions) > 0 {
		completions = append(completions, gcomp)
	}

	return
}

// completeOptionGroup - make completions for a single group of options. Title is optional, not used if empty.
func completeOptionGroup(lastWord string, grp *flags.Group, title string) (prefix string, compGrp *readline.CompletionGroup) {

	compGrp = &readline.CompletionGroup{
		Name:         grp.ShortDescription,
		Descriptions: map[string]string{},
		DisplayType:  readline.TabDisplayList,
		Aliases:      map[string]string{},
	}

	// An optional title for this comp group.
	// Used by global flag options, added to all commands.
	if title != "" {
		compGrp.Name = title
	}

	// Add each option to completion group
	for _, opt := range grp.Options() {

		// Check if option is already set, next option if yes
		// if optionNotRepeatable(opt) && optionIsAlreadySet(args, lastWord, opt) {
		//         continue
		// }

		// Depending on the current last word, either build a group with option longs only, or with shorts
		if strings.HasPrefix("--"+opt.LongName, lastWord) {
			optName := "--" + opt.LongName
			compGrp.Suggestions = append(compGrp.Suggestions, optName)

			// Add short if there is, and that the prefix is only one dash
			if strings.HasPrefix("-", lastWord) {
				if opt.ShortName != 0 {
					compGrp.Aliases[optName] = "-" + string(opt.ShortName)
				}
			}

			// Option default value if any
			var def string
			if len(opt.Default) > 0 {
				def = " (default:"
				for _, d := range opt.Default {
					def += " " + d + ","
				}
				def = strings.TrimSuffix(def, ",")
				def += ")"
			}

			desc := fmt.Sprintf(" -- %s%s%s", opt.Description, def, readline.RESET)
			compGrp.Descriptions[optName] = desc
		}
	}
	return
}

// RecursiveGroupCompletion - Handles recursive completion for nested option groups
// Many categories, one source (a command's root option group). Called by the function just above.
func RecursiveGroupCompletion(args []string, last []rune, group *flags.Group) (lastWord string, completions []*readline.CompletionGroup) {
	return
}
