#!/bin/bash

# Searches Arch for orphans you currently use.  Adopt a package today!

# depends on bash, awk, sed, curl, jshon, expac

# 2010-11-19 Initial release
# 2010-11-19 Null set check
# 2010-11-20 Falconindy's O(N) version for < 50 foreign
# 2010-11-21 url encoding, threading, rpc for everything
# 2010-11-26 Pkg orphans (-p) for Pierre, --help
# 2010-11-29 Prettier -p
# 2010-12-04 Color term check
# 2011-04-09 Work todo (-w) for wonder, jshon
# 2011-04-10 Bugtracker summary (-b)
# 2011-05-09 Cleanup, community release
# 2011-07-11 Check toplevel packages in -p
# 2011-09-21 More https
# 2011-10-03 Eligible AUR packages with -e
# 2011-10-09 Search by maintainer with -m
# 2011-11-24 More expac
# 2012-03-10 Todo 'parser' fixed
# 2012-11-06 RPC resultcount
# 2012-11-14 RPC maintainer
# 2012-11-17 Fix --eligible, faster -e/-a/-p
# 2012-11-18 Fix flagging
# 2013-02-14 Faster --bugs
# 2013-06-28 Fix FS#35960
# 2013-09-12 Fix -p/-m
# 2014-02-12 Fix OOD, flagged packages (-f), refactor aur code

# Bugs:
# Document colors.
# Could be faster. (abuse multiinfo)
# trailing comma on -b
# use https://www.archlinux.de/?page=Statistics
# use https://www.archlinux.org/packages/$REPO/$ARCH/$PKG/json/
# add -f/--flagged to look for any flagged package

AURURL='https://aur.archlinux.org'
# archweb loves pointless URL changes
#PKGURL='https://www.archlinux.org/packages/PAGE/?sort=pkgname&maintainer=NICKNAME&limit=250&repo=Community&repo=Core&repo=Extra&repo=Multilib'
#PKGURL='https://www.archlinux.org/packages/?sort=pkgname&maintainer=NICKNAME&limit=all&repo=Community&repo=Core&repo=Extra&repo=Multilib'
PKGURL='https://www.archlinux.org/packages/?sort=pkgname&maintainer=NICKNAME&page=PAGE&repo=Community&repo=Core&repo=Extra&repo=Multilib'
SVNURL='https://repos.archlinux.org/wsvn/packages'
TODOURL='https://www.archlinux.org/todolists/'
BUGURL='https://bugs.archlinux.org/proj0?project=0&status%5B0%5D=open&do=index'

if [[ -t 1 ]]; then
    RED='\e[1;31m'
    GREEN='\e[1;32m'
    NC='\e[0m'
fi

# globals
declare -A repositories

rpc()  # name : array
{
    j="$(curl -Gs --data-urlencode "arg=$1" "$AURURL/rpc.php?type=info")"
    if [[ $(jshon -e resultcount <<< "$j") == 0 ]]; then 
        return
    fi
    jshon -e results -e Maintainer -tpe NumVotes -upe OutOfDate -u <<< "$j"
}

rpc_thread()  # name : pretty orphans
{
    # thank you falconindy
    pkg=($(rpc "$1"))
    [[ $pkg ]] || return
    [[ "${pkg[0]}" = "null" ]] || return

    (( ${pkg[2]} != 0 )) && pkgcol=$RED || pkgcol=$GREEN
    echo -e "$AURURL/packages/$1/ $pkgcol\t${pkg[1]} votes $NC"
    echo "1" > $found
}

rpc_thread_e()  # name : pretty eligible
{
    pkg=($(rpc "$1"))
    [[ $pkg ]] || return
    (( "${pkg[1]}" >= "10" )) || return

    (( ${pkg[2]} != 0 )) && pkgcol=$RED || pkgcol=$GREEN
    echo -e "$AURURL/packages/$1/ $pkgcol\t${pkg[1]} votes $NC"
    echo "1" > $found
}

rpc_thread_f()  # name : pretty flagged
{
    pkg=($(rpc "$1"))
    [[ $pkg ]] || return

    (( ${pkg[2]} == 0 )) && return
    # color orphans?
    echo -e "$AURURL/packages/$1/ \t${pkg[1]} votes"
    echo "1" > $found
}

official()  # : packages
{
    # thank you falconindy
    curl -s "$PKGURL" | sed -n '/tbody/,/tbody/{s/.*title=[^>]\+>\([^<]\+\)<.*/\1/p}' | uniq
}

depaginate_stream()  # maintainer : table rows
{
    temp=""
    pkgurl2="${PKGURL/NICKNAME/$1}"
    for i in $(seq 1 110); do
        temp=$(curl -s "${pkgurl2/PAGE/$i}")
        if [[ $(grep -c "404 - Page Not Found" <<< "$temp") = 1 ]]; then
            break
        fi
        sed -n '/tbody/,/tbody/p' <<< "$temp"
    done
}

official_pages()  # maintainer : none :: sets global official_{html,names} vars
{
    official_html="$(depaginate_stream "$1")"
    echo "$official_html" > /tmp/kkeen.html
    #official_html="$(curl -s ${PKGURL/NICKNAME/$1})"
    names=$(sed -n 's/.*title=[^>]\+>\([^<]\+\)<.*/\1/p' <<< "$official_html")
    official_names=$(uniq <<< "$names")
}

official_flag()  # : packages
{
    # xmllint complains about missing semicolon?
    grep -B 2 'class="flagged"' <<< "$official_html" | sed -n '/View package details for/{s/.*[^>]\+>\([^<]\+\)<.*/\1/p}' | uniq
}

load_repositories_global() # none : none
{
    # no good way to return an associative array, so global
    if [[ ${#repositories[@]} > 0 ]]; then
        return
    fi
    while read -r name repo; do
        repositories[$name]=$repo
    done < <(expac -S -1 '%n\t%r')
}

mobile_packages()  # list : list
{
    load_repositories_global
    while read -r name reqs; do
        this_repo="${repositories[$name]}"
        mobile='yes'
        for req in $reqs; do
            if [[ "$this_repo" == "${repositories[$req]}" ]]; then
                mobile=''
                break
            fi
        done
        if [[ "$mobile" == "yes" ]]; then
            echo -n " $name "
        fi
    done < <(expac -S -l ' ' '%n\t%N' $1)
    echo
}

official_display()  # maintainer : none
{
    official_pages "$1" # loads some vars
    load_repositories_global
    packages=$(grep -xF "$(pacman -Qq)" <<< "$official_names")
    mobile_list=$(mobile_packages "$packages")
    flagged=$(official_flag)
    for n in $packages; do
        grep -qs "$n" <<< "$flagged" && pkgcol=$RED || pkgcol=$NC
        [[ "$mobile_list" =~ " $n " ]] && mobile="(mobile)" || mobile=""
	repo=${repositories[$n]}
        echo -e "$pkgcol${repo:0:4}\t$n\t$mobile $NC"
    done 
    if [ -z "$packages" ]; then
        echo "No $1 packages found."
    fi
}

todo_needed()  # none : pretty list
# todo, split this into some sane functions
{
    reason=""
    package=""
    prev_pack=""  # haaaack
    count=0
    installed=$(pacman -Qq)
    html=$(egrep '(<h4>|Incomplete|View package details)' <<< "$(curl -s "$TODOURL")" | uniq)
    while read line
    do
    case "$line" in
	*\<h4\>*)
	    reason=$(sed 's/<[^>]*>//g' <<< "$line")
	    count=0
	    ;;
	*View\ package\ details*)
	    prev_pack="$package"
            package=$(cut -d/ -f5 <<< "$line")
	    ;;
	*Incomplete*)
	    if [[ "$prev_pack" == "$package" ]]; then
		continue; fi
	    if ! grep -qsx "$package" <<< "$installed"; then
		continue; fi
	    if [[ $count == 0 ]]; then
		echo -e "\n$GREEN$reason$NC"
		count=1
	    fi
	    echo "$package"
	    ;;
    esac
    done <<< "$html"
}

nap()  # none : none
{
    while (( $(jobs | wc -l) >= 16 )); do
        sleep 0.1
        jobs > /dev/null
    done
}

bug_parse()  # html : packages
# misses a lot of edge cases
{
    grep -o '\[[0-9a-z-]*\]' <<< "$1" | tr -d '[]' | uniq
}

bug_thread()  # url, tmpfile : none
{
    bug_parse "$(curl -s "$1")" > "$2"
}

bugs()  # none : numbered list
# add multithread download, set globals
{
    declare -A bugtally
    tmpdir="$(mktemp -d)"
    installed=$(pacman -Qq)
    echo -n "This is slow, please wait" 1>&2
    html1="$(curl -s "${BUGURL}&pagenum=1")"
    pages="$(grep -o 'Page [0-9]* of [0-9]*<span' <<< "$html1" | grep -o '[0-9]*' | tail -n 1)"
    bug_parse "$html1" > "$tmpdir/page1"
    for i in $(seq 2 $pages); do
        bug_thread "${BUGURL}&pagenum=$i" "$tmpdir/page$i" &
        echo -n '.' 1>&2
        nap
    done
    wait

    for page in "$tmpdir/page"*; do
        while read pack
        do
            if [[ -z "$pack" ]]; then
                continue; fi
            (( ++bugtally["$pack"] ))
        done < "$page"
    done

    echo 1>&2
    for pack in "${!bugtally[@]}"; do
        if ( ! grep -qsx "$pack" <<< "$installed" ); then
            continue; fi
        echo "${bugtally["$pack"]} $pack"
    done

    rm "$tmpdir/page"*
    rmdir "$tmpdir" 
}

aur_sweep()  # thread_function empty_message : pretty
{
    thread_fn="$1"
    empty_msg="$2"
    packages=$(pacman -Qmq)
    if [ -z "$packages" ]; then
        echo "No AUR packages installed."
        exit
    fi
    found=$(mktemp)
    trap "wait; rm $found" TERM EXIT
    for n in $packages; do
        $thread_fn "$n" &
        nap
    done
    wait
    if [ ! -s "$found" ]; then
        echo "$empty_msg"
    fi
}

docs()  # none : text
{
    echo "Use:  aurphan -[p|m|f|t|b|a|h]"
    echo "Finds ways for you to contribute to Arch.  All options produce a list of packages that need help, but only if you have them installed."
    echo
    echo "With no args, it displays the --help."
    echo ""
    echo "    -p/--packages is for devs/TUs and searches the official repos for orphans.  (mobile) means the package can be safely moved to a less exclusive repo."
    echo "    -m/--maintainer [nick] finds packages from a dev/TU, useful when they feel overworked."
    echo "    -f/--flag lists packages flagged out of date."
    echo "    -t/--todo searches the master todo list for pkgbuilds that need an update."
    echo "    -b/--bugs checks the bugtracker for packages with open bugs."
    echo "    -e/--eligible searches the AUR for packages that could enter [community]."
    echo "    -a/--aur searches the AUR for packages that need a maintainer."
}

if [[ "$#" = "0" ]]; then
    docs
    exit
fi

case "$1" in
    -p|--packages)
        official_display orphan
        ;;
    -t|--todo)
        todo_needed
        ;;
    -b|--bugs)
        while read line; do
            if [[ "${line##1 *}" ]]; then
                echo "$line"
            else
                echo -n "${line:2}, "
            fi
        done <<< "$(bugs | sort -nr)"
	echo
        ;;
    -a|--aur)
        aur_sweep rpc_thread "No orphans found."
        ;;
    -m|--maintainer)
        if [[ "$#" != "2" ]]; then
            echo "-m requires a maintainer's name"
            exit 2
        fi
        official_display "$2"
        ;;
    -e|--eligible)
        aur_sweep rpc_thread_e "No eligible packages found."
        ;;
    -f|--flag)
        # haaaaack
        PKGURL="${PKGURL}&flagged=Flagged"
        RED="$NC"
        official_display ""
        aur_sweep rpc_thread_f "No flagged AUR packages found."
        ;;
    -h|--help|*)
        docs
        ;;
esac

