#
#  gpsman --- GPS Manager: a manager for GPS receiver data
#
#  Copyright (c) 2004 Miguel Filgueiras (mig@ncc.up.pt) / Universidade do Porto
#
#    This program is free software; you can redistribute it and/or modify
#      it under the terms of the GNU General Public License as published by
#      the Free Software Foundation; either version 2 of the License, or
#      (at your option) any later version.
#
#      This program 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.  See the
#      GNU General Public License for more details.
#
#      You should have received a copy of the GNU General Public License
#      along with this program.
#
#  File: command_parse.tcl
#  Last change:  6 October 2004
#

# this file is only source-ed in command-line mode

##### parsing command line
###    after source-ing options file
###    before source-ing main.tcl and other files!

##### parse command line

set COMMAND(USAGE) {Usage: gpsman [PREFIXES] COMMAND [ARGS]
  PREFIXES
    -dev DEVICE   device for serial port
    -log          produce a log file of serial communication
    -rec Garmin | Lowrance | Magellan     brand of receiver (only Garmin works)
    -prefs PATH   path to preferences file
    -prot Garmin | NMEA | SText | simul   receiver protocol
  COMMANDS [ARGS]
    is available | connected
    show help | version | formats | protocols | projections | datums | transfs
    haslib gpsmanshp | Img
    getwrite IN-TYPES FORMAT [OUT-PARAMS] [OUT-TYPES] PATH
    readput FORMAT [IN-PARAMS] [IN-TYPES] PATH [OUT-TYPES]  (-rec not allowed!)
    translate IN-FORMAT [IN-PARAMS] [IN-TYPES] IN-PATH \
              OUT-FORMAT [OUT-PARAMS] [OUT-TYPES] OUT-PATH
    getrtimelog PATH [INTERVAL]
    getfix PATH [TIMEOUT]
    project LATD LONGD DATUM PROJECTION [PARAMS]
            (no PREFIXES; DATUM used for projection if PARAMS do not give one)
    georef IMGPATH TRANSF LATD LONGD LATD LONGD [LATD LONGD] DATUM \
           X Y X Y [X Y] PROJECTION [PARAMS]
            (no PREFIXES; DATUM used for projection if PARAMS do not give one)
    source PATH
    exec PATH
       for use in scripts only:
           read FORMAT [PARAMS] [TYPES] PATH
           write FORMAT [PARAMS] [TYPES] PATH
           get TYPES
           put TYPES
    start travel (only with Garmin receiver)
}

set COMMAND(script) 0

proc BadCommandLine {} {
    # parse command line
    # this must be called before source-ing main.tcl and other files!
    # result will be stored on COMMAND array
    # changes in commands accepted must be reflected on $COMMAND(USAGE) above
    # return 1 on error
    global argv COMMAND SERIALPORT GPSREC MAPKNOWNTRANSFS MAPTRANSFNPTS

    if { ! $COMMAND(script) } {
	array set COMMAND {
	    prefsfile ""
	    rec 0
	    log 0
	    prot ""
	}
    }
    set pargs 0
    while 1 {
	switch -- [lindex $argv 0] {
	    -dev {
		set pos 1
		set SERIALPORT [lindex $argv 1]
		set argv [lreplace $argv 0 1]
	    }
	    -log {
		set pos 2
		set COMMAND(log) 1
		set argv [lreplace $argv 0 0]
	    }
	    -rec {
		# not to be used with either  readput  or  -prefs
		#  or after  -prot
		if { $COMMAND(prefsfile) != "" || \
			$COMMAND(prot) != "" } { return 1 }
		set pos 4
		set GPSREC [lindex $argv 1]
		set argv [lreplace $argv 0 1]
		if { [lsearch {Garmin Lowrance Magellan} $GPSREC] == -1 } {
		    return 1
		}
		set COMMAND(rec) 1
	    }
	    -prefs {
		# not to be used with  -rec
		if { $COMMAND(rec) } { return 1 }
		set pos 8
		set COMMAND(prefsfile) [lindex $argv 1]
		set argv [lreplace $argv 0 1]
	    }
	    -prot {
		set pos 16
		set COMMAND(prot) [CmdParseProt [lindex $argv 1]]
		if { $COMMAND(prot) == "" } { return 1 }
		set argv [lreplace $argv 0 1]
	    }
	    default {
		break
	    }
	}
	# no repeated options
	if { $pargs & $pos } { return 1 }
	incr pargs $pos
    }

    set cmd [lindex $argv 0]
    if { ( $pargs != 0 && \
	    ( $COMMAND(script) || \
	      [lsearch -exact "project georef" $cmd] != -1 )) || \
	   ( $cmd == "readput" && $COMMAND(rec) ) } {
	return 1
    }

    set argv [lreplace $argv 0 0]
    switch -- $cmd {
	is {
	    switch -- [lindex $argv 0] {
		available { return "-0" }
		connected {
		    # for the time being this is only supported for
		    #  Garmin receivers
		    if { $GPSREC != "Garmin" } { return 1 }
		    set cmd checkconn
		}
		default { return 1 }
	    }
	}
	show {
	    set showwhat \
		    {help version formats projections protocols datums transfs}
	    set what [lindex $argv 0]
	    if { [lsearch $showwhat $what] == -1 } {
		return 1
	    }
	    set COMMAND(what) $what
	}
	haslib {
	    set what [lindex $argv 0]
	    if { [lsearch "gpsmanshp Img" $what] == -1 } {
		return 1
	    }
	    set COMMAND(what) $what
	}
	getwrite {
	    # IN-TYPES FORMAT [PARAMS] [OUT-TYPES] PATH
	    # for the time being this is only supported for Garmin receivers
	    if { $GPSREC != "Garmin" } { return 1 }
	    foreach "in_ts tail" [CmdParseTypes $argv] {}
	    if { [llength $tail] < 2 || \
		    [set in_ts [CmdRecTypes $in_ts]] == "" } {
		return 1
	    }
	    set fmt [lindex $tail 0]
	    foreach "out_ps out_ts tail" \
		[CmdParseParamsTypes [lreplace $tail 0 0]] {}
	    if { $out_ts == "" } { set out_ts $in_ts }
	    if { [llength $tail] != 1 || \
		    [set COMMAND(path) $tail] == "" || \
		    [CmdFileFmtParamsTypes out $fmt $out_ps $out_ts] == 0 || \
		    [CmdIncompatibleTypes $in_ts $COMMAND(filetypes)] } {
		return 1
	    }
	    set COMMAND(rectypes) $in_ts
	}
	readput {
	    # FORMAT [PARAMS] [IN-TYPES] PATH [OUT-TYPES]
	    # for the time being this is only supported for Garmin receivers
	    if { $GPSREC != "Garmin" } { return 1 }
	    # no  -rec  option
	    if { $COMMAND(rec) } { return 1 }
	    if { [set fmt [lindex $argv 0]] == "" } { return 1 }
	    foreach "in_ps in_ts tail" \
		[CmdParseParamsTypes [lreplace $argv 0 0]] {}
	    if { [set COMMAND(path) [lindex $tail 0]] == "" || \
		    [CmdFileFmtParamsTypes in $fmt $in_ps $in_ts] == 0 } {
		return 1
	    }
	    foreach "out_ts tail" [CmdParseTypes [lreplace $tail 0 0]] {}
	    if { $out_ts == "" } { set out_ts $COMMAND(filetypes) }
	    if { $tail != "" || \
		    [set out_ts [CmdRecTypes $out_ts]] == "" || \
		    [CmdIncompatibleTypes $COMMAND(filetypes) $out_ts] } {
		return 1
	    }
	    set COMMAND(rectypes) $out_ts
	}
	translate {
	    # IN-FORMAT [IN-PARAMS] [IN-TYPES] IN-PATH OUT-FORMAT \
            #    [OUT-PARAMS] [OUT-TYPES] OUT-PATH
	    if { [llength $argv] < 4 } { return 1 }
	    if { [set infmt [lindex $argv 0]] == "" } { return 1 }
	    foreach "in_ps in_ts tail" \
		[CmdParseParamsTypes [lreplace $argv 0 0]] {}
	    if { [set COMMAND(inpath) [lindex $tail 0]] == "" || \
		    [CmdFileFmtParamsTypes in $infmt $in_ps $in_ts] == 0 || \
		    [set outfmt [lindex $tail 1]] == "" } {
		return 1
	    }
	    set COMMAND(infmt) $COMMAND(format)
	    foreach "out_ps out_ts tail" \
		[CmdParseParamsTypes [lreplace $tail 0 1]] {}
	    if { $out_ts == "" } { set out_ts $COMMAND(filetypes) }
	    set COMMAND(intypes) $COMMAND(filetypes)
	    if { [llength $tail] != 1 || \
		  [set COMMAND(path) $tail] == "" || \
		  [CmdFileFmtParamsTypes out $outfmt $out_ps $out_ts] == 0 || \
		  [CmdIncompatibleTypes $COMMAND(intypes) \
					  $COMMAND(filetypes)] } {
	        return 1
	    }
	}
	getrtimelog {
	    # PATH [INTERVAL]
	    # for the time being this is only supported for Garmin receivers
	    if { $GPSREC != "Garmin" } { return 1 }
	    if { [set COMMAND(path) [lindex $argv 0]] == "" || \
		     [set n [llength $argv]] > 2 || \
		     [set COMMAND(interv) \
			  [CmdParseInteger [lindex $argv 1] 2]] < 0 } {
		return 1
	    }
	}
	getfix {
	    # PATH [TIMEOUT]
	    # for the time being this is only supported for Garmin receivers
	    if { $GPSREC != "Garmin" } { return 1 }
	    if { [set COMMAND(path) [lindex $argv 0]] == "" || \
		     [set n [llength $argv]] > 2 || \
		     [set COMMAND(timeout) \
			  [CmdParseInteger [lindex $argv 1] 0]] < 0 } {
		return 1
	    }
	}
	start {
	    # travel [INTERVAL]
	    if { [lindex $argv 0] != "travel" || $GPSREC != "Garmin" || \
		     [set n [llength $argv]] > 2 || \
		     [set COMMAND(interv) \
			  [CmdParseInteger [lindex $argv 1] 2]] < 0 } {
		return 1
	    }
	    set cmd starttravel
	}
	project {
	    # LATD LONGD DATUM PROJECTION [PARAMS]
	    # making only a preliminary check on the correction of the
	    #  arguments as projections.tcl was not yet loaded
	    if { [llength $argv] < 4 } { return 1 }
	    foreach "latd longd" $argv { break }
	    foreach p "latd longd" {
		set x [set $p]
		if { ! [regexp {^-?[0-9]+(\.[0-9]+)?$} $x] } { return 1 }
		set COMMAND($p) $x
	    }
	    set i 1
	    foreach p "ptsdatum proj" {
		if { [set COMMAND($p) [lindex $argv [incr i]]] == "" } {
		    return 1
		}
	    }
	    foreach "params xx xxx" \
		    [CmdParseParamsTypes [lrange $argv [incr i] end]] { break }
	    if { $xx != "" || $xxx != "" } { return 1 }
	    set COMMAND(params) $params	    
	}
	georef {
	    # IMGPATH TRANSF LATD LONGD LATD LONGD [LATD LONGD] DATUM
	    #   X Y X Y [X Y] PROJ [PARAMS]
	    # making only a preliminary check on the correction of the
	    #  arguments as projections.tcl was not yet loaded
	    set COMMAND(image) [lindex $argv 0]
	    set COMMAND(transf) [set tr [lindex $argv 1]]
	    if { [llength $argv] < 12 || \
		    [lsearch -exact $MAPKNOWNTRANSFS $tr] == -1 } { return 1 }
	    if { $tr == "NoRot" } {
		# not using 3 points as when doing this by hand
		set npts 2
	    } else { set npts $MAPTRANSFNPTS($tr) }
	    set COMMAND(npts) $npts
	    set i 1
	    for { set p 0 } { $p < $npts } { incr p } {
		foreach pp "lat long" {
		    set x [lindex $argv [incr i]]
		    if { ! [regexp {^-?[0-9]+(\.[0-9]+)?$} $x] } { return 1 }
		    set COMMAND($p,$pp) $x
		}
	    }
	    if { [set COMMAND(ptsdatum) [lindex $argv [incr i]]] == "" } {
		return 1
	    }
	    for { set p 0 } { $p < $npts } { incr p } {
		foreach pp "x y" {
		    set x [lindex $argv [incr i]]
		    if { ! [regexp {^-?[0-9]+(\.[0-9]+)?$} $x] } { return 1 }
		    set COMMAND($p,$pp) $x
		}
	    }
	    if { [set COMMAND(proj) [lindex $argv [incr i]]] == "" } {
		return 1
	    }
	    foreach "params xx xxx" \
		    [CmdParseParamsTypes [lrange $argv [incr i] end]] { break }
	    if { $xx != "" || $xxx != "" } { return 1 }
	    set COMMAND(params) $params
	}
	source {
	    # PATH
	    if { [llength $argv] != 1 } { return 1 }
	    set COMMAND(path) $argv
	}
	exec {
	    # PATH
	    if { [llength $argv] != 1 } { return 1 }
	    set COMMAND(path) $argv
	}
	read {
	    # FORMAT [PARAMS] [TYPES] PATH
	    # for use in scripts only
	    if { ! $COMMAND(script) || \
		    [set fmt [lindex $argv 0]] == "" } { return 1 }
	    foreach "in_ps in_ts tail" \
		[CmdParseParamsTypes [lreplace $argv 0 0]] {}
	    if { [set COMMAND(path) [lindex $tail 0]] == "" || \
		    [CmdFileFmtParamsTypes in $fmt $in_ps $in_ts] == 0 } {
		return 1
	    }
	}
	write {
	    # FORMAT [PARAMS] [TYPES] PATH
	    # for use in scripts only
	    if { ! $COMMAND(script) } { return 1 }
	    set fmt [lindex $argv 0]
	    foreach "out_ps out_ts tail" \
		    [CmdParseParamsTypes [lreplace $argv 0 0]] {}
	    if { [llength $tail] != 1 || \
		    [set COMMAND(path) $tail] == "" || \
		    [CmdFileFmtParamsTypes out $fmt $out_ps $out_ts] == 0 } {
		return 1
	    }
	}
	get {
	    # TYPES
	    # for use in scripts only
	    # for the time being this is only supported for Garmin receivers
	    if { ! $COMMAND(script) || $GPSREC != "Garmin" } { return 1 }
	    foreach "in_ts tail" [CmdParseTypes $argv] {}
	    if { $tail != "" } { return 1 }
	    set COMMAND(rectypes) $in_ts
	}
	put {
	    # TYPES
	    # for use in scripts only
	    # for the time being this is only supported for Garmin receivers
	    if { ! $COMMAND(script) || $GPSREC != "Garmin" } { return 1 }
	    # no  -rec  option
	    if { $COMMAND(rec) } { return 1 }
	    foreach "out_ts tail" [CmdParseTypes $argv] {}
	    if { $tail != "" } { return 1 }
	    set COMMAND(rectypes) $out_ts
	}
	default { return 1 }
    }
    set COMMAND(name) $cmd
    return 0
}

proc CmdParseProt {prot} {
    # translate external form of protocol
    # return "" on error
    global RECPROTS GPSREC

    if { [catch {set p $RECPROTS($GPSREC,$prot)}] } { return "" }
    return $p
}

proc CmdParseTypes {lst} {
    # find element(s) at the beginning of $lst corresponding to item types:
    #   either a single "all", or a subset of {WP, RT, TR, LN}
    #  (note that $TYPES is not defined yet!)
    # return pair with list of those elements and rest of list

    set pstslst [CmdParseParamsTypes $lst]
    if { [lindex $pstslst 0] != "" } { return [list "" $lst] }
    return [lreplace $pstslst 0 0]
}

proc CmdParseParamsTypes {lst} {
    # find element(s) at the beginning of $lst corresponding to
    #  parameters in the form NAME=VALUE, followed by item types:
    #  either a single "all", or a subset of {WP, RT, TR, LN}
    #  (note that $TYPES is not defined yet!)
    # return triple with list of parameters (with alternating lhs and rhs),
    #  list of types and rest of list

    set ps ""
    while { [regexp {^(.+)=(.+)$} [lindex $lst 0] x lhs rhs] } {
	lappend ps $lhs $rhs
	set lst [lreplace $lst 0 0]
    }
    if { [lindex $lst 0] == "all" } {
	set ts all ; set lst [lreplace $lst 0 0]
    } else {
	set ts ""
	while { [lsearch -exact "WP RT TR LN" [lindex $lst 0]] != -1 } {
	    lappend ts [lindex $lst 0] ; set lst [lreplace $lst 0 0]
	}
    }
    return [list $ps $ts $lst]
}

proc CmdParseInteger {val default} {
    # parse non-negative integer in $val or if it is "" return $default
    # return -1 on error

    if { $val == "" } { return $default }
    if { ! [regexp {^[1-9][0-9]*$} $val] } {
	return -1
    }
    return $val
}

proc CmdRecTypes {lst} {
    # interpret list of item types to be get/put from/into receiver
    #  $lst may be either "all", or non-empty subset of $TYPES-{LN,GR}
    #  (note that $TYPES is not defined yet!)
    # return "" on error, otherwise list of types as non-empty subset
    #  of $TYPES-{LN,GR}

    if { $lst == "all" } { return "WP RT TR" }
    return $lst
}

proc CmdFileFmtParamsTypes {how fmt pars ts} {
    # check that $fmt is a valid file format for I/O operation on file
    #  with given parameters (values not checked here) and item types
    # if $fmt=="gpsman" it is taken to be "GPSMan"
    #  $how in {in, out}
    #  $pars is list with each parameter name followed by corresponding value
    #  $ts is either "all", or subset of $TYPES-{GR}
    # set COMMAND(filetypes), COMMAND(format), COMMAND($how,params)
    #  the latter in the same format as $pars but with all parameters in
    #  the order given by $FILEFORMAT($fmt,$how,params)
    # return 0 on error
    global FILEFORMAT COMMAND

    # compatibility with old versions
    if { $fmt == "gpsman" } {
	set fmt GPSMan
    } elseif { [regexp {^Shapefile_([23])D$} $fmt x dim] } {
	set fmt Shapefile
	lappend pars dim $dim
    }
    # check mode and dependencies
    if { [catch {set mode $FILEFORMAT($fmt,mode)}] || \
	    ! [regexp $how $mode] || \
	    ! ( [catch {set depok $FILEFORMAT($fmt,depends)}] || \
	        [eval $depok] ) } {
	puts stderr "Bad/unvailable format: $fmt"
	return 0
    }
    # check parameter names (values only checked before execution)
    #  set default values for missing parameters, return error if
    #  there is no default for a missing parameter
    if { ! [catch {set pnames $FILEFORMAT($fmt,$how,params)}] } {
	if { [catch {set pds $FILEFORMAT($fmt,$how,pdefts)}] } {
	    set pds ""
	}
	set ps ""
	foreach n $pnames dv $pds {
	    if { [set ix [lsearch -exact $pars $n]] == -1 || \
		     $ix & 1 != 0 } {
		if { $dv == "ERROR" } {
		    puts stderr "Missing value for $n"
		    return 0
		}
		if { [regexp {^global:(.+)$} $dv m dvv] } {
		    global $dvv
		    set dv [set $dvv]
		}
		lappend ps $n $dv
	    } else {
		lappend ps $n [lindex $pars [expr $ix+1]]
		set pars [lreplace $pars $ix [expr $ix+1]]
	    }
	}
	if { $pars != "" } {
	    puts stderr "Unknown parameters: $pars"
	    return 0
	}
	set pars $ps
    } elseif { $pars != "" } {
	puts stderr "No parameters allowed for this format"
	return 0
    }
    # check types
    set fts $FILEFORMAT($fmt,types)
    switch $FILEFORMAT($fmt,filetype) {
	unique {
	    if { $ts != "" && $ts != "all" && $ts != $fts } {
		return 0
	    }
	}
	single {
	    if { $ts == "all" || [llength $ts] != 1 || \
		    [lsearch -exact $fts $ts] == -1 } { return 0 }
	    set fts $ts
	}
	data {
	    if { $ts != "" && $ts != "all" } {
		set fs ""
		foreach f $ts {
		    if { [lsearch -exact $fts $f] == -1 } { return 0 }
		    lappend fs $f
		}
		set fts $fs
	    }
	}
    }
    set COMMAND(format) $fmt
    set COMMAND($how,params) $pars
    set COMMAND(filetypes) $fts
    return 1
}

proc CmdIncompatibleTypes {in_ts out_ts} {
    # check compatibility of input and output types
    # the only possible "conversion" is from RT to WP
    # return 1 on error

    if { $in_ts == $out_ts } { return 0 }
    foreach o $out_ts {
	if { [lsearch $in_ts $o] == -1 && \
		( $o != "WP" || [lsearch $in_ts RT] == -1 ) } {
	    return 1
	}
    }
    return 0
}