#!/bin/sh
#
# kiss - package manager for kiss linux.

die() {
    # Print a message and exit with '1' (error).
    printf '\033[31m!>\033[m %s\n' "$@" >&2
    exit 1
}

log() {
    # Print a message with a colorful arrow to distinguish
    # from other output.
    printf '\033[32m=>\033[m %s\n' "$@"
}

source_type() {
    # Figure out what kind of source we are dealing with.
    # This removes the need to repeat these same tests
    # in each function.
    [ -z "$1" ]                && return 1  # No file.
    [ -f "$1" ]                && return 2  # Local file.
    [ -f "$src_dir/${1##*/}" ] && return 3  # Cached downloaded file.
    [ -z "${1##git:*}" ]       && return 4  # Git repository.
    [ -z "${1##*://*}" ]       && return 5  # Remote file.
}

pkg_clean() {
    # Clean up on exit or error. This removes everything related
    # to the build.
    rm -rf -- "$mak_dir" "$pkg_dir" "$tar_dir" \
              "$cac_dir/manifest-$$" "$cac_dir/checksums-$$" \
              "$cac_dir/mv" "$cac_dir/mkdir" "$cac_dir/find"
}

pkg_search() {
    # Figure out which repository a package belongs to by
    # searching for directories matching the package name
    # in $KISS_PATH.

    # Disable globbing with 'set -f' to ensure that the unquoted
    # variable doesn't expand to anything nasty.
    # shellcheck disable=2086,2046
    {
        set -f
        set -- "$1" $(IFS=:; find $KISS_PATH -maxdepth 1 -name "$1")
        set +f
    }

    [ -z "$2" ] && die "Package '$1' not in any repository."

    printf '%s\n' "$2"
}

pkg_setup() {
    # Check that each mandatory file in the package entry exists.
    pkg_location=$(pkg_search "$1")

    cd "$pkg_location" || die "'$pkg_location' not accessible"
    [ -f sources ]     || die "Sources file not found."
    [ -x build ]       || die "Build file not found or not executable."
    [ -f licenses ]    || die "License file not found or empty."

    read -r pkg_ver pkg_rel < version || die "Version file not found."
    pkg_name=$1
    pkg_tar=$name\#$ver-$rel.tar.gz
}

pkg_depends() {
    [ -f depends ] && while read -r dep opt; do
        pkg_list "$dep" || {
            [ "$1" = install ] && [ "$opt" = make ] && continue

            case $missing in
                *" $dep,"*) ;;
                *) missing="$missing $dep,"
                   pkg_setup "$dep"
                   pkg_depends ;;
            esac
        }
    done < depends
}

pkg_sources() {
    src_dir=$cac_dir/sources/$name
    mkdir -p "$src_dir"

    while read -r src _; do
        case $(source_type "$src"; echo $?) in
            4)   git clone "${src##git:}" "$mak_dir" ;;
            5)   wget -P "$src_dir" "$src" || die "Failed to download $src." ;;
            0|1) die "Source file '$src' not found." ;;
        esac
    done < sources
}

pkg_checksum() {
    while read -r src _; do
        case $(source_type "$src"; echo $?) in
            2) src_path=$src ;;
            3) src_path=$src_dir/${src##*/} ;;
            4) continue
        esac

        (cd "${src_path%/*}" >/dev/null; sha256sum "${src##*/}") ||
            die "Failed to generate checksums."
    done < sources > "${1-checksums}"
}

pkg_verify() {
    pkg_checksum "$cac_dir/checksums-$$"

    cmp -s "$cac_dir/checksums-$$" checksums ||
        die "Checksum mismatch, run '$kiss checksum $name'."
}

pkg_extract() {
    while read -r src dest; do
        [ "$dest" ] && mkdir -p "$mak_dir/$dest"

        case $(source_type "$src"; echo $?)-$src in
            2-*) cp -f "$src" "$mak_dir/$dest" ;;

            3-*.tar*|3-*.tgz)
               tar xf "$src_dir/${src##*/}" -C "$mak_dir/$dest" \
                   --strip-components 1 || die "Couldn't extract ${src##*/}" ;;

            [01]-*) die "${src##*/} not found."
        esac
    done < sources
}

pkg_build() {
    (cd "$mak_dir"; "$OLDPWD/build" "$pkg_dir") || die "Build failed."
    cp -Rf "$rep_dir/$name" "$pkg_db"
    log "Sucessfully built $pkg." 2> "$pkg_db/$name/manifest"
}

pkg_strip() {
    log "Stripping unneeded symbols from binaries and libraries."

    find "$pkg_dir" -type f | while read -r binary; do
        case $(file -bi "$binary") in
            application/x-sharedlib*|application/x-pie-executable*)
                strip_opts=--strip-unneeded
            ;;

            application/x-archive*)    strip_opts=--strip-debug ;;
            application/x-executable*) strip_opts=--strip-all ;;

            *) continue ;;
        esac

        strip "$strip_opts" "$binary" 2>/dev/null
    done
}

pkg_manifest() {
    # Store the file and directory list of the package.
    # Directories have a trailing '/' and the list is sorted in reverse.
    (cd "$pkg_dir" && find ./* -type d -exec printf '%s/\n' {} + -or -print) |
        sort -r | sed -e ss.ss > "$pkg_db/$name/manifest"
}

pkg_tar() {
    tar zpcf "$bin_dir/$pkg" -C "$pkg_dir" . || die "Failed to create package."
    log "Use '$kiss install $name' to install the package."
}

pkg_conflicts() {
    log "Checking for package conflicts."

    # Extract manifest from tarball and strip directories.
    tar xf "$bin_dir/$pkg" -O "./var/db/$kiss/$name/manifest" |
    while read -r line; do
        [ "${line%%*/}" ] && printf '%s\n' "$line" >> "$cac_dir/manifest-$$"
    done

    # Compare extracted manifest to all installed manifests.
    # If there are matching lines (files) there's a package
    # conflict.
    for db in "$sys_db"/*; do
        [ "$name" = "${db##*/}" ] && continue

        grep -Fxf "$cac_dir/manifest-$$" "$db/manifest" 2>/dev/null &&
            die "Package '$name' conflicts with '${db##*/}'."
    done
}

pkg_install() {
    [ -f "$bin_dir/$pkg" ] || args b "$name"

    pkg_conflicts
    tar pxf "$bin_dir/$pkg" -C "$tar_dir/" || die "Failed to extract tarball."

    # Create a backup of 'mv', 'mkdir' and 'find' so they aren't removed
    # during package removal.
    cp "$(command -v mv)"    "$cac_dir"
    cp "$(command -v mkdir)" "$cac_dir"
    cp "$(command -v find)"  "$cac_dir"

    log "Removing previous version of package if it exists."
    pkg_remove

    cd "$tar_dir" || die "Aborting due to tar error."

    # Optimization: Only find the deepest directories.
    "$cac_dir/find" . -type d -links -3 -prune | while read -r dir; do
        "$cac_dir/mkdir" -p "$sys_dir/${dir#./}"
    done

    "$cac_dir/find" ./ -mindepth 1 -not -type d | while read -r file; do
        rpath=${file#.}

        [ -z "${rpath##/etc/*}" ] && [ -f "$sys_dir${rpath%/*}/${file##*/}" ] &&
            return

        "$cac_dir/mv" "$file" "$sys_dir${rpath%/*}"
    done

    "$sys_db/$name/post-install" 2>/dev/null

    log "Installed ${pkg%.tar.gz}"
}

pkg_remove() {
    pkg_list "${1:-${name-null}}" || return 1

    # Create a backup of 'rm' and 'rmdir' so they aren't
    # removed during package removal.
    cp "$(command -v rm)"    "$cac_dir"
    cp "$(command -v rmdir)" "$cac_dir"

    while read -r file; do
        [ "${file##/etc/*}" ] || continue

        if [ -d "$sys_dir$file" ]; then
            "$cac_dir/rmdir" "$sys_dir$file" 2>/dev/null || continue
        else
            "$cac_dir/rm" -f -- "$sys_dir$file" || log "Failed to remove $file."
        fi
    done < "$sys_db/${1:-$name}/manifest"

    # Use the backup of 'rm' to remove 'rmdir' and itself.
    "$cac_dir/rm" "$cac_dir/rmdir" "$cac_dir/rm"

    log "Removed ${1:-$name}."
}

pkg_updates() {
    for item in "$sys_db/"*; do
        pkg_search "${item##*/}"

        read -r db_ver db_rel < "$item/version"
        read -r re_ver re_rel < "$rep_dir/${item##*/}/version"

        [ "$db_ver-$db_rel" != "$re_ver-$re_rel" ] &&
            printf '%s\n' "${item##*/} $re_ver-$re_rel"
    done
}

pkg_list() {
    [ "$1" ] && { [ -d "$sys_db/$1" ]; return "$?"; }

    for item in "$sys_db/"*; do
        read -r version release 2>/dev/null < "$item/version" &&
            printf '%s\n' "${item##*/} $version-$release"
    done
}

args() {
    [ -w "$sys_dir/" ] || case $1 in
        i*|r*) die "No write permissions to \$KISS_ROOT."
    esac

    case $1 in b*|c*|i*) pkg_setup "${2-null}"; esac
    case $1 in
        b*) [ -f checksums ] ||
                die "Checksums missing, run '$kiss checksum $name'"

            pkg_depends

            [ -n "$missing" ] && die "Missing dependencies:${missing%,}"

            pkg_sources
            pkg_verify
            pkg_extract
            pkg_build

            [ -f nostrip ] || pkg_strip

            pkg_manifest
            pkg_tar ;;

        c*) pkg_sources
            pkg_checksum
            log "Generated checksums." ;;

        i*) pkg_depends install
            pkg_install ;;

        l*) pkg_list "$2" ;;
        r*) pkg_remove "${2-null}" || die "Package '${2-null}' not installed." ;;
        u*) pkg_updates ;;
        v*) log "$kiss 0.1.10" ;;

        *)  log "$kiss [b|c|i|l|r|u] [pkg]" \
                "build:     Build a package." \
                "checksum:  Generate checksums." \
                "install:   Install a package (Runs build if needed)." \
                "list:      List packages." \
                "remove:    Remove a package." \
                "update:    Check for updates."
    esac
}

main() {
    trap pkg_clean EXIT INT
    kiss=${0##*/}
    sys_db=${sys_dir:=$KISS_ROOT}/var/db/$kiss

    [ -z "$KISS_PATH" ] && die "Set \$KISS_PATH to a repository location."

    mkdir -p "${cac_dir:=${XDG_CACHE_HOME:=$HOME/.cache}/$kiss}" \
             "${mak_dir:=$cac_dir/build-$$}" \
             "${bin_dir:=$cac_dir/bin}" \
             "${tar_dir:=$cac_dir/extract-$$}" \
             "${pkg_db:=${pkg_dir:=$cac_dir/pkg-$$}/var/db/$kiss}" ||
             die "Couldn't create directories."

    args "$@"
}

main "$@"