#!/bin/bash

plain() {
    local mesg=$1; shift
    printf "$BOLD    $mesg$NC\n" "$@" >&1
}

msg() {
    local mesg=$1; shift
    printf "$GREEN==>$NC$BOLD $mesg$NC\n" "$@" >&1
}

msg2() {
    local mesg=$1; shift
    printf "$BLUE  ->$NC$BOLD $mesg$NC\n" "$@" >&1
}

warning() {
    local mesg=$1; shift
    printf "$YELLOW==> WARNING:$NC$BOLD $mesg$NC\n" "$@" >&2
}

error() {
    local mesg=$1; shift
    printf "$RED==> ERROR:$NC$BOLD $mesg$NC\n" "$@" >&2
}

die() {
    error "$@"
    cleanup 1
}

get_basename() {
  local base=${1%/}
  base=${base##*/}
  printf '%s' "${base:-/}"
}

get_dirname() {
  local dir=${1%/}
  dir=${dir%/*}
  printf '%s' "${dir:-/}"
}

in_array() {
    # Search for an element in an array.
    #   $1: needle
    #   ${@:2}: haystack

    local item= needle=$1; shift

    for item in "$@"; do
        [[ $item = $needle ]] && return 0 # Found
    done
    return 1 # Not Found
}

pathlookup() {
    # a basedir aware 'type -P' (or which) for executables
    #   $1: binary to find

    local path=
    local -a paths=

    IFS=: read -r -a paths <<< "$PATH"

    for path in "${paths[@]}"; do
        [[ ${path:0:1} = [.~] ]] && continue
        if [[ -x $BASEDIR$path/$1 ]]; then
            printf '%s' "$BASEDIR$path/$1"
            return 0
        fi
    done

    return 1
}

_add_file() {
    # add a file to $BUILDROOT
    #   $1: pathname on initcpio
    #   $2: source on disk
    #   $3: mode

    (( $# == 3 )) || return $EINVAL

    if (( ! QUIET )); then
        if [[ -e "$BUILDROOT$1" ]]; then
            plain "overwriting file: %s" "$1"
        else
            plain "adding file: %s" "$1"
        fi
    fi
    command install -Dm$3 "$2" "$BUILDROOT$1"
}

_add_dir() {
    # add a directory (with parents) to $BUILDROOT
    #   $1: pathname on initcpio
    #   $2: mode

    (( $# == 2 )) || [[ "$1" == /?* ]] || return 1 # bad args
    [[ -e "$BUILDROOT$1" ]] && return 0 # file exists

    (( QUIET )) || plain "adding dir: %s" "$1"
    command install -dm$2 "$BUILDROOT$1"
}

_add_symlink() {
    # add a symlink to $buildroot
    #   $1: name on initcpio
    #   $2: target of $1

    (( $# == 2 )) || return $EINVAL
    if (( ! QUIET )); then
        if [[ -L "$BUILDROOT$1" ]]; then
            plain "overwriting symlink %s -> %s" "$1" "$2"
        else
            plain "adding symlink: %s -> %s" "$1" "$2"
        fi
    fi
    ln -sfn "$2" "$BUILDROOT$1"
}

auto_modules() {
    # Perform auto detection of modules via sysfs.

    local mods=

    IFS=$'\n' read -rd '' -a mods < \
        <(find /sys/devices -name modalias -exec sort -u {} + |
        # delimit each input by a newline, expanded in place
        xargs -d $'\n' modprobe -qd "$BASEDIR" -aRS "$KERNELVERSION" |
        sort -u)

    printf "%s\n" "${mods[@]//-/_}"
    (( ${#mods[*]} ))
}

all_modules() {
    # Add modules to the initcpio, filtered by grep.
    #   $@: filter arguments to grep

    local -i count=0
    local mod=

    while read -r -d '' mod; do
        (( ++count ))
        mod=${mod##*/}
        mod="${mod%.ko*}"
        printf '%s\n' "${mod//-/_}"
    done < <(find "$MODULEDIR" -name '*.ko*' -print0 2>/dev/null | grep -Zz "$@")

    (( count ))
}

checked_modules() {
    # Add modules to the initcpio, filtered by the list of autodetected
    # modules.
    #   $@: arguments to all_modules

    if [[ -s "$MODULE_FILE" ]]; then
        grep -xFf "$MODULE_FILE" <(all_modules "$@")
        return 1
    else
        all_modules "$@"
    fi
}

add_module() {
    # Add a kernel module to the initcpio image. Dependencies will be
    # discovered and added.
    #   $1: module name

    local module= path= dep= deps= field= value=
    local -i ign_errors=0

    if [[ $1 = -@(t|-try) ]]; then
        ign_errors=1
        shift
    fi

    module=${1%.ko*}

    # skip expensive stuff if this module has already been added
    in_array "${module//-/_}" "${ADDED_MODULES[@]}" && return

    while IFS=':= ' read -r -d '' field value; do
        case "$field" in
            filename)
                path=$value
                ;;
            depends)
                IFS=',' read -r -a deps <<< "$value"
                for dep in "${deps[@]}"; do
                    add_module "$dep"
                done
                ;;
            firmware)
                if [[ -e "$BASEDIR/lib/firmware/$value" ]]; then
                    _add_file "/lib/firmware/$value" "$BASEDIR/lib/firmware/$value" 644
                fi
                ;;
        esac
    done < <(modinfo -b "$BASEDIR" -k "$KERNELVERSION" -0 "$module" 2>/dev/null)

    if [[ -z $path ]]; then
        (( ign_errors )) && return 0
        error "module not found: \`%s'" "$module"
        return 1
    fi

    # aggregate modules and add them all at once to save some forks
    (( QUIET )) || plain "adding module: %s" "$1"
    MODPATHS+=("$path")
    ADDED_MODULES+=("${module//-/_}")

    # explicit module depends
    case "$module" in
        fat) add_module --try "nls_cp437" ;;
        ocfs2) add_module --try "configfs" ;;
        libcrc32c) add_module --try "crc32c"; add_module --try "crc32c_intel" ;;
    esac
}

add_full_dir() {
    # Add a directory and all its contents, recursively, to the initcpio image.
    # No parsing is performed and the contents of the directory is added as is.
    #   $1: path to directory

    local f=

    if [[ -n $1 && -d $1 ]]; then
        for f in "$1"/*; do
            if [[ -d "$f" ]]; then
                add_full_dir "$f"
            else
                add_file "$f"
            fi
        done
    fi
}

add_dir() {
    # Add a directory to the initcpio image.
    #   $1: absolute path of directory on image

    (( ! $# )) && return 1

    local path=$1 mode=${2:-755}

    _add_dir "$path" "$mode"
}

add_symlink() {
    # Add a symlink to the initcpio image.
    #   $1: pathname of symlink on image
    #   $2: absolute path to target of symlink

    (( $# == 2 )) || return 1

    add_dir "$(get_dirname "$1")"
    _add_symlink "$1" "$2"
}

add_file() {
    # Add a plain file to the initcpio image. No parsing is performed and only
    # the singular file is added.
    #   $1: path to file
    #   $2: destination on initcpio (optional, defaults to same as source)

    (( $# )) || return 1

    # determine source and destination
    local src= dest=${2:-$1} mode=

    src=$BASEDIR$1

    [[ -f "$src" ]] || { error "file not found: \`%s'" "$src"; return 1; }

    mode=$(stat -c %a "$src")
    if [[ -z "$mode" ]]; then
        error "failed to stat file: \`%s'." "$src"
        return 1
    fi

    _add_file "${dest#$BASEDIR}" "$src" "$mode"
}

add_binary() {
    # add a binary file to the initcpio image. library dependencies will
    # be discovered and added.
    #   $1: path to binary
    #   $2: destination on initcpio (optional, defaults to same as source)

    local -a sodeps
    local line= regex= binary= dest= mode= sodep= resolved= dirname=

    if [[ ${1:0:1} != '/' ]]; then
        binary=$(pathlookup "$1")
    else
        binary=$BASEDIR$1
    fi

    [[ -f "$binary" ]] || { error "file not found: \`%s'" "$1"; return 1; }

    dest=${2:-$binary}
    mode=$(stat -c %a "$binary")

    # always add the binary itself
    _add_file "${dest#$BASEDIR}" "$binary" "$mode"

    lddout=$(ldd "$binary" 2>/dev/null) || return 0 # not a binary!

    # resolve sodeps
    regex='(/.+) \(0x[a-fA-F0-9]+\)'
    while read line; do
        [[ "$line" =~ $regex ]] && sodep=${BASH_REMATCH[1]} || continue

        if [[ -f $sodep && ! -e $BUILDROOT$sodep ]]; then
            if [[ ! -L $sodep ]]; then
                _add_file "$sodep" "$BASEDIR$sodep" "$(stat -c %a "$sodep")"
            else
                resolved=$(readlink -e "$BASEDIR$sodep")
                dirname=${resolved%/*}
                _add_dir "${dirname#$BASEDIR}" 755
                _add_symlink "$sodep" "${resolved#$BASEDIR}"
                _add_file "${resolved#$BASEDIR}" "$resolved" 755
            fi
        fi
    done <<< "$lddout"

    return 0
}

parse_hook() {
    # parse key global variables set by install hooks. This function is called
    # prior to the start of hook processing, and after each hook's build
    # function is run.

    local item=

    for item in $MODULES; do
        if [[ ${item:(-1)} = '?' ]]; then
            add_module --try "${item%\?}"
        else
            add_module "$item"
        fi
    done

    for item in $BINARIES; do
        add_binary "$item"
    done

    for item in $FILES; do
        add_file "$item"
    done

    [[ $SCRIPT ]] && add_file "$HOOKDIR/$SCRIPT" "/hooks/$SCRIPT"
}

# vim: set ft=sh ts=4 sw=4 et: