#!/bin/bash

# Add or remove a kernel parameter for grub or sd-boot

Info() { echo "==> $progname: info:" "$@" ; }

DIE() {
    echo "==> $progname: error: $@" >&2
    exit 1
}


ValueOfGrubVar() {
    local -r varname="$1"           # e.g. GRUB_CMDLINE_LINUX_DEFAULT
    local -n current_value1="$2"    # out
    local -n quote1="$3"            # out
    local file="$4"

    source "$file" || return 1

    current_value1="$(echo ${!varname})"
    quote1='"'                            # may change the quote character around the value
}

AddValueToGrubVar() {
    local -r varname="$1"
    local -r addvalue="$2"
    local file="$3"
    local current_value="" quote=""

    ValueOfGrubVar "$varname" current_value quote "$file" || return 1

    case "$current_value" in
        "") current_value="$addvalue" ;;
        *)  current_value+=" $addvalue" ;;
    esac

    sed -i "$file" -e "s|^[ \t]*$varname=.*|$varname=$quote$current_value$quote|"
}

RemoveValueFromGrubVar() {
    local -r varname="$1"
    local -r removevalue="$2"
    local file="$3"
    local current_value="" quote=""

    ValueOfGrubVar "$varname" current_value quote "$file" || return 1

    local pp newvalue=()
    for pp in $current_value ; do
        case "$pp" in
            "$removevalue") ;;
            *) newvalue+=("$pp") ;;
        esac
    done
    newvalue="$(echo ${newvalue[*]})"   # get rid of potential extra spaces

    sed -i "$file" -e "s|^[ \t]*$varname=.*|$varname=$quote$newvalue$quote|"
}


SetKernelParameterGrub() {
    local file="$file_grub"
    local varname="GRUB_CMDLINE_LINUX_DEFAULT"
    local currline="$(grep ^"$varname"= $file | grep "$kernel_param")"
    local op=""

    Info "handling kernel parameter '$kernel_param' in file $file"

    if [ -n "$currline" ] ; then
        case "$mode" in
            add)
                Info "already set: '$currline'"
                exit 1    # nothing changed
                ;;
            remove)
                # caveats:
                #    - $kernel_param value may not be part of some unrelated string inside $varname
                #    - $kernel_param with multiple values may fail, e.g. module_blacklist=aa,bb,cc
                #
                cp "$file" "$file.bak"
                RemoveValueFromGrubVar "$varname" "$kernel_param" "$file"
                ;;
        esac
    else
        case "$mode" in
            add)
                cp "$file" "$file.bak"
                AddValueToGrubVar $varname "$kernel_param" "$file"
                ;;
            remove)
                currline="$(grep ^"$varname"= $file)"
                Info "already removed: '$currline'"
                exit 1    # nothing changed
                ;;
        esac
    fi

    # parameter changed

    case "$mode" in
        add)    op="added to" ;;
        remove) op="removed from" ;;
    esac
    Info "kernel parameter '$kernel_param' $op $varname in file $file"

    if [ "$bootloader" = "" ] ; then
        Info "kernel parameter for grub was changed by $progname, so it is advisable to run commands:"
        Info "    grub-mkconfig -o /boot/grub/grub.cfg"
        Info "    grub-install (with proper parameters for your system!)"
    fi
    exit 0
}

SetKernelParameterSdboot() {
    local file=$file_sdboot
    local paramline=$(grep "$kernel_param" $file)

    Info "handling kernel parameter '$kernel_param' in file $file"

    case "$mode" in
        add)
            if [ "$paramline" != "" ] ; then
                Info "already set: '$paramline'"
                exit 1                            # 1 = nothing changed
            fi
            Info "adding kernel parameter: $kernel_param"
            if [ ! -e $file ] || [ "$(stat -c %s $file)" = "0" ] ; then
                echo "$kernel_param" > $file
            else
                sed -E -i $file -e "s|([ \t]*[^#].*)|$kernel_param \1|"     # insert to a non-comment line
            fi
            ;;
        remove)
            if [ "$paramline" = "" ] ; then
                Info "already removed: '$paramline'"
                exit 1                             # 1 = nothing changed
            fi
            Info "removing kernel parameter: $kernel_param"
            sed -E -i $file -e "s|$kernel_param[ ]{0,1}||g"
            ;;
    esac

    if [ "$bootloader" = "" ] ; then
        Info "running reinstall-kernels after changing kernel parameter"
        reinstall-kernels    # run reinstall-kernels only if not called from ccs
    fi
    exit 0   # parameter changed
}

Options() {
    local opts
    local sopts="h"
    local lopts="help,bootloader:"

    opts="$(/usr/bin/getopt -o=$sopts --longoptions $lopts --name "$progname" -- "$@")" || {
        Options --help
        return 1
    }

    eval set -- "$opts"

    while true ; do
        case "$1" in
            --bootloader) bootloader="$2" ; shift ;;

            --) shift ; break ;;

            -h | --help)
                cat <<EOF >&2
Usage: $progname [options] kernel-parameter [mode]
Options:
  --ccs               Calling from the CCS (chrooted_cleaner_script.sh).
Parameters:
  kernel-parameter    A kernel parameter, e.g. "nvidia-drm.modeset=1".
  mode                Operation: "add" (=default) or "remove".
EOF
                exit 0
                ;;
            *)
                ;;
        esac
        shift
    done
    kernel_param="$1"
    mode="$2"
}

Main() {
    local progname="$(basename "$0")"
    # exit value:
    #   0 = parameter was added
    #   * = parameter was not added (already exists or a failure)

    local kernel_param=""
    local mode=""               # optional: add (default) or remove

    # variables to store option values
    local bootloader=""

    Options "$@" || return 1

    # check given parameters
    [ "$kernel_param" = "" ] && DIE "kernel parameter is required but missing"
    case "$mode" in
        add | remove) ;;
        "") mode=add ;;
        *) DIE "$progname: error: value '$mode' for the second parameter is not supported" ;;
    esac

    local file_sdboot=/etc/kernel/cmdline
    local file_grub=/etc/default/grub

    case "$bootloader" in
        "")
            [ -r $file_sdboot ] && SetKernelParameterSdboot
            [ -r $file_grub ] && SetKernelParameterGrub
            Info "systemd-boot or grub not in use"
            ;;
        systemd-boot)
            SetKernelParameterSdboot
            ;;
        grub)
            SetKernelParameterGrub
            ;;
        *)
            Info "sorry, $progname does not currently support $bootloader bootloader."
            ;;
    esac
}

Main "$@"
