#!/usr/bin/env 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=$(echo "$1" | xargs)
    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 api-ms*.dll ext-ms*.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"
