#!/bin/bash
#

# print version and license
# is hereby part of the code and also displayed to the user 
version() {
	cat <<EOF >&2

Welcome to $( basename $0)!
Authors:  Lars Holger Engelhard - DL5RCW (2016)
          Tiancheng "Timothy" Gu (2014)

Version: 1.3

# This file is part of the MXE Project, sponsored by the named authors
# it supports the shared build approach by providing an easy way to
# check for library dependencies in a recursive manner

# Copyright (c) 2016 Lars Holger Engelhard - DL5RCW
#           (c) 2014 Tiancheng "Timothy" Gu

#
# 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.

EOF
}

# default application objdump
# you can specify your own objdump with --objdump or -o
OBJDUMP=objdump

# create a temp directory
tmp=$( mktemp -d )

# print an help menu
help() {
	cat <<EOF >&2


Usage: $0 -c    -d DEST -s SRC [-f FILE|-F DIR]
  or:  $0    -p                [-f FILE|-F DIR]
  or:  $0 -c -p -d DEST -s SRC [-f FILE|-F DIR]
  or:  $0    -p -d DEST -R SRC [-f FILE|-F DIR]
  or:  $0    -p -d DEST -S "SRC_1 SRC_2 ... SRC_n" [-f FILE|-F DIR]
  or:  $0 -c    -d DEST -R SRC_1 -e SRC_2          [-f FILE|-F DIR]


Copy executable FILE(s) and their DLL dependencies for SRC directory to a
DEST directory, and/or print the recursive dependencies.

Operating modes:
  -c, --copy              print and copy dependencies
  -p, --print             print dependencies (no copy action)

Operating options:
  -d, --destdir           Destination directory - a single destination folder
  -f, --infile            [ multiCall ] The input executable file or DLL.
  -F, --infiles, --indir  [ multiCall ] The input directory of executable files and/or DLLs.
  -s, --srcdir            [ multiCall ] The directory with DLLs that can be copied. 
  -S, --srcdirs           [ multiCall ] List of directories with DLLs that can be copied. Put "" around them, e.g. "/dir1 /root/dir2 /root/dir3" 
  -R, --recursivesrcdir   [ multiCall ] Target directory for recursive search of folders containing *dll files  
  -X, --excludepattern    [ multiCall ] Exclude any path that contains such pattern, e.g. /(PREFIX)/(TARGET)/apps/

Optional binary settings:
  -o, --objdump           Specify the path or name of your objdump application
  -e, --enforcedir        [ multiCall ] Enforce executable files and/or DLLs of a specific directory 
                          It will be entirely copied - flat, non recursive. assumes *.dll and *.exe in the top level directory
 	                  It will copy those into a directory in DESTDIR! 
                          e.g. <path_to_mxe>/mxe/usr/<compiler>/qt5/plugins/platforms/ - for qwindows.dll becomes
                          DESTDIR/platforms/ containing qwindows.dll
Other options:
  -h,-H, --help           Display this message and exit
  -v,-V, --version        Display version of this application
  -l,-L, --loglevel       Display more output - default is 1

multiCall => you can specify this option multiple times!

Authors:  Lars Holger Engelhard - DL5RCW
          Tiancheng "Timothy" Gu
EOF
}

# terminate the application 
# print an error message
# and clean the tmp directory
die() {
	echo $1 >&2
	rm -rf "$tmp"
	help
	exit 1
}

# find all directories containing dll files
# you can pass a list (array) of directories
# and findAllSrcDirectories will hunt for dlls in each one recursively
# it will return a sorted list and duplicates are removed
findAllSrcDirectories(){
	ar_recursiveDirList=${!1}
	string=""
	for curPath in "${ar_recursiveDirList[@]}"; do
		for element in $(find $curPath $excludePattern -iname "*.dll"); do
			#ar_list+="$(dirname $element) "
			string+="$(dirname $element) "
		done
	done
	string=$(echo "$string" | tr -s ' ' | tr ' ' '\n' | nl | sort -u -k2 | sort -n | cut -f2-)
	echo $string #returns the string 
}

while [ $# -gt 0 ]; do
	key="$1"
	shift

	case $key in
	-f|--infile)
		infile+=" $1"
		shift
		;;
	-F|--indir|--infiles)
		indir+=" $1"
		shift
		;;
	-s|--srcdir)
		srcdir+=" $1"
		shift
		;;
	-d|--destdir)
		destdir="$1"
		shift
		;;
	-S|--srcdirs)
		srcdirs+=" $1"
		shift
		;;
	-R|--recursivesrcdir)
		recursivesrcdir+=" $1"
		shift
		;;
	-X|--excludepattern)
		excludepattern+=" $1"
		shift
		;;
	-o|--objdump)
		OBJDUMP="$1"
		shift
		;;
	-e|--enforcedir)
		enforcedir+=" $1"
		shift
		;;
	-l|-L|--loglevel)
		loglevel="$1"
		shift
		;;
	-p|--print)
		opmode="print"
		;;
	-c|--copy)
		opmode="copy"
		;;
	-h|-H|--help)
		help
		exit 0
		;;
	-v|-V|--version)
		version
		exit 0
		;;
	*)
		echo "unknown option $key ignored" >&2
		;;
	esac
done

# setting default values if no arguments were given
if ! [ "$loglevel" ]; then
	loglevel=0
fi

if ! [ "$opmode" ]; then 
	opmode="copy"  	# used as default in productive
	#opmode="print" 	# used as default in development
fi

if ! [ "$destdir" ]; then
	die '--destdir is not specified.'
fi

if [ -n "$(ls -A $destdir 2>/dev/null)" ]; then
    echo 'Warning: --destdir already exists and contains files.' >&2
else
    mkdir -p "$destdir"
    echo "info: created --destdir $destdir"
fi

if [ "$loglevel" -gt 1 ]; then
	echo "filelist=$filelist"
	echo "opmode=$opmode"
fi

excluePattern="" # building an exclude command consisting of patterns. We still contain the first hit of find
if [ ! -z "$excludepattern" ]; then
	for curString in $( echo "$excludepattern" | tr -s ' ' | tr ' ' '\n' ); do
		excludePattern+=" ! -path *$( echo "$curString" | tr -d ' ' )* "
	done
fi
if [ "$loglevel" -gt 1 ]; then
	echo "\$excluePattern: $excludePattern"
fi

str_inputFileList=""
if [ "$indir" ]; then
	for curPath in $( echo "${indir}" | tr -s ' ' | tr ' ' '\n' ); do
	if [ `uname -s` == "Darwin" ]; then
		curList=$( find $curPath -iname *.exe -or -iname *.dll | tr '\n' ' '  )
		else curList=$( find $curPath -iregex '.*\(dll\|exe\)' | tr '\n' ' ' )
	fi
		str_inputFileList+=" $curList"
	done
fi
if [ "$infile" ]; then
	for curFile in $( echo "${infile}" | tr -s ' ' | tr ' ' '\n' ); do
	if [ `uname -s` == "Darwin" ]; then
		curString=$( find $curPath -iname *.exe -or -iname *.dll | tr '\n' ' '  )
		else curString=$( find $curPath -iregex '.*\(dll\|exe\)' | tr '\n' ' ' )
	fi
		str_inputFileList+=" $curString"
	done
fi
if [ -z "$str_inputFileList" ]; then
	die 'there was no input defined. use --indir and/or --infile in your command'
fi
if [ "$loglevel" -gt 1 ]; then
        echo "str_inputFileList=$str_inputFileList"
        echo "opmode=$opmode"
fi

str_srcDirList=""
if [ "$srcdir" ]; then
	str_srcDirList+=" $srcdir"
fi
if [ "$srcdirs" ]; then
	str_srcDirList+=" $srcdirs"
fi
if [ "$recursivesrcdir" ]; then
	result="$( findAllSrcDirectories recursivesrcdir )"
	str_srcDirList+=" $result"
fi
if [ -z "$str_srcDirList" ]; then
	die 'there was no source directory defined. use --srcdirs or --srcdir or --recursivesrcdir in your command'
fi
if [ "$loglevel" -gt 1 ]; then
	#echo "infiles: filelist=$filelist"
	echo "infiles: str_inputFileList=$str_inputFileList"
	echo "          opmode: $opmode"
fi

if [ "$loglevel" -gt 1 ]; then
	echo "list for sources: str_srcDirList=${str_srcDirList}"
	echo "using OBJDUMP=$OBJDUMP in Version $( $OBJDUMP -V)"
fi

if [ "$loglevel" -gt 1 ]; then
	## during development, I like to interrupt here to check the above output and skip the rest
	echo "starting in 5 seconds" && sleep 5
fi

# introducing a whitelist of well known DLLs
str_whiteListDlls="advapi32.dll kernel32.dll msvcrt.dll user32.dll ws2_32.dll gdi32.dll shell32.dll d3d9.dll ole32.dll winmm.dll mpr.dll opengl32.dll"

# function to append dependencies (recursively)
append_deps() {
	if [ "$loglevel" -gt 1 ]; then
		echo "\$1=$1 + \$2=$2 "
		sleep 2
	fi
	local bn="$( basename $1 )"
	if [ -e "$tmp/$bn" ]; then
		return 0
	fi
	if [ $# -eq 2 ]; then
		path="$1"
	else
		path=""
		for curPath in $( echo "${str_srcDirList}" | tr -s ' ' | tr ' ' '\n' ); do
			counter=0
			result=""
			result=$(find $curPath $excludePattern -iname "$bn" -type f | tail -n 1)
			if [ "$loglevel" -gt 1 ]; then
				echo "complete find command in append_deps(): # find $curPath $excludePattern -iname $bn -type f | tail -n 1 # "
			fi
			if [ ! -z "$result" ];then
				path="$result"
				counter=$(expr $counter + 1)
			fi
if [ $counter == 0 ]; then
        #echo "ERROR: could not find \$path for dll $bn, \$counter=$counter: searched $curPath"
        str_test="1"
else
        echo "OKAY:  found path for dll $bn = $path, \$counter=$counter: searched $curPath"
fi
			if [ "$loglevel" -gt 1 ]; then
				if [ $counter == 0 ]; then
					echo "could not find \$path for dll $bn, \$counter=$counter: searched $curPath"
				else
					echo "found path for dll $bn = $path, \$counter=$counter: searched $curPath"
				fi
			fi
		done
		if [ "$loglevel" -gt 1 ]; then
			echo "path for dll $bn now is $path"
			sleep 2
		fi
	fi
	echo "Processing $1" >&2
	if ! [ -e "$path" ]; then
		if [ "$loglevel" -gt 1 ]; then
			echo "path=$path| and we touch $tmp/$bn -> non existent in our src directories!"
			sleep 4
		fi
		touch "$tmp/$bn"
		return 0
	fi
	$OBJDUMP -p "$path" | grep 'DLL Name:' | cut -f3 -d' ' > "$tmp/$bn"
	echo "executing: $OBJDUMP -p "$path" | grep 'DLL Name:' | cut -f3 -d' ' > "$tmp/$bn""
	for dll in $( cat "$tmp/$bn" | tr '\n' ' ' ); do
		append_deps "$dll"
	done
	alldeps=$(printf "$alldeps\n%s" "$(cat $tmp/$bn)" | sort | uniq)
}

process_enforced_deps(){
	enforcedDirectory=$1
	if [ ! -d $enforcedDirectory ]; then
		echo "warning! \$enforcedDirectory=$enforcedDirectory is not valid"
		if [ "$loglevel" -gt 1 ]; then
			sleep 10
		fi
	fi
	# first we append the path to enforced dir to our list of source directories
	# if we would do this file recursively, we should loop to find those and append them all to the list
	str_srcDirList+=" $enforcedDirectory"
	# now we search for the dll and exe files to be included
	if [ `uname -s` == "Darwin" ]; then
		string=$( find $enforcedDirectory -maxdepth 1 -iname *.exe -or -iname *.dll | tr '\n' ' ' )
		else string=$( find $enforcedDirectory -maxdepth 1 -iregex '.*\(dll\|exe\)' | tr '\n' ' ' )
	fi
	if [ "$loglevel" -gt 1 ]; then
		echo "enforcedDirectory=$enforcedDirectory"
		echo "we found dlls and exes:$string"
		sleep 4
	fi
	# we hard copy it to DEST
	if [ `uname -s` == "Darwin" ]; then
		cp -av "${enforcedDirectory}" "$destdir"
		else cp -dpRxv "${enforcedDirectory}" "$destdir"
	fi
}

# beginning of the main function
# we start with the enforced dlls and exe
if [ ! -z "$enforcedir" ]; then
	for curFile in $( echo "${enforcedir}" | tr -s ' ' | tr ' ' '\n'); do
		echo "startig for file $curFile in enforce section"
		append_deps "$curFile" rel
		process_enforced_deps "$curFile" 
	done
fi

# then we start with our indir or infile list
for file in $str_inputFileList; do
	echo "starting for file $file"
	#sleep 4
	append_deps "$file" rel
done

echo "I will now search for \$alldeps"
for debugOut in $( echo $alldeps | tr -s ' ' | tr '\n' ' '); do
	echo "debugOut: $debugOut"
done
if [ "$loglevel" -eq 1 ]; then
	echo "waiting 10 seconds until I proceed - so you can read my debugOut"
	sleep 10

	tmpStr=${str_srcDirList}
	echo "\$alldeps has ${#alldeps[@]} elements"
	echo "and \$str_srcDirList has ${#str_srcDirList} elements"
fi

str_summary="Here is the summary:"
str_summary="${str_summary} # ==== 8< ==== START ==== 8< ==== "
if [ $opmode == "copy" ]; then
	echo "copying files from \${curFolder} to \$destdir:"
elif [ $opmode == "print" ]; then
	echo "printing files:"
fi
for dll in $( echo $alldeps | tr '\n' ' ' ); do
	counter=0
	lowerDll=$( echo $dll | tr '[:upper:]' '[:lower:]' )
	if [ $lowerDll == $dll ]; then
		lowerDll=""
	fi
	for curFolder in $( echo "${str_srcDirList}" | tr -s ' ' | tr ' ' '\n'); do
		if [ "$loglevel" -gt 1 ]; then
			echo "search for dll $dll in curFolder $curFolder"
			sleep 1
		fi
		for curDll in $dll $lowerDll; do
			if [ -e "${curFolder}/${curDll}" ]; then
				counter=$( expr $counter + 1 )
				if [ $opmode == "copy" ]; then
					if [ `uname -s` == "Darwin" ]; then
						cp -av "${curFolder}/${curDll}" "$destdir"
					else cp -dpRxv "${curFolder}/${curDll}" "$destdir"
					fi
					
				elif [ $opmode == "print" ]; then
					echo "found $dll in: ${curFolder}/${curDll}"
				else
					echo "unknown opmode=$opmode"
				fi
			fi
		done
	done
	if [ $counter == 0 ]; then
		lowerDll=$( echo $dll | tr '[:upper:]' '[:lower:]' )
		str_whiteListDlls=$( echo ${str_whiteListDlls} | tr '[:upper:]' '[:lower:]' ) # make whiteListDlls lower case to ensure we find the match (case insensitive)
		if [ -z "${str_whiteListDlls/*${lowerDll}*}" ]; then
			if [ "$loglevel" -gt 1 ]; then
				echo "Info: \"$dll\" not found - but it is white-listed. That means: it is well known by Windows - do not worry too much. "
			fi
			str_summary="${str_summary} # Info: \"$dll\" not found - but it is white-listed. That means: it is well known by Windows - do not worry too much. "
		else
			if [ "$loglevel" -gt 1 ]; then
				echo "Warn: \"$dll\" NOT found. \$counter=$counter."
			fi
			str_summary="${str_summary} # Warn: \"$dll\" NOT found. \$counter=$counter."
		fi
	else
		if [ "$loglevel" -gt 1 ]; then
			echo "Good: \"$dll\" found in the list. \$counter=$counter"
		fi
		str_summary="${str_summary} # Good: \"$dll\" Found in the list. \$counter=$counter"
	fi
done
str_summary="${str_summary} # ==== 8< ==== END ==== 8< ==== "
echo "Job is done."
# print the summary now
for curLine in "$( echo "${str_summary}" | tr -s '#' | tr '#' '\n' )"; do # convert # to a linebreak - string ecomes an array that can be processed in for loop
	echo "$curLine"
done

# clean up the temp directory stored in $tmp
rm -rf "$tmp"