#!/bin/sh

# vars
BUILD_SCRIPT="spkgbuild"
PKGDB_DIR="/var/lib/scratchpkg/db"
CBUILD_DIR="/var/lib/scratchpkg/cbuild"
WORLD_FILE="${PKGDB_DIR%/*}/world" 
REPO_FILE="${REPO_FILE:-/etc/scratchpkg.repo}"
MASK_FILE="${MASK_FILE:-/etc/scratchpkg.mask}"
CONFIG_FILE="${CONFIG_FILE:-/etc/scratchpkg.conf}"
ALIAS_FILE="${ALIAS_FILE:-/etc/scratchpkg.alias}"
# check alias port
grep -qEv '^(#|[[:space:]]*$)' "$ALIAS_FILE" 2>/dev/null && ALIAS_PORT="yes" || ALIAS_PORT="no"

# default value from pkgbuild
SOURCE_DIR="/var/cache/scratchpkg/sources"
PACKAGE_DIR="/var/cache/scratchpkg/packages"
WORK_DIR="/var/cache/scratchpkg/work"
CURL_OPTS=""
COMPRESSION_MODE="xz"
NO_STRIP="no"
IGNORE_CHECKSUM="no"
KEEP_DOC="no"
KEEP_GOING="no"
KEEP_LIBTOOL="no"
KEEP_LOCALE="no"
NOCOLOR="no"
THEME="venom"

# apply scratchpkg.conf
. "$CONFIG_FILE"

# color themes
colors_dracula() {
        COLOR1='\033[38;5;203m' # pink
        COLOR2='\033[38;5;84m'  # cyan-green
        COLOR3='\033[38;5;228m' # yellow
        COLOR4='\033[38;5;117m' # light blue
        COLOR5='\033[38;5;141m' # purple
        CRESET='\033[0m'        # reset
}

colors_nord() {
        COLOR1='\033[38;5;174m' # soft red
        COLOR2='\033[38;5;108m' # desaturated green
        COLOR3='\033[38;5;179m' # warm yellow
        COLOR4='\033[38;5;109m' # nord blue
        COLOR5='\033[38;5;139m' # nord purple
        CRESET='\033[0m'        # reset
}

colors_venom() {
        COLOR1='\033[38;5;1m'   # red
        COLOR2='\033[38;5;2m'   # green
        COLOR3='\033[38;5;3m'   # yellow
        COLOR4='\033[38;5;4m'   # blue
        COLOR5='\033[38;5;248m' # light gray
        CRESET='\033[0m'        # reset
}

load_colors() {
        # reset all vars
	COLOR1= COLOR2= COLOR3= COLOR4= COLOR5= CRESET=

	[ "$NOCOLOR" = "yes" ] && return 0

        case "$THEME" in
                dracula)  colors_dracula ;;
                nord)     colors_nord ;;
                *)        colors_venom ;;
        esac
}

# apply color theme
load_colors

needroot() {	
	[ "$(id -u)" = 0 ] || {
		msgerr "this operation need root access!"
		exit 1
	}
}

msg() { printf "${COLOR2}==>${CRESET} %s\n" "$1"; }
msginst() { printf "[${COLOR2}i${CRESET}] %s\n" "$1"; }
msgmiss() { printf "[${COLOR3}m${CRESET}] %s\n" "$1"; }
msgnoinst() { printf "[-] %s\n" "$1"; }
msgerr() { printf "${COLOR1}ERROR:${CRESET} %s\n" "$1" >&2; }
msgwarn() { printf "${COLOR3}WARNING:${CRESET} %s\n" "$1" >&2; }
msg_portnotfound() { printf "%s\n" "Port '$1' not found."; }
msg_portnotinstalled() { printf "%s\n" "Port '$1' not installed."; }
msg_portalreadyinstalled() { printf "%s\n" "Port '$1' already installed."; }

msg_depsstatus() {
	for i in "$@"; do
		if scratch_isinstalled "$i"; then
			msginst "$i"
		elif getportpath "$i" >/dev/null; then
			msgnoinst "$i"
		else
			msgmiss "$i"
		fi
	done
	unset i
}

getportpath() {	
	for repo in $PORT_REPO; do
		if [ -f "$repo/$1/$BUILD_SCRIPT" ]; then
			printf '%s/%s\n' "$repo" "$1"
			return 0
		fi
	done
	return 1	
}


get_iversion() {
    [ -f "$PKGDB_DIR/$1" ] || return 1
    read -r _iv _ir _ < "$PKGDB_DIR/$1"
    printf '%s-%s\n' "$_iv" "$_ir"
}

get_version() {
	p=$(getportpath "$1") || return 1

	awk '/^version=/{v=$0; sub(/^version=/,"",v); gsub(/["'\'']/,"",v)}
	     /^release=/{r=$0; sub(/^release=/,"",r); gsub(/["'\'']/,"",r)}
		END{
		     if (v=="") v=0
		     if (r=="") r=0
		     print v "-" r
		   }
	    ' "$p/$BUILD_SCRIPT"
}

allinstalled() {
	find "$PKGDB_DIR" -maxdepth 1 -type f -printf '%f\n' 
}

deps_alias() {
    printf '%s\n' $deps | awk -v alias_file="$ALIAS_FILE" '
    BEGIN {
        while ((getline line < alias_file) > 0) {
            if (line ~ /^[[:space:]]*$/ || line ~ /^#/) continue
            split(line, a)
            alias[a[1]] = a[2]
        }
        close(alias_file)
    }
    {
        print ($0 in alias) ? alias[$0] : $0
    }'
}
get_depends() {
    if [ -f "$CBUILD_DIR/${1}.dep" ]; then
        deps_file="$CBUILD_DIR/$1.dep"
    else
        ppath=$(getportpath "$1") || return 0
        deps_file="$ppath/depends"
    fi

    [ -f "$deps_file" ] || return 0
    deps=$(grep -v '^$' "$deps_file")

    [ "$ALIAS_PORT" = "yes" ] && deps_alias "$deps" || printf "%s" "$deps"
}


confirm() {
	printf "%s" "$1 (Y/n) "
	read -r response
	case "$response" in
		[Nn][Oo]|[Nn]) printf "%s\n" "$2"; return 2 ;;
		*) : ;;
	esac
	return 0
}

checktool() {	
	command -v "$1" >/dev/null 2>&1 || {
	msgerr "'$1' not exist in your system!"
	exit 1
	}
}

needarg() {
	[ "$*" ] || {
		printf "%s\n" "This operation required an arguments!"
		exit 1
	}
}

keep_going() {
	[ "$KEEP_GOING" = "yes" ] || [ "$KEEP_GOING_LOCAL" = "1" ]
}

settermtitle() {
	printf "\033]0;%s\a" "$*"
}

summary_show() {
    failed="$1"  # $1 -> list of failed packages
    skipped="$2" # $2 -> list of skipped packages (may be empty)
    action="$3"  # $3 -> action: install, upgrade, build, ...

    # If there is nothing to report, return silently
    [ -n "$failed" ] || [ -n "$skipped" ] || return 0

    # Separate summary from previous output
    printf '\n'

    # Report failed packages
    if [ -n "$failed" ]; then
        msgerr "The following packages failed to $action:"
        for p in $failed; do
            printf "  - %s\n" "$p"
        done
    fi

    # Report skipped packages (if any)
    if [ -n "$skipped" ]; then
        msgwarn "The following packages were skipped:"
        for p in $skipped; do
            printf "  - %s\n" "$p"
        done
    fi
}

summary_title() {
    error="$1"    # $1 -> error flag (0 if success, non-zero if errors occurred)
    done_pkg="$2" # $2 -> list of successfully processed packages
    count="$3"    # $3 -> normal counter (used when no errors occurred)
    total="$4"    # $4 -> total number of packages
    action="$5"   # $5 -> action: installed, upgraded, built, ...


    if [ "$error" -ne 0 ]; then
        # When errors occurred, recalculate the number of successfully processed packages
        successful_count=$(printf "%s\n" "$done_pkg" | wc -w)
        settermtitle "$successful_count/$total package(s) $action."
    else
        # Fast path: no errors, use the normal counter
        settermtitle "$count/$total package(s) $action."
    fi
}

scratch_build() {
	failed_builds=""

	while [ "$1" ]; do
		case $1 in
			-i|-u|-r|-g|-p) ;;
			--log) LOG=1;;
			-k) KEEP_GOING_LOCAL=1;;
			-*) OPTS="$OPTS $1";;
			 *) PKGNAME="$PKGNAME $1";;
		esac
		shift
	done
	[ "$PKGNAME" ] || {
		printf "%s\n" "Please specify package(s) to build."
		return 1
	}

	for pkg in $PKGNAME; do
		ppath=$(getportpath "$pkg") || {
			msg_portnotfound "$pkg"
			# If keep going is enabled, log and continue
			if keep_going; then
				failed_builds="$failed_builds $pkg"
				continue
			else
				return 1
			fi
		}
		cd "$ppath" || return 1
		settermtitle "Building $pkg..."

		# Run pkgbuild and capture the result
		if [ "$LOG" ]; then
			pkgbuild $OPTS | tee "/var/log/$pkg-$(get_version "$pkg")-build-$(date +%Y%m%d-%H%M).log"
			_build_ret=$?
		else
			pkgbuild $OPTS
			_build_ret=$?
		fi

		if [ "$_build_ret" -eq 0 ]; then
			settermtitle "Building $pkg done."
		else
			settermtitle "Building $pkg failed."
			 if keep_going; then
				msgwarn "Build failed for '$pkg', continuing with next package..."
				failed_builds="$failed_builds $pkg"
			else
				return 1
			fi
		fi
		cd - >/dev/null || return 1
	done

	# Show error summary if exist
	FAILED_PKGS="$failed_builds"
	SKIPPED_PKGS=""
	summary_show "$FAILED_PKGS" "$SKIPPED_PKGS" "build"
	[ -n "$FAILED_PKGS" ] && return 1

	return 0
}

human_size() {
	# Converts bytes to a human-readable size with one decimal
	b=$1
        awk -v b="$b" 'BEGIN {
            suf="B K M G T P";
            split(suf,u);
            i=1;
            val=b;
            while(val>=1024 && i<6){ val/=1024; i++ }
            printf "%.1f%s", val, u[i]
        }'
}  

diff_lists() {
	# Diff lists (all - keep)
        all="$1"
        keep="$2"
        out=""
        oldIFS=$IFS
        IFS='|'
        set -- $all
        IFS=$oldIFS
        for f; do
            case "|$keep|" in
                *"|$f|"*) ;;  # keep
                *) out="${out}${out:+|}$f" ;;
            esac
        done
        printf '%s' "$out"
}

show_list() {
	# Show files and sizes
        list="$1"
        dir="$2"
        [ -z "$list" ] && return 0
        
	# Print list, one file per line
        oldIFS=$IFS
        IFS='|'
        set -- $list
        IFS=$oldIFS

        for f; do printf '%s\n' "$f"; done

        # Calculate total size
        total=0
        for f; do
            [ -f "$dir/$f" ] || continue
            sz=$(wc -c <"$dir/$f" 2>/dev/null || printf "0\n")
            total=$((total + sz))
        done

        printf "Total size %s: %s\n\n" "${dir##*/}" "$(human_size "$total")"
    }

scratch_cache() {
    needroot "Clear old caches"

    clean_pkg=1
    clean_src=1

    case "${1:-}" in
        "") : ;;  # default: clean both
        -p|--pkg) clean_src=0 ;;
        -s|--src) clean_pkg=0 ;;
        -h|--help)
            cat <<EOF
Usage: scratch cache [pkg|src]

  (no args)   clean packages AND sources cache
  -p, --pkg   clean packages cache only
  -s, --src   clean sources cache only
EOF
            return 0
            ;;
        *)
            msgerr "Invalid argument: $1"
            return 1
            ;;
    esac
    
    # Internal lists (compact with | separator)
    allpkg=""
    allsrc=""
    keeppkg=""
    keepsrc=""

    # Scan package cache
    if [ "$clean_pkg" -eq 1 ]; then
        for f in $(find "$PACKAGE_DIR" -type f 2>/dev/null); do
            file=${f##*/}
            allpkg="${allpkg}${allpkg:+|}$file"
        done
    fi

    # Scan source cache
    if [ "$clean_src" -eq 1 ]; then
        for f in $(find "$SOURCE_DIR" -type f 2>/dev/null); do
            file=${f##*/}
            allsrc="${allsrc}${allsrc:+|}$file"
        done
    fi

    # Scan PKGDB to determine what to keep
    for portname in $(allinstalled); do

	# Cache portpath to avoid calling getportpath multiple times
        portpath=$(getportpath "$portname")

	if [ -z "$portpath" ]; then
            msgerr "$portname archived, remove it."
            continue
        fi

        # Keep packages - use get_iversion function
        if [ "$clean_pkg" -eq 1 ]; then
            pvr=$(get_iversion "$portname")
            [ -n "$pvr" ] && pkgname="${portname}-${pvr}.spkg.tar.${COMPRESSION_MODE}"
            keeppkg="${keeppkg}${keeppkg:+|}$pkgname"
        fi

        # Keep sources - read from .checksums file
        if [ "$clean_src" -eq 1 ]; then
            checksums_file="$portpath/.checksums"

            if [ -f "$checksums_file" ]; then
                # Read checksum and filename directly (space-separated)
                while read -r checksum filename; do
                    # Skip empty lines and ensure filename exists
                    [ -n "$filename" ] && keepsrc="${keepsrc}${keepsrc:+|}$filename"
                done < "$checksums_file"
            fi
        fi
    done

    to_remove_pkg=""
    to_remove_src=""
    [ "$clean_pkg" -eq 1 ] && to_remove_pkg=$(diff_lists "$allpkg" "$keeppkg")
    [ "$clean_src" -eq 1 ] && to_remove_src=$(diff_lists "$allsrc" "$keepsrc")

    show_list "$to_remove_pkg" "$PACKAGE_DIR"
    show_list "$to_remove_src" "$SOURCE_DIR"

    # Nothing to remove?
    if [ -z "$to_remove_pkg" ] && [ -z "$to_remove_src" ]; then
        msg "Cache is already clean."
        return 0
    fi

    # Confirm deletion
    if confirm "Remove old caches?" "Old caches are kept."; then
        # delete in parallel
        [ -n "$to_remove_pkg" ] && \
        printf '%s' "$to_remove_pkg" | tr '|' '\n' | xargs -P "$(nproc)" -I{} rm -f "$PACKAGE_DIR/{}"

        [ -n "$to_remove_src" ] && \
        printf '%s' "$to_remove_src" | tr '|' '\n' | xargs -P "$(nproc)" -I{} rm -f "$SOURCE_DIR/{}"
  
	msg "Old caches removed!"
    fi
}

scratch_cat() {	
	needarg "$@"
	PPATH=$(getportpath "$1") || {
		msg_portnotfound "$1"
		return 1
	}
	cat "$PPATH/$BUILD_SCRIPT"

}

scratch_config() {
	printf '%-12b%s%b %s\n' "$COLOR5" "MAINOPTS:" "$CRESET" "$MAINOPTS"
	printf '%-12b%s%b %s\n' "$COLOR5" "REPO_FILE:" "$CRESET" "$REPO_FILE"
	printf '%-12b%s%b %s\n' "$COLOR5" "MASK_FILE:" "$CRESET" "$MASK_FILE"
	printf '%-12b%s%b %s\n' "$COLOR5" "CONFIG_FILE:" "$CRESET" "$CONFIG_FILE"
	printf '%-12b%s%b %b%s%b\n' "$COLOR5" "CFLAGS:" "$CRESET" "$COLOR2" "$CFLAGS" "$CRESET"
	printf '%-12b%s%b %b%s%b\n' "$COLOR5" "CXXFLAGS:" "$CRESET" "$COLOR2" "$CXXFLAGS" "$CRESET"
	printf '%-12b%s%b %b%s%b\n' "$COLOR5" "FCFLAGS:" "$CRESET" "$COLOR2" "$FCFLAGS" "$CRESET"
	printf '%-12b%s%b %b%s%b\n' "$COLOR5" "FFLAGS:" "$CRESET" "$COLOR2" "$FFLAGS" "$CRESET"
	printf '%-12b%s%b %b%s%b\n' "$COLOR5" "MAKEFLAGS:" "$CRESET" "$COLOR2" "$MAKEFLAGS" "$CRESET"
	printf '%-12b%s%b %b%s%b\n' "$COLOR5" "LDFLAGS:" "$CRESET" "$COLOR2" "$LDFLAGS" "$CRESET"
	printf '%-12b%s%b %b%s%b\n' "$COLOR5" "RUSTFLAGS:" "$CRESET" "$COLOR2" "$RUSTFLAGS" "$CRESET"
	printf '%-12b%s%b %b%s%b\n' "$COLOR5" "CURL_OPTS:" "$CRESET" "$COLOR4" "$CURL_OPTS" "$CRESET"
	printf '%-12b%s%b %b%s%b\n' "$COLOR5" "COMPRESSION_MODE:" "$CRESET" "$COLOR4" "$COMPRESSION_MODE" "$CRESET"
	printf '%-12b%s%b %b%s%b\n' "$COLOR5" "NO_STRIP:" "$CRESET" "$COLOR4" "$NO_STRIP" "$CRESET"
	printf '%-12b%s%b %b%s%b\n' "$COLOR5" "IGNORE_CHECKSUM:" "$CRESET" "$COLOR4" "$IGNORE_CHECKSUM" "$CRESET"
	printf '%-12b%s%b %b%s%b\n' "$COLOR5" "KEEP_LIBTOOL:" "$CRESET" "$COLOR4" "$KEEP_LIBTOOL" "$CRESET"
	printf '%-12b%s%b %b%s%b\n' "$COLOR5" "KEEP_LOCALE:" "$CRESET" "$COLOR4" "$KEEP_LOCALE" "$CRESET"
	printf '%-12b%s%b %b%s%b\n' "$COLOR5" "KEEP_DOC:" "$CRESET" "$COLOR4" "$KEEP_DOC" "$CRESET"
	printf '%-12b%s%b %b%s%b\n' "$COLOR5" "KEEP_GOING:" "$CRESET" "$COLOR4" "$KEEP_GOING" "$CRESET"
	printf '%-12b%s%b %s\n' "$COLOR5" "SOURCE_DIR:" "$CRESET" "$SOURCE_DIR"
	printf '%-12b%s%b %s\n' "$COLOR5" "PACKAGE_DIR:" "$CRESET" "$PACKAGE_DIR"
	printf '%-12b%s%b\n' "$COLOR5" "PORT_REPO:" "$CRESET"
	for i in $PORT_REPO; do
		printf '  %-10s %s\n' "" "$i"
	done

	return 0
}

scratch_dependent() {
	needarg "$@"
	if getportpath "$1" >/dev/null 2>&1; then
		for repo in $PORT_REPO; do
			awk -v pkg="$1" 'FNR==1{f=FILENAME} $0==pkg{found=1} found{n=split(f,a,"/"); 
			print a[n-1]; found=0; nextfile}' "$repo"/*/depends 2>/dev/null
		done | sort -u | while read -r dpd; do
			msg_depsstatus "$dpd"
		done
	else
		msg_portnotfound "$1"
		return 1
	fi
}

scratch_depends() {
	needarg "$@"
	if getportpath "$1" >/dev/null; then
		depends=$(get_depends "$1")
	else
		msg_portnotfound "$1"
		return 1
	fi
	msg_depsstatus $depends
}

deplist() {
    local pkg="$1"

    # skip excluded dependencies
    case " $exclude " in *" $pkg "*) return 0 ;; esac

    # check for circular dependencies
    case " $CHECK " in *" $pkg "*) return 0 ;; esac

    # add package to currently processing
    CHECK="$CHECK $pkg "

    # check dependencies
    for dep in $(get_depends "$pkg"); do
        if [ "$quick" = 1 ] && scratch_isinstalled "$dep"; then
            continue
        fi

        case " $DEP " in *" $dep "*) continue ;; esac

	if ! getportpath "$dep" >/dev/null 2>&1; then
            MISSINGDEP="$MISSINGDEP $dep($pkg) "
        else
            deplist "$dep"
        fi
    done

    # add dependency to list
    case " $DEP " in
        *" $pkg "*) ;;
        *)
            if [ "$quick" = 1 ]; then
                scratch_isinstalled "$pkg" || DEP="$DEP $pkg "
            else
                DEP="$DEP $pkg "
            fi
            ;;
    esac

    # remove from processing list
    CHECK=$(printf "%s" "$CHECK" | sed "s/ $pkg / /g")
}

scratch_deplist() {

    # parse arguments
    OLDIFS=$IFS
    IFS=,
    while [ "$1" ]; do
        case $1 in
            -q) quick=1 ;;
            --exclude=*)
                for i in ${1#*=}; do
                    exclude="$exclude $i "
                done
                ;;
            -*) ;;
            *) PKG="$PKG $1 " ;;
        esac
        shift
    done
    IFS=$OLDIFS

    [ "$PKG" ] || {
        printf "%s\n" "Please specify port(s) to list dependencies."
        return 1
    }

    # validate packages
    for p in $PKG; do
        if getportpath "$p" >/dev/null 2>&1; then
            PPKG="$PPKG $p "
        elif [ "$quick" != 1 ]; then
            msg_portnotfound "$p"
        fi
    done

    # process dependencies
    for p in $PPKG; do
        deplist "$p"
    done

    [ "$DEP" ] || return 0

    # output results
    if [ "$quick" = 1 ]; then
        printf "%s\n" $DEP
    else
        msg_depsstatus $DEP
        if [ "$MISSINGDEP" ]; then
            for m in $MISSINGDEP; do
                printf "%s\n" "Missing deps: $m" | sed 's/(/ (/'
            done
        fi
    fi
}

dup_status() {
    IFS='|' read -r dversion drelease <<EOF
$(awk '
    /^version=/ {
        gsub(/version=|["'\'']/, "")
        gsub(/[^A-Za-z0-9._+-]/, "")
        v=$0
    }
    /^release=/ {
        gsub(/release=|["'\'']/, "")
        gsub(/[^A-Za-z0-9._+-]/, "")
        r=$0
    }
    END {
        printf "%s|%s", v, r
    }
' "$2/$1/$BUILD_SCRIPT")
EOF

	scratch_isinstalled "$1" && ins="[${COLOR2}i${CRESET}]" || ins="[ ]"
	printf "%b %b(%s)%b %s %b%s-%s%b\n" "$ins" "$COLOR5" "${2##*/}" "$CRESET" "$1" "$COLOR4" "$dversion" "$drelease" "$CRESET"
}

scratch_dup() {	
	dup=$(find $PORT_REPO -type d -printf '%P\n'| sort | uniq -d)
	
	if [ "$dup" ] ; then
		for dport in $dup; do
			for drepo in $PORT_REPO; do
				[ -d "$drepo/$dport" ] && dup_status "$dport" "$drepo"
			done
		done 
	else
		msg "No duplicate ports found."	
	fi

	return 0
}

scratch_files() {	
	needarg "$@"
	if scratch_isinstalled "$1"; then
		cat "$PKGDB_DIR/$1"
	else
		msg_portnotinstalled "$1"
	fi	
}

scratch_foreign() {

	foreign_found=0
	for pkg in $(allinstalled); do
		if ! getportpath "$pkg" >/dev/null; then
			iversion=$(get_iversion "$pkg")
			printf "%s %s\n" "$pkg" "$iversion"
			foreign_found=1
		fi
	done

	[ "$foreign_found" -eq 0 ] && msg "No foreign packages found."
	return 0
}

scratch_help() {
cat << 'EOF'
Usage:
    scratch <options> [<arg>]
    
Options:
    sync                   <arg>    update ports database (use portsync arg)
                                    -p, --prune   prune local git repository
                                    -r, --reset   reset git repositories

    build        <ports>   <arg>    build ports (use pkgbuild arg, except '-i', '-u', '-r', '-g', & '-p')
                                    -f            force rebuild package
				    -k            keep going building next package on error
                                    -m            skip checksum checking
                                    -o            fetch sources only
                                    -q, --quiet   show only status messages and errors
                                    -w            keep working directory
                                    -y            skip ask user confirmation
                                    -C, --clean   remove downloaded sources and prebuilt packages
                                    --log         log build process (/var/log/name-version-release-date.log)
	
    install      <ports>   <arg>    install ports (use pkgbuild arg, except '-i' & '-u')
                                    -c            ignore conflict when installing package
                                    -f            force rebuild package
				    -k            keep going installing next package on error
                                    -m            skip checksum checking
                                    -n            skip dependencies
                                    -o            fetch sources only
                                    -q, --quiet   show only status messages and errors
                                    -r            reinstall
                                    -w            keep working directory
                                    -y            skip ask user confirmation
                                    --exclude=*   exclude dependencies, comma separated
                                    --no-backup   skip backup when reinstalling package

    upgrade      <ports>   <arg>    upgrade ports (use pkgbuild arg, except '-i' & '-r')
	                            -c            ignore conflict when installing package
                                    -f            force rebuild package
				    -k            keep going upgrading next package on error
                                    -m            skip checksum checking
                                    -n            skip dependencies
                                    -o            fetch sources only
                                    -q, --quiet   show only status messages and errors
                                    -w            keep working directory
                                    -y            skip ask user confirmation
                                    --exclude=*   exclude dependencies, comma separated
                                    --no-backup   skip backup when upgrading package

    sysup                  <arg>    full system upgrade (use pkgbuild arg, except '-i', '-r'  & '-u')
                                    -c            ignore conflict when installing package
                                    -f            force rebuild package
				    -k            keep going upgrading next package on error
                                    -m            skip checksum checking
                                    -n            skip dependencies
                                    -o            fetch sources only
                                    -q, --quiet   show only status messages and errors
                                    -w            keep working directory
                                    -y            skip ask user confirmation
                                    --exclude=*   exclude dependencies, comma separated
                                    --no-backup   skip backup when upgrading package

    remove       <ports>   <arg>    remove installed ports (use pkgdel arg)
                                    -y            skip ask user confirmation
									
    deplist      <ports>            print all dependencies for ports
                                    -q            skip installed ports
                                    --exclude=*   exclude dependencies, comma separated

    redeps       <ports>   <arg>    find redundant dependencies
                                    -f, --fix     fix (remove) redundant port deps

    cache                           (no args)   clean packages AND sources cache
                                    -p, --pkg   clean packages cache only
                                    -s, --src   clean sources cache only	

    cat          <port>             print spkgbuild
    config                          print scratchpkg config
    dependent    <port>             print reverse dependencies
    depends      <port>             print dependencies
    dup                             print duplicate ports
    files        <port>             print files installed
    foreign                         print foreign ports
    help                            print this help
    hook  <stage> <action> <port>   run port hook on demand (run 'scratch hook' for help)
    info         <port>             print information
    installed                       print all installed ports
    integrity                       check installed port integrity
    isinstalled  <port>             check whether port is installed (status 0=installed, 1=not installed)
    locate       <file>             print location of file in ports repo
    missingdep                      print missing dependencies
    news         <args>             news manager (run 'scratch news help' for usage)
    orphan                          print orphan installed ports
    outdate                         print outdated ports
    path         <port>             print path in repo
    provide      <file>             print ports provided file
    purge        [ports]            remove installed ports and its orphan dependencies

    search       <pattern>          find ports in repo
                                    -n, --name   search in port names only

    trigger      [ports]            run system trigger
    world        [ports]            print/add/remove world list

Global options:
    --append-repo=<repo path>       append custom local repo path (can use multiple times)
    --prepend-repo=<repo path>      prepend custom local repo path (can use multiple times)
    --override-repo=<repo path>     override repo in $REPO_FILE with custom local repo (can use multiple times)
    --repo-file=<repo file>         use custom repo file (default: $REPO_FILE)
    --config-file=<config file>     use custom config file (default: $CONFIG_FILE)
    --alias-file=<alias file>       use custom alias file (default: $ALIAS_FILE)
    --mask-file=<mask file>         use custom mask file (default: $MASK_FILE)
EOF
}

scratch_info() {
    needarg "$@"
    ppath=$(getportpath "$1") || {
        msg_portnotfound "$1"
        return 1
    }

IFS='|' read -r version release commit desc maint homep <<EOF
$(awk '
    /^version=/ {
        sub(/version=/, "")
        v=$0
    }
    /^release=/ {
        sub(/release=/, "")
        r=$0
    }
    /^commit=/ {
        sub(/commit=/, "")
        c=$0
    }
    /^description=/ {
        sub(/description=/, "")
        gsub(/"/, "")
        d=$0
    }
    /^maintainer=/ {
        sub(/maintainer=/, "")
        gsub(/"/, "")
        m=$0
    }
    /^homepage=/ {
        sub(/homepage=/, "")
        gsub(/"/, "")
        h=$0
    }
    END { 
        printf "%s|%s|%s|%s|%s|%s", v, r, c, d, m, h 
    }
' "$ppath/$BUILD_SCRIPT")
EOF

    # Read installed version
    iver="$(get_iversion "$1")"
    [ -z "$iver" ] && iver="Not installed"

    # Read depends file
    deps=$(get_depends "$1" | tr '\n' ' ')
    [ -z "$deps" ] && deps="None"

    # Check dependent (required by)
    required_by="$(scratch_dependent "$1" | awk '{printf "%s ", $2}')"

    [ -z "$required_by" ] && required_by="None"


    printf "%-12b%s%b %s\n" "$COLOR5" "Description:" "$CRESET" "$desc"
    printf "%-12b%s%b %s\n" "$COLOR5" "Maintainer:" "$CRESET" "$maint"
    printf "%-12b%s%b %s\n" "$COLOR5" "Homepage:" "$CRESET" "$homep"
    printf "%-12b%s%b %s\n" "$COLOR5" "Path:" "$CRESET" "$ppath"
    printf "%-12b%s%b %b%s%b\n" "$COLOR5" "Name:" "$CRESET" "$COLOR2" "$1" "$CRESET"
    printf "%-12b%s%b %b%s%b\n" "$COLOR5" "Version:" "$CRESET" "$COLOR4" "$version" "$CRESET"
    printf "%-12b%s%b %b%s%b\n" "$COLOR5" "Release:" "$CRESET" "$COLOR4" "$release" "$CRESET"
    [ "$commit" ] && printf "%-12b%s%b %s\n" "$COLOR5" "Commit:" "$CRESET" "$commit"
    printf "%-12b%s%b %b%s%b\n" "$COLOR5" "Installed:" "$CRESET" "$COLOR2" "$iver" "$CRESET"
    printf "%-12b%s%b %s\n" "$COLOR5" "Depends:" "$CRESET" "$deps"
    printf "%-12b%s%b %s\n" "$COLOR5" "Required by:" "$CRESET" "$required_by"
}

hook() {
	# hook <stage> <action> [port]

	# if using -o (download only), dont run hook
	[ "$DOWNLOAD_ONLY" = "1" ] && return 0

	# silent sanity check
	[ "$1" ] && [ "$2" ] || return 0

	hook_stage="$1"
	hook_action="$2"
	port="$3"

	if [ "$port" ]; then
		hook_dir="$(getportpath "$port")" || return 1
	else
		hook_dir="."
	fi

	hook="$hook_dir/$hook_stage-$hook_action"

	# hook are optional
	if [ -f "$hook" ]; then
		msg "hook $hook_stage-$hook_action"
		sh "$hook"
	fi
}

scratch_hook() {
	needroot
	if [ -z "$1" ] || [ -z "$2" ] || [ -z "$3" ]; then
		msgwarn "Usage: scratch hook <stage> <action> <port>"
		msg "scratch hook <pre|post> <install|remove> <port>"
		msg "Example: scratch hook post install linux"
		return 1
	fi

	stage="$1"
	action="$2"
	port="$3"

	case "$stage" in
	        pre|post)
	            ;;
	        *)
	            msgerr "stage must be 'pre' or 'post'"
	            return 1
	            ;;
	esac

	case "$action" in
	        install|remove)
	            ;;
	        *)
	            msgerr "action must be 'install' or 'remove'"
	            return 1
	            ;;
	esac

	hook "$stage" "$action" "$port"
}

scratch_install() {
	needroot "Installing package"

	failed_installs=""
	skipped_installs=""

	while [ "$1" ]; do
		case $1 in
			-i|-u) ;;
			-r) REINSTALL=1;;
			-o) DOWNLOAD_ONLY=1; OPTS="$OPTS $1";;
			-y) NOCONFIRM=1;;
			-n) NO_DEP=1;;
			-k) KEEP_GOING_LOCAL=1;;
			--exclude=*) EXOPT=$1;;
			-*) OPTS="$OPTS $1";;
			*) PKGNAME="$PKGNAME $1";;
		esac
		shift
	done	
	[ "$PKGNAME" ] || {
		printf "%s\n" "Please specify package(s) to install."
		return 1
	}

	# if reinstall, dont calculate dep, just reinstall it then exit
	if [ "$REINSTALL" = 1 ]; then
		error=0
		for ii in $PKGNAME; do
			if ! ppath=$(getportpath "$ii"); then
				msg_portnotfound "$ii"
				if keep_going; then
					failed_installs="$failed_installs $ii"
					continue
				else
					return 1
				fi
			elif ! scratch_isinstalled "$ii"; then
				msg_portnotinstalled "$ii"
				if keep_going; then
					failed_installs="$failed_installs $ii"
					continue
				else
					return 1
				fi
			else
				cd "$ppath" || return 1
					settermtitle "Reinstalling $ii..."
					if pkgbuild $OPTS -r; then
						done_pkg="$done_pkg $ii"
					else
						if keep_going; then
							msgwarn "Reinstall failed for '$ii', continuing with next package..."
							failed_installs="$failed_installs $ii"
							error=1
						else
							error=1
							cd - >/dev/null || return 1
							break
						fi
					fi
				cd - >/dev/null || return 1
			fi
		done
		[ "$done_pkg" ] && scratch_trigger "$done_pkg"

		# Show error summary if exist
		FAILED_PKGS="$failed_installs"
		SKIPPED_PKGS=""
		summary_show "$FAILED_PKGS" "$SKIPPED_PKGS" "reinstall"

		settermtitle "Package(s) reinstalled."
		return "$error"
	fi

	# No dep
	if [ "$NO_DEP" = 1 ]; then
		error=0
		for ii in $PKGNAME; do
			if ! ppath=$(getportpath "$ii"); then
				msg_portnotfound "$ii"
				if keep_going; then
					failed_installs="$failed_installs $ii"
					continue
				else
					return 1
				fi
			elif scratch_isinstalled "$ii"; then
				msg_portalreadyinstalled "$ii"
				skipped_installs="$skipped_installs $ii"
				continue
			else
				cd "$ppath" || return 1
					settermtitle "Installing $ii..."
					if pkgbuild -i $OPTS; then
						done_pkg="$done_pkg $ii"
						world_add "$ii"
					else
						if keep_going; then
							msgwarn "Install failed for '$ii', continuing with next package..."
							failed_installs="$failed_installs $ii"
							error=1
						else
							error=1
							cd - >/dev/null || return 1
							break
						fi
					fi
				cd - >/dev/null || return 1
			fi
		done
		[ "$done_pkg" ] && scratch_trigger "$done_pkg"

		# Show error summary if exist
		FAILED_PKGS="$failed_installs"
		SKIPPED_PKGS="$skipped_installs"
		summary_show "$FAILED_PKGS" "$SKIPPED_PKGS" "install"

		settermtitle "Package(s) installed."
		return "$error"
	fi

	for i in $PKGNAME; do
		if ! getportpath "$i" >/dev/null 2>&1; then
			msg_portnotfound "$i"
		elif scratch_isinstalled "$i"; then
			msg_portalreadyinstalled "$i"
		else
			IPKG="$IPKG $i"
		fi
	done
	[ "$IPKG" ] || return 0
	printf "%s\n" "Resolving dependencies..."
	INST="$(scratch_deplist -q "$IPKG" "$EXOPT")"
	if [ "$INST" ]; then
		printf "\n"
		pkgcount=0
		installmsg="ACTION#PORTNAME#VERSION"
		for pkg in $INST; do
			pkgcount=$(( pkgcount + 1 ))
			installmsg="$installmsg install#$pkg#$(get_version "$pkg")"
		done
		printf "%s\n" "$installmsg" | tr ' ' '\n' | tr '#' ' ' | column -t
		printf "\n"
		printf "( %s install )\n" "$pkgcount"
		printf "\n"
		if [ "$NOCONFIRM" != "1" ]; then
			confirm "Continue install package(s)?" "Package installation cancelled." || exit $?
			printf "\n"
		fi
		error=0
		count=0
		total=$(printf "%s\n" "$INST" | wc -w)
		for int in $INST; do
			count=$(( count + 1 ))
			if portpathh=$(getportpath "$int"); then
				cd "$portpathh" || return 1
					settermtitle "[ $count/$total ] installing $int..."
					hook "pre" "install"
					if pkgbuild -i $OPTS; then
						case " $IPKG " in *" $int "*) world_add "$int" ;; esac
						hook "post" "install"
						done_pkg="$done_pkg $int"
					else
						if keep_going; then
							msgwarn "Install failed for '$int', continuing with next package..."
							failed_installs="$failed_installs $int"
							error=1
						else
							error=1
							cd - >/dev/null || return 1
							break
						fi
					fi
				cd - >/dev/null || return 1
			else
				msgwarn "Skipping missing package: $int"
				skipped_installs="$skipped_installs $int"
			fi
			unset portpathh
		done
		[ "$done_pkg" ] && scratch_trigger "$done_pkg"

		# Show error summary if exist
		FAILED_PKGS="$failed_installs"
		SKIPPED_PKGS="$skipped_installs"
		summary_show "$FAILED_PKGS" "$SKIPPED_PKGS" "install"
		summary_title "$error" "$done_pkg" "$count" "$total" "installed"

		return "$error"
	fi
}

scratch_installed() {
    find "$PKGDB_DIR" -maxdepth 1 -type f | while IFS= read -r _f; do
        read -r _v _r _ < "$_f"
        printf '%s %s-%s\n' "${_f##*/}" "$_v" "$_r"
    done
}

scratch_integrity() {
	needroot
	if [ "$1" ]; then
		pkg="$1"

		printf "%s\n" "Checking '$pkg' integrity..."

		[ -f "$PKGDB_DIR/$pkg" ] || {
			msg_portnotinstalled "$pkg"
			return 1
		}

		found=0

		{
		read -r _   # skip header
		while IFS= read -r line; do
			[ -e "/$line" ] && continue
			found=1
			[ -L "/$line" ] && \
				printf "broken symlink %s: /%s\n" "$pkg" "$line" || \
				printf "file missing %s: /%s\n" "$pkg" "$line"
		done
		} < "$PKGDB_DIR/$pkg"

		[ "$found" -eq 0 ] && msg "'$pkg' integrity is OK!"

	else
		printf "%s\n" "Checking ports integrity..."

		interr=$(mktemp) || return 1

		allinstalled | xargs -P "$(nproc)" -I{} sh -c '
			pkg="$1"
			db="$2/$pkg"
			interr="$3"

			[ -f "$db" ] || exit 0

			{
			read -r _
			while IFS= read -r line; do
				[ -e "/$line" ] && continue

				# mark error
				printf 1 >> "$interr"

				[ -L "/$line" ] && \
					printf "broken symlink %s: /%s\n" "$pkg" "$line" || \
					printf "missing file %s: /%s\n" "$pkg" "$line"
			done
			} < "$db"
		' _ {} "$PKGDB_DIR" "$interr"

		[ ! -s "$interr" ] && msg "No integrity issues found!"

		rm -f "$interr"

	fi
}

scratch_isinstalled() {
	[ -n "$1" ] && [ -f "$PKGDB_DIR/$1" ]
}

scratch_locate() {
	needarg "$@"
	for repo in $PORT_REPO; do
		grep -Ri "$@" "$repo"/*/.pkgfiles 2>/dev/null | sed 's/:/ /;s/\/\.pkgfiles//' | awk '{print $1,$4}'
	done
}

scratch_missingdep() {
    printf "%s\n" "Checking ports missing deps..."
    found=0

    while IFS= read -r pkg; do
        getportpath "$pkg" >/dev/null || continue
        depends=$(get_depends "$pkg")
        [ -z "$depends" ] && continue

        msd=""
        for d in $depends; do
            scratch_isinstalled "$d" || msd="$msd $d"
        done

        if [ -n "$msd" ]; then
            msg "$pkg:$msd"
            found=1
        fi
    done <<EOF
$(allinstalled)
EOF

    [ "$found" -eq 0 ] && msg "No ports missing deps found!"
    return 0
}

scratch_orphan() {
	if [ -f "$WORLD_FILE" ]; then
	        while IFS= read -r i; do
		        case " $* "   in *" $i "*) continue ;; esac
		        case " $DEP " in *" $i "*) continue ;; esac
		        deplist "$i"
		done < "$WORLD_FILE"
	fi

    allinstalled | awk -v dep=" $DEP " '
        { if (index(dep, " " $0 " ") == 0) print }
    '
}

vercomp() {
	[ "$1" = "$2" ] && return 0
	# - If sorted (exit code 0): $1 <= $2, but we know $1 != $2, so $1 < $2 → return 1
	# - If not sorted (exit code 1): $1 > $2 → return 2
	printf '%s\n%s\n' "$1" "$2" | sort -VC && return 1 || return 2
}

check_outdated() {
	_MASKED_LIST=$(awk '!/^[[:space:]]*$/ && !/^#/ {printf "%s ", $0}' "$MASK_FILE" 2>/dev/null)

	# Build port version map in memory (first-repo-wins via dedup).
	_vmap=$(
		for _repo in $PORT_REPO; do
			awk '
			FNR==1 { if (name != "") printf "%s %s-%s\n", name, (ver?ver:0), (rel?rel:0)
			         name = ver = rel = "" }
			/^name=/    { sub(/^name=/, "");    gsub(/[^[:alnum:]._+~-]/, ""); name = $0 }
			/^version=/ { sub(/^version=/, ""); gsub(/[^[:alnum:]._+~-]/, ""); ver  = $0 }
			/^release=/ { sub(/^release=/, ""); gsub(/[^[:alnum:]._+~-]/, ""); rel  = $0 }
			END { if (name != "") printf "%s %s-%s\n", name, (ver?ver:0), (rel?rel:0) }
			' "$_repo"/*/spkgbuild 2>/dev/null
		done | awk '!seen[$1]++'
	)
	[ -n "$_vmap" ] || return 0

	# Pipe installed map (built with shell read builtins, zero extra forks) into
	# a single awk that loads portver[] from _vmap and joins in memory.
	for _pkg in $(allinstalled); do
		read -r _v _r _ < "$PKGDB_DIR/$_pkg" && printf '%s %s-%s\n' "$_pkg" "$_v" "$_r"
	done | awk -v vmap="$_vmap" '
	BEGIN {
		n = split(vmap, lines, "\n")
		for (i = 1; i <= n; i++) {
			if (split(lines[i], a, " ") == 2)
				portver[a[1]] = a[2]
		}
	}
	($1 in portver) && portver[$1] != $2 { printf "%s#%s#%s\n", $1, $2, portver[$1] }
	' | while IFS='#' read -r pkg ivr pvr; do
		NEWINSTALLED=0
		pversion=${pvr%-*}; prelease=${pvr#*-}
		iversion=${ivr%-*};  irelease=${ivr#*-}

		if [ "$pversion" != "$iversion" ]; then
			vercomp "$pversion" "$iversion"
			[ $? = 1 ] && NEWINSTALLED=1
		elif [ "$prelease" != "$irelease" ]; then
			vercomp "$prelease" "$irelease"
			[ $? = 1 ] && NEWINSTALLED=1
		fi
		MASKED=0
		case " $_MASKED_LIST " in *" $pkg "*) MASKED=1 ;; esac
		# Example: 'glib#2.87.0-1#2.88.1-1#1#0'
		printf "%s#%s#%s#%s#%s\n" "$pkg" "$ivr" "$pvr" "$NEWINSTALLED" "$MASKED"
	done
}

scratch_outdate() {
	out=$(check_outdated)
	[ -z "$out" ] && { msg "All packages are up to date."; return 0; }

	printf '%s\n' "$out" | while IFS='#' read -r pname oldver newver newins masked; do
		unset ii
	        [ "$newins" = 1 ] && ii=" [newer installed]"
		[ "$masked" = 1 ] && ii="$ii [masked]"
		printf "%s %s => %s%s\n" "$pname" "$oldver" "$newver" "$ii"
	 done
}

scratch_path() {
	needarg "$@"
	if PPATH=$(getportpath "$1"); then
		printf "%s\n" "$PPATH"
	else
		msg_portnotfound "$1"
		return 1
	fi
}

scratch_provide() {
	needarg "$@"
	grep -RF "$1" "$PKGDB_DIR" | sed "s:$PKGDB_DIR/::" | tr : "\t"
}

scratch_purge() {
	needroot "Purging package"
	while [ "$1" ]; do
		case $1 in
			-*) PURGEOPTS="$PURGEOPTS $1";;
			*) PURGENAME="$PURGENAME $1";;
		esac
		shift
	done
	if [ "$PURGENAME" ]; then
		for i in $PURGENAME; do
			scratch_isinstalled "$i" || {
				msg_portnotinstalled "$i"
				needexit=1
			}
		done
	fi
	[ "$needexit" ] && exit 1
	printf "%s\n" "Resolving dependencies..."
	orphan=$(scratch_orphan "$PURGENAME" | tr '\n' ' ')
	if [ "$PURGENAME" ]; then
		for purge in $PURGENAME; do
			unset DEP pkg
			deplist "$purge"
			for ii in $DEP; do
				case " $orphan $PURGENAME " in *" $ii "*) pkg="$pkg $ii" ;; esac
			done
			if [ "$remove" ]; then
				remove="$remove $pkg"
			else
				remove="$pkg"
			fi
		done
	else
		remove=$orphan
	fi
	if [ "$remove" ]; then
		scratch_remove $remove $PURGEOPTS
	else
		printf "%s\n" "No orphan packages found..."
	fi
}

scratch_redeps() {
	needarg "$@"
	if [ "$1" ]; then
		while [ "$1" ]; do
			case $1 in
				--fix|-f) fix="yes";;
				--help|-h) scratch_help;;
				 *) pkg="$pkg $1";;
			esac
			shift
		done
	fi
	
	ppath=$(getportpath $pkg) || {
		msg_portnotfound $pkg
		return 1
	}

	for i in $pkg; do
		deps_i=$(get_depends "$i")
		[ "$deps_i" ] || continue
		for d in $deps_i; do
			for dd in $deps_i; do
				[ "$dd" = "$d" ] && continue
				[ "$(scratch deplist "$d" | awk '{print $2}' | grep -x "$dd")" ] && {
					printf "[%s] %s (already pulled from %s)\n" "$i" "$dd" "$d"
					[ "$fix" = "yes" ] && {
						if [ -f "$CBUILD_DIR/$i.dep" ]; then
							sed -i "/^$dd$/d" "$CBUILD_DIR/$i.dep"
							msg " $dd removed from $i cbuild deps"
						else
							sed -i "/^$dd$/d" "$ppath/depends"
							msg " $dd removed from $i depends"
						fi
					}
				}
			done
		done
	done
}

scratch_remove() {
	needroot "Removing package"
	while [ "$1" ]; do
		case $1 in
			-y|--yes) NOCONFIRM=1;;
			-*) OPTS="$OPTS $1";;
			*) PKGNAME="$PKGNAME $1";;
		esac
		shift
	done
	[ "$PKGNAME" ] || {
		printf "%s\n" "Please specify package(s) to remove."
		return 1
	}
	for i in $PKGNAME; do
		if ! scratch_isinstalled "$i"; then
			msg_portnotinstalled "$i"
		else
			IPKG="$IPKG $i"
		fi
	done
	[ "$IPKG" ] || return 0
	printf "%s\n" "Removing packages..."
	printf "\n"
	pkgcount=0
	count=0
	installmsg="ACTION#PORTNAME#VERSION"
	for pkg in $IPKG; do
		pkgcount=$(( pkgcount + 1 ))
		installmsg="$installmsg remove#$pkg#$(get_iversion "$pkg")"
	done
	printf "%s\n" "$installmsg" | tr ' ' '\n' | tr '#' ' ' | column -t
	printf "\n"
	printf "( %s remove )\n" "$pkgcount"
	printf "\n"
	[ "$NOCONFIRM" = "1" ] || {
		confirm "Continue remove package(s)?" "Package removing cancelled." || exit $?
		printf "\n"
	}
	for pkg in $IPKG; do
		count=$(( count + 1 ))
		settermtitle "[ $count/$pkgcount ] Removing $pkg..."
		hook "pre" "remove" "$pkg"
		printf "remove: %s %s...\n" "$pkg" "$(get_iversion "$pkg")"
		pkgdel "$pkg" $OPTS || {
			error=1
			break
		}
		world_del "$pkg"
		hook "post" "remove" "$pkg"
	done
	settermtitle "$pkgcount package(s) removed."
}

scratch_search() {
    needarg "$@"

    NAME_ONLY=0
    PATTERN=""
    while [ "$1" ]; do
        case $1 in
            -n|--name) NAME_ONLY=1 ;;
            -*) msgerr "Invalid search option: $1"; return 1 ;;
            *)  PATTERN="$1" ;;
        esac
        shift
    done
    [ "$PATTERN" ] || { printf "%s\n" "Please specify a search pattern."; return 1; }

    _sr_found=0
    for repo in $PORT_REPO; do
        [ -d "$repo" ] || continue
        _sr_out=$(awk -v pat="$PATTERN" -v nonly="$NAME_ONLY" '
        FNR == 1 {
            if (NR > 1 && matched)
                printf "%s|%s|%s|%s\n", name, ver, rel, desc
            name = ver = rel = desc = ""; matched = 0
        }
        /^name=/        { sub(/^name=/, "");        gsub(/"/, ""); name = $0
                          if (name ~ pat) matched = 1 }
        /^version=/     { sub(/^version=/, "");     gsub(/"/, ""); ver  = $0 }
        /^release=/     { sub(/^release=/, "");     gsub(/"/, ""); rel  = $0 }
        /^description=/ { sub(/^description=/, ""); gsub(/"/, ""); desc = $0
                          if (!nonly && desc ~ pat) matched = 1 }
        END { if (matched) printf "%s|%s|%s|%s\n", name, ver, rel, desc }
        ' "$repo"/*/"$BUILD_SCRIPT" 2>/dev/null | sort)

        [ -n "$_sr_out" ] || continue
        _sr_found=1
        printf '%s\n' "$_sr_out" | while IFS='|' read -r name version release desc; do
            scratch_isinstalled "$name" && ins="[${COLOR2}i${CRESET}]" || ins="[ ]"
            printf "%b %b(%s)%b %b%s%b %b%s-%s%b %s\n" \
                "$ins" "$COLOR5" "${repo##*/}" "$CRESET" \
                "$COLOR2" "$name"    "$CRESET" \
                "$COLOR4" "$version" "$release" "$CRESET" "$desc"
        done
    done

    [ "$_sr_found" -eq 0 ] && msg "No matching package found."
    return 0
}

scratch_news() {	
	portnews "$@"
}

scratch_sync() {	
	portsync "$@"
}

scratch_sysup() {
	needroot "Upgrading package"

	failed_upgrades=""

	while [ "$1" ]; do
		case $1 in
			-i|-u|-r) ;;
			-o) DOWNLOAD_ONLY=1; OPTS="$OPTS $1";;
			-y) NOCONFIRM=1;;
			-n) NODEP=1;;
			-k) KEEP_GOING_LOCAL=1;;
			--exclude=*) EXOPT=$1;;
			-*) OPTS="$OPTS $1";;
		esac
		shift
	done
	printf "%s\n" "Checking for outdated packages..."
	PKGOUTDATE=$(check_outdated)
	# filter out masked (format: name#ivr#pvr#newer_installed#masked)
	for i in $PKGOUTDATE; do
		case $i in
			*#1) continue;;  # masked: skip, example: 'glib#2.87.0-1#2.88.1-1#1#1'
			*) ii="$ii ${i%%#*}";;
		esac
	done
	EXPKG=${EXOPT#*=}
	for i in $ii; do
		# Wrap list and item with commas so first/last entries are always surrounded by delimiters.
		case ",$EXPKG," in
			*",$i,"*) ;;
			*) PKG="$PKG $i" ;;
		esac
	done
	[ "$PKG" ] || {
		printf "%s\n" "All packages are up to date."
		return 0
	}
	case " $PKG " in
		*" scratchpkg "*)
			printf "\n"
			msgwarn "Please upgrade 'scratchpkg' first."
			msg "Run 'scratch upgrade scratchpkg' as root."
			return 1
			;;
	esac

	UPGPKG=0
	NEWPKG=0
	installmsg="ACTION#PORTNAME#VERSION#INSTALLED"
	if [ "$NODEP" != 1 ]; then
		printf "%s\n" "Resolving dependencies..."
		DEP=$(scratch_deplist "$PKG" "$EXOPT" | awk '{print $2}')
		printf "\n"
		for d in $DEP; do
			case " $PKG " in
				*" $d "*)
					installmsg="$installmsg upgrade#$d#$(get_version "$d")#$(get_iversion "$d")"
					WILLINSTALL="$WILLINSTALL $d"
					UPGPKG=$(( UPGPKG + 1 ))
					;;
				     *)
					if ! scratch_isinstalled "$d" && getportpath "$d" >/dev/null 2>&1; then
						installmsg="$installmsg install#$d#$(get_version "$d")#$(get_iversion "$d")"
						WILLINSTALL="$WILLINSTALL $d"
						NEWPKG=$(( NEWPKG + 1 ))
					fi
					;;
			esac
		done
	else
		printf "\n"
		for dd in $PKG; do
			printf "[${COLOR2}u${CRESET}] %s  " "$dd"
			WILLINSTALL="$WILLINSTALL $dd"
			UPGPKG=$(( UPGPKG + 1 ))
		done
	fi
	printf "%s\n" "$installmsg" | tr ' ' '\n' | tr '#' ' ' | column -t
	printf "\n"
	printf "( %s upgrade, %s new install )\n" "$UPGPKG" "$NEWPKG"
	printf "\n"
	[ "$NOCONFIRM" = "1" ] || {
		confirm "Continue upgrade/install these package(s)?" "Package upgrade cancelled." || exit $?
		printf "\n"
	}
	error=0
	count=0
	total=$(printf "%s\n" "$WILLINSTALL" | wc -w)
	for inst in $WILLINSTALL; do # install all required dependencies and target packages itself
		count=$(( count + 1 ))
		cd "$(getportpath "$inst")" || return 1
		if ! scratch_isinstalled "$inst"; then
			settermtitle "[ $count/$total ] Installing $inst..."
			hook "pre" "install"
			if pkgbuild -i $OPTS; then
				hook "post" "install"
				done_pkg="$done_pkg $inst"
			else
				if keep_going; then
					msgwarn "Install failed for '$inst', continuing with next package..."
					failed_upgrades="$failed_upgrades $inst"
					error=1
				else
					error=1
					cd - >/dev/null || return 1
					break
				fi
			fi
		else
			settermtitle "[ $count/$total ] Upgrading $inst..."
			hook "pre" "install"
			if pkgbuild -u $OPTS; then
				hook "post" "install"
				done_pkg="$done_pkg $inst"
			else
				if keep_going; then
					msgwarn "Upgrade failed for '$inst', continuing with next package..."
					failed_upgrades="$failed_upgrades $inst"
					error=1
				else
					error=1
					cd - >/dev/null || return 1
					break
				fi
			fi
		fi
		cd - >/dev/null || return 1
	done
	[ "$done_pkg" ] && scratch_trigger "$done_pkg"

	# Show error summary if exist
	FAILED_PKGS="$failed_upgrades"
	SKIPPED_PKGS=""
	summary_show "$FAILED_PKGS" "$SKIPPED_PKGS" "upgrade"
	summary_title "$error" "$done_pkg" "$count" "$total" "upgraded"

	return "$error"
}

trigger_fccache() {
	command -v fc-cache >/dev/null || return 0
	printf '%s\n' "$PKGFILES" | grep -q "^usr/share/fonts/" && {
		msg "Updating fontconfig cache..."
		fc-cache -sf
	}
}

trigger_gdkpixbufcache() {
	command -v gdk-pixbuf-query-loaders >/dev/null || return 0
	printf '%s\n' "$PKGFILES" | grep -q "^usr/lib/gdk-pixbuf-2.0/2.10.0/loaders/" && {
		msg "Probing GDK-Pixbuf loader modules..."
		gdk-pixbuf-query-loaders --update-cache
	}
}

trigger_giomodules() {
	command -v gio-querymodules >/dev/null || return 0
	printf '%s\n' "$PKGFILES" | grep -q "^usr/lib/gio/modules/" && {
		msg "Updating GIO module cache..."
		gio-querymodules /usr/lib/gio/modules
	}
}

trigger_glibschema() {
	command -v glib-compile-schemas >/dev/null || return 0
	printf '%s\n' "$PKGFILES" | grep -q "^usr/share/glib-2.0/schemas/" && {
		msg "Compiling GSettings XML schema files..."
		glib-compile-schemas /usr/share/glib-2.0/schemas
	}
}

trigger_gtk2immodules() {
	command -v gtk-query-immodules-2.0 >/dev/null || return 0
	printf '%s\n' "$PKGFILES" | grep -q "^usr/lib/gtk-2.0/2.10.0/immodules/" && {
		msg "Probing GTK2 input method modules..."
		gtk-query-immodules-2.0 --update-cache
	}
}

trigger_gtk3immodules() {
	command -v gtk-query-immodules-3.0 >/dev/null || return 0
	printf '%s\n' "$PKGFILES" | grep -q "^usr/lib/gtk-3.0/3.0.0/immodules/" && {
		msg "Probing GTK3 input method modules..."
		gtk-query-immodules-3.0 --update-cache
	}
}

trigger_iconcache() {
	command -v gtk-update-icon-cache >/dev/null || return 0
	printf '%s\n' "$PKGFILES" | grep -q "^usr/share/icons/" && {
		msg "Updating icon theme caches..."
		for dir in /usr/share/icons/* ; do
            [ -d "$dir" ] || continue # skip non-directory entries in /usr/share/icons
			if [ -e "$dir/index.theme" ]; then
				gtk-update-icon-cache -q "$dir" 2>/dev/null
			else
				rm -f "$dir/icon-theme.cache"
				rmdir "$dir" 2>/dev/null
			fi
		done
	}
}

trigger_udevdb() {
	command -v udevadm >/dev/null || return 0
	printf '%s\n' "$PKGFILES" | grep -q "^etc/udev/hwdb.d/" && {
		msg "Updating hardware database..."
		udevadm hwdb --update
	}
}

trigger_fontcache() {
	command -v mkfontdir >/dev/null || return 0
	command -v mkfontscale >/dev/null || return 0
	printf '%s\n' "$PKGFILES" | grep -q "^usr/share/fonts/" && {
		msg "Updating X fontdir indices..."
		for dir in $(find /usr/share/fonts -maxdepth 1 -type d \( ! -path /usr/share/fonts \)); do
			rm -f "$dir/fonts.scale" "$dir/fonts.dir" "$dir/.uuid"
			rmdir "$dir" 2>/dev/null
			[ -d "$dir" ] || continue
			mkfontdir "$dir"
			mkfontscale "$dir"
		done
	}
}

trigger_desktopdb() {
	command -v update-desktop-database >/dev/null || return 0
	printf '%s\n' "$PKGFILES" | grep -q "^usr/share/applications/" && {
		msg "Updating desktop file MIME type cache..."
		update-desktop-database --quiet
	}
}

trigger_mimedb() {
	command -v update-mime-database >/dev/null || return 0
	printf '%s\n' "$PKGFILES" | grep -q "^usr/share/mime/" && {
		msg "Updating the MIME type database..."
		update-mime-database /usr/share/mime
	}
}

scratch_trigger() {
    needroot "Run trigger"
    if [ "$DOWNLOAD_ONLY" != "1" ]; then
        [ "$#" -gt 0 ] || set -- $(allinstalled)
        # Read all DB files once in memory
        PKGFILES=$(for _p in $@; do cat "$PKGDB_DIR/$_p" 2>/dev/null; done)
        for t in mimedb desktopdb fontcache fccache udevdb iconcache gtk3immodules \
		 gtk2immodules glibschema giomodules gdkpixbufcache; do
            trigger_$t
        done
    fi
}

scratch_upgrade() {
	needroot "Upgrading package"

	failed_upgrades=""

	while [ "$1" ]; do
		case $1 in
			-i|-r) ;;
			-y) NOCONFIRM=1;;
			-o) DOWNLOAD_ONLY=1; OPTS="$OPTS $1";;
			-n) NO_DEP=1;;
			-k) KEEP_GOING_LOCAL=1;;
			--exclude=*) EXOPT=$1;;
			-*) OPTS="$OPTS $1";;
			*) PKGNAME="$PKGNAME $1";;
		esac
		shift
	done
	[ "$PKGNAME" ] || {
		printf "%s\n" "Please specify package(s) to upgrade."
		return 1
	}
	for pkg in $PKGNAME; do
		if ! scratch_isinstalled "$pkg"; then
			msg_portnotinstalled "$pkg"
			if keep_going; then
				failed_upgrades="$failed_upgrades $pkg"
				continue
			else
				continue
			fi
		elif ! getportpath "$pkg" >/dev/null 2>&1; then
			printf "%s\n" "Package '$pkg' not exist."
			if keep_going; then
				failed_upgrades="$failed_upgrades $pkg"
				continue
			else
				continue
			fi
		else
			if [ "$(get_iversion "$pkg")" = "$(get_version "$pkg")" ]; then
				printf "%s\n" "Package '$pkg' is up to date."
				continue
			fi
		fi
		upkg="$upkg $pkg"
	done
	[ "$upkg" ] || return 0

	UPGPKG=0
	NEWPKG=0
	installmsg="ACTION#PORTNAME#VERSION#INSTALLED"
	if [ "$NO_DEP" != 1 ]; then
		printf "%s\n" "Resolving dependencies..."
		DEP=$(scratch_deplist "$upkg" "$EXOPT" | awk '{print $2}')
		printf "\n"
		for d in $DEP; do
			case " $upkg " in
				*" $d "*)
					installmsg="$installmsg upgrade#$d#$(get_version "$d")#$(get_iversion "$d")"
					WILLINSTALL="$WILLINSTALL $d"
					UPGPKG=$(( UPGPKG + 1 ))
					;;
				*)
					if ! scratch_isinstalled "$d" && getportpath "$d" >/dev/null 2>&1; then
						installmsg="$installmsg install#$d#$(get_version "$d")#$(get_iversion "$d")"
						WILLINSTALL="$WILLINSTALL $d"
						NEWPKG=$(( NEWPKG + 1 ))
					fi
					;;
			esac
		done
	else
		printf "\n"
		for dd in $upkg; do
			installmsg="$installmsg upgrade#$dd#$(get_version "$dd")#$(get_iversion "$dd")"
			WILLINSTALL="$WILLINSTALL $dd"
			UPGPKG=$(( UPGPKG + 1 ))
		done
	fi
	printf "%s\n" "$installmsg" | tr ' ' '\n' | tr '#' ' ' | column -t
	printf "\n"
	printf "( %s upgrade, %s new install )\n" "$UPGPKG" "$NEWPKG"
	printf "\n"
	[ "$NOCONFIRM" = "1" ] || {
		confirm "Continue upgrade/install these package(s)?" "Package upgrade cancelled." || exit $?
		printf "\n"
	}
	error=0
	count=0
	total=$(printf "%s\n" "$WILLINSTALL" | wc -w)
	for inst in $WILLINSTALL; do # install all required dependencies and target packages itself
		count=$(( count + 1 ))
		cd "$(getportpath "$inst")" || return 1
		if ! scratch_isinstalled "$inst"; then
			settermtitle "[ $count/$total ] Installing $inst..."
			hook "pre" "install"
			if pkgbuild -i $OPTS; then
				hook "post" "install"
				done_pkg="$done_pkg $inst"
			else
				if keep_going; then
					msgwarn "Install failed for '$inst', continuing with next package..."
					failed_upgrades="$failed_upgrades $inst"
					error=1
				else
					error=1
					cd - >/dev/null || return 1
					break
				fi
			fi
		else
			settermtitle "[ $count/$total ] Upgrading $inst..."
			hook "pre" "install"
			if pkgbuild -u $OPTS; then
				hook "post" "install"
				done_pkg="$done_pkg $inst"
			else
				if keep_going; then
					msgwarn "Upgrade failed for '$inst', continuing with next package..."
					failed_upgrades="$failed_upgrades $inst"
					error=1
				else
					error=1
					cd - >/dev/null || return 1
					break
				fi
			fi
		fi
		cd - >/dev/null || return 1
	done
	[ "$done_pkg" ] && scratch_trigger "$done_pkg"

	# Show error summary if exist
	FAILED_PKGS="$failed_upgrades"
	SKIPPED_PKGS=""
	summary_show "$FAILED_PKGS" "$SKIPPED_PKGS" "upgrade"
	summary_title "$error" "$done_pkg" "$count" "$total" "upgraded"

	return "$error"
}

world_add() {
	grep -qx "$1" "$WORLD_FILE" && return
	scratch_isinstalled "$1" || {
		msg_portnotinstalled "$1"
		return 1
	}
	printf "%s\n" "$1" >> "$WORLD_FILE"
	sort -u "$WORLD_FILE" -o "$WORLD_FILE" # sort world
}

world_del() {
	grep -qx "$1" "$WORLD_FILE" || return
	sed "/^$1$/d;/^$/d" -i "$WORLD_FILE"
}

scratch_world() {
	if [ "$1" ]; then
		needroot
		touch "$WORLD_FILE"
		while [ "$1" ]; do
			if ! grep -qx "$1" "$WORLD_FILE" ; then
				printf "world: '%s' added to world\n" "$1"
				world_add "$1"
			else
				printf "world: '%s' deleted from world\n" "$1"
				world_del "$1"
			fi
			shift
		done
	else
		[ -s "$WORLD_FILE" ] && {
			cat "$WORLD_FILE"
		} || {
			printf "%s\n" "world is empty"
		}
	fi

	return 0
}

# check for 'pkgadd', required for package database path
command -v pkgadd >/dev/null 2>&1 || {
	printf "%s\n" "'pkgadd' not found in \$PATH!"
	exit 1
}

mode=$1

print_runhelp_msg() {
	printf "%s\n" "Run '${0##*/} help' to see available options."
	exit 2
}

[ "$mode" ] || print_runhelp_msg

shift

for opt in "$@"; do
	case $opt in
		--append-repo=*) APPENDREPO="$APPENDREPO ${opt#*=}";;
		--prepend-repo=*) PREPENDREPO="$PREPENDREPO ${opt#*=}";;
		--override-repo=*) OVERRIDEREPO="$OVERRIDEREPO ${opt#*=}";;
		--repo=*) PORT_REPO="$PORT_REPO ${opt#*=}";;
		--repo-file=*) REPO_FILE="${opt#*=}";;
		--alias-file=*) ALIAS_FILE="${opt#*=}";;
		--config-file=*) CONFIG_FILE="${opt#*=}";;
		--*) MAINOPTS="$MAINOPTS $opt";;
		-[!-]?*)
			_rest=${opt#-}
			while [ -n "$_rest" ]; do
				_chr="${_rest%"${_rest#?}"}"
				MAINOPTS="$MAINOPTS -$_chr"
				_rest=${_rest#?}
			done;;
		*) MAINOPTS="$MAINOPTS $opt";;
	esac
	shift
done

if [ -f "$REPO_FILE" ]; then
	for repodir in $(awk '!/^[[:space:]]*$/ && !/^#/ {print $1}' "$REPO_FILE"); do
		PORT_REPO="$PORT_REPO $repodir"
	done
fi

if [ "$OVERRIDEREPO" ]; then
	PORT_REPO="$OVERRIDEREPO"
else
	PORT_REPO="$PREPENDREPO $PORT_REPO $APPENDREPO"
fi

for f in $REPO_FILE $ALIAS_FILE $MASK_FILE $CONFIG_FILE; do
	if [ ! -f "$f" ]; then
		msgerr "file not found: $f"
		exit 3
	fi
done

if [ "$(command -v "scratch_$mode")" ]; then
	"scratch_$mode" $MAINOPTS
else
	print_runhelp_msg
fi

exit $?
