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

die() {
    printf '\033[31m!>\033[m %s\n' "$@" >&2
    exit 1
}

log() {
    printf '\033[32m=>\033[m %s\n' "$@"
}

source_type() {
    [ -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() {
    rm -rf -- "$mak_dir" "$pkg_dir" \
              "$cac_dir/manifest-$$" "$cac_dir/tar" "$cac_dir/checksums-$$"
}

pkg_search() {
    set -f
    # shellcheck disable=2086,2046
    set -- "$1" $(IFS=: find $KISS_PATH -maxdepth 1 -name "$1")
    set +f

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

    rep_dir=${2%/$1}
}

pkg_setup() {
    pkg_search "$1"

    cd "$rep_dir/$1" || die "'$rep_dir/$1' 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 ver rel < version || die "Version file not found."
    pkg=${name:=$1}\#$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() {
    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*)
               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"
    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 | tee manifest > "$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" "./var/db/$kiss/$name/manifest" -O |
    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" &&
            die "Package '$name' conflicts with '${db##*/}'."
    done
}

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

    pkg_conflicts

    # Create a backup of 'tar' so it isn't removed during
    # package installation.
    cp "$(command -v tar)" "$cac_dir"

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

    "$cac_dir/tar" kpxf "$bin_dir/$pkg" -C "$sys_dir/" 2>/dev/null
    "$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 && log "Removed $file"
    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"
}

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() {
    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 ;;

        *)  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##*/}

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

    [ -z "$KISS_ROOT" ] && [ "$(id -u)" != 0 ] &&
        die "\$KISS_ROOT is set to '/' so you need to be root."

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

    args "$@"
}

main "$@"