#!/usr/bin/env bash

#
# checkversion: package update checking tool
#
# Written by Kevin MacMartin
# Released under the MIT license
#
# Requirements:
#   pacaur
#   vercmp
#   archversion (environment config patched version)
#
# archversion_conf: config file with archversion entries for non-VCS packages
# develversion_conf: config file listing VCS packages to check
# noversion_conf: text file listing packages that won't be checked
#
# specialpkg_check(): Contained in this script, custom version checks go in this function
#

cd "${0%/*}"
script_directory="$PWD" # Directory containing this script
script_name="${0//*\/}" # Name of this script
package_rootdir="$(readlink -f "$script_directory"/..)" # Directory containing a collection of packages contained in folders

archversion_conf="$script_directory/archversion.conf" # A config file containing Archversion checks for non-VCS packages
develversion_conf="$script_directory/develversion.conf" # A file containing a list of VCS packages to check
noversion_conf="$script_directory/noversion.txt" # A file containing a list of packages that shouldn't be checked

temp_directory="/tmp/$script_name" # The root folder containing any temporary files used
temp_config="$temp_directory/upversion.tmp.conf" # Location to create temp archversion configs
package_cache="$temp_directory/.archversion.cache" # Location to create the archversion cache file

# Set all the colour variable values blank for when this script outputs to a pipe
unset c_blue c_white c_yellow c_grey c_red c_green c_reset
# Set the terminal colours to use when this script outputs to stdout
[[ -t 1 ]] && {
    c_blue=$'\e[1;34m'   # BLUE
    c_white=$'\e[1;37m'  # WHITE
    c_grey=$'\e[1;30m'   # DARK GREY
    c_yellow=$'\e[1;33m' # YELLOW
    c_red=$'\e[1;31m'    # RED
    c_green=$'\e[1;32m'  # GREEN
    c_reset=$'\e[0m'     # DISABLES COLOUR
}

# SPECIALPKG CHECK: function for custom version check functions
function specialpkg_check() {
    # Enter the package root directory
    cd "$package_rootdir"

    # PKGVER CHECK: terminfo-italics
    specialpkg=terminfo-italics
    upstream_version=$(pacman -Si ncurses \
        | grep Version \
        | sed 's|^[^:]*:\ ||;s|-.*$||')
    pkgver=''
    eval "$(egrep '^\s*pkgver\s*=' $specialpkg/PKGBUILD)"
    vercmp_check "$specialpkg" "$upstream_version" "$pkgver"

    # Return to the script folder
    cd "$script_directory"
}

# HELPER FUNCTION: Output readible results of a version comparison
function vercomp_display() {
    printf '%s\n' "$c_blue[$c_white$1$c_blue]$c_reset ${c_yellow}up: $2$c_reset $c_blue|$c_reset $3"
}

# HELPER FUNCTION: Compares versions and handles accordingly
function vercmp_check() {
    package="$1" upstream_version="$2" package_version="$3"
    version_comparison=$(vercmp "$upstream_version" "$package_version")

    if [ "$version_comparison" -gt 0 ]; then
        # Upstream > Package (New Version)
        vercomp_display "$package" "$upstream_version" "${c_red}aur: $package_version$c_reset"
    elif [ "$version_comparison" -lt 0 ]; then
        # Upstream < Package (Error)
        [[ "$only_newpkgs" = '0' ]] \
            && vercomp_display "$package" "$upstream_version" "${c_yellow}aur: $package_version$c_reset"
    else
        # Upstream = Package (Up to Date)
        [[ "$only_newpkgs" = '0' ]] \
            && vercomp_display "$package" "$upstream_version" "${c_green}aur: $package_version$c_reset"
    fi
}

# archversion_conf CHECK
function archversion_check() {
    # Fail if the archversion config file is missing
    [[ ! -f "$archversion_conf" ]] && {
        printf '%s\n' "${c_red}ERROR$c_reset: $archversion_conf is missing" >&2
        exit 1
    }

    # Define the arguments for archversion, then add '--debug' if $archversion_debug is set
    archversion_command='check'
    [[ "$archversion_debug" = '1' ]] \
        && archversion_command="--debug $archversion_command"

    # Create a vanilla archversion cache file if one doesn't already exist
    [[ ! -f "$package_cache" ]] \
        && printf '%s\n' '{"downstream": {}, "compare": {}}' > "$package_cache"

    # Run for each package defined in the $archversion_conf file
    while read -r pkg; do
        # Write (or write over) the config file with a template for everything above packages
        sed 's|^\s*#\s*||;/\[\s*DEFAULT\s*\]/,$!d;/^$/q' "$archversion_conf" \
            > "$temp_config"

        # Define $package_definition as an empty archversion.conf template then add the $pkg entry from $archversion_conf
        if egrep -q "\[\s*$pkg\s*\]" "$archversion_conf"; then
            package_definition="$(sed 's|^\s*#\s*||;/\[\s*'"$pkg"'\s*\]/,$!d;/^$/q' "$archversion_conf")"
        else
            package_definition="[$pkg]"
        fi

        # Add the definition to the package
        printf '%s\n' "$package_definition" \
            >> "$temp_config"

        # Grab the simple archversion output for parsing
        archversion_output=$(CONFIG_PACKAGES="$temp_config" CACHE_PACKAGES="$package_cache" \
            archversion $archversion_command)
        upstream_version=$(egrep -o 'up: [^ ]*' <<< "$archversion_output" \
            | sed 's|up: ||')
        package_version=$(egrep -o 'aur: [^ ]*' <<< "$archversion_output" \
            | sed 's|aur: ||' \
            | sed 's|^[0-9][0-9]*:||')

        # Compare versions and handle accordingly
        vercmp_check "$pkg" "$upstream_version" "$package_version"

        # Add a blank line after each package when running debug for easier parsing
        [[ "$archversion_debug" = '1' ]] \
            && printf '\n'

        # Remove the tmp config
        [[ -f "$temp_config" ]] \
            && rm "$temp_config"
    done < <(egrep -v '\[DEFAULT\]' "$archversion_conf" | egrep '^\s*\[[^]]*\]' | egrep -o '[^][]*')
}

# develversion_conf CHECK
function develversion_check() {
    # Fail if the develversion config file is missing
    [[ ! -f "$develversion_conf" ]] && {
        printf '%s\n' "${c_red}ERROR$c_reset: $develversion_conf is missing" >&2
        exit 1
    }

    # Check each package in $develversion_conf that exists in $package_rootdir
    while read -r pkg; do
        if [[ -d "$package_rootdir/$pkg" ]]; then
            # Find the current pkgver() of the package in the AUR
            package_version=$(pacaur -i "$pkg" \
                | sed -r 's/\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K]//g' \
                | egrep '^Version' \
                | sed 's|^[^:]*: ||;s|-[0-9]*$||')
            pkgbuild_file="$package_rootdir/$pkg/PKGBUILD"

            # Exit and skip this package if either its folder or the PKGBUILD it should contain are missing
            [[ ! -f "$pkgbuild_file" ]] && {
                if [[ ! -d "$package_rootdir/$pkg" ]]; then
                    printf '%s\n' "$c_blue[$c_white$pkg$c_blue]$c_reset ${c_red}ERROR$c_reset: $pkg does not exist in $package_rootdir" >&2
                else
                    printf '%s\n' "$c_blue[$c_white$pkg$c_blue]$c_reset ${c_red}ERROR$c_reset: $package_rootdir/$pkg does not contain a PKGBUILD" >&2
                fi
                continue
            }

            # Exit and skip this package if a pkgver() function doesn't exist (ie: not VCS)
            egrep -q 'pkgver\(' "$pkgbuild_file" || {
                printf '%s\n' "$c_blue[$c_white$pkg$c_blue]$c_reset ${c_red}ERROR$c_reset: package doesn't contain a pkgver() function" >&2
                continue
            }

            # Delete the temporary build folder if it already exists
            [[ -d "$temp_directory/build" ]] \
                && rm -rf "$temp_directory/build"

            # Create and enter the temporary build folder, exiting with an error if this fails
            if install -d "$temp_directory/build"; then
                cd "$temp_directory/build"
            else
                printf '%s\n' "${c_red}ERROR$c_reset: Failure to create and enter the temporary build folder @ $temp_directory/build" >&2
                exit 1
            fi

            # Link all folders in the package directory except pkg+src to avoid re-cloning repos each check
            for dir in $(find "$package_rootdir/$pkg" -maxdepth 1 -mindepth 1 -type d | egrep -v '(src|pkg)'); do
                ln -s "$dir" "$temp_directory/build"
            done

            # Copy the package's PKGBUILD to the temporary build folder with its functions stripped out
            sed '/^.*\(\).*{[^}]*$/,/^[^{]]*}/d' "$pkgbuild_file" \
                | egrep -v '^\s*install\s*=' \
                    > PKGBUILD

            # Add the package's pkgver() function from the original PKGBUILD into the copy
            sed '/pkgver(/,$!d' "$pkgbuild_file" \
                | sed '/^\s*}\s*$/q' \
                    >> PKGBUILD

            # Reset the values we'll be using then source the new PKGBUILD
            unset pkgname epoch pkgver pkgrel
            source PKGBUILD

            [[ ! "$pkgname" = "$pkg" ]] && {
                printf '%s\n' "$c_blue[$c_white$pkg$c_blue]$c_reset ${c_red}ERROR$c_reset: This pkgname for this package is $pkgname" >&2
                continue
            }

            # Create a blank package function (or a blank one for each split package)
            if [[ "${#pkgname[*]}" = 1 ]]; then
                # If this is not a split package, add one package() to the PKGBUILD
                printf '%s\n%s\n%s\n' 'package() {' '    return 0' '}' >> PKGBUILD
            else
                # If this is a split package, add one package() per split to the PKGBUILD
                for name in "${pkgname[@]}"; do
                    printf '%s%s\n%s\n%s\n' "package_$name" '() {' '    return 0' '}' >> PKGBUILD
                done
            fi

            # Unset all checksums then add each VCS source and an associated 'SKIP' checksum to the PKGBUILD
            printf '%s\n' 'unset md5sums sha1sums sha256sums sha384sums sha512sums' >> PKGBUILD
            source_array='source=('
            sha512sum_array='sha512sums=('
            printf '\n' >> PKGBUILD
            for src in "${source[@]}"; do
                egrep -q '^\s*(bzr|csv|git|hg|darcs|svn)[^a-zA-Z0-9]' < <(sed 's|^[^:]*::||' <<< "$src") && {
                    source_array="$source_array '$src'"
                    sha512sum_array="$sha512sum_array 'SKIP'"
                }
            done
            sed 's|( |(|' <<< "${source_array})" >> PKGBUILD
            sed 's|( |(|' <<< "${sha512sum_array})" >> PKGBUILD

            # Update sources with makepkg and compare the local pkgver against the one in the AUR
            makepkg_output=$(makepkg -od 2>&1)
            if [[ $? = 0 ]]; then
                # Calculate the full package version, including epoch (if applicable), pkgver and pkgrel
                unset epoch pkgver pkgrel
                eval "$(egrep '^\s*(epoch|pkgver|pkgrel)\s*=' PKGBUILD)"
                [[ -n "$epoch" ]] && epoch="$epoch:"
                upstream_version="$epoch$pkgver-$pkgrel"

                # Compare versions and handle accordingly
                vercmp_check "$pkg" "$upstream_version" "$package_version"
            else
                # Exit with a failure and display $makepkg_output if makepkg fails
                printf '%s\n%s\n' "${c_red}ERROR$c_reset: Failed to update sources" "$makepkg_output" >&2
            fi
        else
            # Display an error if the package can't be found in the package root directory
            printf '%s\n' "${c_red}ERROR$c_reset: Failed to find the package $pkg in $package_rootdir" >&2
        fi
    done < <(egrep -v '^#' "$develversion_conf")

    # Move back to $script_directory
    cd "$script_directory"
}

# check_missingpkgsPKG CHECK
function missingpkg_check() {
    # Create lists of archversion, develversion, specialversion and noversion packages
    specialversion_packages='terminfo-italics'
    noversion_packages=$(sed 's|\[||;s|\]||;s|\s*#.*$||' "$noversion_conf")
    develversion_packages=$(egrep -v '^\s*#' "$develversion_conf")
    archversion_packages=$(egrep -v '^\s*#' "$archversion_conf" \
        | grep -v '[DEFAULT]' \
        | egrep '^\s*\[[^]]*\]' \
        | sed 's|\[||;s|\]||')

    # Create a list of packages in the package root directory that aren't in any of the above lists
    check_missing_pkgs=$(
        cd "$package_rootdir"
        for pkg in *; do
            [[ -f "$pkg/PKGBUILD" ]] && {
                _pkg=$(sed 's|.*\/||' <<< "$pkg")
                ! egrep -q "^$_pkg$" <<< "$archversion_packages" \
                    && ! egrep -q "^$_pkg$" <<< "$develversion_packages" \
                    && ! egrep -q "^$_pkg$" <<< "$specialversion_packages" \
                    && ! egrep -q "^$_pkg$" <<< "$noversion_packages" \
                        && printf '%s\n' "$_pkg"
            }
        done
        cd "$script_directory"
    )

    # Display information about any packages missing from all the package lists
    printf '%s' "$c_blue[${c_white}missing-packages$c_blue]$c_reset: "
    if [[ -n "$check_missing_pkgs" ]]; then
        # Display how many packages are missing
        printf '%s\n' "$c_red$(wc -l <<< "$check_missing_pkgs")$c_reset"
        # Display the list of missing packages
        printf '%s\n' "$check_missing_pkgs"
    else
        # Display 0 to show that there are no packages missing
        printf '%s\n' "${c_green}0$c_reset"
    fi
}

# HELP + EXIT
function showhelp_exit(){
    printf '%s\n\n' "Usage: $script_name [OPTION(S)]"
    printf '%s\n' "Version Options:"
    printf '%s\n' "  -b|b $c_grey|$c_reset include debugging information with archversion checks"
    printf '%s\n\n' "  -n|n $c_grey|$c_reset only display packages that have a new version available"
    printf '%s\n' "Check Options:"
    printf '%s\n\n' "  -m|m $c_grey|$c_reset find packages missing from all the version check configs"
    printf '%s\n' "Help Options:"
    printf '%s\n' "  -h|h $c_grey|$c_reset display this help output"
    exit "$1"
}

# HELPER FUNCTION: Set settings variables depending on the arguments pasted to this function
function paramparse(){
    case "$1" in
        -b|b)
            archversion_debug=1
            ;;
        -n|n)
            only_newpkgs=1
            ;;
        -m|m)
            check_missingpkgs=1
            ;;
        -h|h|--help)
            showhelp_exit 0
            ;;
        *)
            printf '%s\n\n' "${c_red}ERROR$c_reset: invalid argument '$param'" >&2
            showhelp_exit 1
            ;;
    esac
}

# Set all setting variables off before parsing for arguments
check_missingpkgs=0 archversion_debug=0 only_newpkgs=0

# Parse command-line arguments
for param in "$@"; do
    if egrep -q '^-[a-z][a-z]' <<< "$param"; then
        # Parse the argument character by character
        while read -r char; do
            paramparse "$char"
        done < <(grep -o '.' <<< "$param" | grep -v '-')
    else
        # Parse the whole argument at once
        paramparse "$param"
    fi
done

# Run the missing packages function then exit if configured to do so
[[ "$check_missingpkgs" = '1' ]] && {
    missingpkg_check
    exit 0
}

# Initialize the temp folder
[[ -d "$temp_directory" ]] \
    && rm -rf "$temp_directory"
install -d "$temp_directory"

archversion_check
develversion_check
specialpkg_check

# Cleanup the temp folder
[[ -d "$temp_directory" ]] \
    && rm -rf "$temp_directory"