# shellcheck shell=bash

# Copyright (c) 2017 Eric Wendelin

# Permission is hereby granted, free of charge, to any person obtaining a copy of
# this software and associated documentation files (the "Software"), to deal in
# the Software without restriction, including without limitation the rights to
# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
# of the Software, and to permit persons to whom the Software is furnished to do
# so, subject to the following conditions:

# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.

# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

# Bash breaks words on : by default. Subproject tasks have ':'
# Avoid inaccurate completions for subproject tasks
COMP_WORDBREAKS="${COMP_WORDBREAKS//:/}"

function __gradle-set-project-root-dir() {
	project_root_dir="$(_bash-it-find-in-ancestor "settings.gradle" "gradlew")"
	return "$?"
}

__gradle-init-cache-dir() {
	cache_dir="$HOME/.gradle/completion"
	mkdir -p "$cache_dir"
}

__gradle-set-build-file() {
	# Look for default build script in the settings file (settings.gradle by default)
	# Otherwise, default is the file 'build.gradle' in the current directory.
	gradle_build_file="$project_root_dir/build.gradle"
	if [[ -f "$project_root_dir/settings.gradle" ]]; then
		local build_file_name
		build_file_name=$(grep "^rootProject\.buildFileName" "$project_root_dir/settings.gradle" \
			| sed -n -e "s/rootProject\.buildFileName = [\'\"]\(.*\)[\'\"]/\1/p")
		gradle_build_file="$project_root_dir/${build_file_name:-build.gradle}"
	fi
}

__gradle-set-cache-name() {
	# Cache name is constructed from the absolute path of the build file.
	cache_name=${gradle_build_file//"/"/_}
}

__gradle-set-files-checksum() {
	# Cache MD5 sum of all Gradle scripts and modified timestamps
	if _command_exists md5; then
		gradle_files_checksum=$(md5 -q -s "$(xargs ls -o < "$cache_dir/$cache_name" 2> /dev/null)")
	elif _command_exists md5sum; then
		gradle_files_checksum=$(xargs ls -o < "$cache_dir/$cache_name" 2> /dev/null | md5sum | awk '{print $1}')
	else
		echo "Cannot generate completions as neither md5 nor md5sum exist on \$PATH"
	fi
}

__gradle-generate-script-cache() {
	# Invalidate cache after 3 weeks by default
	local cache_ttl_mins=${GRADLE_CACHE_TTL_MINUTES:-30240}
	local script_exclude_pattern=${GRADLE_COMPLETION_EXCLUDE_PATTERN:-"/(build|integTest|out)/"}

	if [[ ! $(find "$cache_dir/$cache_name" -mmin "-$cache_ttl_mins" 2> /dev/null) ]]; then
		# Cache all Gradle scripts
		local gradle_build_scripts
		gradle_build_scripts=$(
			find "$project_root_dir" -type f \
				\( -name "*.gradle" -o -name "*.gradle.kts" \) 2> /dev/null \
				| grep -E -v "$script_exclude_pattern"
		)
		printf "%s\n" "${gradle_build_scripts[@]}" > "$cache_dir/$cache_name"
	fi
}

__gradle-long-options() {
	local args="--build-cache           - Enables the Gradle build cache
--build-file            - Specifies the build file
--configure-on-demand   - Only relevant projects are configured
--console               - Type of console output to generate (plain auto rich)
--continue              - Continues task execution after a task failure
--continuous            - Continuous mode. Automatically re-run build after changes
--daemon                - Use the Gradle Daemon
--debug                 - Log at the debug level
--dry-run               - Runs the build with all task actions disabled
--exclude-task          - Specify a task to be excluded
--full-stacktrace       - Print out the full (very verbose) stacktrace
--gradle-user-home      - Specifies the Gradle user home directory
--gui                   - Launches the Gradle GUI app (Deprecated)
--help                  - Shows a help message
--include-build         - Run the build as a composite, including the specified build
--info                  - Set log level to INFO
--init-script           - Specifies an initialization script
--max-workers           - Set the maximum number of workers that Gradle may use
--no-build-cache        - Do not use the Gradle build cache
--no-daemon             - Do not use the Gradle Daemon
--no-rebuild            - Do not rebuild project dependencies
--no-scan               - Do not create a build scan
--no-search-upwards     - Do not search in parent directories for a settings.gradle
--offline               - Build without accessing network resources
--parallel              - Build projects in parallel
--profile               - Profile build time and create report
--project-cache-dir     - Specifies the project-specific cache directory
--project-dir           - Specifies the start directory for Gradle
--project-prop          - Sets a project property of the root project
--quiet                 - Log errors only
--recompile-scripts     - Forces scripts to be recompiled, bypassing caching
--refresh-dependencies  - Refresh the state of dependencies
--rerun-tasks           - Specifies that any task optimization is ignored
--scan                  - Create a build scan
--settings-file         - Specifies the settings file
--stacktrace            - Print out the stacktrace also for user exceptions
--status                - Print Gradle Daemon status
--stop                  - Stop all Gradle Daemons
--system-prop           - Set a system property
--version               - Prints Gradle version info
--warn                  - Log warnings and errors only"
	COMPREPLY=()
	while IFS='' read -r line; do
		COMPREPLY+=("$line")
	done < <(compgen -W "$args" -- "${COMP_WORDS[COMP_CWORD]}")
}

__gradle-properties() {
	local args="-Dorg.gradle.cache.reserved.mb=   - Reserve Gradle Daemon memory for operations
-Dorg.gradle.caching=             - Set true to enable Gradle build cache
-Dorg.gradle.daemon.debug=        - Set true to debug Gradle Daemon
-Dorg.gradle.daemon.idletimeout=  - Kill Gradle Daemon after # idle millis
-Dorg.gradle.debug=               - Set true to debug Gradle Client
-Dorg.gradle.jvmargs=             - Set JVM arguments
-Dorg.gradle.java.home=           - Set JDK home dir
-Dorg.gradle.logging.level=       - Set default Gradle log level (quiet warn lifecycle info debug)
-Dorg.gradle.parallel=            - Set true to enable parallel project builds (incubating)
-Dorg.gradle.parallel.intra=      - Set true to enable intra-project parallel builds (incubating)
-Dorg.gradle.workers.max=         - Set the number of workers Gradle is allowed to use"
	COMPREPLY=()
	while IFS='' read -r line; do
		COMPREPLY+=("$line")
	done < <(compgen -W "$args" -- "${COMP_WORDS[COMP_CWORD]}")
	return 0
}

__gradle-short-options() {
	local args="-?                      - Shows a help message
-a                      - Do not rebuild project dependencies
-b                      - Specifies the build file
-c                      - Specifies the settings file
-d                      - Log at the debug level
-g                      - Specifies the Gradle user home directory
-h                      - Shows a help message
-i                      - Set log level to INFO
-m                      - Runs the build with all task actions disabled
-p                      - Specifies the start directory for Gradle
-q                      - Log errors only
-s                      - Print out the stacktrace also for user exceptions
-t                      - Continuous mode. Automatically re-run build after changes
-u                      - Do not search in parent directories for a settings.gradle
-v                      - Prints Gradle version info
-w                      - Log warnings and errors only
-x                      - Specify a task to be excluded
-D                      - Set a system property
-I                      - Specifies an initialization script
-P                      - Sets a project property of the root project
-S                      - Print out the full (very verbose) stacktrace"
	COMPREPLY=()
	while IFS='' read -r line; do COMPREPLY+=("$line"); done < <(compgen -W "$args" -- "${COMP_WORDS[COMP_CWORD]}")
}

__gradle-notify-tasks-cache-build() {
	# Notify user of cache rebuild
	echo -e " (Building completion cache. Please wait)\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\c"
	__gradle-generate-tasks-cache
	# Remove "please wait" message by writing a bunch of spaces then moving back to the left
	echo -e "                                         \b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\c"
}

__gradle-generate-tasks-cache() {
	__gradle-set-files-checksum

	# Use Gradle wrapper when it exists.
	local gradle_cmd="gradle"
	if [[ -x "$project_root_dir/gradlew" ]]; then
		gradle_cmd="$project_root_dir/gradlew"
	fi

	# Run gradle to retrieve possible tasks and cache.
	# Reuse Gradle Daemon if IDLE but don't start a new one.
	local gradle_tasks_output
	if "$gradle_cmd" --status 2> /dev/null | grep -q IDLE; then
		gradle_tasks_output="$("$gradle_cmd" -b "$gradle_build_file" --daemon -q tasks --all)"
	else
		gradle_tasks_output="$("$gradle_cmd" -b "$gradle_build_file" --no-daemon -q tasks --all)"
	fi
	local output_line
	local task_description
	local -a gradle_all_tasks=()
	local -a root_tasks=()
	local -a subproject_tasks=()
	for output_line in $gradle_tasks_output; do
		if [[ $output_line =~ ^([[:lower:]][[:alnum:][:punct:]]*)([[:space:]]-[[:space:]]([[:print:]]*))? ]]; then
			task_name="${BASH_REMATCH[1]}"
			task_description="${BASH_REMATCH[3]}"
			gradle_all_tasks+=("$task_name  - $task_description")
			# Completion for subproject tasks with ':' prefix
			if [[ $task_name =~ ^([[:alnum:][:punct:]]+):([[:alnum:]]+) ]]; then
				gradle_all_tasks+=(":$task_name  - $task_description")
				subproject_tasks+=("${BASH_REMATCH[2]}")
			else
				root_tasks+=("$task_name")
			fi
		fi
	done

	# subproject tasks can be referenced implicitly from root project
	if [[ $GRADLE_COMPLETION_UNQUALIFIED_TASKS == "true" ]]; then
		local -a implicit_tasks
		while IFS='' read -r line; do
			implicit_tasks+=("$line")
		done < <(comm -23 <(printf "%s\n" "${subproject_tasks[@]}" | sort) \
			<(printf "%s\n" "${root_tasks[@]}" | sort))
		for task in "${implicit_tasks[@]}"; do
			gradle_all_tasks+=("$task")
		done
	fi

	printf "%s\n" "${gradle_all_tasks[@]}" > "$cache_dir/$gradle_files_checksum"
	echo "$gradle_files_checksum" > "$cache_dir/$cache_name.md5"
}

__gradle-completion-init() {
	local cache_dir cache_name gradle_build_file gradle_files_checksum project_root_dir

	local OLDIFS="$IFS"
	local IFS=$'\n'

	__gradle-init-cache-dir
	__gradle-set-project-root-dir
	__gradle-set-build-file
	if [[ -f $gradle_build_file ]]; then
		__gradle-set-cache-name
		__gradle-generate-script-cache
		__gradle-set-files-checksum
		__gradle-notify-tasks-cache-build
	fi

	IFS="$OLDIFS"

	return 0
}

_gradle() {
	local cache_dir cache_name gradle_build_file gradle_files_checksum project_root_dir
	local cur=${COMP_WORDS[COMP_CWORD]}
	# Set bash internal field separator to '\n'
	# This allows us to provide descriptions for options and tasks
	local OLDIFS="$IFS"
	local IFS=$'\n'

	if [[ ${cur} == --* ]]; then
		__gradle-long-options
	elif [[ ${cur} == -D* ]]; then
		__gradle-properties
	elif [[ ${cur} == -* ]]; then
		__gradle-short-options
	else
		__gradle-init-cache-dir
		__gradle-set-project-root-dir
		__gradle-set-build-file
		if [[ -f $gradle_build_file ]]; then
			__gradle-set-cache-name
			__gradle-generate-script-cache
			__gradle-set-files-checksum

			# The cache key is md5 sum of all gradle scripts, so it's valid if it exists.
			if [[ -f "$cache_dir/$cache_name.md5" ]]; then
				local cached_checksum
				cached_checksum="$(cat "$cache_dir/$cache_name.md5")"
				local -a cached_tasks
				if [[ -z $cur ]]; then
					while IFS='' read -r line; do
						cached_tasks+=("$line")
					done < "$cache_dir/$cached_checksum"
				else
					while IFS='' read -r line; do
						cached_tasks+=("$line")
					done < <(grep "^$cur" "$cache_dir/$cached_checksum")
				fi
				while IFS='' read -r line; do
					COMPREPLY+=("$line")
				done < <(compgen -W "${cached_tasks[*]}" -- "$cur")
			else
				__gradle-notify-tasks-cache-build
			fi

			# Regenerate tasks cache in the background
			if [[ $gradle_files_checksum != "$(cat "$cache_dir/$cache_name.md5")" ||
			! -f "$cache_dir/$gradle_files_checksum" ]]; then
				(__gradle-generate-tasks-cache &> /dev/null &)
			fi
		else
			# Default tasks available outside Gradle projects
			local args="buildEnvironment     - Displays all buildscript dependencies declared in root project.
components           - Displays the components produced by root project.
dependencies         - Displays all dependencies declared in root project.
dependencyInsight    - Displays the insight into a specific dependency in root project.
dependentComponents  - Displays the dependent components of components in root project.
help                 - Displays a help message.
init                 - Initializes a new Gradle build.
model                - Displays the configuration model of root project.
projects             - Displays the sub-projects of root project.
properties           - Displays the properties of root project.
tasks                - Displays the tasks runnable from root project.
wrapper              - Generates Gradle wrapper files."
			COMPREPLY=()
			while IFS='' read -r line; do
				COMPREPLY+=("$line")
			done < <(compgen -W "${args}" -- "${cur}")
		fi
	fi

	IFS="$OLDIFS"

	# Remove description ("[:space:]" and after) if only one possibility
	if [[ ${#COMPREPLY[*]} -eq 1 ]]; then
		COMPREPLY=("${COMPREPLY[0]%%  *}")
	fi

	return 0
}
complete -F _gradle gradle
complete -F _gradle gradle.bat
complete -F _gradle gradlew
complete -F _gradle gradlew.bat
complete -F _gradle ./gradlew
complete -F _gradle ./gradlew.bat

if hash gw 2> /dev/null || alias gw > /dev/null 2>&1; then
	complete -F _gradle gw
fi
