#!/bin/bash

Line2()   { echo "$@" >&2 ; }
Printf2() { printf "$@" >&2 ; }
Verbose2() { [ $quiet = no ] && Line2 "$@"; }

FormatVersion()     { printf "%s%s  " "$1" "$2" ; }
FormatName()        { FormatVersion "$1" ; }

Header() {
    local fname="${FUNCNAME[1]}"
    case "$fname" in
        KernelOrg) fname="$KOName" ;;
        KOVersion) fname="$KOVersionName" ;;
    esac
    local hdr="<b>$fname</b>"

    if [ "$fname" = "$RepoType" ] ; then
        hdr="<i>$hdr</i>"                    # currently used repo: italics font
    fi
    FormatVersion "$hdr"
}

LocalVersion() {
    local pkg="${1##*/}"
    expac -Q %v "$pkg"
}
Exist()        { [ -n "$version" ] && echo TRUE || echo FALSE ; }

Choose() {
    local version exist xx
    cmd+=(--field="${smallBegin}$(Header)${smallEnd}!Checkboxes for installing/uninstalling kernel packages":LBL "")
    for xx in "${kernels[@]}" ; do
        version="$(LocalVersion "$xx")"
        exist="$(Exist)"
        cmd+=(--field="":CHK "$exist")   # Field has one space, workaround for a yad bug.
                                         # Fixed in yad 7.2-1, removed workaround.
    done
}

PkgName() {
    local xx
    cmd+=(--field="${smallBegin}$(Header)${smallEnd}!Kernel package name":LBL "")
    for xx in "${kernels[@]}" ; do
        cmd+=(--field="${smallBegin}$(FormatName "$xx")${smallEnd}":LBL "")
    done
}

Installed() {
    local version xx
    local kernel=$(eos_running_kernel)
    case "$kernel" in
        linux*) ;;
        *) DIE "Internal error, cannot determine the running kernel." ;;
    esac
    local running=$(expac -Q %v $kernel)
    local ind1 ind2
    cmd+=(--field="${smallBegin}$(Header)${smallEnd}!Installed kernel package versions. The currently running kernel is in <i>italics</i>. A <b>bolded</b> string means the latest K.o. version.":LBL "")
    for xx in "${kernels[@]}" ; do
        version="$(LocalVersion "$xx")"
        ind1=""
        ind2=""
        if [ "$version" = "$running" ] ; then
            case "$xx" in
                *-headers) ;;
                *) ind1="<i>"; ind2="</i>" ;;       # currently running kernel: italics font
            esac
        fi
        if IsLatestKO "$version" ; then
            cmd+=(--field="${smallBegin}$(FormatVersion "$ind1<b>$version</b>$ind2")${smallEnd}":LBL "")
        else
            cmd+=(--field="${smallBegin}$(FormatVersion "$ind1$version$ind2")${smallEnd}":LBL "")
        fi
    done
}

StableVersions() {
    local pkg pkgs=()

    for pkg in "${kernels[@]}" ; do
        # prevent usage of the '*-testing' repos:
        case "$pkg" in
            linux | linux-headers | linux-lts | linux-lts-headers)
                pkgs+=("core/$pkg") ;;
            linux-zen | linux-zen-headers | linux-hardened | linux-hardened-headers | linux-rt | linux-rt-headers | linux-rt-lts | linux-rt-lts-headers)
                pkgs+=("extra/$pkg") ;;
            core/linux* | extra/linux*)
                pkgs+=("$pkg") ;;
        esac
    done
    readarray -t versions < <(expac -S %v "${pkgs[@]}")
}

Stable() {
    local version in_use=""
    if [ "$FUNCNAME" = "$RepoType" ] ; then
        in_use=" (currently in use)"
    fi
    cmd+=(--field="${smallBegin}$(Header)${smallEnd}!Versions available from the stable repos$in_use":LBL "")
    local versions=() ix=0
    StableVersions
    for ((ix=0; ix < ${#kernels[@]}; ix++)) ; do
        version="${versions[$ix]}"
        cmd+=(--field="${smallBegin}$(FormatVersion "$version")${smallEnd}":LBL "")
    done
}

TestingVersion() {
    local pkg="${1##*/}"
    if true ; then
        local tmp="$testinginfo"
        tmp=$(echo "$tmp" | grep -A2 /packages/core-testing/x86_64/$pkg/)
        tmp=$(echo "$tmp" | tail -n1)
        tmp=$(echo "$tmp" | sed -E 's|.*>([0-9][0-9a-z\.\-]+)<.*$|\1|')
        echo "$tmp"
    else
        echo "$testinginfo" | grep -A2 /packages/core-testing/x86_64/$pkg/ | tail -n 1 | sed -E 's|.*>([0-9][0-9a-z\.\-]+)<.*$|\1|'
    fi
}

UNAVAILABLE() { DIE "$1 is currently unavailable${2}!" ; }

Testing() {
    local in_use=""
    if [ "$FUNCNAME" = "$RepoType" ] ; then
        in_use=" (currently in use)"
    fi
    local URL="https://archlinux.org/packages/?repo=Core-Testing&q=linux+kernel+and+modules"
    local testinginfo
    local version xx

    testinginfo="$(/usr/bin/curl --fail -Ls $URL_timeout $URL)" || UNAVAILABLE "${URL%%/packages*}"

    [ -n "$testinginfo" ] || DIE "Fetching info from https://archlinux.org failed!"

    cmd+=(--field="${smallBegin}$(Header)${smallEnd}!Versions available from the testing repos$in_use":LBL "")
    for xx in "${kernels[@]}" ; do
        version="$(TestingVersion "$xx")"
        test -n "$version" || version="$na"
        cmd+=(--field="${smallBegin}$(FormatVersion "$version")${smallEnd}":LBL "")
    done
}

KernelOrgVersion() {
    local type="$1"
    local count="$2"
    [ -n "$count" ] || count=1
    local result="$(echo "$kernelorg" | grep -A2 $type: | grep -v EOL | grep strong | sed -e 's|^.*<strong>||' -e 's|</strong>.*$||')"
    case "$type" in
        mainline) ;;
        longterm) result="$(echo "$result" | head -n $count)" ;;
        *) result="$(echo "$result" | grep -v "[0-9][0-9\.]*-rc")" ;;
    esac
    echo "$result"
}

KernelOrg() {
    local version versions xx count=${#kernels[@]}
    cmd+=(--field="${smallBegin}$(Header)${smallEnd}!Kernel category at kernel.org":LBL "")
    for xx in "${org_kernels[@]}" ; do
        readarray -t versions < <(KernelOrgVersion "$xx")
        for version in "${versions[@]}" ; do
            cmd+=(--field="${smallBegin}$(FormatName "$xx")${smallEnd}":LBL "")
            ((count--))
        done
    done
    PaddingLines $count
}

KOVersion() {
    local version versions xx count=${#kernels[@]}
    local stableversion="$(KernelOrgVersion stable)"
    cmd+=(--field="${smallBegin}$(Header)${smallEnd}!Latest version announced at kernel.org":LBL "")
    for xx in "${org_kernels[@]}" ; do
        readarray -t versions < <(KernelOrgVersion "$xx")
        case "$xx" in
            stable|longterm)
                for version in "${versions[@]}" ; do
                    cmd+=(--field="${smallBegin}$(FormatVersion "<b>$version</b>")${smallEnd}":LBL "")
                    ((count--))
                done
                ;;
            *)
                for version in "${versions[@]}" ; do
                    cmd+=(--field="${smallBegin}$(FormatVersion "$version")${smallEnd}":LBL "")
                    ((count--))
                done
                ;;
        esac
    done
    PaddingLines $count
}

IsLatestKO() {
    local installed_version="$1"
    local type versions version

    installed_version="$(echo "$installed_version" | sed -e 's|-.*||' -e 's|\.[a-z].*||')"   # skip Arch suffixes

    for type in stable longterm ; do
        readarray -t versions < <(KernelOrgVersion "$type" 100)
        for version in "${versions[@]}" ; do
            if [ "$installed_version" = "$version" ] ; then
                return 0
            fi
        done
    done
    return 1
}

PaddingLines() {
    local count="$1"
    local xx
    for ((xx=0; xx<count; xx++)) ; do
        cmd+=(--field="${smallBegin}$(FormatVersion "")${smallEnd}":LBL "")
    done
}

CommonStuff() {
    local EOS_SCRIPTS_YAD=/usr/share/endeavouros/scripts/eos-script-lib-yad
    if [ ! -f $EOS_SCRIPTS_YAD ] ; then
        Line2 "Sorry, package 'eos-bash-shared' is not installed."
        exit 1
    fi
    source $EOS_SCRIPTS_YAD
    export -f eos_yad
    export -f eos_yad_RunCmdTermBash
    export -f eos_yad_terminal
    AssignIconVariables_in_eos_bash_shared
}

AkmConfig() {
    local conf=/etc/akm.conf
    if [ -f $conf ] ; then
        source $conf

        [ -n "$KERNEL_HEADER_WITH_KERNEL" ] && connect_header_with_kernel="$KERNEL_HEADER_WITH_KERNEL"
        [ -n "$AKM_KERNELS_HEADERS" ]       && akm_kernels_headers_user=("${AKM_KERNELS_HEADERS[@]}")
        [ -n "$AKM_WINDOW_WIDTH" ]          && akm_window_width=$AKM_WINDOW_WIDTH
        [ -n "$AKM_PREFER_SMALL_WINDOW" ]   && small_font=$AKM_PREFER_SMALL_WINDOW

        Options "$@"
        Verbose2 "==> Config file $conf read."
    else
        Options "$@"
    fi
}

DIE() {
    eos_yad --form --width=400 --title="AKM message" --image=$ICO_ERROR --text="$1" --button=yad-quit
    exit 1
}

UniqueArr() {
    # remove duplicates from an array 'arr'
    local -n arr=$1
    local to=()
    local xx yy

    for xx in "${arr[@]}" ; do
        for yy in "${to[@]}" ; do
            [ "$xx" = "$yy" ] && break
        done
        [ "$xx" != "$yy" ] && to+=("$xx")
    done
    arr=("${to[@]}")
}

Options() {
    local arg
    for arg in "$@" ; do
        case "$arg" in
            --connect-header) connect_header_with_kernel=yes ;;
            --no-progress) progress=no ;;
            --scroll) scroll=yes ;;
            --small) small_font=yes ;;
            -q | --quiet) quiet=yes ;;
            -h | --help)
                cat <<EOF >&2
Usage: $progname [options]
options:
    --connect-header      Make kernel and header package go together at install or uninstall.
    --scroll              Enable scrolling in the kernel info window. Useful if additional kernels are available.
    --small               Make the akm window smaller, meant for helping with small displays at 1366x768 resolution.
    --quiet | -q          Be less verbose.
    --no-progress         Don't show the progress window.
    --help | -h           This help.
EOF
                exit 0
                ;;
            -*) DIE "unsupported option '$arg'" ;;
            *) DIE "unsupported parameter '$arg'" ;;
        esac
    done
}

AvailableKernelsAndHeaders() {

    # Find kernel packages by finding packages which have words 'linux' and 'headers'.
    # From the output of 'pacman -Sl'
    # - find lines that have words: 'linux' and 'headers'
    # - drop lines containing 'testing' (=testing repo, causes duplicates) and 'linux-api-headers' (=not a kernel header)
    # - show the (header) package names
    # Now we have names of the kernel headers.
    # Then add the kernel packages to proper places and output the result.
    # Then display possible kernels and headers added by the user.

    # The output consists of a list of reponame and a package name formatted as: "reponame/pkgname"
    # For example:
    #    reponame/linux-xxx reponame/linux-xxx-headers
    #    reponame/linux-yyy reponame/linux-yyy-headers
    #    ...

    local header
    local kernel
    local pacman=/usr/bin/pacman
    local headers=$(expac -Ss '%r/%n' 'linux[-]*[^ pi]*-headers' | grep -Pv 'testing/linux-')    # 'pi' removes all 'linux-api-headers'

    # Get kernel names from header names and display them all
    for header in $headers ; do
        kernel=${header%-headers}
        echo "$kernel $header"
    done

    # Display kernels added by the user
    [ -n "${akm_kernels_headers_user}" ] && echo "${akm_kernels_headers_user[@]}"
}

Manual() {
    xdg-open $(eos-github2gitlab https://github.com/endeavouros-team/PKGBUILDS/blob/master/akm/README.md)
}
export -f Manual

Changelog() {
    xdg-open $(eos-github2gitlab https://github.com/endeavouros-team/PKGBUILDS/commits/master/akm)
}
export -f Changelog

RepoType() {
    pacman-conf --repo-list | grep "\-testing$" >/dev/null && echo "Testing" || echo "Stable"
}

Main() {
    local progname="${0##*/}"
    local quiet=no

    CommonStuff                   # fetches common bash definitions
    eos_assert_deps $progname yad || return 1
    Main2 "$@"
}

Main2() {
    local pacman=/usr/bin/pacman
    local na="(-)"
    local kernels=()
    local connect_header_with_kernel=no    # default, change with option --connect-header
    local akm_kernels_headers_user=()
    local akm_window_width=900
    local scroll=yes
    local small_font=no                    # yes = suitable for a small window, like 1366x768 resolution
    local smallBegin=""
    local smallEnd=""
    local nrofkernelitems                  # 8 with only upstream kernels
    local RepoType=$(RepoType)
    local progress=yes

    AkmConfig "$@"

    if [ "$small_font" = "yes" ] ; then
        smallBegin="<small>"
        smallEnd="</small>"
    fi

    Verbose2 "==> Info: detecting available kernel packages."
    kernels=( $(AvailableKernelsAndHeaders) )

    local functions=(  # order is important!
        Choose
        PkgName
        Installed
        Stable
        Testing
        KernelOrg
        KOVersion
    )

    nrofkernelitems=${#kernels[@]}
    [ $nrofkernelitems -gt 8 ] && scroll=yes

    local URL_timeout="--max-time 30"                      # used for all curl calls here

    local URL="https://www.kernel.org"
    local kernelorg=""
    local org_kernels=(mainline stable longterm linux-next)
    local KOName="K.o.category"
    local KOVersionName="K.o.version"
    local xx
    local t1=""
    local km="*"

    t1+="$smallBegin"
    t1+="Here's information about currently installed and available Linux kernels.\n"
    t1+="Install and/or uninstall kernel packages using the checkboxes on the leftmost column.\n\n"
    t1+="<i>Please make sure the system is fully updated when starting <b>$progname</b></i>.\n"
    t1+="\n"
    if [ "$small_font" = "no" ] ; then
        t1+="Column headers:\n"
        t1+="<tt>"
        t1+="    Choose        Checkboxes for installing/uninstalling kernel packages.\n"
        t1+="                  The installed packages are checked at start.\n"
        t1+="    PkgName       Kernel package name.\n"
        t1+="    Installed     Installed kernel package versions.\n"
        t1+="                  The currently running kernel is printed in italics.\n"
        t1+="                  The latest kernel.org version is printed in bold.\n"
        t1+="    Stable        Versions available from the stable repos.\n"
        t1+="    Testing       Versions available from the testing repos.\n"
        t1+="    $KOName  Kernel category at kernel.org.\n"
        t1+="    $KOVersionName   Latest version announced at kernel.org.\n\n"
        t1+="</tt>"
    fi
    t1+="Your currently used repo (Stable or Testing) is printed in italics (see also /etc/pacman.conf).\n"
    t1+="Marking <b>$na</b> means: info not available.\n\n"
    t1+="See file <tt>/etc/akm.conf</tt> for configuration details.\n"
    t1+="$smallEnd"

    local cmd=(
        eos_yad
        --width=$akm_window_width
        --use-interp
        --form --title="Simple kernel manager" --columns=${#functions[@]}
        --text="$t1"
        --image=$ICO_UPDATE
        --button=" ${smallBegin}Changelog${smallEnd}!applications-development!Show latest changes of this program":"Changelog"
        --button=" ${smallBegin}Help${smallEnd}!help-contents!Helps to use $progname":"Manual"
        --button=" ${smallBegin}Cancel${smallEnd}!gtk-cancel!Change nothing":1
        --button=" ${smallBegin}Execute${smallEnd}!system-run!Make your changes":0
    )
    if [ "$scroll" = "yes" ] ; then
        cmd+=(--scroll
              --height=700   # 660 is about the default
             )
    fi

    local progress_cmd=(
        eos_yad --progress --enable-log --log-on-top --log-expanded --log-height 150 --auto-close
        --width=400 --no-buttons
        --image=$ICO_INFO
        --title="AKM progress"
        --text="Fetching information, please wait..."
    )
    local ix=1 count=${#functions[@]} val
    local cmdfile="$(mktemp $HOME/.cache/akm.tmp.XXXXX)"    # will gather the whole value of 'cmd' into this file (used due to the progress window)

    if [ -z "$kernelorg" ] ; then
        local errcode=0
        kernelorg="$(/usr/bin/curl --fail -Ls $URL_timeout $URL)" || errcode=$?
        case $errcode in
            0) ;;
            *) eos-connection-checker || DIE "Network access required.\nPlease check the internet connection." && UNAVAILABLE "$URL" ;;
        esac
        test -n "$kernelorg" || DIE "Fetching info from $URL failed.\n"
    fi

    RunWithProgressOrNot
    
    local installed install=() uninstall=() pkg partner result result1="" retval

    result="$("${cmd[@]}")"
    retval=$?

    [ $retval -ne 0 ] && return
    test -z "$result" && return

    # For some users the yad output contains 2 lines instead of one ... ???
    if [ "$(echo "$result" | wc -l)" != "1" ] ; then
        Printf2 "==> Warning: yad output should have been exactly one line, but is:\n'$result'\n"
        result1="$result"
        result="$(echo "$result" | tail -n1)"
        Printf2 "==> Warning: using the last line:\n'$result'\n"
    fi
    if [ "$(echo "$result" | wc -l)" != "1" ] ; then
        DIE "cannot parse yad output:\n'$result1'\n"
    fi

    result="${result:1}"                               # skip first LBL field !!

    declare -A pkgstatus
    for ((ix=0; ix < ${#kernels[@]}; ix++)) ; do
        pkg="${kernels[$ix]}"
        pkgstatus[$pkg]="$(echo "$result" | cut -d '|' -f $((ix+1)))"   # TRUE or FALSE

        if [ "$connect_header_with_kernel" = "yes" ] ; then

            # Note: a kernel may have TRUE but header FALSE!
            # When connecting header with kernel this may conflict.
            # To solve this, assume kernel value is stronger.
            # Remember that the kernel value exists because it is set
            # before the header value in the local variable 'kernels' above.

            case "$pkg" in
                *-headers)
                    partner="${pkg%-headers}"
                    pkgstatus[$pkg]="${pkgstatus[$partner]}"  # kernel value overrides the header value!
                    ;;
            esac
        fi
    done

    for ((ix=0; ix < ${#kernels[@]}; ix++)) ; do
        pkg="${kernels[$ix]}"
        if [ "$connect_header_with_kernel" = "yes" ] ; then
            case "$pkg" in
                *-headers) partner="${pkg%-headers}" ;;
                *)         partner="$pkg"-headers    ;;
            esac
        fi

        case "${pkgstatus[$pkg]}" in
            TRUE)
                [ -z "$(LocalVersion "$pkg")" ] && install+=("$pkg")
                if [ "$connect_header_with_kernel" = "yes" ] ; then
                    [ -z "$(LocalVersion "$partner")" ] && install+=("$partner")
                fi
                ;;
            FALSE)
                [ -n "$(LocalVersion "$pkg")" ] && uninstall+=($(basename "$pkg"))
                if [ "$connect_header_with_kernel" = "yes" ] ; then
                    [ -n "$(LocalVersion "$partner")" ] && uninstall+=($(basename "$partner"))
                fi
                ;;
        esac
    done

    UniqueArr install
    UniqueArr uninstall

    cmd=()
    test -n "$uninstall" && cmd+=($pacman -Rsn ${uninstall[*]} ";")
    test -n "$install"   && cmd+=($pacman -S ${install[*]} ";")

    if [ -n "$cmd" ] ; then
        case "$pacman" in
            yay | paru | /usr/bin/yay | /usr/bin/paru)
                RunInTerminal "echo '${cmd[*]}' ; ${cmd[*]}"
                ;;
            pacman | /usr/bin/pacman)
                RunInTerminal "echo '${cmd[*]}' ; $EOS_ROOTER '${cmd[*]}'"
                ;;
        esac
    fi
}

RunWithProgressOrNot() {
    local func1

    case "$progress" in
        no)
            for func1 in "${functions[@]}" ; do
                $func1 || DIE "Function '$func1' failed.\n"     # run each function to add stuff to the '$cmd'
            done
            ;;
        yes)
            {
                for func1 in "${functions[@]}" ; do
                    $func1 || DIE "Function '$func1' failed.\n"     # run each function to add stuff to the '$cmd'
                    val="$(( ix < count ? ix*100/count : 99 ))"     # progress window stuff
                    ((ix++))
                    printf "#%s\n%s\n" "$func1" "$val"
                done
                printf "%s\n" "${cmd[@]}" > "$cmdfile"              # now print the gathered value of 'cmd' into $cmdfile
                printf "#===> Done.\n100\n"                         # stop the progress window
            } | "${progress_cmd[@]}"

            readarray -t cmd < <(cat "$cmdfile")                  # now get the full 'cmd' from the file
            rm -f "$cmdfile"
            ;;
    esac
}

Main "$@"
