#!/usr/bin/env zsh
## -*- origami-fold-style: triple-braces -*-
#
# Zuper - Zsh Ultimate Programmer's Extensions Refurbished
#
# Copyright (C) 2015 Dyne.org Foundation
#
# Zuper is designed, written and maintained by Denis Roio <jaromil@dyne.org>
#
# This source  code is free  software; you can redistribute  it and/or
# modify it under the terms of  the GNU Public License as published by
# the Free  Software Foundation; either  version 3 of the  License, or
# (at your option) any later version.
#
# This source code is distributed in  the hope that it will be useful,
# but  WITHOUT ANY  WARRANTY;  without even  the  implied warranty  of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# Please refer to the GNU Public License for more details.
#
# You should have received a copy of the GNU Public License along with
# this source code; if not, write to:
# Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.


##########################
typeset -aU vars
typeset -aU arrs
typeset -aU maps

vars=(DEBUG QUIET LOG)
arrs=(req freq)

vars+=(zuper_version)
zuper_version=0.2

zmodload zsh/system
zmodload zsh/net/tcp

# {{{ Messaging

# Messaging function with pretty coloring
autoload colors
colors

vars+=(last_act last_func last_notice)

function _msg() {
    local msg="$2"
    command -v gettext 1>/dev/null 2>/dev/null && msg="$(gettext -s "$2")"
    for i in $(seq 3 ${#});
    do
        msg=${(S)msg//::$(($i - 2))*::/$*[$i]}
    done

    local command="print -P"
    local progname="$fg[magenta]${PROGRAM##*/}$reset_color"
    local message="$fg_bold[normal]$fg_no_bold[normal]$msg$reset_color"
    local -i returncode

    case "$1" in
        inline)
            command+=" -n"; pchars=" > "; pcolor="yellow"
            ;;
        message)
            last_act="$msg"
            pchars=" . "; pcolor="white"; message="$fg_no_bold[$pcolor]$msg$reset_color"
            ;;
        verbose)
            last_func="$msg"
            pchars="[D]"; pcolor="blue"
            ;;
        success)
            last_notice="$msg"
            pchars="(*)"; pcolor="green"; message="$fg_no_bold[$pcolor]$msg$reset_color"
            ;;
        warning)
            pchars="[W]"; pcolor="yellow"; message="$fg_no_bold[$pcolor]$msg$reset_color"
            ;;
        failure)
            pchars="[E]"; pcolor="red"; message="$fg_no_bold[$pcolor]$msg$reset_color"
            returncode=1
            ;;
        print)
            progname=""
            ;;
        *)
            pchars="[F]"; pcolor="red"
            message="Developer oops!  Usage: _msg MESSAGE_TYPE \"MESSAGE_CONTENT\""
            returncode=127
            zerr
            ;;
    esac
    ${=command} "${progname} $fg_bold[$pcolor]$pchars$reset_color ${message}$color[reset_color]" >&2

    # write the log if its configured
    [[ "$LOG" = "" ]] || {
        touch $LOG || return $?
        ${=command} "${progname} $fg_bold[$pcolor]$pchars$reset_color ${message}$color[reset_color]" >> $LOG
    }

    return $returncode
}

function _message say act() {
    local notice="message"
    [[ "$1" = "-n" ]] && shift && notice="inline"
    [[ $QUIET = 1 ]] || _msg "$notice" $@
    return 0
}

function _verbose xxx func() {
    [[ $DEBUG = 1 ]] && _msg verbose $@
    return 0
}

function _success yes notice() {
    [[ $QUIET = 1 ]] || _msg success $@
    return 0
}

function _warning no warn warning() {
    [[ $QUIET = 1 ]] || _msg warning $@
    return 0
}

function _failure fatal die error() {
    #    typeset -i exitcode=${exitv:-1}
    [[ $QUIET = 1 ]] || _msg failure $@
    return 1
}

function _print() {
    [[ $QUIET = 1 ]] || _msg print $@
    return 0
}

# }}} Messaging

# {{{ Debugging

fn() {
    fun="$@"
    req=()
    freq=()
    func "$fun"
}

zerr() {
    error "error in: ${fun:-$last_notice}"
    [[ "$last_func"   = "" ]] || warn "called in: $last_func"
    [[ "$last_act"    = "" ]] || warn "called in: $last_act"
    [[ "$last_notice" = "" ]] || warn "called in: $last_notice"
    # [[ "$fun"         = "" ]] || warn "called in: $fun"
    TRAPEXIT() {
        error "error reported, operation aborted."
    }
    return 1
}


ckreq reqck() {
    err=0
    for v in $req; do
        [[ "${(P)v}" = "" ]] && {
            warn "${fun[(ws: :)1]}(): required setting is blank: $v"
            err=1
        }
    done

    [[ $err = 1 ]] && return $err

    for f in $freq; do
        # exists and has size greater than zero
        [[ -s $f ]] || {
            warn "required file empty: $f"
            err=1
        }
    done
    [[ $err == 1 ]] && zerr
    return $err
}

zdump() {
    fn zdump
    [[ ${#vars} -gt 0 ]] && {
        print "Global variables:"
        for _v in $vars; do
            print " $_v = \t ${(P)_v}"
        done
    }
    [[ ${#arrs} -gt 0 ]] && {
        print "Global arrays:"
        for _a in $arrs; do
        print " $_a \t ( ${(P)_a} )"
        done
    }
    [[ ${#maps} -gt 0 ]] && {
        print "Global maps:"
        for _m in $maps; do
            print " $_m [key] \t ( ${(Pk)_m} )"
            print " $_m [val] \t ( ${(Pv)_m} )"
        done
    }
}

# handy wrappers for throw/catch execution of blocks where we need the
# program to exit on any error (non-zero) returned by any function
throw() { function TRAPZERR() { zerr; return 1 } }
catch() { function TRAPZERR() { } }

##########################
# Endgame handling

arrs+=(destruens)
destruens=()

# Trap functions for the endgame event
TRAPINT()  { endgame INT;   return $? }
# TRAPEXIT() { endgame EXIT;  return $? }
TRAPHUP()  { endgame HUP;   return $? }
TRAPQUIT() { endgame QUIT;  return $? }
TRAPABRT() { endgame ABORT; return $? }
TRAPKILL() { endgame KILL;  return $? }
# TRAPPIPE() { endgame PIPE;  return $? }
TRAPTERM() { endgame TERM;  return $? }
TRAPSTOP() { endgame STOP;  return $? }
# TRAPZERR() { func "function returns non-zero." }


endgame() {
    fn "endgame $*"

    # execute all no matter what
    TRAPZERR() { }

    # process registered destructors
    for d in $destruens; do
        fn "destructor: $d"
        $d
    done
    return 0
}

# Register endgame() to be called at exit.
# unlike TRAPEXIT, the zshexit() hook is not called when functions exit.
zshexit() { endgame EXIT; return $? }

# }}} Debugging

# {{{ Tempfiles

##########################
# Temp file handling

vars+=(ztmpfile)
# ztmp() fills in $ztmpfile global. Caller must copy that variable as
# it will be overwritten at every call.
ztmp() {
    fn ztmp

    ztmpfile=`mktemp`
    tmpfiles+=($ztmpfile)
}

# All tempfiles are freed in endgame()
_ztmp_destructor() {
    fn _ztmp_destructor

    for f in $tmpfiles; do
        rm -f "$f"
    done
    tmpfiles=()
}

arrs+=(tmpfiles)
destruens+=(_ztmp_destructor)

# }}} Tempfiles

# {{{ Strings

# tokenizer, works only with one char length delimiters
# saves everything in global array tok=()
arrs+=(tok)
strtok() {
    fn "strtok $*"
    _string="$1"
    _delim="$2"
    req=(_string _delim)
    ckreq || return $?

    tok=()
    f=0
    c=0
    for c in {1..${#_string}}; do
        if [[ "${_string[(e)$c]}" == "$_delim" ]]; then
            # check if not empty
            t=${_string[(e)$(($f + 1)),$(($c - 1))]}
            [[ "$t" == "" ]] || tok+=($t)
            # save last found
            f=$c
        fi
    done
    # add last token
    t=${_string[(e)$(($f + 1)),$c]}
    [[ "$t" == "" ]] || tok+=($t)
}

# TODO: move in here some helpers

# }}} Strings

# {{{ Networking

# This is only tested on GNU/Linux and makes use of sysfs

# index of all network devices
arrs+=(net_devices)

# map of ipv4 assigned addresses: [dev addr]
maps+=(net_ip4_addr)
# map of ipv6 assigned addresses: [dev addr]
maps+=(net_ip6_addr)

# map of dhcp served ipv4
maps+=(ip4dhcps)
# map of dhcp served ipv6
maps+=(ip6dhcps)

# map of external ipv4 addresses
maps+=(net_ip4_exit)
# map of internal ipv6 addresses
# maps+=(ip6exits)

net.scan_devices() {
    for i in `find /sys/devices/ -name net`; do
        dev=`ls $i`

        # skip the loopback device
        [[ "$dev" = "lo" ]] && continue

        net_devices+=($dev)
    done

    # return error if no device found
    if [[ ${#net_devices} = 0 ]]; then return 1
    else return 0; fi
}

net.scan_addresses() {
    [[ ${#net_devices} = 0 ]] && {
        error "No network device found."
        func "Have you ran net.scan_devices() first?"
        return 1
    }

    for dev in ${net_devices}; do
        # check ipv4 connections
        conn=`ip addr show $dev | awk '/inet / {print $2}'`
        [[ "$conn" = "" ]] || {
            net_ip4_addr+=($dev $conn) }
        # check ipv6 connections
        conn=`ip addr show $dev | awk '/inet6/ {print $2}'`
        [[ "$conn" = "" ]] || {
            net_ip6_addr+=($dev $conn) }
    done

    # list ipv4
    notice "${#net_ip4_addr} ipv4 connected devices found"
    for c in ${(k)net_ip4_addr}; do
        act " $c ${net_ip4_addr[$c]}"
    done

    # list ipv6
    notice "${#net_ip6_addr} ipv6 connected devices found"
    for c in ${(k)net_ip6_addr}; do
        act " $c ${net_ip6_addr[$c]}"
    done
    # find out network addresses

    return 0
}

net.scan_exits() {
    # just ipv4 for now, also we use curl to drive the call over the
    # specific interface, but if that wouldn't matter then rest.get is
    # better to avoid this dependency

    for dev in ${(k)net_ip4_addr}; do
        addr=`curl --silent --interface $dev https://api.ipify.org`
        if [[ "$?" != "0" ]]; then
            error "curl returns $?: $addr"
        else
            [[ "$addr" = "" ]] || {
                notice "$dev external ip: $addr"
                net_ip4_exit+=($dev $addr)
            }
        fi
    done

    for dev in ${(k)net_ip6_addr}; do
        addr=`curl --silent --ipv6 --interface $dev https://api.ipify.org`
        if [[ $? != 0 ]]; then
            error "curl returns $?: $addr"
        else
            [[ "$addr" = "" ]] || {
                notice "$dev external ip: $addr"
                net_ip4_exit+=($dev $addr)
            }
        fi
    done

}

# }}} Networking

# {{{ Key/Value filesave

# optional: define zkv=1 on source

[[ "$zkv" = "" ]] || {

    ##########################
    # Key/Value file storage using ZSh associative maps


    # load a map from a file
    # map must be already instantiated with typeset -A by called
    # name of map is defined inside the file
    function zkv.load() {
        fn "zkv-load $*"

        file=$1
        [[ "$file" = "" ]] && {
            error "zkv-open() missing argument: file-path"
            zerr
            return 1    }
        [[ -r "$file" ]] || {
            error "zkv-open() file not found $file"
            zerr
            return 1    }
        [[ -s "$file" ]] || {
            error "zkv-open() file is empty"
            zerr
            return 1    }

        source $file
    }

    # save a map in a file
    # $1 = name of the map associative array
    # $2 = full path to the file
    function zkv.save() {
        fn "zkv.save $*"

        _map=$1
        _path=$2
        [[ "$_path" = "" ]] && {
            error "zkv.save() missing argument: map-name path-to-file"
            zerr
            return 1
        }
        [[ -r $_path ]] && {
            func "zkv.close() overwriting $_path"
            func "backup turd left behind: ${_path}~"
            mv $_path $_path~
        }
        touch $_path

        # wondering about http://www.zsh.org/mla/users/2015/msg00286.html
        # meanwhile solved using a double array, wasting a full map memcpy
        _karr=(${(Pk)_map})
        _varr=(${(Pv)_map})
        _num="${#_karr}"
        for c in {1..$_num}; do
            # can also be cat here, however for speed we use builtins
            # switch to cat if compatibility is an issue
            sysread -o 1 <<EOF >> $_path
$_map+=("${_karr[$c]}" "${(v)_varr[$c]}")
EOF
        done
        func "$_num key/values stored in $_path"
    }

}

# }}} Key/Value filesave

# {{{ Get/Set REST API

########
# Restful API client
# there is a clear zsh optimization here in get/set kv
# using zsh/tcp instead of spawning curl
# and perhaps querying with one call using ?recursive

vars+=(rest_reply_body rest_reply_header)
maps+=(rest_header)

function rest.put() {
    fn "rest.put $*"

    # $1 = hostname
    # $2 = port
    # $3 = path
    # value from stdin |

    # to check if the http service is running is up to the caller

    _host=${1} # ip address
    _port=${2}
    _path=${3}
    sysread _v

    req=(_host)
    ckreq || return $?

    if ztcp $_host $_port; then

        # TODO: work out various parsers, this one works with consul.io

        _fd=$REPLY
        #    func "tcp open on fd $fd"
        cat <<EOF >& $_fd
PUT ${_path} HTTP/1.1
User-Agent: Zuper/$zuper_version
Host: ${_host}:${_port}
Accept: */*
Content-Length: ${#_v}
Content-Type: application/x-www-form-urlencoded

EOF

        print -n "$_v" >& $_fd

        sysread -i $_fd _res

        # close connection
        ztcp -c $_fd

        [[ "$_res" =~ "true" ]] || {
            warn "failed PUT on restful key/value"
            warn "host: ${_host}"
            warn "port: ${_port}"
            warn "path: ${_path}"
            warn "value: $_v"
            print - "$_res"
            zerr
            return 1
        }

    else
        error "cannot connect to restful service: $_host:$_port"
        zerr
        return 1
    fi

    return 0

}

function rest.get() {
    fn "rest.get $*"

    _host=${1}
    _port=${2}
    _path=${3}

    req=(_host _port)
    ckreq || return $?

    ztcp $_host $_port || {
        zerr
        return 1
    }

    _fd=$REPLY

    # TODO: work out various parsers, this one works with consul.io

    cat <<EOF >& $_fd
GET ${_path} HTTP/1.1
User-Agent: Zuper/$zuper_version
Host: $_host:$_port
Accept: */*

EOF

    # read header response
    rest_reply=`sysread -i $_fd -o 1`

    for i in "${(f)rest_reply}"; do
        print $i | hexdump -C
        # first line is the response code

        [[ "$i" -regex-match "\x0d\x0a$" ]] && {
            func BLANK
            break }

        # # save other lines in map for fast retrieval
        # _field=${i[(ws@:@)1]}
        # func "$_field - header field parsed"
        # rest_header[$_field]="${i[(ws@:@)2]}"

        # c=$(( $c + 1 ))
    done
    # rest_reply_header="${(f)$(cat <&$_fd)}"

    func "${#rest_reply_header} bytes response header stored in rest_reply_header"
    # | awk -F: '
    #/"Value":/ { gsub(/"|}]/,"",$7) ; print $7 }' | base64 -d

    # TODO: read content-length and use it here

    rest_reply_body="${(f)$(sysread -i $_fd -o 1)}"
    func "${#rest_reply_body} bytes response body stored in rest_reply_body"

    # close connection
    ztcp -c $_fd

    return 0

}


# }}} Get/Set REST API

# {{{ Helpers
[[ "$helpers" = "" ]] || {

    function helper.isfound isfound() {
        command -v $1   1>/dev/null 2>/dev/null
        return $?
    }

    # remote leading and trailing spaces in a string taken from stdin
    function helper.trim trim() {
        sed -e 's/^[[:space:]]*//g ; s/[[:space:]]*\$//g'
    }

    zmodload zsh/mapfile
    # faster substitute for cat
    function helper.printfile printfile() {
        print ${mapfile[$1]}
    }

    # extract all emails found in a text from stdin
    # outputs them one per line
    function helper.extract-emails extract_emails() {
        awk '{ for (i=1;i<=NF;i++)
     if ( $i ~ /[[:alnum:]]@[[:alnum:]]/ ) {
       gsub(/<|>|,/ , "" , $i); print $i } }'
    }


    zmodload zsh/regex
    # takes a string as argument, returns success if is an email
    function helper.isemail isemail() {
        [[ "$1" -regex-match "\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,6}\b" ]] && return 0

        return 1
    }

    # takes a numeric argument and prints out a human readable size
    function helper.human-size human_size() {
        [[ $1 -gt 0 ]] || {
            error "human_size() called with invalid argument"
            return 1
        }

        # we use the binary operation for speed
        # shift right 10 is divide by 1024

        # gigabytes
        [[ $1 -gt 1073741824 ]] && {
            print -n "$(( $1 >> 30 )) GB"
            return 0
        }

        # megabytes
        [[ $1 -gt 1048576 ]] && {
            print -n "$(( $1 >> 20 )) MB"
            return 0
        }
        # kilobytes
        [[ $1 -gt 1024 ]] && {
            print -n "$(( $1 >> 10 )) KB"
            return 0
        }
        # bytes
        print -n "$1 Bytes"
        return 0
    }


    # strips out all html/xml tags (everything between < >)
    function helper.html-strip xml_strip html_strip() { sed 's/<[^>]\+>//g' }

    # changes stdin string special chars to be shown in html
    function helper.escape-html escape_html() {
        sed -e '
s/\&/\&amp;/g
s/>/\&gt;/g
s/</\&lt;/g
s/"/\&quot;/g
'
    }

    # escapes special chars in urls
    function helper.decode-url decode_url urldecode() {
        sed -e '
s/%25/%/gi
s/%20/ /gi
s/%09/ /gi
s/%21/!/gi
s/%22/"/gi
s/%23/#/gi
s/%24/\$/gi
s/%26/\&/gi
s/%27/'\''/gi
s/%28/(/gi
s/%29/)/gi
s/%2a/\*/gi
s/%2b/+/gi
s/%2c/,/gi
s/%2d/-/gi
s/%2e/\./gi
s/%2f/\//gi
s/%3a/:/gi
s/%3b/;/gi
s/%3d/=/gi
s/%3e//gi
s/%3f/?/gi
s/%40/@/gi
s/%5b/\[/gi
s/%5c/\\/gi
s/%5d/\]/gi
s/%5e/\^/gi
s/%5f/_/gi
s/%60/`/gi
s/%7b/{/gi
s/%7c/|/gi
s/%7d/}/gi
s/%7e/~/gi
s/%09/      /gi
'
    }

    function helper.encode-url encode_url urlencode() {
        sed -e '
s/%/%25/g
s/ /%20/g
s/ /%09/g
s/!/%21/g
s/"/%22/g
s/#/%23/g
s/\$/%24/g
s/\&/%26/g
s/'\''/%27/g
s/(/%28/g
s/)/%29/g
s/\*/%2a/g
s/+/%2b/g
s/,/%2c/g
s/-/%2d/g
s/\./%2e/g
s/\//%2f/g
s/:/%3a/g
s/;/%3b/g
s//%3e/g
s/?/%3f/g
s/@/%40/g
s/\[/%5b/g
s/\\/%5c/g
s/\]/%5d/g
s/\^/%5e/g
s/_/%5f/g
s/`/%60/g
s/{/%7b/g
s/|/%7c/g
s/}/%7d/g
s/~/%7e/g
s/      /%09/g
'
    }

}
# }}} Helpers

# {{{ Config

# This is not a full config parser, but its a mechanism to read single
# sections of configuration files that are separated using various
# syntax methods. The only method supported is now org-mode whose
# sections start with #+ . It fills in the global array
# $config_section which can be read out to a file or interpreted in
# memory, whatever syntax it may contain.

vars+=(config_section_type)
arrs+=(config_section)
config_section_type=org-mode

config.section.type() {
    fn config.section.type
    _type=$1
    req=(_type)
    ckreq || return $?

    case $_type in
        org-mode)
            config_section_type=org-mode
            ;;
        *)
            error "Unknown config type:$_type"
            return 1
            ;;
    esac

    act "$_type config section parser initialized"
    return 0

}

# fills in contents of section in array config_section
config.section.read() {
    fn config.section.read
    _file=$1
    _section=$2
    req=(_file _section)
    freq=($_file)
    ckreq || return $?

    case $config_section_type in
        org-mode)
            _contents=`awk '
BEGIN { found=0 }
/^#\+ '"$_section"'/ { found=1; next }
/^#\+/ { if(found==1) exit 0 }
/^$/ { next }
{ if(found==1) print $0 }
' $_file`

            ;;
        *)
            error "Unknown config type:$_type"
            ;;
    esac

    config_section=()
    for c in ${(f)_contents}; do
        config_section+=("$c")
    done
    return 0

}

# }}} Config
