
# Tcl parser for optional arguments in function calls and
#  commandline arguments
#
# (c) 2001 Bastien Chevreux

#  Index of exported commands
#     - argp::registerArgs
#     - argp::setArgDefaults
#     - argp::setArgsNeeded
#     - argp::parseArgs

#  Internal commands
#     - argp::CheckValues

# See end of file for an example on how to use

package provide argp 0.2

namespace eval argp {
    variable Optstore
    variable Opttypes {
	boolean integer double string
    }

    namespace export {[a-z]*}
}


proc argp::registerArgs { func arglist } {
    variable Opttypes
    variable Optstore

    set parentns [string range [uplevel 1 [list namespace current]] 2 end]
    if { $parentns != "" } {
	append caller $parentns :: $func
    } else {
	set caller $func
    }
    set cmangled [string map {:: _} $caller]

    #puts $parentns
    #puts $caller
    #puts $cmangled

    set Optstore(keys,$cmangled) {}
    set Optstore(deflist,$cmangled) {}
    set Optstore(argneeded,$cmangled) {}

    foreach arg $arglist {
	foreach {opt type default allowed} $arg {
	    set optindex [lsearch -glob $Opttypes $type*]
	    if { $optindex < 0} {
		return -code error "$caller, unknown type $type while registering arguments.\nAllowed types: [string trim $Opttypes]"
	    }
	    set type [lindex $Opttypes $optindex]

	    lappend Optstore(keys,$cmangled) $opt 
	    set Optstore(type,$opt,$cmangled) $type
	    set Optstore(default,$opt,$cmangled) $default
	    set Optstore(allowed,$opt,$cmangled) $allowed
	    lappend Optstore(deflist,$cmangled) $opt $default
	}
    }

    if { [catch {CheckValues $caller $cmangled $Optstore(deflist,$cmangled)} res]} {
	 return -code error "Error in declaration of optional arguments.\n$res"
    }
}

proc argp::setArgDefaults { func arglist } {
    variable Optstore

    set parentns [string range [uplevel 1 [list namespace current]] 2 end]
    if { $parentns != "" } {
	append caller $parentns :: $func
    } else {
	set caller $func
    }
    set cmangled [string map {:: _} $caller]

    if {![info exists Optstore(deflist,$cmangled)]} {
	return -code error "Arguments for $caller not registered yet."
    }
    set Optstore(deflist,$cmangled) {}
    foreach {opt default} $arglist {
	if {![info exists Optstore(default,$opt,$cmangled)]} {
	    return -code error "$caller, unknown option $opt, must be one of: $Optstore(keys,$cmangled)"
	}
	set Optstore(default,$opt,$cmangled) $default
    }

    # set the new defaultlist
    foreach opt $Optstore(keys,$cmangled) {
	lappend Optstore(deflist,$cmangled) $opt $Optstore(default,$opt,$cmangled)
    }
}

proc argp::setArgsNeeded { func arglist } {
    variable Optstore

    set parentns [string range [uplevel 1 [list namespace current]] 2 end]
    if { $parentns != "" } {
	append caller $parentns :: $func
    } else {
	set caller $func
    }
    set cmangled [string map {:: _} $caller]

    #append caller $parentns :: $func
    #set cmangled ${parentns}_$func

    if {![info exists Optstore(deflist,$cmangled)]} {
	return -code error "Arguments for $caller not registered yet."
    }

    set Optstore(argneeded,$cmangled) {}
    foreach opt $arglist {
	if {![info exists Optstore(default,$opt,$cmangled)]} {
	    return -code error "$caller, unknown option $opt, must be one of: $Optstore(keys,$cmangled)"
	}
	lappend Optstore(argneeded,$cmangled) $opt 
    }
}


proc argp::parseArgs { args } {
    variable Optstore

    if {[llength $args] == 0} {
	upvar args a opts o
    } else {
	upvar args a [lindex $args 0] o
    }

    if { [ catch { set caller [lindex [info level -1] 0]}]} {
	set caller "main program"
	set cmangled ""
    } else {
	set cmangled [string map {:: _} $caller]
    }

    if {![info exists Optstore(deflist,$cmangled)]} {
	return -code error "Arguments for $caller not registered yet."
    }

    # set the defaults
    array set o $Optstore(deflist,$cmangled)

    # but unset the needed arguments
    foreach key $Optstore(argneeded,$cmangled) {
	catch { unset o($key) }
    }

    foreach {key val} $a {
	if {![info exists Optstore(type,$key,$cmangled)]} {
	    return -code error "$caller, unknown option $key, must be one of: $Optstore(keys,$cmangled)"
	}
	switch -exact -- $Optstore(type,$key,$cmangled) {
	    boolean -
	    integer {
		if { $val == "" } {
		    return -code error "$caller, $key empty string is not $Optstore(type,$key,$cmangled) value."
		}
		if { ![string is $Optstore(type,$key,$cmangled) $val]} {
		    return -code error "$caller, $key $val is not $Optstore(type,$key,$cmangled) value."
		}
	    }
	    double {
		if { $val == "" } {
		    return -code error "$caller, $key empty string is not double value."
		}
		if { ![string is double $val]} {
		    return -code error "$caller, $key $val is not double value."
		}
		if { [string is integer $val]} {
		    set val [expr {$val + .0}]
		}
	    }
	    default {
	    }
	}
	set o($key) $val
    }

    foreach key $Optstore(argneeded,$cmangled) {
	if {![info exists o($key)]} {
	    return -code error "$caller, needed argument $key was not given."
	}
    }

    if { [catch { CheckValues $caller $cmangled [array get o]} err]} {
	return -code error $err
    }

    return
}


proc argp::CheckValues { caller cmangled checklist } {
    variable Optstore

    #puts "Checking $checklist"

    foreach {key val} $checklist {
	if { [llength $Optstore(allowed,$key,$cmangled)] > 0 } {
	    switch -exact -- $Optstore(type,$key,$cmangled) {
		string {
		    if { [lsearch $Optstore(allowed,$key,$cmangled) $val] < 0} {
			return -code error "$caller, $key $val is not in allowed values: $Optstore(allowed,$key,$cmangled)"
		    }
		}
		double -
		integer {
		    set found 0
		    foreach range $Optstore(allowed,$key,$cmangled) {
			if {[llength $range] == 1} {
			    if { $val == [lindex $range 0] } {
				set found 1
				break
			    }
			} elseif {[llength $range] == 2} {
			    set low [lindex $range 0]
			    set high [lindex $range 1]

			    if { ![string is integer $low] \
				    && [string compare "-" $low] != 0} {
				return -code error "$caller, $key of type $Optstore(type,$key,$cmangled) has a lower value range that is not integer and not ´-´: $range"
			    }
			    if { ![string is integer $high] \
				    && [string compare "+" $high] != 0} {
				return -code error "$caller, $key of type $Optstore(type,$key,$cmangled) has a upper value range that is not integer and not ´+´: $range"
			    }
			    if {[string compare "-" $low] == 0} {
				if { [string compare "+" $high] == 0 \
					|| $val <= $high } {
				    set found 1
				    break
				}
			    }
			    if { $val >= $low } {
				if {[string compare "+" $high] == 0 \
					|| $val <= $high } {
				    set found 1
				    break
				}
			    }
			} else {
			    return -code error "$caller, $key of type $Optstore(type,$key,$cmangled) has an allowed value range containing more than 2 elements: $range"
			}
		    }
		    if { $found == 0 } {
			return -code error "$caller, $key $val is not covered by allowed ranges: $Optstore(allowed,$key,$cmangled)"
		    }
		}
	    }
	}
    }
}
