#!/bin/bash
version_script="2.1.1"

print_help() {
cat <<'HELPDOC'
NAME
    rambooter - enable/disable initcpio hook and/or generate config file

SYNOPSIS
    rambooter <OPTIONS>

OPTIONS
    -C, --config-gen
        Attempt to detect the root file system partitions and
        generate a new config file.

    -D, --disable
        Remove rambooter hook from /etc/mkinitcpio.conf and rebuild
        initramfs image.

    -E, --enable
        Add rambooter hook to /etc/mkinitcpio.conf and rebuild
        initramfs image.

    -o, --output <FILE>
        Save new config to FILE instead of /etc/mkinitcpio.conf.

    -Y, --yes
        Overwrite output files without asking.

    -H, --help
        Display help text and exit.

VERSION
HELPDOC
printf '    %s\n' "$version_script"
}

##=========================  VARIABLES  ==========================##
dir_script="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# settings:
file_config=/etc/rambooter.conf
ram_machine=8000
ps_default=yes
ps_timeout=32
ram_min=750
ram_pref=4000
zram_min=250
zram_max=1000
# args:
flag_config_gen=false
flag_disable=false
flag_enable=false
flag_yes=false
opt_output="$file_config"
parms=()
count_parms=0
count_flags=0
count_opts=0
# control:
hooks=
# colors:
c_blue_b=$'\e[1;38;5;27m'
c_green_b=$'\e[1;38;5;46m'
c_red_b=$'\e[1;38;5;196m'
c_yellow_b=$'\e[1;33m'
c_white=$'\e[0;38;5;15m'
c_white_b=$'\e[1;37m'
c_gray=$'\e[0;37m'

##=========================  FUNCTIONS  ==========================##
msg() {
# print status message:
    printf '%s==>%s %s%s\n' "$c_green_b" "$c_white_b" "$1" "$c_gray"
}

msg_ask() {
# print ask message:
    printf '%s::%s %s%s ' "$c_blue_b" "$c_white_b" "$1" "$c_gray"
}

msg_error() {
# print error message:
    printf '%s==> ERROR:%s %s%s\n' "$c_red_b" "$c_white_b" \
        "$1" "$c_gray" 1>&2
}

msg_warn() {
# print warning message:
    printf '%s==> WARNING:%s %s%s\n' "$c_yellow_b" "$c_white_b" \
        "$1" "$c_gray"
}

args_parse() {
# parse all input arguments; fill positional parameter array:
    local arg args flag_arg_unknown flag_help  flag_opt_empty n_args
    #global count_flags count_opts count_parms parms
    args=("$@")
    n_args=0
    arg="${args[n_args]}"
    flag_opt_empty=false
    flag_arg_unknown=false
    flag_help=false
    count_flags=0
    count_opts=0
    count_parms=0
    parms=()
    while [ -n "$arg" ]; do case "$arg" in
        # flags:
        -C|--config_gen)
            flag_config_gen=true
            arg="${args[((++n_args))]}"
            ((count_flags++)) ;;
        -D|--disable)
            flag_disable=true
            arg="${args[((++n_args))]}"
            ((count_flags++)) ;;
        -E|--enable)
            flag_enable=true
            arg="${args[((++n_args))]}"
            ((count_flags++)) ;;
        -Y|--yes)
            flag_yes=true
            arg="${args[((++n_args))]}"
            ((count_flags++)) ;;
        # options:
        -o|--output)
            opt_output="${args[((++n_args))]}"
            if [ -z "${args[n_args]}" ]; then
                flag_opt_empty=true
                break
            fi
            arg="${args[((++n_args))]}"
            ((count_opts++)) ;;
        # help:
        -H|--help|-h)
            flag_help=true
            break ;;
        # all flags:
        -[CDEYHh]*)
            # all flags and options:
            if [[ "${arg:2:1}" =~ [CDEYoHh] ]]; then
                args[((n_args--))]="-${arg:2}"
                arg="${arg:0:2}"
            else
                arg="${arg:2:1}"
                flag_arg_unknown=true
                break
            fi ;;
        # all options:
        -[o]*)
            args[$n_args]="${arg:2}"
            arg="${arg:0:2}"
            ((n_args--)) ;;
        # parms:
        --)
            ((n_args++))
            break ;;
        *)
            break ;;
    esac; done
    # get parameters:
    while [ -n "${args[n_args]}" ]; do
        parms+=("${args[((n_args++))]}")
    done
    count_parms=${#parms[@]}
    # FAIL: arg unknown:
    if [ "$flag_arg_unknown" = true ]; then
        msg_error "argument not recognized: $arg"
        exit 5
    # FAIL: arg empty:
    elif [ "$flag_opt_empty" = true ]; then
        msg_error "argument requires an option: $arg"
        exit 5
    # HELP:
    elif [ "$flag_help" = true ]; then
        print_help
        exit 0
    fi
}

rambooter_config_gen() {
# generate rambooter config file:
    local config mount mount_id mount_path mounts_fstab mounts_zram \
        part ps_input zram
    #global file_config flag_yes opt_output ps_default ps_timeout \
        #ram_machine ram_min ram_pref zram_max zram_min

    # RETURN: trying to save to directory:
    if [ -d "$opt_output" ]; then
        msg_error "$opt_output is a directory"
        return
    # ASK: overwrite existing file?
    elif [ -f "$opt_output" ] && [ "$flag_yes" != true ]; then
        msg_ask "overwrite $opt_output? [y/N]"
        read -r -t 300 ps_input
        ps_input="${ps_input:-no}"
        # RETURN:
        if [ "$ps_input" != 'y' ] && [ "$ps_input" != 'yes' ]; then
            return 0
    fi; fi

    # only possible to generate custom config if root is mounted:
    if (mountpoint -q /); then
        zram=$(($(df -m / | \
            awk 'FNR==2 {print int($3)}')+ram_min+zram_min))
        # source /etc/fstab:
        if [ -f /etc/fstab ]; then
            mapfile -t mounts_fstab < <(sed -En \
            's@^\s*((UUID|PARTUUID)=[^\s]+)\s+(/[^ \s]+).*$@\3:\1@p;' \
                /etc/fstab | sort)
            for mount in ${mounts_fstab[@]}; do
                mount_id="${mount#*:}"
                mount_path="${mount%:*}"
                # get /boot, /efi, /esp, /home from /etc/fstab:
                if [[ "${mount_path,,}" =~ /(boot|efi|esp|home)$ ]] && \
                (mountpoint -q "$mount_path"); then
                    # attempt to determine if mount is small enough:
                    part=$(df -m "$mount_path" | \
                        awk 'FNR==2 {print int($3)}')
                    if [ $((ram_machine-zram-part)) -gt 0 ]; then
                        zram=$((zram+part))
                        mounts_zram+=("$mount_id:$mount_path")
    fi; fi; done; fi; fi

    # build config:
    config=$'#!usr/bin/bash\n\n# mounts loaded to zram:\nmounts_zram=\''
    if [ -n "${mounts_zram[1]}" ]; then
        config+=$'\n'
        for mount in ${mounts_zram[@]}; do
            config+="    $mount"
            config+=$'\n'
        done
    else
        config+="${mounts_zram[0]}"
    fi
    config+=$'\'\n\n# mounts ignored:\nmounts_null=\'\'\n\n'
    config+=$'# prompt default:\nps_default='
    config+="$ps_default"
    config+=$'\n\n# prompt timout:\nps_timeout='
    config+="$ps_timeout"
    config+=$'\n\n# minimum MiB free ram:\nram_min='
    config+="$ram_min"
    config+=$'\n\n# minimum MiB free zram:\nzram_min='
    config+="$zram_min"
    config+=$'\n\n# preferred MiB free ram:\nram_pref='
    config+="$ram_pref"
    config+=$'\n\n# maximum MiB free ram:\nzram_max='
    config+="$zram_max"

    # write config:
    printf '%s\n' "$config" > "$opt_output"
    msg "rambooter config written to $opt_output"
}

rambooter_disable() {
# remove rambooter from mkinitcpio.conf HOOKS, rebuild all preset images:
    #global hooks
    if [[ " $hooks " =~ ' rambooter ' ]]; then
        hooks="$(sed -E 's/^(.*) rambooter(.*)/\1\2/g' <<<"$hooks")"
        sed -Ei "s/^(\s*HOOKS=).*/\1\(${hooks}\)/g" /etc/mkinitcpio.conf
        msg 'rambooter removed from /etc/mkinitcpio.conf HOOKS'
    fi
    mkinitcpio -P
}

rambooter_enable() {
# add rambooter to mkinitcpio.conf HOOKS, rebuild all preset images:
    #global hooks
    if [[ ! " $hooks " =~ ' rambooter ' ]]; then
        hooks="$(sed -E \
            's/^(.*(encrypt|udev|base)) (.*)/\1 rambooter \3/g' \
            <<<"$hooks")"
        sed -Ei "s/^(\s*HOOKS=).*/\1\(${hooks}\)/g" /etc/mkinitcpio.conf
        msg 'rambooter added to /etc/mkinitcpio.conf HOOKS'
    fi
    mkinitcpio -P
}

##===========================  SCRIPT  ===========================##
# parse args:
args_parse "$@"

# HELP: no args:
if [ -z "$1" ]; then
    print_help
    exit 0
fi
# FAIL: /usr/bin/mkinitcpio required:
if [ ! -f /usr/bin/mkinitcpio ] || [ ! -f /etc/mkinitcpio.conf ]; then
    msg_error 'mkinitcpio required for rambooter to function'
fi
# FAIL: unknown arguments:
if [ "$count_parms" -ne 0 ]; then
    msg_error "unknown arguments: ${parms[*]}"
    exit 5
fi
# FAIL: must be root to perform operation:
if [ $EUID -ne 0 ]; then
    if [ "$flag_disable" = true ] || [ "$flag_enable" = true ]; then
        msg_error 'you must be root to perform this operation'
        exit 1
    fi
    if [ "$flag_config_gen" = true ]; then
        if [ -f "$opt_output" ]; then
            if (! touch -c "$opt_output" &>/dev/null); then
                msg_error 'you must be root to perform this operation'
                exit 1
            fi
        else
            if [ "$(ls -ld $(dirname "$opt_output") | \
                cut -d' ' -f3)" != "$(whoami)" ] && \
            [ "$(ls -ld $(dirname "$opt_output") | \
            cut -c9)" != 'w' ]; then
                msg_error 'you must be root to perform this operation'
                exit 1
fi; fi; fi; fi
# FAIL: both disable and enable:
if [ "$flag_disable" = true ] && [ "$flag_enable" = true ]; then
    msg_error 'cannot both disable and enable'
    exit 1
fi

# get current hooks:
hooks="$(grep -Po '^\s*HOOKS=\(\K.*?(?=\))' /etc/mkinitcpio.conf | \
        sed -E 's/\s+/ /g; s/\s+$//g')"
# generate rambooter config file:
if [ "$flag_config_gen" = true ]; then
    rambooter_config_gen
fi
# disable/enable rambooter:
if [ "$flag_disable" = true ]; then
    rambooter_disable
elif [ "$flag_enable" = true ]; then
    rambooter_enable
fi
