#!/bin/sh
#description: instant coding answers via the command line
#usage: howdoi [options] query

#example: howdoi format date bash
#DATE=`date +%Y-%m-%d`

PROGNAME="$(expr "${0}" : '.*/\([^/]*\)')"
SEARCH_ENGINES="google" #TODO 13-08-2017 02:30 >> add more search engines

VERSION="2017.08.13-04:08"

_usage() {
    printf "%b\\n" "Usage: ${PROGNAME} [options] query ..."
    printf "%b\\n" "Instant coding answers via the command line."
    printf "\\n"
    printf "%b\\n" "Positional arguments:"
    printf "%b\\n" "  query       the question to answer"
    printf "\\n"
    printf "%b\\n" "Optional arguments:"
    printf "%b\\n" "  -l           display only links"
    printf "%b\\n" "  -a           display all the answers in full text"
    printf "%b\\n" "  -n num       number of answers to return"
    printf "%b\\n" "  -p pos       select answer from specified link position (default: 1)"
    printf "%b\\n" "  -c           enable colorized output"
    printf "%b\\n" "  -C           clear cache"
    printf "\\n"
    printf "%b\\n" "  -U, --update update this program to latest version"
    printf "%b\\n" "  -h, --help   show this help message and exit"
}

_mkdir_p() { #portable mkdir -p
    [ -n "${1}" ] || return 1
    for _mkdir_p__dir in "${@}"; do
        [ -d "${_mkdir_p__dir}" ] && continue
        _mkdir_p__IFS="${IFS}"
        IFS="/"
        set -- ${_mkdir_p__dir}
        IFS="${_mkdir_p__IFS}"
        (
        case "${_mkdir_p__dir}" in
            /*) cd /; shift ;;
        esac
        for _mkdir_p__subdir in "${@}"; do
            [ -z "${_mkdir_p__subdir}" ] && continue
            if [ -d "${_mkdir_p__subdir}" ] || mkdir "${_mkdir_p__subdir}"; then
                if cd "${_mkdir_p__subdir}"; then
                    :
                else
                    printf "%s\\n" "_mkdir_p: Can't enter ${_mkdir_p__subdir} while creating ${_mkdir_p__dir}"
                    return 1
                fi
            else
                return 1
            fi
        done
        )
    done
}

_is_int() {
    #look for an integer, returns 0 on success, 1 otherwise
    #http://www.unix.com/shell-programming-and-scripting/172070-help-scripting-command.html
    case "${1}" in
        *[!0-9]*|"") return 1 ;;
    esac
}

_die() {
    [ -z "${1}" ] || printf "%s\\n" "${*}" >&2
    _usage >&2; exit 1
}

_update() {
    _u__current_file="$(cd "$(dirname "${0}")" && pwd)/${PROGNAME}"
    _u__current_version_long="$(awk -F\" \
        '/VERSION=/{print $2;exit}' "${_u__current_file}" 2>/dev/null)"
    _u__current_version="$(printf "%s\\n" "${_u__current_version_long}" | \
        awk '{gsub(/[\.:]/,"");gsub(/-/,"");print;exit}' 2>/dev/null)"
    [ -z "${_u__current_version}" ] && printf "%s\\n%s\\n%s\\n"           \
        "ERROR: Failed to detect current version, please update manually" \
        "${PROGNAME}         = ${_u__current_file}"                       \
        "${PROGNAME} version = ${_u__current_version}" >&2 && return 1

    command -v "wget" >/dev/null 2>&1 || command -v "curl" >/dev/null 2>&1 || \
        { printf "%s\\n" "ERROR: Install either 'wget' or 'curl' to upgrade" >&2; return 1; }

    _u__url="https://raw.githubusercontent.com/javier-lopez/learn/master/sh/tools/${PROGNAME}"
    _u__tmpfile="/tmp/${PROGNAME}.${$}.update"

    _u__error_msg="$(wget -q -O- --no-check-certificate "${_u__url}" \
        > "${_u__tmpfile}" 2>&1   || curl -s "${_u__url}"  \
        > "${_u__tmpfile}" 2>&1)" || { printf "%s\\n%s\\n" \
        "ERROR: Failed to fetch update, please try later or update manually" \
        "${_u__error_msg}" >&2; return 1; }

    _u__update_version_long="$(awk -F\" \
        '/VERSION=/{print $2;exit}' "${_u__tmpfile}" 2>/dev/null)"
    _u__update_version="$(printf "%s" "${_u__update_version_long}" | awk \
        '{gsub(/[\.:]/,"");gsub(/-/,"");print;exit}' 2>/dev/null)"
    [ -n "${_u__update_version}" ] || _u__update_version="0"

    if [ "${_u__current_version}" -lt "${_u__update_version}" ]; then
        printf "%s %s\\n" "Updating from version" \
            "${_u__current_version_long} to ${_u__update_version_long} ..."
        chmod +x "${_u__tmpfile}"
        if ! mv -f "${_u__tmpfile}" "${_u__current_file}" 2>/dev/null; then
            printf "%s\\n" "ERROR: no write permissions on ${_u__current_file}" >&2
            printf "%s\\n" "INFO : trying with sudo ..." >&2
            if command -v "sudo" >/dev/null 2>&1; then
                sudo mv "${_u__tmpfile}" "${_u__current_file}" || return 1
            else
                printf "%s\\n" "ERROR: sudo isn't available, exiting ..." >&2
                rm -rf "${_u__tmpfile}"
                return 1
            fi
        fi
        printf "%s %s\\n" "${PROGNAME} is up-to-date (${_u__update_version_long})"
        return 0
    fi
    printf "%s %s\\n" "${PROGNAME} is up-to-date (${_u__current_version_long})"
    rm -rf "${_u__tmpfile}"
}

_uniq() {
    awk 'a !~ $0; {a=$0}'
}

_get_links() {
    [ -z "${1}" ] && return 1
    __stackexchange_sites="stackoverflow.com superuser.com askubuntu.com serverfault.com stackexchange.com"
    __question_hash="$(printf "%s\\n" "${*}" | tr ' ' '_')"

    _get_links_google() {
        #for some reason the google api doesn't return the same results
        #http://ajax.googleapis.com/ajax/services/search/web?v=1.0
        __query=""; for __site in ${__stackexchange_sites}; do
            __query="${__query} __site:${__site} OR"
        done; __query="${__query# }"; __query="${__query%OR} ${@}"
        wget -q -U "Mozilla/5.0" "http://www.google.com/search?q=${__query}" -O- | \
            grep -Po '(http|https)://.*/questions/[0-9]*/' | awk -F'&' '{print $1}' | awk '!/ /' | _uniq
    }

    if [ "${HOWDOI_CACHE_ENABLED}" ]; then
        __links_cache_file="$(find ~/.cache/howdoi/ -iname \
            "${__question_hash}".links 2>/dev/null | head -n 1)"
        __links_content="$(cat "${__links_cache_file}" 2>/dev/null)"
        [ -n "${__links_content}" ] && printf "%s\\n" "${__links_content}" && return
    fi

    for __search_engine in ${SEARCH_ENGINES}; do
        __links_content="$(_get_links_"${__search_engine}" "${@}")"
          [ -z "${__links_content}" ] || break
    done; [ -z "${__links_content}" ] && return 1

    if [ "${HOWDOI_CACHE_ENABLED}" ]; then
        [ -d ~/.cache/howdoi/ ] || _mkdir_p ~/.cache/howdoi/
        printf "%s\\n" "${__links_content}" > ~/.cache/howdoi/"${__question_hash}".links
    fi

    printf "%s\\n" "${__links_content}"
}

_get_answers() {
    [ -z "${1}" ] && return 1
    __link_hash="$(printf "%s\\n" "${*}" | tr '/' '#')"

    _get_answers_stackexchange() {
        #the stackoverflow api doesn't print answers body
        #http://api.stackoverflow.com/2.0/usage
        wget --no-check-certificate -q -U "Firefox/3.0.5" "${@}" -O- | \
            awk '/answercell/,/div>/ {if ($0 ~ "div>") print "-----"; else print;}' | \
            awk '/<pre><code>/,/<\/code><\/pre>/ {gsub(/^[ \t]+<pre>/, ""); print ">>", $0}1' | \
            awk '/>>/ {print $0; getline; next}1' | \
            awk '{gsub("<[^>]*>", ""); gsub(/^[ \t]+|[ \t]+$/, "");
                  gsub(/>>$/, ""); gsub(/>>[ \t]/, ">"); gsub(/&lt;/, "<");
                  gsub(/&gt;/, ">"); sub(/\r$/,""); if ($0 != "") print}'
    }

    if [ "${HOWDOI_CACHE_ENABLED}" ]; then
        __answers_cache_file="$(find ~/.cache/howdoi/ -iname \
            "${__link_hash}".answers 2>/dev/null | head -n 1)"
        __answers_content="$(cat "${__answers_cache_file}" 2>/dev/null)"
        [ -n "${__answers_content}" ] && printf "%s\\n" "${__answers_content}" && return
    fi

    __answers_content="$(_get_answers_stackexchange "${@}")"
    [ -z "${__answers_content}" ] && return 1

    if [ "${HOWDOI_CACHE_ENABLED}" ]; then
        [ -d ~/.cache/howdoi/answers ] || _mkdir_p ~/.cache/howdoi/answers
        printf "%s\\n" "${__answers_content}" > ~/.cache/howdoi/answers/"${__link_hash}".answers
    fi

    printf "%s\\n" "${__answers_content}"
}

_howdoi() {
    [ -z "${1}" ] && return 1
    [ -z "${position_link}" ]     && position_link="1"
    [ -z "${number_of_answers}" ] && number_of_answers="1"

    __links="$(_get_links "${@}")"
    [ -z "${__links}" ] && { printf "%s\\n" "${PROGNAME}: No results" >&2; exit 1; }

    [ "${display_only_links}" ] && printf "%s\\n" "${__links}" && exit

    __link="$(printf "%s\\n" "${__links}" | awk -v l="${position_link}" 'NR == l {print; exit}')"
    #if a user request a link out of scope, return the last link instead
    [ -z "${__link}" ] && __link="$(printf "%s\\n" "${__links}" | awk 'END {print}')"

    __answers="$(_get_answers "${__link}")"
    [ -z "${__answers}" ] && { printf "%s\\n" "${PROGNAME}: error parsing ${__link}" >&2; exit 1; }
    __answers_len="$(printf "%s" "${__answers}" | awk '/-----/ {i++} END {print i}')"

    if [ "${display_all_answers}" ]; then
        printf "%s\\n" "${__answers}" | awk '{gsub(/>/, "   "); print $0}'
        printf "%s\\n" "${__link}"; exit;
    else
        if [ "${number_of_answers}" -lt "2" ] || [ "${__answers_len}" -lt "2" ]; then
            printf "%s" "${__answers}" | \
                awk '/^>/ {sub(/^>/, ""); print $0; getline t;
                    if ( t ~ "^>") {sub(/^>/, "", t); print t;} else exit }'
        else
            printf "%s" "${__answers}" | \
                awk -v l="${number_of_answers}" 'BEGIN {i++} /^>/ {
                if (flagend !~ "set") {print "--- Answer", i, "---"; sub(/^>/, "");
                print $0; getline; while ( $0 ~ "^>") {sub(/^>/, ""); print;
                getline;} i++; flagend="set";} else
                { if ($0 !~ "^-----$") flagend="none"} if (i == l+1) exit}'
        fi
    fi
}

if [ ! -t 0 ]; then
    #there is input comming from pipe or file, add to the end of $@
    set -- "${@}" $(cat)
fi

[ "${#}" -eq "0" ] && _die

if ! command -v "wget" >/dev/null 2>&1; then
    printf "%s\\n" "install 'wget' to run this program" >&2
    exit 1
fi

for arg in "${@}"; do #parse options
    case "${arg}" in
        -h|--help) _usage && exit ;;
        -U|--update) _update; exit "${?}" ;;
        -l) display_only_links="true"; shift;;
        -a) display_all_answers="true"; shift;;
        -c) display_colors="true"; shift;;
        -C) rm -rf ~/.cache/howdoi
            printf "%s\\n" "Cache cleared successfully" && exit ;;
        -p) if [ "${#}" -gt "1" ]; then
                position_link="$(printf "%s " "${@}" | awk '{print $2}')"
                shift 2
                if ! _is_int "${position_link}" ; then
                    _die "Option '${arg}' requires a number: '${position_link}'"
                fi
            else
                _die "Option '${arg}' requires a parameter"
            fi ;;
        -n) if [ "${#}" -gt "1" ]; then
                number_of_answers="$(printf "%s " "${@}" | awk '{print $2}')"
                shift 2
                if ! _is_int "${number_of_answers}" ; then
                    _die "Option '${arg}' requires a number: '${number_of_answers}'"
                fi
            else
                _die "Option '${arg}' requires a parameter"
            fi ;;
        -*) _die "${PROGNAME}: unrecognized option '${arg}'" ;;
    esac
done

[ -z "${HOWDOI_DISABLE_CACHE}" ] && HOWDOI_CACHE_ENABLED="true"

_howdoi "${@}"