#!/bin/sh

# Packages data
#
# Notes:
# * python3-codecs: Also contains codecs for CJK encodings but we don't
#   have a good way to check for uses
# * python3-openssl: Don't include hashlib as it supports several
#   standard algorithms without requiring OpenSSL

packages="
python3-asyncio: asyncio
python3-cgi: cgi
python3-cgitb: cgitb
python3-codecs: unicodedata
python3-ctypes: ctypes
python3-dbm: dbm dbm.dumb dbm.gnu dbm.ndbm
python3-decimal: decimal
python3-distutils: distutils
python3-email: email
python3-logging: logging
python3-lzma: lzma
python3-multiprocessing: multiprocessing
python3-ncurses: ncurses
python3-openssl: ssl
python3-pydoc: doctest pydoc
python3-readline: readline
python3-sqlite3: sqlite3
python3-unittest: unittest
python3-urllib: urllib
python3-uuid: uuid
python3-xml: xml xmlrpc
"


# Constants

stdin_name="<stdin>"
grep_dir_filters="
-Ir
--include=*.py
--exclude=setup.py
--exclude=test_*.py
--exclude=*_test.py
--exclude-dir=test
--exclude-dir=tests
--exclude-dir=ipkg-*
--exclude-dir=.pkgdir
"

log_level_notice=5
log_level_info=6
log_level_debug=7

# /usr/include/sysexits.h
ex_usage=64
ex_noinput=66
ex_unavailable=69
ex_software=70

newline="
"
oifs="$IFS"


# Defaults

grep_output_default_max_count=3
grep_output_default_color_when="auto"
grep_output_default_line_prefix="-HnT --label=$stdin_name"
grep_output_default_context_num=1

log_level_default="$log_level_info"


# Global variables

log_level=
grep=
grep_output_options=
grep_output_description=
stdin=
output_name=
is_first_search=


# Logging

log() {
	printf '%s\n' "$*"
}

can_log_notice() {
	[ "$log_level" -ge "$log_level_notice" ]
}

can_log_info() {
	[ "$log_level" -ge "$log_level_info" ]
}

can_log_debug() {
	[ "$log_level" -ge "$log_level_debug" ]
}

log_notice() {
	if can_log_notice; then
		log "$@"
	fi
}

log_info() {
	if can_log_info; then
		log "$@"
	fi
}

log_debug() {
	if can_log_debug; then
		log "$@"
	fi
}

log_error() {
	printf '%s\n' "Error: $*" >&2
}


# Usage

usage() {
	cat <<- EOF
	Usage: ${0##*/} [OPTION]... [FILE]...
	Search for imports of certain Python standard libraries in each FILE,
	generate a list of suggested package dependencies.

	With no FILE, or when FILE is -, read standard input.

	Options:
	  -c WHEN   use color in output;
	            WHEN is 'always', 'never', or 'auto' (default: '$grep_output_default_color_when')
	  -h        display this help text and exit
	  -m NUM    show max NUM matches per package per file (default: $grep_output_default_max_count);
	            use 0 to show all matches
	  -n NAME   when one or no FILE is given, use NAME instead of FILE in
	            displayed information
	  -q        show suggested dependencies only
	  -v        show verbose output (also show all matches)
	  -x NUM    show NUM lines of context (default: $grep_output_default_context_num)

	EOF
}


# Imports search

get_package_modules() {
	local line="$1"
	local field_num=0
	local IFS=:

	for field in $line; do
		# strip leading and trailing whitespace
		field="${field#"${field%%[! 	]*}"}"
		field="${field%"${field##*[! 	]}"}"

		# set variables in search_path()
		if [ "$field_num" -eq 0 ]; then
			package="$field"
			field_num=1
		elif [ "$field_num" -eq 1 ]; then
			modules="$field"
			field_num=2
		else
			field_num=3
		fi
	done

	if [ "$field_num" -ne 2 ] || [ -z "$package" ] || [ -z "$modules" ]; then
		log_error "invalid package data \"$line\""
		exit "$ex_software"
	fi
}

search_path_for_modules() {
	local path="$1"
	local path_is_dir="$2"
	local path_is_stdin="$3"
	local package="$4"
	local modules="$5"
	local modules_regex
	local regex
	local remove_dir_prefix
	local grep_results
	local IFS="$oifs"

	log_debug "    Looking for modules in $package ($modules)"

	modules_regex=$(printf '%s' "$modules" | sed -e 's/\./\\./g' -e 's/\s\+/|/g')
	regex="\b(import\s+($modules_regex)|from\s+($modules_regex)\S*\s+import)\b"

	if [ -n "$path_is_dir" ]; then
		remove_dir_prefix="s|^\(\(\x1b[[0-9;]*[mK]\)*\)$path|\1|"
		grep_results=$($grep $grep_output_options $grep_dir_filters -E "$regex" "$path")

	elif [ -n "$path_is_stdin" ]; then
		grep_results=$(printf '%s\n' "$stdin" | $grep $grep_output_options -E "$regex")

	else
		grep_results=$($grep $grep_output_options -E "$regex" "$path")
	fi

	if [ "$?" -ne 0 ]; then
		log_debug "    No imports found"
		log_debug ""
		return 0
	fi

	log_info "    Found imports for: $modules"

	if can_log_info; then
		printf '%s\n' "$grep_results" | sed -e "$remove_dir_prefix" -e "s/^/        /"
	fi

	log_info ""

	# set variable in search_path()
	suggested="$suggested +$package"
}

search_path() {
	local path="$1"
	local name="$2"
	local path_is_dir
	local path_is_stdin
	local package
	local modules
	local suggested
	local IFS="$newline"

	if [ "$path" = "-" ]; then
		path_is_stdin=1

	else
		if ! [ -e "$path" ]; then
			log_error "$path does not exist"
			exit "$ex_noinput"
		fi

		if [ -d "$path" ]; then
			path="${path%/}/"
			path_is_dir=1
		fi
	fi

	log_info ""
	log_info "Searching $name (showing $grep_output_description):"
	log_info ""

	if [ -n "$path_is_stdin" ]; then
		stdin="$(cat)"
	fi

	for line in $packages; do
		# strip leading whitespace
		line="${line#"${line%%[! 	]*}"}"

		# skip blank lines or comments (beginning with #)
		if [ -z "$line" ] ||  [ "$line" != "${line###}" ]; then
			continue
		fi

		package=
		modules=

		get_package_modules "$line"
		search_path_for_modules "$path" "$path_is_dir" "$path_is_stdin" "$package" "$modules"
	done

	log_notice "Suggested dependencies for $name:"

	if [ -z "$suggested" ]; then
		suggested="(none)"
	fi
	IFS="$oifs"
	for package in $suggested; do
		log_notice "    $package"
	done

	log_notice ""
}


# Find GNU grep

if ggrep --version 2>&1 | grep -q GNU; then
	grep="ggrep"
elif grep --version 2>&1 | grep -q GNU; then
	grep="grep"
else
	log_error "cannot find GNU grep"
	exit "$ex_unavailable"
fi


# Process environment variables

case $PYTHON3_FIND_STDLIB_DEPENDS_LOG_LEVEL in
	notice)
		log_level="$log_level_notice"
		;;
	info)
		log_level="$log_level_info"
		;;
	debug)
		log_level="$log_level_debug"
		;;
	*)
		log_level="$log_level_default"
		;;
esac

grep_output_max_count="${PYTHON3_FIND_STDLIB_DEPENDS_MAX_COUNT:-$grep_output_default_max_count}"
grep_output_color_when="${PYTHON3_FIND_STDLIB_DEPENDS_COLOR_WHEN:-$grep_output_default_color_when}"
grep_output_line_prefix="${PYTHON3_FIND_STDLIB_DEPENDS_LINE_PREFIX:-$grep_output_default_line_prefix}"
grep_output_context_num="${PYTHON3_FIND_STDLIB_DEPENDS_CONTEXT_NUM:-$grep_output_default_context_num}"


# Process command line options

while getopts c:hm:n:qvx: OPT; do
	case $OPT in
		c)
			grep_output_color_when="$OPTARG"
			;;
		h)
			usage
			exit 0
			;;
		m)
			grep_output_max_count="$OPTARG"
			;;
		n)
			output_name="$OPTARG"
			;;
		q)
			log_level="$log_level_notice"
			;;
		v)
			log_level="$log_level_debug"
			;;
		x)
			grep_output_context_num="$OPTARG"
			;;
		\?)
			usage
			exit "$ex_usage"
			;;
	esac
done

shift $((OPTIND - 1))


# Set up grep output options

if can_log_info; then
	if [ "$grep_output_color_when" = "auto" ]; then
		if [ -t 1 ]; then
			grep_output_color_when="always"
		else
			grep_output_color_when="never"
		fi
	fi

	if ! can_log_debug && [ "$grep_output_max_count" -gt 0 ]; then
		grep_output_options="-m $grep_output_max_count"

		if [ "$grep_output_max_count" -eq 1 ]; then
			grep_output_description="max 1 match per file"
		else
			grep_output_description="max $grep_output_max_count matches per file"
		fi

	else
		grep_output_description="all matches"
	fi

	if [ "$grep_output_context_num" -gt 0 ]; then
		grep_output_options="$grep_output_options -C $grep_output_context_num"

		if [ "$grep_output_context_num" -eq 1 ]; then
			grep_output_description="$grep_output_description, 1 line of context"
		else
			grep_output_description="$grep_output_description, $grep_output_context_num lines of context"
		fi
	fi

	grep_output_options="$grep_output_options --color=$grep_output_color_when $grep_output_line_prefix"

else
	grep_output_options="-q"
fi


# Main

if [ "$#" -gt 0 ]; then
	is_first_search=1

	if [ "$#" -gt 1 ]; then
		output_name=
	fi

	for path; do
		if [ -z "$is_first_search" ]; then
			log_info "===="
		fi

		if [ -z "$output_name" ]; then
			if [ "$path" = "-" ]; then
				output_name="$stdin_name"
			else
				output_name="$path"
			fi
		fi

		search_path "$path" "$output_name"

		is_first_search=
		output_name=
	done

else
	search_path "-" "${output_name:-$stdin_name}"
fi
