#!/bin/bash
# *******************************************************************
# **  @file eisman_tree.sh - resolve requirement tree
# **
# **  Creation:     2013-01-13 dv
# **  Last update:  $Id: eisman_tree.sh 44743 2017-01-24 19:21:48Z dv $
# **
# **  Copyright (c) 2013-2017 the eisfair team, team(at)eisfair(dot)org
# **
# **  This program is free software; you can redistribute it and/or modify
# **  it under the terms of the GNU General Public License as published by
# **  the Free Software Foundation; either version 2 of the License, or
# **  (at your option) any later version.
# *******************************************************************

. /var/install/include/eislib
. /var/install/include/eismanlib

# debug file
# DEBUG_FILE="/tmp/eisman_tree.log"

# global search mode
SEARCH_MODE="stable"
SEARCH_URL=""

# query filters
STATE_FILTER="s"
URL_FILTER=".*"

# global tree index
PACKIDX="0"

# constant for unlimited number of results
UNLIMITED="9999"

# setup global database path for testing
if [ -n "${EISMAN_TEST_DB}" ]
then
    EISMAN_PATH="${EISMAN_TEST_DB}"
fi

# setup global binary path for testing
if [ -n "${EISMAN_TEST_BIN}" ]
then
    EISMAN_BIN_PATH="${EISMAN_TEST_BIN}"
fi


# *******************************************************************
# **  print current package list to debug file
# **  @param  $1  optional text
# **  @return n/a
# *******************************************************************

print_package_list()
{
    {
        echo "   -----------------------------------------------------"
        echo "    ${1} "
        echo "   -----------------------------------------------------"
        # output results to stdout
        # list found packages in reverse order
        local idx=${PACKIDX}
        echo "   idx | level | version  | status     | name           "
        echo "   ----+-------+----------+------------+----------------"
        while [ "${idx}" -gt 0 ]
        do
            eval local name='${PACKAGE_'${idx}'_NAME}'
            eval local version='${PACKAGE_'${idx}'_VERSION}'
            eval local status='${PACKAGE_'${idx}'_STATUS}'
            eval local level='${PACKAGE_'${idx}'_LEVEL}'

            printf "   %-3s | %-5s | %-8s | %-10s | %-17s\n" \
                "${idx}" "${level}" "${version}" "${status}" "${name}"
            idx=$((${idx} - 1))
        done
        echo "   ------------------------------------------------------"
    } >> ${DEBUG_FILE}
}


# *******************************************************************
# **  use grep to perform a query on the package.db file. Note that
# **  the global variable $STATE_FILTER is used to decide if the search
# **  covers only stable or all packages.
# **  @param  $1  package
# **  @param  $2  version (can be an empty string!)
# **  @param  $3  the info field to be returned
# **  @param  $4  max number of results
# **  @return (on stdout) data found in packages.db meeting the
# **          search criteria
# *******************************************************************

query_package_db_value()
{
    local package="${1}"
    local version="${2}"
    local field="${3}"
    local maxresults="${4}"
    local header=$((${FIELDHEADER} + 1))

    # perform query
    query_package_db      \
        ""                \
        ""                \
        "${package}"      \
        "${version}"      \
        "${STATE_FILTER}" \
        "${URL_FILTER}"   \
        "${field}"        \
        "${EISMAN_PATH}/packages.db" |
            cut  -c "${header}-"     |
            head -n "${maxresults}"
}


# *******************************************************************
# **  use grep to perform a query on the outdated.db file.
# **  @param  $1  package
# **  @param  $2  field
# **  @return (on stdout) data found in outdated.db meeting the
# **          search criteria
# *******************************************************************

query_outdated_db()
{
    local package="${1}"
    local field="${2}"
    local header=$((${FIELDHEADER} + 1))

    # perform query
    query_package_db      \
        "outdated"        \
        ""                \
        "${package}"      \
        ""                \
        ""                \
        ""                \
        "${field}"        \
        "${EISMAN_PATH}/outdated.db" |
            cut  -c "${header}-"
}


# *******************************************************************
# **  add a package to the list stored in
# **     PACKAGE_%_NAME
# **     PACKAGE_%_VERSION
# **     PACKAGE_%_SPACE
# **     PACKAGE_%_STATUS
# **     PACKAGE_%_URL
# **  @param  $1  package
# **  @param  $2  version
# **  @return n/a
# *******************************************************************

add_package()
{
    local package="${1}"
    local version="${2}"
    local level="${3}"

    local packspace=$(query_package_db_value  "${package}" "${version}" 'space' '1')
    local packstatus=$(query_package_db_value "${package}" "${version}" 'status' '1')
    local packurl=$(query_package_db_value    "${package}" "${version}" 'url' '1')

    if [ -z "${packspace}" ]
    then
        packspace='1'
    fi

    PACKIDX=$((${PACKIDX} + 1))
    eval 'PACKAGE_'${PACKIDX}'_NAME'='${package}'
    eval 'PACKAGE_'${PACKIDX}'_VERSION'='${version}'
    eval 'PACKAGE_'${PACKIDX}'_SPACE'='${packspace}'
    eval 'PACKAGE_'${PACKIDX}'_STATUS'='${packstatus}'
    eval 'PACKAGE_'${PACKIDX}'_URL'='${packurl}'
    eval 'PACKAGE_'${PACKIDX}'_LEVEL'='${level}'

    if [ -n "${DEBUG_FILE}" ]
    then
        print_package_list "Add package  ${package} (${version}) ${level}"
    fi
}


# *******************************************************************
# **  check if the package issues exclusions
# **  @param  $1  name of package
# **  @param  $2  version of package
# **  @return n/a
# *******************************************************************

check_exclusion()
{
    local package="${1}"
    local version="${2}"

    local exclusions=$(query_package_db_value  "${package}" "${version}" 'exclude' ${UNLIMITED})

    local IFS_BAK="${IFS}"
    IFS="
"

    for exclude in ${exclusions}
    do
        # first check if excluded package is already installed
        local state=$(${EISMAN_BIN_PATH}/eisman_check_version.sh ${exclude} "0.0.0")
        if [ "${state}" != "not-installed" ]
        then
            IFS="${IFS_BAK}"

            echo "error: unable to install required package \"${package}\"!" >&2
            echo "error: \"${package}\" <== exclude ==> \"${exclude}\" (installed)!" >&2
            exit 1
        fi

        # check if package is in the list of packages that are going to
        # be installed
        local idx=$(find_package ${exclude})
        if [ -n "${idx}" ]
        then
            IFS="${IFS_BAK}"

            echo "error: unable to install required package \"${package}\"!" >&2
            echo "error: \"${package}\" <== exclude ==> \"${exclude}\" (queued)!" >&2
            exit 1
        fi
    done

    IFS="${IFS_BAK}"
}


# *******************************************************************
# **  move single package to top of the list and set new index
# **  @param  $1  index of entry to move to top
# **  @param  $2  new iteration level of the entry
# **  @return n/a
# *******************************************************************

single_package_to_top()
{
    local index="${1}"
    local level="${2}"
    local next
    local name
    local version
    local status
    local space
    local url

    if [ "${index}" -lt "${PACKIDX}" ]
    then
        eval name='${PACKAGE_'${index}'_NAME}'
        eval version='${PACKAGE_'${index}'_VERSION}'
        eval status='${PACKAGE_'${index}'_STATUS}'
        eval space='${PACKAGE_'${index}'_SPACE}'
        eval url='${PACKAGE_'${index}'_URL}'

        while [ ${index} -lt ${PACKIDX} ]
        do
            next=$((${index} + 1))
            eval 'PACKAGE_'${index}'_NAME'='$PACKAGE_'${next}'_NAME'
            eval 'PACKAGE_'${index}'_VERSION'='$PACKAGE_'${next}'_VERSION'
            eval 'PACKAGE_'${index}'_SPACE'='$PACKAGE_'${next}'_SPACE'
            eval 'PACKAGE_'${index}'_STATUS'='$PACKAGE_'${next}'_STATUS'
            eval 'PACKAGE_'${index}'_URL'='$PACKAGE_'${next}'_URL'
            eval 'PACKAGE_'${index}'_LEVEL'='$PACKAGE_'${next}'_LEVEL'
            index=${next}
        done

        eval 'PACKAGE_'${PACKIDX}'_NAME'=${name}
        eval 'PACKAGE_'${PACKIDX}'_VERSION'=${version}
        eval 'PACKAGE_'${PACKIDX}'_SPACE'=${space}
        eval 'PACKAGE_'${PACKIDX}'_STATUS'=${status}
        eval 'PACKAGE_'${PACKIDX}'_URL'=${url}
        eval 'PACKAGE_'${PACKIDX}'_LEVEL'=${level}
    fi

    if [ -n "${DEBUG_FILE}" ]
    then
        print_package_list "Single Package To Top  index=${1} level=${2} PACKIDX=${PACKIDX}"
    fi
}


# *******************************************************************
# **  remove single package from list
# **  @param  $1  index of entry to remove
# **  @return n/a
# *******************************************************************

remove_single_package()
{
    local index="${1}"
    local next
    local name
    local version
    local status
    local space
    local url

    while [ ${index} -lt ${PACKIDX} ]
    do
        next=$((${index} + 1))
        eval 'PACKAGE_'${index}'_NAME'='${PACKAGE_'${next}'_NAME}'
        eval 'PACKAGE_'${index}'_VERSION'='${PACKAGE_'${next}'_VERSION}'
        eval 'PACKAGE_'${index}'_SPACE'='${PACKAGE_'${next}'_SPACE}'
        eval 'PACKAGE_'${index}'_STATUS'='${PACKAGE_'${next}'_STATUS}'
        eval 'PACKAGE_'${index}'_URL'='${PACKAGE_'${next}'_URL}'
        eval 'PACKAGE_'${index}'_LEVEL'='${PACKAGE_'${next}'_LEVEL}'
        index=${next}
    done

    PACKIDX=$((${PACKIDX} - 1))

    if [ -n "${DEBUG_FILE}" ]
    then
        print_package_list "  Remove Single Package  index=${1}"
    fi
}


# *******************************************************************
# **  search the list of packages stored in
# **     PACKAGE_%_NAME
# **     PACKAGE_%_VERSION
# **  for a specific entry
# **  @param  $1  package
# **  @return (on stdout) index of entry or "" if not found
# *******************************************************************

find_package()
{
    local package="${1}"
    local idx=1
    while [ "${idx}" -le "${PACKIDX}" ]
    do
        eval local name='${PACKAGE_'${idx}'_NAME}'
        if [ "${name}" = "${package}" ]
        then
            echo "${idx}"
            return
        fi

        idx=$((${idx} + 1))
    done
}


# *******************************************************************
# **  search for the best version of a package that is newer than
# **  a given version (newest update). In case of a requirement
# **  that can't be met by a already installed package, this is used
# **  to get the best version available.
# **  If -unstable switch has been specified the best version may have
# **  an unstable state, otherwise only stable packages are searched.
# **  @param  $1  package
# **  @param  $2  min. version (may be empty or 0.0.0 if not specified)
# **  @return (on stdout) best version found
# *******************************************************************

find_best_match()
{
    local package="${1}"
    local version="${2}"
    local resline=""
    local bestver=""
    local beststatus=""
    local result=$(query_package_db_value "${package}" "" "(version|status)" "${UNLIMITED}")
    local state

    if [ -z "${version}" ]
    then
        version="0.0.0"
    fi

    local IFS_BAK=${IFS}
    IFS="
"

    local foundver=""
    local foundstatus=""
    local idx=0
    for resline in ${result}
    do
        if [ "${idx}" -eq 0 ]
        then
            foundver="${resline}"
            idx=1
        else
            foundstatus="${resline}"
            idx=0

            local res=$(diff_version ${foundver} ${version})
            if [ "${res}" = "new" ] || [ "${res}" = "installed" ]
            then
                if [ -z "${bestver}" ]
                then
                    # if version has not been set yet, take the first match
                    # even if it is unstable
                    bestver="${foundver}"
                    beststatus="${foundstatus}"
                else
                    if [ "${beststatus}" != "stable" ] && [ "${foundstatus}" = "stable" ]
                    then
                        # always prefer the stable package even if the unstable package is
                        # more recent
                        bestver="${foundver}"
                        beststatus="${foundstatus}"

                    elif [ "${beststatus}" != "stable" ] || [ "${foundstatus}" = "stable" ]
                    then
                        # only if the previous result is unstable or the newly found package is
                        # (also) stable, we proceed (never go from stable to unstable!):
                        # compare the version of both packages and take the one that is more recent
                        state=$(diff_version "${foundver}" ${bestver})
                        if [ "${state}" = "new" ]
                        then
                            bestver="${foundver}"
                            beststatus="${foundstatus}"
                        fi
                    fi
                fi
            fi
        fi
    done

    IFS=${IFS_BAK}

    echo "${bestver}"
}


# *******************************************************************
# **  given is a certain requirement of a package. Check if it is already
# **  met by an installed package. If not search for the best
# **  available version and add the found package to the list
# **  of needed packages (if not already there).
# **  If the package has been added, it also may have requirements
# **  that have to be considered. Therefore the routine calls
# **  "find_requirements" or "find_linked_packages" and therefore enters
# **  a new recursion loop.
# **  Note: if a requirement cannot be resolved, an error is written
# **  to stdout and the whole script is terminated with exit code 1
# **  @param  $1  required package
# **  @param  $2  required version
# **  @param  $2  current iteration level
# **  @return n/a
# *******************************************************************

check_and_add_requirement()
{
    local packname="${1}"
    local packversion="${2}"
    local level="${3}"

    local idx=$(find_package ${packname})
    if [ -z "${idx}" ]
    then
        # package is new!

        # check if package can be installed
        check_exclusion "${packname}" "${packversion}"

        # add package to list
        add_package "${packname}" "${packversion}" "${level}"

        # find linked packages to this link (recursively)
        find_linked_packages "${packname}" "${packversion}" "$((${level} + 1))"

        # find requirements (recursively)
        find_requirements "${packname}" "${packversion}" "$((${level} + 1))"
    else
        # package already in the list, check if it's version is sufficient
        # for us. If not, remove the old package and (re)add the new one
        eval local listver='${PACKAGE_'${idx}'_VERSION}'

        local res=$(diff_version ${listver} ${packversion})
        if [ "${res}" = "old" ]
        then
            # listed package is old! This means we have to replace it with
            # the new one and have to deal with the new requirements.
            # This is a quite seldom and unusual situation and only happens, when
            # a stable package has to be replaced by an unstable one due to some
            # obscure compination of package requirements.
            # We only remove the single package and redo search for linked packages
            # and requirements. It may happen, that some requirements of the
            # original package remain in the list if the new version of the package
            # no longer requires them. But that is not really harmful! Therefore
            # we currently have to live with this behaviour

            # remove previous version from list
            remove_single_package "${idx}"

            # add package to list
            add_package       "${packname}" "${packversion}" "${level}"

            # find linked packages to this link (recursively)
            find_linked_packages "${packname}" "${packversion}" "$((${level} + 1))"

            # find requirements (recursively)
            find_requirements "${packname}" "${packversion}" "$((${level} + 1))"
        else
            # listed package is sufficient for us! We only have to move it
            # to the top of the list and redo the requirement search to bring
            # up all required packages too. But we have no need to search
            # for linked packages at this point, since they already are in
            # the list.

            # move package to the top of the list
            single_package_to_top "${idx}" "${level}"

            # find requirements (recursively)
            find_requirements "${packname}" "${packversion}" "$((${level} + 1))"
        fi
    fi
}


# *******************************************************************
# **  get requirements of a package and check if they are already
# **  met by an installed package. If not search for the best
# **  available version and call "check_and_add_requirement"
# **  to add the new requirement to the list
# **  @param  $1  package
# **  @param  $2  version
# **  @return n/a
# *******************************************************************

find_requirements()
{
    local package="${1}"
    local version="${2}"
    local level="${3}"
    local resline
    local result=$(query_package_db_value "${package}" "${version}" "require" "${UNLIMITED}")

    if [ -n "${DEBUG_FILE}" ]
    then
        {
            echo "${level} -------------------------------------------------------"
            echo "${level} BEGIN Find Requirements ${package} (${version})"
            echo "${level} -------------------------------------------------------"
        } >> ${DEBUG_FILE}
    fi

    local IFS_BAK=${IFS}
    IFS="
"

    for resline in ${result}
    do
        local packname="${resline%% *}"
        local packver="${resline#* }"

        if [ -z "${packver}" -o "${packver}" = "${packname}" ]
        then
            packver="0.0.0"
        fi

        if [ -n "${DEBUG_FILE}" ]
        then
            {
                echo "${level} --> Found ${packname} (${packver})"
            } >> ${DEBUG_FILE}
        fi

        # special handling of kernel requirements, since it must be trans-
        # lated into it's installed variant
        if [ "${packname}" = "eiskernel" ]
        then
            packname=${packname}-$(uname -r |
                awk -F'-' '{ print tolower($NF) }')
        fi

        # check if package is outdated
        local outdated=$(query_outdated_db "${packname}" "substitude")
        if [ -n "${outdated}" ]
        then
            if [ "${outdated}" = "dummy" ]
            then
                outdated=$(query_outdated_db "${packname}" "hint")
                echo "error: package \"${packname}\" required from \"${package}\" is outdated!" >&2
                echo "error: ${outdated}" >&2
                exit 1
            else
                packname=$(echo ${outdated} | cut -d ' ' -f 1)
                packver=$(echo ${outdated} | cut -d ' ' -f 2)

                # check if version has been supplied
                if [ "${packname}" = "${packver}" ]
                then
                    version="0.0.0"
                fi
            fi
        fi

        # if packname is still valid...
        if [ -n "${packname}" ]
        then
            local state=$(${EISMAN_BIN_PATH}/eisman_check_version.sh ${packname} ${packver}svn0)
            if [ "${state}" = "new" -o "${state}" = "not-installed" ]
            then
                local bestver=$(find_best_match ${packname} ${packver}svn0)

                if [ -z "${bestver}" ]
                then
                    echo "error: requirement ${packname} >= ${packver} not found!" >&2
                    exit 1
                fi

                # check if requirement is needed and recursively add the package
                # and all of it's requirements
                check_and_add_requirement "${packname}" "${bestver}" "${level}"
            fi
        fi
    done

    IFS=${IFS_BAK}

    if [ -n "${DEBUG_FILE}" ]
    then
        {
            echo "${level} -------------------------------------------------------"
            echo "${level} END Find Requirements ${package} (${version})"
            echo "${level} -------------------------------------------------------"
            echo ""
        } >> ${DEBUG_FILE}
    fi
}


# *******************************************************************
# **  find all packages that are specified with the linked-package
# **  tag. This packages are added to the requirement list if
# **  they are installed on the machine and the specified version
# **  is newer than the installed on
# **  @param  $1  package
# **  @param  $2  version
# **  @param  $3  level
# **  @return n/a
# *******************************************************************

find_linked_packages()
{
    local package="${1}"
    local version="${2}"
    local level="${3}"
    local resline

    local result=$(query_package_db_value "${package}" "${version}" "link" "${UNLIMITED}")

    if [ -n "${DEBUG_FILE}" ]
    then
        {
            echo "${level} -------------------------------------------------------"
            echo "${level} BEGIN Find Linked Packages ${package} (${version})"
            echo "${level} -------------------------------------------------------"
        } >> ${DEBUG_FILE}
    fi

    local ifs_bak=${IFS}
    IFS="
"

    for resline in ${result}
    do
        local packname="${resline%% *}"
        local packver="${resline#* }"
        local state

        if [ -z "${packver}" -o "${packver}" = "${packname}" ]
        then
            packver="${version}"
        fi

        if [ -n "${DEBUG_FILE}" ]
        then
            {
                echo "${level} --> Found ${packname} (${packver})"
            } >> ${DEBUG_FILE}
        fi

        # special handling of kernel requirements, since it must be trans-
        # lated into it's installed variant
        if [ "${packname}" = "eiskernel" ]
        then
            packname=${packname}-$(uname -r |
                awk -F'-' '{ print tolower($NF) }')
        fi

        # if the package in question is installed and it's version is older
        # than the requested one, we need to update the package!
        state=$(${EISMAN_BIN_PATH}/eisman_check_version.sh ${packname} ${packver}svn0)
        if [ "${state}" = "new" ]
        then
            # if the package that is linked to is in outdated packages but is
            # installed under it's original name, then it obviously has been
            # renamed or has been replaced by an other package
            local outdated=$(query_outdated_db "${packname}" "substitude")
            if [ -n "${outdated}" ]
            then
                if [ "${outdated}" != "dummy" ]
                then
                    packname=$(echo ${outdated} | cut -d ' ' -f 1)
                    packver=$(echo ${outdated} | cut -d ' ' -f 2)

                    # check if version has been supplied
                    if [ "${packname}" = "${packver}" ]
                    then
                        packver="0.0.0"
                    fi
                else
                    # if the successor is dummy, there is no replacement available
                    # in this case we ignore the linkage by setting the package
                    # name to zero
                    packname=""
                fi
            fi

            if [ -n "${packname}" ]
            then
                # now the best available version is beeing looked up, that is equal
                # or newer than the requested version
                local bestver=$(find_best_match ${packname} ${packver}svn0)

                # if the result is zero, then there is no package installed
                # that meets our needs
                if [ -z "${bestver}" ]
                then
                    echo "error: link requirement ${packname} >= ${packver} not found!" >&2
                    IFS=${ifs_bak}
                    exit 1
                fi

                # check if requirement is needed and recursively add the package
                # and all of it's requirements
                check_and_add_requirement "${packname}" "${bestver}" "${level}"
            fi
        fi
    done

    IFS=${ifs_bak}

    if [ -n "${DEBUG_FILE}" ]
    then
        {
            echo "${level} -------------------------------------------------------"
            echo "${level} END Find Linked Packages ${package} (${version})"
            echo "${level} -------------------------------------------------------"
            echo ""
        } >> ${DEBUG_FILE}
    fi
}


# *******************************************************************
# **  do complete package processing (search for requirements).
# **  in case of an error, it is output to stderr and the script is
# **  terminated with exit code 1
# **  @param  $1  package
# **  @param  $2  version
# **  @return n/a
# *******************************************************************

process_root_package()
{
    local package="${1}"
    local version="${2}"
    local result

    if [ -n "${DEBUG_FILE}" ]
    then
        {
            echo "0 -------------------------------------------------------"
            echo "0 BEGIN Process Root Package ${package} (${version})"
            echo "0 -------------------------------------------------------"
        } >> ${DEBUG_FILE}
    fi

    # check if package exists
    result=$(query_package_db_value "${package}" "${version}" 'version' '1')
    if [ -z "${result}" ]
    then
        echo "error: package ${package} not found!" >&2
        exit 1
    fi

    # check if package can be installed
    check_exclusion "${package}" "${version}"

    # add the root package
    add_package "${package}" "${version}" 0

    # when the root package has been found, any other required package
    # may be from any source. Therefore reset URL filter
    URL_FILTER=".*"

    # even if the global search mode tells us to prefer only stable
    # packages, any required package may also be unstable. Therefore
    # reset state filter
    STATE_FILTER=".*"

    # find linked packages to this package. Note that linked packages
    # normally state a requirement to the current package. Therefore
    # they must be searched before the package is added to the list
    find_linked_packages "${package}" "${version}" 1

    # search for requirements. From here on, no further URL or status
    # filtering is done.
    find_requirements "${package}" "${version}" 1

    if [ -n "${DEBUG_FILE}" ]
    then
        {
            echo "0 -------------------------------------------------------"
            echo "0 END Process Root Package ${package} (${version})"
            echo "0 -------------------------------------------------------"
        } >> ${DEBUG_FILE}
    fi
}


# *******************************************************************
# **  start package processing
# **  @param  $1  package
# **  @param  $2  version (optional)
# **  @return n/a
# *******************************************************************

process_single_package()
{
    local package="${1}"
    local version="${2}"

    # according to the global search mode, the first package must be
    # stable or may also be unstable. Therefore the state filter is
    # set accordingly
    if [ "${SEARCH_MODE}" = "stable" ]
    then
        STATE_FILTER="s"
    else
        STATE_FILTER=".*"
    fi

    # according to the global search URL, this first package must be
    # from a specific source or may be also from other sources. Therefore
    # the URL-filter is set accordingly
    if [ -n "${SEARCH_URL}" ]
    then
        URL_FILTER=$(echo "${SEARCH_URL}.*" | sed 's|\?|\\?|g')
    else
        URL_FILTER=".*"
    fi

    # check if outdated packages db is present
    if [ ! -f "${EISMAN_PATH}/outdated.db" ]
    then
        echo "error: file \"${EISMAN_PATH}/outdated.db\" missing!" >&2
        echo "       reinstall recent base update please." >&2
        exit 1
    fi

    # check if package is outdated. In case of a root package, we
    # issue an error and exit. In case of requirements we try to
    # use the substitude found in outdated db.
    local outdated=$(query_outdated_db "${package}" "substitude")
    if [ -n "${outdated}" ]
    then
        outdated=$(query_outdated_db "${package}" "hint")
        echo "error: package \"${package}\" is outdated!" >&2
        echo "error: ${outdated}" >&2
        exit 1
    fi

    # if version is not specified, search for best available package
    # if not found output error and exit script
    if [ -z "${version}" ]
    then
        version=$(find_best_match ${package} '0.0.0')
        if [ -z "${version}" ]
        then
            echo "error: package ${package} not found!" >&2
            exit 1
        fi
    fi

    # check if package is already in the tree (propably has been a
    # requirement found in a previous run of 'process_single_package')
    local idx=$(find_package ${package})
    if [ -z "${idx}" ]
    then
        # process the needed package (root of requirement tree)
        process_root_package "${package}" "${version}"
    else
        # package already in the list, check if it's version is sufficient
        # for us. If not, remove the old package and (re)add the new one
        eval local listver='${PACKAGE_'${idx}'_VERSION}'
        local res=$(diff_version ${listver} ${version})
        if [ "${res}" = "old" ]
        then
            # remove previous version from list
            remove_single_package "${idx}"

            # process the needed package (root of requirement tree)
            process_root_package "${package}" "${version}"
        fi
    fi
}


# *******************************************************************
# **  main program
# *******************************************************************
main()
{
    if [ -n "${DEBUG_FILE}" ]
    then
        echo "" > ${DEBUG_FILE}
    fi

    # read option switches
    while [ "${1:0:2}" = "--" ]
    do
        case ${1} in
        --unstable)
            SEARCH_MODE="unstable"
            shift
            ;;
        --url)
            shift
            SEARCH_URL="${1}"
            shift
            ;;
        --url=*)
            SEARCH_URL="${1:6}"
            shift
            ;;
        *)
            echo "error: invalid switch \"${1}\"!" >&2
            ;;
        esac
    done


    while [ "$#" -gt "0" ]
    do
        name="$(echo ${1} | cut -d '=' -f 1)"
        version="$(echo ${1} | cut -d '=' -f 2)"

        if [ "${version}" = "${name}" ]
        then
            # no version found
            version=""
        fi

        process_single_package \
            "${name}" \
            "${version}"

        shift
    done

    if [ -n "${DEBUG_FILE}" ]
    then
        print_package_list "Final result"
    fi

    # output results to stdout
    # list found packages in reverse order
    idx=${PACKIDX}
    total="0"
    while [ "${idx}" -gt 0 ]
    do
        eval name='${PACKAGE_'${idx}'_NAME}'
        eval version='${PACKAGE_'${idx}'_VERSION}'
        eval status='${PACKAGE_'${idx}'_STATUS}'
        eval space='${PACKAGE_'${idx}'_SPACE}'
        eval url='${PACKAGE_'${idx}'_URL}'
        eval level='${PACKAGE_'${idx}'_LEVEL}'

        total=$((${total} + ${space}))

        printf "%-8s %-8s %-17s %s\n" "${version}" "${status}" "${name}" "${url%/*}"
        idx=$((${idx} - 1))
    done
    echo "space = ${total}"
}

main "${@}"
