package completers

import (
	"os/exec"
	"reflect"
	"strings"
	"unicode"

	"github.com/jessevdk/go-flags"
)

// These functions are just shorthands for checking various conditions on the input line.
// They make the main function more readable, which might be useful, should a logic error pop somewhere.

// [ Parser Commands & Options ] --------------------------------------------------------------------------
// ArgumentByName Get the name of a detected command's argument
func argumentByName(command *flags.Command, name string) *flags.Arg {
	args := command.Args()
	for _, arg := range args {
		if arg.Name == name {
			return arg
		}
	}
	return nil
}

// optionByName - Returns an option for a command or a subcommand, identified by name
func optionByName(cmd *flags.Command, option string) *flags.Option {

	if cmd == nil {
		return nil
	}
	// Get all (root) option groups.
	groups := cmd.Groups()

	// For each group, build completions
	for _, grp := range groups {
		// Add each option to completion group
		for _, opt := range grp.Options() {
			if opt.LongName == option {
				return opt
			}
		}
	}
	return nil
}

// [ Menus ] --------------------------------------------------------------------------------------------
// Is the input line is either empty, or without any detected command ?
func noCommandOrEmpty(args []string, last []rune, command *flags.Command) bool {
	if len(args) == 0 || len(args) == 1 && command == nil {
		return true
	}
	return false
}

// [ Commands ] -------------------------------------------------------------------------------------
// detectedCommand - Returns the base command from parser if detected, depending on context
func (c *CommandCompleter) detectedCommand(args []string) (command *flags.Command) {
	arg := strings.TrimSpace(args[0])
	command = c.parser.Find(arg)
	return
}

// is the command a special command, usually not handled by parser ?
func isSpecialCommand(args []string, command *flags.Command) bool {

	// If command is not nil, return
	if command == nil {
		// Shell
		if args[0] == "!" {
			return true
		}
		// Exit
		if args[0] == "exit" {
			return true
		}
		return false
	}
	return false
}

// The commmand has been found
func commandFound(command *flags.Command) bool {
	if command != nil {
		return true
	}
	return false
}

// Search for input in $PATH
func commandFoundInPath(input string) bool {
	_, err := exec.LookPath(input)
	if err != nil {
		return false
	}
	return true
}

// [ SubCommands ]-------------------------------------------------------------------------------------
// Does the command have subcommands ?
func hasSubCommands(command *flags.Command, args []string) bool {
	if len(args) < 2 || command == nil {
		return false
	}

	if len(command.Commands()) != 0 {
		return true
	}

	return false
}

// Does the input has a subcommand in it ?
func subCommandFound(lastWord string, raw []string, command *flags.Command) (sub *flags.Command, ok bool) {
	// First, filter redundant spaces. This does not modify the actual line
	args := ignoreRedundantSpaces(raw)

	if len(args) <= 1 || command == nil {
		return nil, false
	}

	sub = command.Find(args[1])
	if sub != nil {
		return sub, true
	}

	return nil, false
}

// Is the last input PRECISELY a subcommand. This is used as a brief hint for the subcommand
func lastIsSubCommand(lastWord string, command *flags.Command) bool {
	if sub := command.Find(lastWord); sub != nil {
		return true
	}
	return false
}

// [ Arguments ]-------------------------------------------------------------------------------------
// Does the command have arguments ?
func hasArgs(command *flags.Command) bool {
	if len(command.Args()) != 0 {
		return true
	}
	return false
}

// commandArgumentRequired - Analyses input and sends back the next argument name to provide completion for
func commandArgumentRequired(lastWord string, raw []string, command *flags.Command) (name string, yes bool) {

	// First, filter redundant spaces. This does not modify the actual line
	args := ignoreRedundantSpaces(raw)

	// Trim command and subcommand args
	var remain []string
	if args[0] == command.Name {
		remain = args[1:]
	}
	if len(args) > 1 && args[1] == command.Name {
		remain = args[2:]
	}

	// The remain may include a "" as a last element,
	// which we don't consider as a real remain, so we move it away
	switch lastWord {
	case "":
	case command.Name:
		return "", false
	}

	// Trim all --option flags and their arguments if they have
	remain = filterOptions(remain, command)

	// For each argument, check if needs completion. If not continue, if yes return.
	// The arguments remainder is popped according to the number of values expected.
	for i, arg := range command.Args() {

		// If it's required and has one argument, check filled.
		if arg.Required == 1 && arg.RequiredMaximum == 1 {

			// If last word is the argument, and we are
			// last arg in: line keep completing.
			if len(remain) < 1 {
				return arg.Name, true
			}

			// If the we are still writing the argument
			if len(remain) == 1 {
				if lastWord != "" {
					return arg.Name, true
				}
			}

			// If filed and we are not last arg, continue
			if len(remain) > 1 && i < (len(command.Args())-1) {
				remain = remain[1:]
				continue
			}

			continue
		}

		// If we need more than one value and we knwo the maximum,
		// either return or pop the remain.
		if arg.Required > 0 && arg.RequiredMaximum > 1 {
			// Pop the corresponding amount of arguments.
			var found int
			for i := 0; i < len(remain) && i < arg.RequiredMaximum; i++ {
				remain = remain[1:]
				found++
			}

			// If we still need values:
			if len(remain) == 0 && found <= arg.RequiredMaximum {
				if lastWord == "" { // We are done, no more completions.
					break
				} else {
					return arg.Name, true
				}
			}
			// Else go on with the next argument
			continue
		}

		// If has required arguments, with no limit of needs, return true
		if arg.Required > 0 && arg.RequiredMaximum == -1 {
			return arg.Name, true
		}

		// Else, if no requirements and the command has subcommands,
		// return so that we complete subcommands
		if arg.Required == -1 && len(command.Commands()) > 0 {
			continue
		}

		// Else, return this argument
		// NOTE: This block is after because we always use []type arguments
		// AFTER individual argument fields. Thus blocks any args that have
		// not been processed.
		if arg.Required == -1 {
			return arg.Name, true
		}
	}

	// Once we exited the loop, it means that none of the arguments require completion:
	// They are all either optional, or fullfiled according to their required numbers.
	// Thus we return none
	return "", false
}

// getRemainingArgs - Filters the input slice from commands and detected option:value pairs, and returns args
func getRemainingArgs(args []string, last []rune, command *flags.Command) (remain []string) {

	var input []string
	// Clean subcommand name
	if args[0] == command.Name && len(args) >= 2 {
		input = args[1:]
	} else if len(args) == 1 {
		input = args
	}

	// For each each argument
	for i := 0; i < len(input); i++ {
		// Check option prefix
		if strings.HasPrefix(input[i], "-") || strings.HasPrefix(input[i], "--") {
			// Clean it
			cur := strings.TrimPrefix(input[i], "--")
			cur = strings.TrimPrefix(cur, "-")

			// Check if option matches any command option
			if opt := command.FindOptionByLongName(cur); opt != nil {
				boolean := true
				if opt.Field().Type == reflect.TypeOf(boolean) {
					continue // If option is boolean, don't skip an argument
				}
				i++ // Else skip next arg in input
				continue
			}
		}

		// Safety check
		if input[i] == "" || input[i] == " " {
			continue
		}

		remain = append(remain, input[i])
	}

	return
}

// [ Options ]-------------------------------------------------------------------------------------
// commandOptionsAsked - Does the user asks for options in a root command ?
func commandOptionsAsked(args []string, lastWord string, command *flags.Command) bool {
	if len(args) >= 2 && (strings.HasPrefix(lastWord, "-") || strings.HasPrefix(lastWord, "--")) {
		return true
	}
	return false
}

// commandOptionsAsked - Does the user asks for options in a subcommand ?
func subCommandOptionsAsked(args []string, lastWord string, command *flags.Command) bool {
	if len(args) > 2 && (strings.HasPrefix(lastWord, "-") || strings.HasPrefix(lastWord, "--")) {
		return true
	}
	return false
}

// Is the last input argument is a dash ?
func isOptionDash(args []string, last []rune) bool {
	if len(args) > 2 && (strings.HasPrefix(string(last), "-") || strings.HasPrefix(string(last), "--")) {
		return true
	}
	return false
}

// optionIsAlreadySet - Detects in input if an option is already set
func optionIsAlreadySet(args []string, lastWord string, opt *flags.Option) bool {
	return false
}

// Check if option type allows for repetition
func optionNotRepeatable(opt *flags.Option) bool {
	return true
}

// [ Option Values ]-------------------------------------------------------------------------------------
// Is the last input word an option name (--option) ?
func optionArgRequired(args []string, last []rune, group *flags.Group) (opt *flags.Option, yes bool) {

	var lastItem string
	var lastOption string
	var option *flags.Option

	// If there is argument required we must have 1) command 2) --option inputs at least.
	if len(args) <= 2 {
		return nil, false
	}

	// Check for last two arguments in input
	if strings.HasPrefix(args[len(args)-2], "-") || strings.HasPrefix(args[len(args)-2], "--") {

		// Long opts
		if strings.HasPrefix(args[len(args)-2], "--") {
			lastOption = strings.TrimPrefix(args[len(args)-2], "--")
			if opt := group.FindOptionByLongName(lastOption); opt != nil {
				option = opt
			}

			// Short opts
		} else if strings.HasPrefix(args[len(args)-2], "-") {
			lastOption = strings.TrimPrefix(args[len(args)-2], "-")
			if len(lastOption) > 0 {
				if opt := group.FindOptionByShortName(rune(lastOption[0])); opt != nil {
					option = opt
				}
			}
		}

	}

	// If option is found, and we still are in writing the argument
	if (lastItem == "" && option != nil) || option != nil {
		// Check if option is a boolean, if yes return false
		boolean := true
		if option.Field().Type == reflect.TypeOf(boolean) {
			return nil, false
		}

		return option, true
	}

	// Check for previous argument
	if lastItem != "" && option == nil {
		if strings.HasPrefix(args[len(args)-2], "-") || strings.HasPrefix(args[len(args)-2], "--") {

			// Long opts
			if strings.HasPrefix(args[len(args)-2], "--") {
				lastOption = strings.TrimPrefix(args[len(args)-2], "--")
				if opt := group.FindOptionByLongName(lastOption); opt != nil {
					option = opt
					return option, true
				}

				// Short opts
			} else if strings.HasPrefix(args[len(args)-2], "-") {
				lastOption = strings.TrimPrefix(args[len(args)-2], "-")
				if opt := group.FindOptionByShortName(rune(lastOption[0])); opt != nil {
					option = opt
					return option, true
				}
			}
		}
	}

	return nil, false
}

// [ Other ]-------------------------------------------------------------------------------------
// Does the user asks for Environment variables ?
func envVarAsked(args []string, lastWord string) bool {

	// Check if the current word is an environment variable, or if the last part of it is a variable
	if len(lastWord) > 1 && strings.HasPrefix(lastWord, "$") {
		if strings.LastIndex(lastWord, "/") < strings.LastIndex(lastWord, "$") {
			return true
		}
		return false
	}

	// Check if env var is asked in a path or something
	if len(lastWord) > 1 {
		// If last is a path, it cannot be an env var anymore
		if lastWord[len(lastWord)-1] == '/' {
			return false
		}

		if lastWord[len(lastWord)-1] == '$' {
			return true
		}
	}

	// If we are at the beginning of an env var
	if len(lastWord) > 0 && lastWord[len(lastWord)-1] == '$' {
		return true
	}

	return false
}

// filterOptions - Check various elements of an option and return a list
func filterOptions(args []string, command *flags.Command) (processed []string) {

	for i := 0; i < len(args); i++ {
		arg := args[i]
		// --long-name options
		if strings.HasPrefix(arg, "--") {
			name := strings.TrimPrefix(arg, "--")
			if opt := optionByName(command, name); opt != nil {
				var boolean = true
				if opt.Field().Type == reflect.TypeOf(boolean) {
					continue
				}
				// Else skip the option argument (next item)
				i++
			}
			continue
		}
		// -s short options
		if strings.HasPrefix(arg, "-") {
			name := strings.TrimPrefix(arg, "-")
			if opt := optionByName(command, name); opt != nil {
				var boolean = true
				if opt.Field().Type == reflect.TypeOf(boolean) {
					continue
				}
				// Else skip the option argument (next item)
				i++
			}
			continue
		}
		processed = append(processed, arg)
	}

	return
}

// Other Functions -------------------------------------------------------------------------------------------------------------//

// formatInput - Formats & sanitize the command line input
func formatInput(line []rune) (args []string, last []rune, lastWord string) {
	args = strings.Split(string(line), " ")         // The readline input as a []string
	last = trimSpaceLeft([]rune(args[len(args)-1])) // The last char in input
	lastWord = string(last)
	return
}

// FormatInput - Formats & sanitize the command line input
func formatInputHighlighter(line []rune) (args []string, last []rune, lastWord string) {
	args = strings.SplitN(string(line), " ", -1)
	last = trimSpaceLeft([]rune(args[len(args)-1])) // The last char in input
	lastWord = string(last)
	return
}

// ignoreRedundantSpaces - We might have several spaces between each real arguments.
// However these indivual spaces are counted as args themselves.
// For each space arg found, verify that no space args follow,
// and if some are found, delete them.
func ignoreRedundantSpaces(raw []string) (args []string) {

	for i := 0; i < len(raw); i++ {
		// Catch a space argument.
		if raw[i] == "" {
			// The arg evaulated is always kept, because we just adjusted
			// the indexing to avoid the ones we don't need
			// args = append(args, raw[i])

			for y, next := range raw[i:] {
				if next != "" {
					i += y - 1
					break
				}
				// If we come to the end while not breaking
				// we push the outer loop straight to the end.
				if y == len(raw[i:])-1 {
					i += y
				}
			}
		} else {
			// The arg evaulated is always kept, because we just adjusted
			// the indexing to avoid the ones we don't need
			args = append(args, raw[i])
		}
	}

	return
}

func trimSpaceLeft(in []rune) []rune {
	firstIndex := len(in)
	for i, r := range in {
		if unicode.IsSpace(r) == false {
			firstIndex = i
			break
		}
	}
	return in[firstIndex:]
}

func equal(a, b []rune) bool {
	if len(a) != len(b) {
		return false
	}
	for i := 0; i < len(a); i++ {
		if a[i] != b[i] {
			return false
		}
	}
	return true
}

func hasPrefix(r, prefix []rune) bool {
	if len(r) < len(prefix) {
		return false
	}
	return equal(r[:len(prefix)], prefix)
}