OPENSSL_VERSION=""
JAVA_VERSION=""
REQUIRED_SQLITE_VERSION="3"
REQUIRED_PYTHON_VERSION="2.7"
REQUIRED_NODEJS_VERSION="6.10"
REQUIRED_JAVA_VERSION="8"
MINIMUM_REQUIRED_OPENSSL_VERSION="1.0.1"

no_op() {
    :
}

################################################################################
## Checks if SQLite is installed on the device and if installed, whether its
## version is at least the minimum required version of SQLite for Greengrass.
################################################################################
check_sqlite_version() {
    local sqlite_version_info
    local sqlite_version
    local message="Could not find the binary 'sqlite$REQUIRED_SQLITE_VERSION'.\n"
    message="$message\nIf SQLite version $REQUIRED_SQLITE_VERSION or later is"
    message="$message installed on the device, name the binary"
    message="$message 'sqlite$REQUIRED_SQLITE_VERSION'\nand add its parent"
    message="$message directory to the PATH environment variable."

    {
        sqlite_version_info="$(sqlite$REQUIRED_SQLITE_VERSION -version 2>/dev/null)"
    } || {
        wrap_bad "SQLite $REQUIRED_SQLITE_VERSION" "Not found"
        add_to_dependency_failures "$message"
        return
    }

    sqlite_version="$(echo $sqlite_version_info | $CUT -d" " -f1 )"
    if [ -n "$sqlite_version" ]
    then
        wrap_good "SQLite version" "$sqlite_version"
    else
        message="Failed to extract the SQLite version from the string: "
        message="$message '$sqlite_version_info'"
        error "$message"
        add_to_errors "$message"
    fi
}

################################################################################
## Checks if Python is installed on the device and if installed, whether its
## version meets the requirement for Greengrass lambdas.
################################################################################
check_python_version() {
    local python_version_info
    local python_version
    local message="Could not find the binary 'python$REQUIRED_PYTHON_VERSION'.\n"
    message="$message\nIf Python $REQUIRED_PYTHON_VERSION is installed on the"
    message="$message device, name the binary 'python$REQUIRED_PYTHON_VERSION'"
    message="$message and add its parent \ndirectory to the PATH environment variable."
    message="$message Python $REQUIRED_PYTHON_VERSION is required to execute Python"
    message="$message\nlambdas on Greengrass core."

    {
        ## Python reports the version to STDERR, so have to redirect STDERR to
        ## STDOUT and not capture the output in a variable.
        python$REQUIRED_PYTHON_VERSION --version >/dev/null 2>&1
    } || {
        wrap_warn "Python $REQUIRED_PYTHON_VERSION" "Not found"
        add_to_dependency_warnings "$message"
        return
    }

    python_version_info="$(python$REQUIRED_PYTHON_VERSION --version 2>&1)"
    python_version="$(echo $python_version_info | $CUT -d" " -f2)"
    if [ -n "$python_version_info" ]
    then
        wrap_good "Python version" "$python_version"
    else
        message="Failed to extract the Python version from the string:"
        message="$message '$python_version_info'"
        warn "$message"
        add_to_warnings "$message"
    fi
}

################################################################################
## Checks if NodeJS is installed on the device and if installed, whether its
## version is at least the minimum required version of NodeJS for Greengrass
## lambdas.
################################################################################
check_nodejs_version() {
    local node_version_info
    local node_version
    local message="Could not find the binary 'nodejs$REQUIRED_NODEJS_VERSION'.\n"
    message="$message\nIf NodeJS $REQUIRED_NODEJS_VERSION or later is installed"
    message="$message on the device, name the binary 'nodejs$REQUIRED_NODEJS_VERSION'"
    message="$message and\nadd its parent directory to the PATH environment variable."
    message="$message NodeJS $REQUIRED_NODEJS_VERSION or later is\nrequired to execute"
    message="$message NodeJS lambdas on Greengrass core."

    {
        node_version_info="$(nodejs$REQUIRED_NODEJS_VERSION --version 2>/dev/null)"
    } || {
        wrap_warn "NodeJS $REQUIRED_NODEJS_VERSION" "Not found"
        add_to_dependency_warnings "$message"
        return
    }

    node_version="$(echo ${node_version_info#v})"
    if [ -n "$node_version" ]
    then
        wrap_good "NodeJS version" "$node_version"
    else
        message="Failed to extract the NodeJS version from the string: '$node_version_info'"
        warn "$message"
        add_to_warnings "$message"
    fi
}

################################################################################
## Extracts the java version from the output of "java -version".
################################################################################
extract_java_version() {
    local java_version_info="$1"

    {
        JAVA_VERSION="$(echo "$java_version_info" | $HEAD -n 1 | $GREP -o "[_0-9\.]*")"
    } || {
        message="Failed to extract the Java version from the string: '$java_version_info'"
        warn "$message"
        add_to_warnings "$message"
    }
    
}

################################################################################
## Checks if Java is installed on the device and if installed, whether its
## version is at least the minimum required version of Java for Greengrass
## lambdas.
################################################################################
check_java_version() {
    local java_version_info
    local java_version
    local message="Could not find the binary 'java$REQUIRED_JAVA_VERSION'.\n"
    message="$message\nIf Java $REQUIRED_JAVA_VERSION or later is installed on"
    message="$message the device name the binary 'java$REQUIRED_JAVA_VERSION'"
    message="$message and add its\nparent directory to the PATH environment"
    message="$message variable. Java $REQUIRED_JAVA_VERSION or later is required"
    message="$message to\nexecute Java lambdas on Greengrass core."
    
    {
        ## Java reports the version to STDERR, so have to redirect STDERR to
        ## STDOUT and not capture the output in a variable.
        java$REQUIRED_JAVA_VERSION -version >/dev/null 2>&1
    } || {
        wrap_warn "Java $REQUIRED_JAVA_VERSION" "Not found"
        add_to_dependency_warnings "$message"
        return
    }

    java_version_info="$(java$REQUIRED_JAVA_VERSION -version 2>&1)"
    extract_java_version "$java_version_info"
    if [ -n "$JAVA_VERSION" ]
    then
        wrap_good "Java version" "$JAVA_VERSION"
    else
        message="Failed to extract the Java version from the string: '$java_version_info'"
        warn "$message"
        add_to_warnings "$message"
    fi
}

################################################################################
## Sorts and removes duplicates from the list of OpenSSL versions available on
## the device, sets the global variable 'OPENSSL_VERSION' if an OpenSSL version
## >= MINIMUM_REQUIRED_OPENSSL_VERSION is found.
################################################################################
get_openssl_version() {
    local openssl_versions="$1"

    openssl_unique_versions="$(echo "$openssl_versions" | $SORT | $UNIQ | $CUT -d" " -f2)"
    for version in $openssl_unique_versions
    do
        compare_versions "$version" "$MINIMUM_REQUIRED_OPENSSL_VERSION"
        if [ $GREATER_OR_EQUALS -eq 1 ]
        then
            OPENSSL_VERSION="$version"
            return
        fi
    done
}

################################################################################
## Finds the OpenSSL versions available on the device. The result may contain
## duplicates.
##
## For each file in the variable "cmd_output", prints the readable characters of
## the file using the "strings" command, greps the file contents for the pattern
## "OpenSSL <openssl_version>"
##
## alinux % echo "$cmd_output" | xargs strings | grep -Eo "## ## OpenSSL[0-9]+\.[0-9]+\.[0-9]*"
## OpenSSL 0.9.8
## OpenSSL 0.9.8
## OpenSSL 0.9.8
## OpenSSL 0.9.8
## OpenSSL 0.9.8
## OpenSSL 1.0.1
## OpenSSL 1.0.1
## OpenSSL 1.0.1
## OpenSSL 1.0.1
## OpenSSL 1.0.1
## OpenSSL 1.0.1
## OpenSSL 1.0.1
## OpenSSL 1.0.1
## OpenSSL 1.0.1
## OpenSSL 1.0.1
## OpenSSL 1.0.1
## OpenSSL 1.0.1
## OpenSSL 1.0.1
## OpenSSL 1.0.1
## OpenSSL 1.0.1
## OpenSSL 0.9.8
## OpenSSL 0.9.8
## OpenSSL 0.9.8
## OpenSSL 0.9.8
################################################################################
process_libssl_paths() {
    local cmd_output="$1"
    local openssl_versions

    {
        openssl_versions="$(echo "$cmd_output" | $XARGS $STRINGS | \
        $GREP -Eo "OpenSSL [0-9]+\.[0-9]+\.[0-9]*")"
    } && {
        get_openssl_version "$openssl_versions"
    } || {
        no_op
        ## Ignore the error. The error will be detected in the parent functions
        ## since the global variable OPENSSL_VERSION will be an empty string.
    }
}

################################################################################
## Rerieves the abolute paths of the candidate libssl .so files by searching
## recursively in /lib, /usr/lib and /usr/local/lib (the standard paths where
## shared object files are found).
##
## alinux % find /usr*/lib* /lib* -name "libssl*" | grep "\.so"
## /usr/lib/libssl.so.0.9.8e
## /usr/lib/libssl.so.10
## /usr/lib/libssl.so.1.0.1k
## /usr/lib/libssl.so.1.0.0
## /usr/lib/libssl3.so
## /usr/lib/libssl.so.6
## /usr/lib64/libssl.so.0.9.8e
## /usr/lib64/libssl.so.10
## /usr/lib64/libssl.so.1.0.1k
## /usr/lib64/libssl3.so
## /usr/lib64/libssl.so.6
################################################################################
get_openssl_version_using_find() {
    local find_output

    local search_paths="/usr/lib* /lib*"
    if [ -d "/usr/local/bin" ]
    then
        search_paths="$search_paths /usr/local/bin"
    fi

    {
        find_output="$($FIND $search_paths -name "libssl*" | $GREP "\.so")"
    } && {
        process_libssl_paths "$find_output"
    } || {
        no_op
        ## Ignore the error. The error will be detected in the calling function
        ## since the global variable OPENSSL_VERSION will be an empty string.
    }
}

################################################################################
## Retrieves the list of candidate libssl paths using the 'ldconfig' command.
##
## Prints the lists of libraries stored in the cache, filters the results by
## 'libssl' and retrieves the absolute paths of candidate libssl .so files by
## retrieving the fourth field delimited by space:
## 
## alinux % ldconfig -p | grep libssl
## libssl3.so (libc6,x86-64) => /usr/lib64/libssl3.so
## libssl3.so (libc6) => /usr/lib/libssl3.so
## libssl.so.10 (libc6,x86-64) => /usr/lib64/libssl.so.10
## libssl.so.10 (libc6) => /usr/lib/libssl.so.10
## libssl.so.6 (libc6,x86-64) => /usr/lib64/libssl.so.6
## libssl.so.6 (libc6) => /usr/lib/libssl.so.6
##
## alinux % ldconfig -p | grep libssl | cut -d" " -f 4
## /usr/lib64/libssl3.so
## /usr/lib/libssl3.so
## /usr/lib64/libssl.so.10
## /usr/lib/libssl.so.10
## /usr/lib64/libssl.so.6
## /usr/lib/libssl.so.6
################################################################################
get_openssl_version_using_ldconfig() {
    local ldconfig_output

    {
        ldconfig_output="$(ldconfig -p 2>/dev/null | $GREP libssl)"
    } && {
        ldconfig_output="$(echo "$ldconfig_output" | $CUT -d" " -f 4)"
        process_libssl_paths "$ldconfig_output"
    } || {
        no_op
        ## Ignore the error. The error will be detected in the calling function
        ## since the global variable OPENSSL_VERSION will be an empty string.
    }
}

################################################################################
## Checks if the version of OpenSSL on the device is at least the minimum
## required version for the Over The Air(OTA) agent.
################################################################################
check_openssl_version() {
    local message

    ## First, try to get the version of OpenSSL using the 'ldconfig' command.
    get_openssl_version_using_ldconfig

    ## If the global variable 'OPENSSL_VERSION' is an empty string, fall back to
    ## finding the OpenSSL version by searching through the shared libraries
    ## using the 'find' command.
    if [ "$OPENSSL_VERSION" = "" ]
    then
        get_openssl_version_using_find
    fi

    ## Warn, if the global variable is still an empty string.
    if [ "$OPENSSL_VERSION" = "" ]
    then
        wrap_warn "OpenSSL (>= $MINIMUM_REQUIRED_OPENSSL_VERSION)" "Not found"
        message="Could not find OpenSSL version >= $MINIMUM_REQUIRED_OPENSSL_VERSION"
        message="$message on the device after searching through the\nstandard paths"
        message="$message : /lib, /usr/lib and /usr/local/lib.\n"
        message="$message\nThe Over The Air(OTA) agent requires OpenSSL version"
        message="$message $MINIMUM_REQUIRED_OPENSSL_VERSION or later to run."
        add_to_dependency_warnings "$message"
        return
    fi

    wrap_good "OpenSSL version" "$OPENSSL_VERSION"
}

################################################################################
## Checks if a command is present on the device.
################################################################################
check_if_command_present() {
    local cmd="$1"
    local message="The '$cmd' command was not found on the device. '$cmd'"
    message="$message is required if the\nOver The Air(OTA) agent is used."

    {
        command -v $cmd 2>/dev/null 1>&2 && wrap_good "$cmd" "Present"
    } || {
        wrap_warn "$cmd" "Not found"
        add_to_dependency_warnings "$message"
    }
}

################################################################################
## Checks if the software packages and commands required for the Over The
## Air(OTA) agent are present on the device.
################################################################################
check_ota_agent_req() {
    check_openssl_version
    check_if_command_present "wget"
    check_if_command_present "realpath"
    check_if_command_present "tar"
    check_if_command_present "readlink"
    check_if_command_present "basename"
    check_if_command_present "dirname"
    check_if_command_present "pidof"
    check_if_command_present "df"
    check_if_command_present "grep"
    check_if_command_present "umount"
}

check_sw_packages() {
    info "----------------------------Commands and software packages--------------------------"
    check_sqlite_version
    check_python_version
    check_nodejs_version
    check_java_version
    check_ota_agent_req
}