#!/bin/bash
# *******************************************************************
# **  @file eisman_rtree.sh - reverse resolve uninstall tree
# **
# **  Creation:     2016-06-09 dv
# **  Last update:  $Id: eisman_rtree.sh 43349 2016-09-15 11:42:42Z hbfl $
# **
# **  Copyright (c) 2016-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

# global tree index
PACKIDX="0"

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


# *******************************************************************
# **  use grep to perform a query upon 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
# **  @param  $5  packages to be excluded
# **  @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 excludes="$5"
    local result
    local resline
    local idx="1"
    local header=$((${FIELDHEADER} + 1))

    if [ -z "$version" ]
    then
        version='.*'
    fi

    if [ "${maxresults}" -eq "${UNLIMITED}" ]
    then
        if [ -z "${excludes}" ]
        then
            grep -E "^\[.*\]\[$package\]\[$version\]\[.*\]\[.*\]${field} " \
                 "${EISMAN_PATH}/installed.db" |
            cut  -d ']' -f 6-                  |
            cut  -c "${header}-"
        else
            grep -E "^\[.*\]\[$package\]\[$version\]\[.*\]\[.*\]${field} " \
                 "${EISMAN_PATH}/installed.db" |
            grep -v -E "^\[.*\]\[${excludes}\]\[.*\]\[.*\]\[.*\]" |
            cut  -d ']' -f 6-                  |
            cut  -c "${header}-"
        fi
    else
        if [ -z "${excludes}" ]
        then
            grep -E "^\[.*\]\[$package\]\[$version\]\[.*\]\[.*\]${field} " \
                 "${EISMAN_PATH}/installed.db" |
            cut  -d ']' -f 6-                  |
            cut  -c "${header}-"               |
            head -n "${maxresults}"
        else
            grep -E "^\[.*\]\[$package\]\[$version\]\[.*\]\[.*\]${field} " \
                 "${EISMAN_PATH}/installed.db" |
            grep -v -E "^\[.*\]\[${excludes}\]\[.*\]\[.*\]\[.*\]" |
            cut  -d ']' -f 6-                  |
            cut  -c "${header}-"               |
            head -n "${maxresults}"
        fi
    fi
}


# *******************************************************************
# **  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'
}


# *******************************************************************
# **  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
}


# *******************************************************************
# **  check database if the package in question is still beeing
# **  referenced to
# **  @param  $1  package
# **  @return 0 (true) if required, 1 (false) if not required
# *******************************************************************

package_still_required()
{
    local package="$1"
    local excludelist=""
    local result

    # never uninstall base
    if [ "${package}" = "base" ]
    then
        return 0
    fi

    # never uninstall eiskernel
    if [ "${package}" = "eiskernel"     -o \
         "${package}" = "eiskernel-smp" -o \
         "${package}" = "eiskernel-pae" -o \
         "${package}" = "eiskernel-virt" ]
    then
        return 0
    fi

    # output results to stdout
    # list found packages in reverse order
    local idx=$PACKIDX
    while [ "$idx" -gt 0 ]
    do
        eval name='$PACKAGE_'$idx'_NAME'

        if [ -z "${excludelist}" ]
        then
            excludelist="${name}"
        else
            excludelist="${excludelist}|${name}"
        fi
        idx=$(($idx - 1))
    done

    if [ -n "${excludelist}" ]
    then
        result=$(query_package_db_value ".*" "" "require    ${package}([ ]?|$)" "${UNLIMITED}" "(${excludelist})")
        if [ -z "${result}" ]
        then
            # ${package} is not in use"
            return 1
        else
            # "${package} is in use"
            return 0
        fi
    else
        result=$(query_package_db_value ".*" "" "require    ${package}([ ]?|$)" "${UNLIMITED}" "")
        if [ -z "${result}" ]
        then
            # "${package} is not in use"
            return 1
        else
            # "${package} is in use"
            return 0
        fi
    fi
}


# *******************************************************************
# **  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 a certain package. Build a filter from the list of
# **  packages that are already in the list of packages to uninstall.
# **  Now search for requirements for the package stated by packages
# **  that are not members of the package-filter
# **  @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
        if ! package_still_required "${packname}"
        then
            # add package to list
            add_package       "${packname}" "${packversion}" "${level}"

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


# *******************************************************************
# **  find all requirements of an installed package. Check if
# **  these can be uninstalled too
# **  @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}" "")

    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 packname is still valid...
        if [ -n "${packname}" ]
        then
            local bestver=$(find_best_match ${packname} ${packver})

            if [ -z "${bestver}" ]
            then
                # the package is obviously not installed. Check if this
                # requirements is in outdated list and retry if that is
                # the case
                local outdated=$(query_outdated_db "${packname}" "substitude")

                if [ -n "${outdated}" ]
                then
                    packname="${outdated%% *}"
                    packver="${outdated#* }"

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

                    if [ -n "${packname}" ]
                    then
                        bestver=$(find_best_match ${packname} ${packver})
                    fi
                fi
            fi

            if [ -n "${bestver}" ]
            then
                # 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
}


# *******************************************************************
# **  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"

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

    # check if package may be removed
    if ! package_still_required "${package}"
    then
        # now we can add our root package to the list.
        add_package "${package}" "${version}" 0

        # find requirements and check their dependencies
        find_requirements "${package}" "${version}" 1
    fi
}


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

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

    # 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 installed!" >&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}"
    fi
}


# *******************************************************************
# **  main program
# *******************************************************************
main()
{
    # list of processed packages
    packages=""

    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

        # add package to the list of processed packages
        packages="${packages} ${name}"

        # process package
        process_single_package \
            "${name}" \
            "${version}"

        shift
    done

    # now, all processed packages should be in the result list
    # this has to be checked after all packages have been processed
    for package in $packages
    do
        local idx=$(find_package ${package})
        if [ -z "${idx}" ]
        then
            # package not found! This means that one of the root
            # packages specified on the command line havn't been added
            # to the result list due to pending requirements. Hence,
            # the complete result is invalid!
            PACKIDX=0
        fi
    done

    # output results to stdout
    # list found packages in reverse order
    if [ "$PACKIDX" -gt 0 ]
    then
        idx="1"
        total="0"
        while [ "$idx" -le "$PACKIDX" ]
        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"
    fi
}

main "${@}"
