#!/bin/bash

parseopts() {
    local opt= optarg= i= shortopts=$1
    local -a longopts=() unused_argv=()

    shift
    while [[ $1 && $1 != '--' ]]; do
        longopts+=("$1")
        shift
    done
    shift

    longoptmatch() {
        local o longmatch=()
        for o in "${longopts[@]}"; do
            if [[ ${o%:} = "$1" ]]; then
                longmatch=("$o")
                break
            fi
            [[ ${o%:} = "$1"* ]] && longmatch+=("$o")
        done

        case ${#longmatch[*]} in
            1)
                # success, override with opt and return arg req (0 == none, 1 == required)
                opt=${longmatch%:}
                if [[ $longmatch = *: ]]; then
                    return 1
                else
                    return 0
                fi ;;
            0)
                # fail, no match found
                return 255 ;;
            *)
                # fail, ambiguous match
                printf "%s: option '%s' is ambiguous; possibilities:%s\n" "${0##*/}" \
                    "--$1" "$(printf " '%s'" "${longmatch[@]%:}")"
                return 254 ;;
        esac
    }

    while (( $# )); do
        case $1 in
            --) # explicit end of options
                shift
                break
                ;;
            -[!-]*) # short option
                for (( i = 1; i < ${#1}; i++ )); do
                    opt=${1:i:1}

                    # option doesn't exist
                    if [[ $shortopts != *$opt* ]]; then
                        printf "%s: invalid option -- '%s'\n" "${0##*/}" "$opt"
                        OPTRET=(--)
                        return 1
                    fi

                    OPTRET+=("-$opt")
                    # option requires optarg
                    if [[ $shortopts = *$opt:* ]]; then
                        # if we're not at the end of the option chunk, the rest is the optarg
                        if (( i < ${#1} - 1 )); then
                            OPTRET+=("${1:i+1}")
                            break
                        # if we're at the end, grab the the next positional, if it exists
                        elif (( i == ${#1} - 1 )) && [[ $2 ]]; then
                            OPTRET+=("$2")
                            shift
                            break
                        # parse failure
                        else
                            printf "%s: option '%s' requires an argument\n" "${0##*/}" "-$opt"
                            OPTRET=(--)
                            return 1
                        fi
                    fi
                done
                ;;
            --?*=*|--?*) # long option
                IFS='=' read -r opt optarg <<< "${1#--}"
                longoptmatch "$opt"
                case $? in
                    0)
                        if [[ $optarg ]]; then
                            printf "%s: option '--%s' doesn't allow an argument\n" "${0##*/}" "$opt"
                            OPTRET=(--)
                            return 1
                        else
                            OPTRET+=("--$opt")
                            shift
                            continue 2
                        fi
                        ;;
                    1)
                        # --longopt=optarg
                        if [[ $optarg ]]; then
                            OPTRET+=("--$opt" "$optarg")
                            shift
                        # --longopt optarg
                        elif [[ $2 ]]; then
                            OPTRET+=("--$opt" "$2" )
                            shift 2
                        else
                            printf "%s: option '--%s' requires an argument\n" "${0##*/}" "$opt"
                            OPTRET=(--)
                            return 1
                        fi
                        continue 2
                        ;;
                    254)
                        # ambiguous option -- error was reported for us by longoptmatch()
                        OPTRET=(--)
                        return 1
                        ;;
                    255)
                        # parse failure
                        printf "%s: unrecognized option '%s'\n" "${0##*/}" "--$opt"
                        OPTRET=(--)
                        return 1
                        ;;
                esac
                ;;
            *) # non-option arg encountered, add it as a parameter
                unused_argv+=("$1")
                ;;
        esac
        shift
    done

    # add end-of-opt terminator and any leftover positional parameters
    OPTRET+=('--' "${unused_argv[@]}" "$@")
    unset longoptmatch

    return 0
}

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/usr/lib/firmware/$value" ]]; then
                    _add_file "/usr/lib/firmware/$value" "$BASEDIR/usr/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= script=

    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

    if [[ $SCRIPT ]]; then
        script=$(find_in_dirs "$SCRIPT" "${HOOKDIR[@]}") &&
            add_file "$script" "/hooks/$SCRIPT"
    fi
}

find_in_dirs() {
    local dir

    for dir in "${@:2}"; do
        if [[ -e $dir/$1 ]]; then
            printf '%s' "$dir/$1"
            return 0
        fi
    done

    return 1
}

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