#!/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.


if [[ ! -z ${zuper_version} ]]; then 
    warning "zuper version ::1 version:: was already loaded -- doing nothing" ${zuper_version}
    return;
fi


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

typeset -aU funs

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

vars+=(zuper_version)
zuper_version=0.4

# load necessary zsh extensions
zmodload zsh/regex
zmodload zsh/system
zmodload zsh/net/tcp
zmodload zsh/mapfile

# {{{ Messaging

# Messaging function with pretty coloring
autoload colors
colors

vars+=(last_act last_func last_notice)

function _msg() {
    local msg="$2"
	local i
    command -v gettext 1>/dev/null 2>/dev/null && msg="$(gettext -s "$2")"
    for i in {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
}


function 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
}

# dump all variables, arrays and maps declared as global in zuper
# do not print out what is empty
zdump() {
    fn zdump
    [[ ${#vars} -gt 0 ]] && {
        print "Global variables:"
        for _v in $vars; do
			_c=${(P)_v}
			[[ "$_c" = "" ]] ||
				print " $_v = \t $_c"
        done
    }
    [[ ${#arrs} -gt 0 ]] && {
        print "Global arrays:"
        for _a in $arrs; do
			_c=${(P)_a}
			[[ "$_c" = "" ]] ||
				print " $_a \t ( ${(P)_a} )"
        done
    }
    [[ ${#maps} -gt 0 ]] && {
        print "Global maps:"
        for _m in $maps; do
			[[ "${(Pv)_m}" = "" ]] || {
				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)

# 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." }


funs+=(__test_fn)

__test_fn(){
    echo "foo"
}

function zuper_end endgame() {
    fn "endgame $*"

    # execute all no matter what
    TRAPZERR() { }

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

    # unset all the variables included  in "vars" 
    for v in $vars; do 
        unset $v
    done

    # unset all the assoc-arrays included  in "arrs" 
    for a in $arrs; do 
        unset $a
    done

    # unset all the maps included  in "maps" 
    for m in $maps; do 
        unset $m
    done
    
    ## We should also undefine the core zuper functions to make it
    ## really idempotent. I have added an array "funs" which contains
    ## the names of the functions to be undefined by endgame/zuper_end
    ## FIXME!!!! The only "registered" function so far is __test_fn,
    ## but if we like this we should register all the core zuper
    ## functions as soon as they are declared
    for f in $funs; do 
        unfunction $f
    done
    unset maps
    unset arrs
    unset vars
    unset funs

    return 0
}

## This function should reinitialise zuper and all the variables
# zuper_restart(){
#     endgame 
#     source zuper
# }


# Use this to make sure endgame() is called at exit.
# unlike TRAPEXIT, the zshexit() hook is not called when functions exit.
function zuper.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)
}

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

ztmpd() {
    fn ztmpd

    ztmpdir=`mktemp -d`
    tmpdirs+=($ztmpdir)
}

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

    for f in $tmpfiles; do
        rm -f "$f"
    done
    for d in $tmpdirs; do 
        [[ $d == "" || ! -d $d ]] && continue
        pushd $d
        [[ `pwd` == "/" ]] && {popd; continue}
        popd 
        rm -rf "$d"
    done

    tmpfiles=()
    tmpdirs=()
}

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

# }}} Tempfiles

# {{{ Strings

# tokenizer, works only with one char length delimiters
# saves everything in global array tok=()
arrs+=(tok)
function string.strtok 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))]}"
            if [[ "$t" == "" ]]; then
                tok+=("null")
            else
                tok+=("$t")
            fi
            # save last found
            f=$c
        fi
    done
    # add last token
    t=${_string[(e)$(($f + 1)),$c]}
    if [[ "$t" == "" ]]; then
        tok+=("null")
    else
        tok+=("$t")
    fi
}

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

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

# takes a string as argument, returns success if is an email
function string.isemail isemail() {
    [[ "$1" =~ "\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,6}\b" ]] && return 0
	# print "$1" | grep -q -E '[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,6}' && return 0
    return 1
}

# takes a numeric argument and prints out a human readable size
function string.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 string.html_strip xml_strip html_strip() { sed 's/<[^>]\+>//g' }

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

# escapes special chars in urls
function string.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
'
}

# Check if a version number (semantic format) is greater than the other
# returns 0 if the first argument is greater or equal than the second.
function string.version_greatoreq version_greatoreq() {
	[[ "$(printf '%s\n' "$@" | sort -rV | head -n 1)" = "$1" ]] && return 0
	return 1
}

# }}} 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 ${(f)"$(find /sys/devices/ -name net)"}; do
		for dev in ${(f)"$(ls --indicator-style=none $i)"}; do
			# skip the loopback device
			[[ "$dev" =~ "^lo" ]] && continue
			func "found network device: $dev"
			net_devices+=($dev)
		done
	done

    # return error if no device found
    if [[ ${#net_devices} = 0 ]]; then return 1
    else
		act "${#net_devices} devices found: ${net_devices}"
		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
    act "${#net_ip4_addr} ipv4 connected devices found"
    for c in ${(k)net_ip4_addr}; do
        act " $c ${net_ip4_addr[$c]}"
    done

    # list ipv6
    act "${#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

##########################
# 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 (WIP, needs more testing)
# 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

# {{{ Parse commandline options

# for example usage, see Tomb http://tomb.dyne.org
vars+=(subcommand)
arrs+=(option_main option_params)
maps+=(option option_subcommands)

# Hi, dear developer!  Are you trying to add a new subcommand, or
# to add some options?  Well, keep in mind that option names are
# global: they cannot bear a different meaning or behaviour across
# subcommands.  The only exception is "-o" which means: "options
# passed to the local subcommand", and thus can bear a different
# meaning for different subcommands.
#
# For example, "-s" means "size" and accepts one argument. If you
# are tempted to add an alternate option "-s" (e.g., to mean
# "silent", and that doesn't accept any argument) DON'T DO IT!
#
# There are two reasons for that:
#    I. Usability; users expect that "-s" is "size"
#   II. Option parsing WILL EXPLODE if you do this kind of bad
#       things (it will complain: "option defined more than once")
#
# If you want to use the same option in multiple commands then you
# can only use the non-abbreviated long-option version like:
# -force and NOT -f

option.is_set() {

	# Check whether a commandline option is set.
	#
	# Synopsis: option_is_set -flag [out]
	#
	# First argument is the commandline flag (e.g., "-s").
	# If the second argument is present and set to 'out', print out the
	# result: either 'set' or 'unset' (useful for if conditions).
	#
	# Return 0 if is set, 1 otherwise
    local -i r   # the return code (0 = set, 1 = unset)

    [[ -n ${(k)option[$1]} ]];
    r=$?

    [[ $2 == "out" ]] && {
        [[ $r == 0 ]] && { print 'set' } || { print 'unset' }
    }

    return $r;
}
# Print the option value matching the given flag
# Unique argument is the commandline flag (e.g., "-s").
option.value() {
    print -n - "${option[$1]}"
}
option.parse() {

    ### Detect subcommand
    local -aU every_opts #every_opts behave like a set; that is, an array with unique elements
    for optspec in ${option_subcommands}${option_main}; do
        for opt in ${=optspec}; do
            every_opts+=${opt}
        done
    done
    local -a oldstar
    oldstar=("${(@)argv}")
    #### detect early: useful for --option-parsing
    zparseopts -M -D -Adiscardme ${every_opts}
    if [[ -n ${(k)discardme[--option-parsing]} ]]; then
        print $1
        if [[ -n "$1" ]]; then
            return 1
        fi
        return 0
    fi
    unset discardme
    if ! zparseopts -M -E -D -Adiscardme ${every_opts}; then
        _failure "Command parses error."
        return 1
    fi
    unset discardme
    subcommand=${1}
    if [[ -z $subcommand ]]; then
        subcommand="__empty"
    fi

    if [[ -z ${(k)option_subcommands[$subcommand]} ]]; then
		subcommand="__unknown:$subcommand"
        # _warning "There's no such command \"::1 subcommand::\"." $subcommand
		# _failure "Please try -h for help."
    fi
    argv=("${(@)oldstar}")
    unset oldstar

    ### Parsing global + command-specific options
    # zsh magic: ${=string} will split to multiple arguments when spaces occur
    set -A cmd_opts ${option_main} ${=option_subcommands[$subcommand]}
    # if there is no option, we don't need parsing
    if [[ -n $cmd_opts ]]; then
        zparseopts -M -E -D -Aoption ${cmd_opts}
        if [[ $? != 0 ]]; then
            _warning "Some error occurred during option processing."
            _failure "See zuper option.parse for more info."
			return 1
        fi
    fi
    #build option_params (array of arguments) and check if there are unrecognized options
    ok=0
    option_params=()
    for arg in $*; do
        if [[ $arg == '--' || $arg == '-' ]]; then
            ok=1
            continue #it shouldn't be appended to option_params
        elif [[ $arg[1] == '-'  ]]; then
            if [[ $ok == 0 ]]; then
                _failure "Unrecognized option ::1 arg:: for subcommand ::2 subcommand::" $arg $subcommand
				return 1
            fi
        fi
        option_params+=$arg
    done
    # First parameter actually is the subcommand: delete it and shift
    [[ $subcommand != '__empty' ]] && { option_params[1]=(); shift }

    ### End parsing command-specific options

    [[ "$option_params" == "" ]] && {
        func "arg command: ::1 subcommand::" $subcommand
    } || {
        func "arg command: ::1 subcommand:: ::2 param::" $subcommand $option_params
    }

}

# Later: process subcommand
# case "$subcommand" in
# 	help)
# 		print "TODO: help"
# 		;;
# 	__empty)
# 		zdump
# 		;;

#     # Reject unknown command and suggest help
#     *)
#         _warning "Command \"::1 subcommand::\" not recognized." $subcommand
#         _message "Try -h for help."
#         return 1
#         ;;
# esac

# }}}

# {{{ Helpers

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

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

# }}} 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
