// Copyright (c) 2019-2023 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
//
// Utility functions helping integrate with various shell auto-completion systems.
// The install process and communication is inspired from that of [kitty](https://sw.kovidgoyal.net/kitty/#completion-for-kitty)
// This method avoids writing and maintaining external files on the user's file system.
// The user will be responsible for adding a small line to their .*rc - that will ensure *live* (i.e. not-static)
// auto-completion features.
//
// # bash
// To install auto-completion for V in bash, simply add this code to your `~/.bashrc`:
// `source /dev/stdin <<<"$(v complete setup bash)"`
// On more recent versions of bash (>3.2) this should suffice:
// `source <(v complete setup bash)`
//
// # fish
// For versions of fish <3.0.0, add the following to your `~/.config/fish/config.fish`
// `v complete setup fish | source`
// Later versions of fish source completions by default.
//
// # zsh
// To install auto-completion for V in zsh - please add the following to your `~/.zshrc`:
// ```
// autoload -Uz compinit
// compinit
// # Completion for v
// v complete setup zsh | source /dev/stdin
// ```
// Please note that you should let v load the zsh completions after the call to compinit
//
// # powershell
// To install auto-complete for V in PowerShell, simply do this
// `v complete setup powershell >> $PROFILE`
// and reload profile
// `& $PROFILE`
// If `$PROFILE` didn't exist yet, create it before
// `New-Item -Type File -Force $PROFILE`
//
module main

import os

const (
	auto_complete_shells = ['bash', 'fish', 'zsh', 'powershell'] // list of supported shells
	vexe                 = os.getenv('VEXE')
	help_text            = "Usage:
  v complete [options] [SUBCMD] QUERY...

Description:
  Tool for bridging auto completion between various shells and v

Supported shells:
  bash, fish, zsh, powershell

Examples:
  Echo auto-detected shell install script to STDOUT
    v complete
  Echo specific shell install script to STDOUT
    v complete setup bash
  Auto complete input `v tes`*USER PUSHES TAB* (in Bash compatible format).
  This is not meant for manual invocation - it's called by the relevant
  shell via the script installed with `v complete` or `v complete setup SHELL`.
    v complete bash v tes

Options:
  -h, --help                Show this help text.

SUBCMD:
  setup     : setup [SHELL] - returns the code for completion setup for SHELL
  bash      : [QUERY]       - returns Bash compatible completion code with completions computed from QUERY
  fish      : [QUERY]       - returns Fish compatible completion code with completions computed from QUERY
  zsh       : [QUERY]       - returns ZSH  compatible completion code with completions computed from QUERY
  powershell: [QUERY]       - returns PowerShell compatible completion code with completions computed from QUERY"
)

// Snooped from cmd/v/v.v, vlib/v/pref/pref.v
const (
	auto_complete_commands      = [
		// simple_cmd
		'ast',
		'doc',
		'vet',
		// tools in one .v file
		'bin2v',
		'bug',
		'build-examples',
		'build-tools',
		'build-vbinaries',
		'bump',
		'check-md',
		'complete',
		'compress',
		'create',
		'doctor',
		'fmt',
		'gret',
		'ls',
		'repl',
		'self',
		'setup-freetype',
		'shader',
		'symlink',
		'test-all',
		'test-cleancode',
		'test-fmt',
		'test-parser',
		'test-self',
		'test',
		'tracev',
		'up',
		'watch',
		'wipe-cache',
		// commands
		'help',
		'new',
		'init',
		'translate',
		'self',
		'search',
		'install',
		'update',
		'upgrade',
		'outdated',
		'list',
		'remove',
		'vlib-docs',
		'get',
		'version',
		'run',
		'build',
		'build-module',
		'missdoc',
	]
	// Entries in the flag arrays below should be entered as is:
	// * Short flags, e.g.: "-v", should be entered: '-v'
	// * Long flags, e.g.: "--version", should be entered: '--version'
	// * Single-dash flags, e.g.: "-version", should be entered: '-version'
	auto_complete_flags         = [
		'-apk',
		'-show-timings',
		'-check-syntax',
		'-check',
		'-v',
		'-progress',
		'-silent',
		'-g',
		'-cg',
		'-repl',
		'-live',
		'-sharedlive',
		'-shared',
		'--enable-globals',
		'-enable-globals',
		'-autofree',
		'-compress',
		'-freestanding',
		'-no-builtin',
		'-no-parallel',
		'-no-preludes',
		'-prof',
		'-profile',
		'-profile-no-inline',
		'-prod',
		'-simulator',
		'-stats',
		'-obfuscate',
		'-translated',
		'-color',
		'-nocolor',
		'-showcc',
		'-show-c-output',
		'-experimental',
		'-usecache',
		'-prealloc',
		'-parallel',
		'-native',
		'-W',
		'-keepc',
		'-w',
		'-print-v-files',
		'-error-limit',
		'-message-limit',
		'-os',
		'-printfn',
		'-cflags',
		'-define',
		'-d',
		'-cc',
		'-o',
		'-b',
		'-path',
		'-custom-prelude',
		'-name',
		'-bundle',
		'-V',
		'-version',
		'--version',
	]
	auto_complete_flags_doc     = [
		'-all',
		'-f',
		'-h',
		'-help',
		'-m',
		'-o',
		'-readme',
		'-v',
		'-filename',
		'-pos',
		'-no-timestamp',
		'-inline-assets',
		'-theme-dir',
		'-open',
		'-p',
		'-s',
		'-l',
	]
	auto_complete_flags_fmt     = [
		'-c',
		'-diff',
		'-l',
		'-w',
		'-debug',
		'-verify',
	]
	auto_complete_flags_bin2v   = [
		'-h',
		'--help',
		'-m',
		'--module',
		'-p',
		'--prefix',
		'-w',
		'--write',
	]
	auto_complete_flags_shader  = [
		'--help',
		'-h',
		'--force-update',
		'-u',
		'--verbose',
		'-v',
		'--slang',
		'-l',
		'--output',
		'-o',
	]
	auto_complete_flags_missdoc = [
		'--help',
		'-h',
		'--tags',
		'-t',
		'--deprecated',
		'-d',
		'--private',
		'-p',
		'--no-line-numbers',
		'-n',
		'--exclude',
		'-e',
		'--relative-paths',
		'-r',
		'--js',
		'--verify',
		'--diff',
	]
	auto_complete_flags_bump    = [
		'--patch',
		'--minor',
		'--major',
	]
	auto_complete_flags_self    = [
		'-prod',
	]
	auto_complete_compilers     = [
		'cc',
		'gcc',
		'tcc',
		'tinyc',
		'clang',
		'mingw',
		'msvc',
	]
)

// auto_complete prints auto completion results back to the calling shell's completion system.
// auto_complete acts as communication bridge between the calling shell and V's completions.
fn auto_complete(args []string) {
	if args.len <= 1 || args[0] != 'complete' {
		if args.len == 1 {
			shell_path := os.getenv('SHELL')
			if shell_path.len > 0 {
				shell_name := os.file_name(shell_path).to_lower()
				if shell_name in auto_complete_shells {
					println(setup_for_shell(shell_name))
					exit(0)
				}
				eprintln('Unknown shell ${shell_name}. Supported shells are: ${auto_complete_shells}')
				exit(1)
			}
			eprintln('auto completion require arguments to work.')
		} else {
			eprintln('auto completion failed for "${args}".')
		}
		exit(1)
	}
	sub := args[1]
	sub_args := args[1..]
	match sub {
		'setup' {
			if sub_args.len <= 1 || sub_args[1] !in auto_complete_shells {
				eprintln('please specify a shell to setup auto completion for (${auto_complete_shells}).')
				exit(1)
			}
			shell := sub_args[1]
			println(setup_for_shell(shell))
		}
		'bash' {
			if sub_args.len <= 1 {
				exit(0)
			}
			mut lines := []string{}
			list := auto_complete_request(sub_args[1..])
			for entry in list {
				lines << "COMPREPLY+=('${entry}')"
			}
			println(lines.join('\n'))
		}
		'fish', 'powershell' {
			if sub_args.len <= 1 {
				exit(0)
			}
			mut lines := []string{}
			list := auto_complete_request(sub_args[1..])
			for entry in list {
				lines << '${entry}'
			}
			println(lines.join('\n'))
		}
		'zsh' {
			if sub_args.len <= 1 {
				exit(0)
			}
			mut lines := []string{}
			mut dirs := []string{}
			mut files := []string{}
			list := auto_complete_request(sub_args[1..])
			for entry in list {
				match true {
					os.is_dir(entry) { dirs << entry }
					os.is_file(entry) { files << entry }
					else { lines << entry }
				}
			}
			println('compadd -q -- ${lines.join(' ')}')
			println('compadd -J "dirs" -X "directory" -d -- ${dirs.join(' ')}')
			println('compadd -J "files" -X "file" -f -- ${files.join(' ')}')
		}
		'-h', '--help' {
			println(help_text)
		}
		else {}
	}
	exit(0)
}

// append_separator_if_dir returns the input `path` with an appended
// `/` or `\`, depending on the platform, when `path` is a directory.
fn append_separator_if_dir(path string) string {
	if os.is_dir(path) && !path.ends_with(os.path_separator) {
		return path + os.path_separator
	}
	return path
}

// nearest_path_or_root returns the nearest valid path searching
// backwards from `path`.
fn nearest_path_or_root(path string) string {
	mut fixed_path := path
	if !os.is_dir(fixed_path) {
		fixed_path = path.all_before_last(os.path_separator)
		if fixed_path == '' {
			fixed_path = '/'
		}
	}
	return fixed_path
}

// auto_complete_request returns a list of completions resolved from a full argument list.
fn auto_complete_request(args []string) []string {
	// Using space will ensure a uniform input in cases where the shell
	// returns the completion input as a string (['v','run'] vs. ['v run']).
	split_by := ' '
	request := args.join(split_by)
	mut do_home_expand := false
	mut list := []string{}
	// new_part := request.ends_with('\n\n')
	mut parts := request.trim_right(' ').split(split_by)
	if parts.len <= 1 { // 'v <tab>' -> top level commands.
		for command in auto_complete_commands {
			list << command
		}
	} else {
		mut part := parts.last().trim(' ')
		mut parent_command := ''
		for i := parts.len - 1; i >= 0; i-- {
			if parts[i].starts_with('-') {
				continue
			}
			parent_command = parts[i]
			break
		}
		if part.starts_with('-') { // 'v [subcmd] -<tab>' or 'v [subcmd] --<tab>'-> flags.
			get_flags := fn (base []string, flag string) []string {
				mut results := []string{}
				for entry in base {
					if entry.starts_with(flag) {
						results << entry
					}
				}
				return results
			}

			match parent_command {
				'bin2v' { // 'v bin2v -<tab>'
					list = get_flags(auto_complete_flags_bin2v, part)
				}
				'build' { // 'v build -<tab>' -> flags.
					list = get_flags(auto_complete_flags, part)
				}
				'doc' { // 'v doc -<tab>' -> flags.
					list = get_flags(auto_complete_flags_doc, part)
				}
				'fmt' { // 'v fmt -<tab>' -> flags.
					list = get_flags(auto_complete_flags_fmt, part)
				}
				'self' { // 'v self -<tab>' -> flags.
					list = get_flags(auto_complete_flags_self, part)
				}
				'shader' { // 'v shader -<tab>' -> flags.
					list = get_flags(auto_complete_flags_shader, part)
				}
				'missdoc' { // 'v missdoc -<tab>' -> flags.
					list = get_flags(auto_complete_flags_missdoc, part)
				}
				'bump' { // 'v bump -<tab>' -> flags.
					list = get_flags(auto_complete_flags_bump, part)
				}
				else {
					for flag in auto_complete_flags {
						if flag == part {
							if flag == '-cc' { // 'v -cc <tab>' -> list of available compilers.
								for compiler in auto_complete_compilers {
									path := os.find_abs_path_of_executable(compiler) or { '' }
									if path != '' {
										list << compiler
									}
								}
							}
						} else if flag.starts_with(part) { // 'v -<char(s)><tab>' -> flags matching "<char(s)>".
							list << flag
						}
					}
				}
			}
			// Clear the list if the result is identical to the part examined
			// (the flag must have already been completed)
			if list.len == 1 && part == list[0] {
				list.clear()
			}
		} else {
			match part {
				'help' { // 'v help <tab>' -> top level commands except "help".
					list = auto_complete_commands.filter(it != part && it != 'complete')
				}
				else {
					// 'v <char(s)><tab>' -> commands matching "<char(s)>".
					// Don't include if part matches a full command - instead go to path completion below.
					for command in auto_complete_commands {
						if part != command && command.starts_with(part) {
							list << command
						}
					}
				}
			}
		}
		// Nothing of value was found.
		// Mimic shell dir and file completion
		if list.len == 0 {
			mut ls_path := '.'
			mut collect_all := part in auto_complete_commands
			mut path_complete := false
			do_home_expand = part.starts_with('~')
			if do_home_expand {
				add_sep := if part == '~' { os.path_separator } else { '' }
				part = part.replace_once('~', os.home_dir().trim_right(os.path_separator)) + add_sep
			}
			is_abs_path := part.starts_with(os.path_separator) // TODO Windows support for drive prefixes
			if part.ends_with(os.path_separator) || part == '.' || part == '..' {
				// 'v <command>(.*/$|.|..)<tab>' -> output full directory list
				ls_path = '.' + os.path_separator + part
				if is_abs_path {
					ls_path = nearest_path_or_root(part)
				}
				collect_all = true
			} else if !collect_all && part.contains(os.path_separator) && os.is_dir(os.dir(part)) {
				// 'v <command>(.*/.* && os.is_dir)<tab>'  -> output completion friendly directory list
				if is_abs_path {
					ls_path = nearest_path_or_root(part)
				} else {
					ls_path = os.dir(part)
				}
				path_complete = true
			}

			entries := os.ls(ls_path) or { return list }
			mut last := part.all_after_last(os.path_separator)
			if is_abs_path && os.is_dir(part) {
				last = ''
			}
			if path_complete {
				path := part.all_before_last(os.path_separator)
				for entry in entries {
					if entry.starts_with(last) {
						list << append_separator_if_dir(os.join_path(path, entry))
					}
				}
			} else {
				// Handle special case, where there is only one file in the directory
				// being completed - if it can be resolved we return that since
				// handling it in the generalized logic below will result in
				// more complexity.
				if entries.len == 1 && os.is_file(os.join_path(ls_path, entries[0])) {
					mut keep_input_path_format := ls_path
					if !part.starts_with('./') && ls_path.starts_with('./') {
						keep_input_path_format = keep_input_path_format.all_after('./')
					}
					return [os.join_path(keep_input_path_format, entries[0])]
				}
				for entry in entries {
					if collect_all || entry.starts_with(last) {
						list << append_separator_if_dir(entry)
					}
				}
			}
		}
	}
	if do_home_expand {
		return list.map(it.replace_once(os.home_dir().trim_right(os.path_separator), '~'))
	}
	return list
}

fn setup_for_shell(shell string) string {
	mut setup := ''
	match shell {
		'bash' {
			setup = '
_v_completions() {
	local src
	local limit
	# Send all words up to the word the cursor is currently on
	let limit=1+\$COMP_CWORD
	src=\$(${vexe} complete bash \$(printf "%s\\n" \${COMP_WORDS[@]: 0:\$limit}))
	if [[ \$? == 0 ]]; then
		eval \${src}
		#echo \${src}
	fi
}

complete -o nospace -F _v_completions v
'
		}
		'fish' {
			setup = '
function __v_completions
	# Send all words up to the one before the cursor
	${vexe} complete fish (commandline -cop)
end
complete -f -c v -a "(__v_completions)"
'
		}
		'zsh' {
			setup = '
#compdef v
_v() {
	local src
	# Send all words up to the word the cursor is currently on
	src=\$(${vexe} complete zsh \$(printf "%s\\n" \${(@)words[1,\$CURRENT]}))
	if [[ \$? == 0 ]]; then
		eval \${src}
		#echo \${src}
	fi
}
compdef _v v
'
		}
		'powershell' {
			setup = '
Register-ArgumentCompleter -Native -CommandName v -ScriptBlock {
	param(\$commandName, \$wordToComplete, \$cursorPosition)
		${vexe} complete powershell "\$wordToComplete" | ForEach-Object {
			[System.Management.Automation.CompletionResult]::new(\$_, \$_, \'ParameterValue\', \$_)
		}
}
'
		}
		else {}
	}
	return setup
}

fn main() {
	args := os.args[1..]
	// println('"$args"')
	auto_complete(args)
}
