#!/bin/bash
# *******************************************************************
# **  @file eisman_stats.sh - show statistics
# **
# **  Creation:     2016-08-17 dv
# **  Last update:  $Id: eisman_stats.sh 44252 2016-12-30 23:14:30Z dv $
# **
# **  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


# *******************************************************************
# **  show help on arguments and possible options
# **  @param  n/a
# **  @return n/a
# *******************************************************************

show_help()
{
cat <<EOF

Show package database statistics

usage:
    eisman stats [OPTIONS]

options:
    --help                 show this help screen
    --print-age-stats      print statistics of package ages
    --print-require-stats  print requirement consistency analysis

EOF
}


# *******************************************************************
# **  return date of package database
# **  @param  n/a
# **  @return date on stdout
# *******************************************************************

get_last_update_time()
{
    date -r "${EISMAN_PATH}/packages.db" +"%F %X"
}


# *******************************************************************
# **  return date of installed packages database
# **  @param  n/a
# **  @return date on stdout
# *******************************************************************

get_last_installation_time()
{
    date -r "${EISMAN_PATH}/installed.db" +"%F %X"
}


# *******************************************************************
# **  return then number of available packages
# **  @param  $1  package status either "s" or "u" or empty
# **  @return number of packages on stdout
# *******************************************************************

get_number_of_packages()
{
    local status="${1}"

    # perform query
    query_package_db      \
        ""                \
        ""                \
        ""                \
        ""                \
        "${status}"       \
        ""                \
        "name"            \
        "${EISMAN_PATH}/packages.db" |
            wc -l
}


# *******************************************************************
# **  return then number of installed packages
# **  @param  $1  package status either "s" or "u" or empty
# **  @return number of packages on stdout
# *******************************************************************

get_installed_packages()
{
    local status="${1}"

    # perform query
    query_package_db      \
        ""                \
        ""                \
        ""                \
        ""                \
        "${status}"       \
        ""                \
        "name"            \
        "${EISMAN_PATH}/installed.db" |
            wc -l
}


# *******************************************************************
# **  show graph bar
# **  @param  $1 value
# **  @param  $2 normalisation value
# **  @return n/a
# *******************************************************************

show_bar()
{
    local value="${1}"
    local normal="${2}"

    local num=$((${value} * 40 / ${normal}))
    local idx=0

    echo -n "["

    while [ "${idx}" -lt "${num}" ]
    do
        echo -n "#"
        idx=$((${idx} + 1))
    done

    while [ "${idx}" -lt "40" ]
    do
        echo -n " "
        idx=$((${idx} + 1))
    done

    echo -n "]"
}


# *******************************************************************
# **  show current progress in percent
# **  @param  $1 progress in percent
# **  @return n/a
# *******************************************************************

show_progress()
{
    echo -en "\033[7D"
    printf "[%3d%%] completed!" "${1}"
    echo -en "\033[10D"
}


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


# *******************************************************************
# **  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 header=$((${FIELDHEADER} + 1))
    local package="$1"
    local version="$2"
    local resline=""
    local bestver=""
    local beststatus=""
    local state

    local result=$(query_package_db \
        ""                \
        ""                \
        "${package}"      \
        ""                \
        ""                \
        ""                \
        "version|status"  \
        "${EISMAN_PATH}/packages.db" |
            cut  -c "${header}-")

    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}"
}


# *******************************************************************
# **  print statistics of require integrity
# **  @param  n/a
# **  @return n/a
# *******************************************************************

do_print_require_stats()
{
    local header=$((${FIELDHEADER} + 1))
    local bad_requirements
    local outdated_requirements
    local bad_outdated_requirements
    local dummy_outdated_requirements
    local satisfied_requirements
    local line
    local linetotal
    local linecount
    local progress

    # perform query
    local packages=$(query_package_db \
        ""                \
        ""                \
        ""                \
        ""                \
        ""                \
        ""                \
        "name|version|----"  \
        "${EISMAN_PATH}/packages.db" |
            cut  -c "${header}-"                  |
            sed ':a;N;$!ba;s/\n/|/g;s/|-[-]*/\n/g;s/\n|/\n/g' |
            sort -u)

    mecho --info "Collecting requirement statistic data..."

    echo -n "       "
    show_progress 0

    # get total number of lines
    linetotal="$(echo "${packages}" | wc -l)"
    linecount="0"
    progress="0"

    IFS="
"

    for line in ${packages}
    do
        local package=$(echo "${line}" | cut -d '|' -f 1)
        local version=$(echo "${line}" | cut -d '|' -f 2)

        linecount=$((${linecount} + 1))

        local p=$((${linecount} * 100 / ${linetotal}))
        if [ "${p}" != "${progress}" ]
        then
            progress="${p}"
            show_progress "${progress}"
        fi

        # perform query
        local requirements=$(query_package_db \
            ""                \
            ""                \
            "${package}"      \
            "${version}"      \
            ""                \
            ""                \
            "require"         \
            "${EISMAN_PATH}/packages.db" |
                cut  -c "${header}-"     |
                sort -u)

        for require in ${requirements}
        do
            if [ -n "${require}" ]
            then
                local req_package=$(echo "${require}" | cut -d ' ' -f 1)
                local req_version=$(echo "${require}" | cut -d ' ' -f 2)

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

                if [ "${req_package}" = "${req_version}" ]
                then
                    req_version="0.0.0"
                fi

                # perform query
                local result=$(find_best_match "${req_package}" "${req_version}svn0")

                if [ -z "${result}" ]
                then

                    # check if package is outdated
                    local outdated=$(query_outdated_db "${req_package}" "substitude")
                    if [ -n "${outdated}" ]
                    then
                        od_req_package=$(echo ${outdated} | cut -d ' ' -f 1)
                        od_req_version=$(echo ${outdated} | cut -d ' ' -f 2)

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

                        if [ "${od_req_package}" = "dummy" ]
                        then
                            # dummy requirement
                            dummy_outdated_requirements="${dummy_outdated_requirements}${IFS}${package} ${version} > ${req_package} ${req_version} / ${od_req_package} ${od_req_version}"
                        else
                            result=$(find_best_match "${od_req_package}" "${od_req_version}svn0")
                            if [ -z "${result}" ]
                            then
                                # broken outdated requirement
                                bad_outdated_requirements="${bad_outdated_requirements}${IFS}${package} ${version} > ${req_package} ${req_version} / ${od_req_package} ${od_req_version}"
                            else
                                # valid outdated requirement
                                outdated_requirements="${outdated_requirements}${IFS}${package} ${version} > ${req_package} ${req_version} / ${od_req_package} ${od_req_version}"
                            fi
                        fi
                    else
                        bad_requirements="${bad_requirements}${IFS}${package} ${version} > ${req_package} ${req_version}"
                    fi
                else
                    satisfied_requirements="${satisfied_requirements}${IFS}"
                fi
            fi
        done
    done

    show_progress 100
    mecho
    mecho

    mecho --info "Requirement statistics:"
    mecho -n "  Number of satisfied:       "
    mecho    "$(($(echo "${satisfied_requirements}" | wc -l) - 1))"
    mecho -n "  Number of broken:          "
    mecho    "$(($(echo "${bad_requirements}" | wc -l) - 1))"
    mecho -n "  Number of outdated:        "
    mecho    "$(($(echo "${outdated_requirements}" | wc -l) - 1))"
    mecho -n "  Number of dummy outdated:  "
    mecho    "$(($(echo "${dummy_outdated_requirements}" | wc -l) - 1))"
    mecho -n "  Number of broken outdated: "
    mecho    "$(($(echo "${bad_outdated_requirements}" | wc -l) - 1))"

    if [ "$(($(echo "${bad_requirements}" | wc -l) - 1))" -gt "0" ]
    then
        mecho
        mecho --info "Packages with broken requirements:"
        for line in ${bad_requirements}
        do
            local name=$(echo ${line} | cut -d '>' -f '1')
            local req=$(echo ${line} | cut -d '>' -f '2-')

            printf "  %-26s -> %s\n" "${name}" "${req}"
        done
    fi

    if [ "$(($(echo "${outdated_requirements}" | wc -l) - 1))" -gt "0" ]
    then
        mecho
        mecho --info "Packages with valid outdated requirements:"
        for line in ${outdated_requirements}
        do
            local name=$(echo ${line} | cut -d '>' -f '1')
            local req=$(echo ${line} | cut -d '>' -f '2-')

            printf "  %-26s -> %s\n" "${name}" "${req}"
        done
    fi

    if [ "$(($(echo "${dummy_outdated_requirements}" | wc -l) - 1))" -gt "0" ]
    then
        mecho
        mecho --info "Packages with dummy outdated requirements:"
        for line in ${dummy_outdated_requirements}
        do
            local name=$(echo ${line} | cut -d '>' -f '1')
            local req=$(echo ${line} | cut -d '>' -f '2-')

            printf "  %-26s -> %s\n" "${name}" "${req}"
        done
    fi

    if [ "$(($(echo "${bad_outdated_requirements}" | wc -l) - 1))" -gt "0" ]
    then
       mecho
       mecho --info "Packages with broken outdated requirements:"
       for line in ${bad_outdated_requirements}
       do
           local name=$(echo ${line} | cut -d '>' -f '1')
           local req=$(echo ${line} | cut -d '>' -f '2-')

           printf "  %-26s -> %s\n" "${name}" "${req}"
       done
    fi

    mecho
    IFS=${OLDIFS}
}


# *******************************************************************
# **  run through the list of packages and print old ones, i.e.
# **  - older than 6  years
# **  - older than 8  years
# **  - older than 10 years
# **  @param  n/a
# **  @return statistics on stdout
# *******************************************************************

do_print_age_stats()
{
    local header=$((${FIELDHEADER} + 1))
    local year=$(date +%Y)
    local month=$(date +%m)
    local day=$(date +%d)

    local seconds_ts10=$(date -d $((${year} - 10))-${month}-${day} +%s)

    # perform query
    local packages=$(query_package_db \
        ""                \
        ""                \
        ""                \
        ""                \
        ""                \
        ""                \
        "name|version|date|----"  \
        "${EISMAN_PATH}/packages.db" |
            cut  -c "${header}-"                  |
            sed ':a;N;$!ba;s/\n/|/g;s/|-[-]*/\n/g;s/\n|/\n/g')

    local OLDIFS=IFS
    local line
    local packagedate
    local package_ts
    local old_packages_10

    local year_0_ts=$(date -d $((${year} -  0))-${month}-${day} +%s)
    local year_1_ts=$(date -d $((${year} -  1))-${month}-${day} +%s)
    local year_2_ts=$(date -d $((${year} -  2))-${month}-${day} +%s)
    local year_3_ts=$(date -d $((${year} -  3))-${month}-${day} +%s)
    local year_4_ts=$(date -d $((${year} -  4))-${month}-${day} +%s)
    local year_5_ts=$(date -d $((${year} -  5))-${month}-${day} +%s)
    local year_6_ts=$(date -d $((${year} -  6))-${month}-${day} +%s)
    local year_7_ts=$(date -d $((${year} -  7))-${month}-${day} +%s)
    local year_8_ts=$(date -d $((${year} -  8))-${month}-${day} +%s)
    local year_9_ts=$(date -d $((${year} -  9))-${month}-${day} +%s)
    local year_10_ts=$(date -d $((${year} - 10))-${month}-${day} +%s)

    local year_0_count="0"
    local year_1_count="0"
    local year_2_count="0"
    local year_3_count="0"
    local year_4_count="0"
    local year_5_count="0"
    local year_6_count="0"
    local year_7_count="0"
    local year_8_count="0"
    local year_9_count="0"
    local year_10_count="0"

    local total_count="0"
    local max_count="0"


    mecho --info "Collecting age statistic data..."

    IFS="
"

    for line in ${packages}
    do
        packagedate=$(echo "${line}" | cut -d '|' -f 3)
        package_ts=$(date -d ${packagedate} +%s)

        idx=10
        while [ "${idx}" -ge 0 ]
        do
            eval local limit='${year_'${idx}'_ts}'
            if [ "${package_ts}" -le "${limit}" ]
            then
                eval local count='${year_'${idx}'_count}'

                count=$((${count} + 1))
                if [ "${count}" -gt "${max_count}" ]
                then
                    max_count="${count}"
                fi

                eval 'year_'${idx}'_count'="${count}"

                break
            fi

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


        if [ "${package_ts}" -le "${seconds_ts10}" ]
        then
            old_packages_10="${old_packages_10}${IFS}${line}"
        fi
    done

    mecho
    mecho --info "Statistic diagram of package ages:"
    mecho "  older than  0 years:  $(show_bar ${year_0_count} ${max_count}) ${year_0_count}"
    mecho "  older than  1 years:  $(show_bar ${year_1_count} ${max_count}) ${year_1_count}"
    mecho "  older than  2 years:  $(show_bar ${year_2_count} ${max_count}) ${year_2_count}"
    mecho "  older than  3 years:  $(show_bar ${year_3_count} ${max_count}) ${year_3_count}"
    mecho "  older than  4 years:  $(show_bar ${year_4_count} ${max_count}) ${year_4_count}"
    mecho "  older than  5 years:  $(show_bar ${year_5_count} ${max_count}) ${year_5_count}"
    mecho "  older than  6 years:  $(show_bar ${year_6_count} ${max_count}) ${year_6_count}"
    mecho "  older than  7 years:  $(show_bar ${year_7_count} ${max_count}) ${year_7_count}"
    mecho "  older than  8 years:  $(show_bar ${year_8_count} ${max_count}) ${year_8_count}"
    mecho "  older than  9 years:  $(show_bar ${year_9_count} ${max_count}) ${year_9_count}"
    mecho "  older than 10 years:  $(show_bar ${year_10_count} ${max_count}) ${year_10_count}"

    mecho
    mecho --info "Packages older than 10 years:"
    old_packages_10=$(echo "${old_packages_10}" | sort -u)
    for line in ${old_packages_10}
    do
        local name=$(echo ${line} | cut -d '|' -f '1')
        local version=$(echo ${line} | cut -d '|' -f '2')
        local packdate=$(echo ${line} | cut -d '|' -f '3' | sed 's|/|-|g')
        printf "  %-22s %-15s (%s)\n" "${name}" "${version}" "${packdate}"
    done

    IFS=${OLDIFS}
}


# *******************************************************************
# **  main program
# *******************************************************************
main()
{
    local print_age_stats="no"
    local print_require_stats="no"

    while [ "${1:0:2}" = "--" ]
    do
        case $1 in
        --help)
            show_help
            exit 0
            ;;
        --print-age-stats)
            shift
            print_age_stats="yes"
            ;;
        --print-require-stats)
            shift
            print_require_stats="yes"
            ;;
        *)
            echo "error: unknown option \"$1\"!" >&2
            echo ""
            show_help
            exit 1
            ;;
        esac
    done

    mecho -n --info "Last database update:       "
    mecho    "$(get_last_update_time)"
    mecho -n --info "Last (un)installation time: "
    mecho    "$(get_last_installation_time)"
    mecho
    mecho -n --info "Total number of packages:   "
    mecho    "$(get_number_of_packages)"
    mecho -n --info "- Stable packages:          "
    mecho    "$(get_number_of_packages s)"
    mecho -n --info "- Unstable packages:        "
    mecho    "$(get_number_of_packages u)"
    mecho
    mecho -n --info "Installed packages:         "
    mecho    "$(get_installed_packages)"
    mecho -n --info "- Stable packages:          "
    mecho    "$(get_installed_packages s)"
    mecho -n --info "- Unstable packages:        "
    mecho    "$(get_installed_packages u)"

    if [ "${print_age_stats}" = "yes" ]
    then
        mecho
        do_print_age_stats
    fi

    if [ "${print_require_stats}" = "yes" ]
    then
        mecho
        do_print_require_stats
    fi
}

main "${@}"
