#!/usr/bin/tclsh8.4
# -*- tcl -*-

# Copyright 2016,2020 Ian Jackson
# SPDX-License-Identifier: GPL-3.0-or-later
# There is NO WARRANTY.

# This is unfinished attempt at a program to multiplex multiple
# clients all wanting to speak to a /dev/ttyACM GSM modem / GPS.

set expected_devs 3

proc debug {m} { puts "DEBUG $m" }

proc log {m} { puts "LOG $m" }

proc experror {m} {
    error $m {} EXPECTED
}

proc find-devices {} {
    global errorCode errorInfo devices expected_devs
    set base /sys/class/tty
    foreach candidate [glob -nocomplain -directory $base -tails ttyACM*] {
	debug "candidate $candidate"
	if {[catch { file link $base/$candidate/device } ltarget]} {
	    debug " readlink failed [lrange $errorCode 0 1]"
	    switch -glob $errorCode {
		{POSIX EINVAL *} continue
		{POSIX ENOENT *} continue
		default { error "$ltarget \[$errorCode] $errorInfo $errorCode" }
	    }
	}
	if {![regexp {^(.*)\.(\d+)$} $ltarget dummy dbase interf]} {
	    debug " readlink bad target $ltarget"
	    continue
	}
	debug " approved $dbase $interf $candidate"
	lappend devs($dbase) [list $interf $candidate]
    }
    set howmany [array size devs]
    if {!$howmany} {
	experror "no appropriate device(s) found"
    }
    if {$howmany > 1} {
	experror "several appropriate device(s) found [array names $devs]"
    }
    set devices {}
    foreach dev [lsort -index 0 -integer $devs([lindex [array names devs] 0])] {
	lappend devices [lindex $dev 1]
    }
    if {[llength $devices] != $expected_devs} {
	experror "wrong # devices ($devices), expected $expected"
    }
}

proc create-dev-nodes {} {
    global devices expected_devs
    set ourdevs /dev/atmux
    set ttyat ttyAT
    for {set i 0} {$i < $expected_devs-1} {incr i} {
	set new $ttyAT$i
	file link -symbolic ../$device $ourdevs/.new.$new
	file rename -force $ourdevs/.new.$new $ourdevs/$new
	set wanted($new) 1
    }
    foreach candidate [glob -nocomplain -directory $ourdevs -tails ttyAT*] {
	if {![info exists wanted($candidate)]} {
	    file remove $ourdevs/$candidate
	}
    }
}

proc reopen-our-device {} {
    global devices dchan
    set dchan [open /dev/[lindex $devices 1] r+]
    fconfigure $dchan -blocking no -buffering line -translation {crlf cr}
    read $dchan; # flush input
    puts $dchan ATE0
    flush $dchan
    after 250
    set result [read $dchan]
    if {![regexp -line {^OK$} $result]} { experror "got [logquote $result]" }
    fileevent $dchan readable chan-readable $dchan dchan "modem device"
}

proc devices {} {
    find-devices
    create-dev-nodes
    reopen-our-device
}

proc dchan-failure {dummy} {
    global dchan
    if {[info exists dchan]} {
	catch { close $dchan }
	catch { unset dchan }
    }
}

proc try-open-our-device {} {
    global devices
    if {[catch {
	reopen-our-device
    } emsg]} {
	devfailure $emsg
	return
    }
    sendout-async *TTYATMUX "*TTYATMUXDEVS [join $devices ,]"
    sendout-async *TTYATMUX "*TTYATMUXOPEN"
}

proc trap-log {what body var} {
    global errorCode errorInfo
    upvar 1 $var result
    set rc [catch {
	uplevel 1 $body
    } result]
    switch -exact $rc {
	1 {
	    switch -glob $errorCode {
		{POSIX *} - EXPECTED { log "$what: $result" }
		default {
		    log "unexpected: $what: $result"
		    foreach l [split $errorInfo "\n"] { log "  $l" }
		}
	    }
	    return 1
	}
	0 {
	    return 0
	}
	default {
	    return -code $rc -errorinfo $errorInfo \
		-errorcode $errorCode $result
	}
    }
}

proc chan-readable {chan how what args} {
    while 1 {
	if {[trap-log "$what failure" {
	    gets $chan l
	} r]} {
	    $how-failure $chan
	    return
	}
	if {$r<0} {
	    if {![eof $chan]} return
	    log "device eof"
	    $how-failure $chan
	    return
	}
	trap-log "error processing $what data" {
	    $how-line $l
	} dummy
    }
}

proc async-notif fixme this has wrong arguments

proc async-notif-or-resp-fixed {asid l} {
    global current_command_asid
    if {![string compare $asid $current_command_asid]} {
	sync-reply $l
    } else {
	async-notif $asid $l
    }
}

proc async-notif-creg {asid l} {
    set ll [llength [split $l ,]]
    switch -exact $ll {
	4 { sync-reply $l }
	3 { 
	    async-notif {+CREG} 2 2 $l
	    async-notif {+CREG} 1 1 [lindex [split $l ,] 0]
	}
	2 { sync-reply $l }
	1 { 
	    async-notif {+CREG} 1 2 $l
	}
	default {
	    bad-data $l "async-notif-creg $ll"
	}
    }
}

proc async-control-max0 {c l allows} {
    async-control-core $c $l $allows {
	set wanted 0
    } {
	set tw 0
	manyset $ca($c) tw
	if {$tw} { set wanted 1 }
    } {
	set send $wanted
	foreach allow $allows { lappend $send [lindex $allow 0] }
	sync-subcommand $c "$cmd=[join $send ,]" async-updated-ok $c
    }
}

proc async-control-cmer {c l allows} {
    async-control-core $c $l $allows {
	set send 0,0,0,0
    } {
	set mode 0; set ind 0
	manyset $ca($c) mode keyp disp ind bfr
	if {$mode==3 && $ind} { set send 3,0,0,1 }
    } {
	sync-subcommand $c "$cmd=$send" async-updated-ok $c
    }
}

proc async-updated-ok

proc async-control-core {c l allows ubody_init ubody_perclient ubody_finish} {
    global clients
    uplevel 1 cmd cmd
    if {[regexp {^(AT[^=?])\?$} $l dummy cmd]} {
	sync-subcommand $c $l async-massage-result-subs $c $cmd
    } elseif {[regexp {^(AT[^=?])=\?$} $l dummy cmd]} {
	sync-subcommand $c $l async-massage-result-support $c $cmd $allows
    } elseif {[regexp {^(AT[^=?])=([0-9,]+)$} $l dummy cmd values]} {
	set values [split $values ,]
	if {[llength $values] > [llength $allows]} {
	    bad-command "too many values"
	    return
	}
	while {[llength $values] < [llength $allows]} {
	    lappend values 0
	}
	foreach val $values allow $allows {
	    if {[lsearch -exact $allow $val]<0} {
		bad-command "$val not in allowed $allow ($allows)"
		return
	    }
	}
	uplevel 1 [list upvar #0 client_async/$cmd ca]
	upvar #0 client_async/$cmd ca
	set ca($c) $values
	uplevel 1 $ubody_init
	upvar 1 c uc
	foreach uc [array names clients] {
	    uplevel 1 $ubody_perclient
	}
	uplevel 1 $ubody_finish
    } else {
	bad-command "unknown async control syntax"
    }
}

proc set-client-echo {c yn} {
    global client_echo
    set client_echo($c) 0
    client-command-complete $c OK
}

proc simple-command {c l} {
    sync-subcommand $c $l simple-command-complete
}

proc client-command-complete

proc process-client-command {c nl} {
    switch -regexp $l {
	{^AT\+CREG\b} { async-control-max0 $c $l {{0 1 2}} }
	{^AT\+CGREG\b} { async-control-max0 $c $l {{0 1 2}} }
	{^AT\*ERINFO\b} { async-control-max0 $c $l {{0 1}} }
	{^AT\+CGEREP\b} { async-control-max0 $c $l {{0 1 2} 0} }
	{^AT\+CMER\b} { async-control-cmer $c $l {{0 3} 0 0 {0 1} 0} }
	{^ATE0$} { set-client-echo $c 0 }
	{^ATE1$} { set-client-echo $c 1 }
	{^AT\+CFUN\b} { simple-command $c $l }
	default { bad-command "unknown command" }
    }
}

proc dchan-line {l} {
    global cclient
    switch -regexp $l {
	{\+CREG:}   { async-notif-creg             +CREG     $l }
	{\+CGREG:}  { async-notif-creg             +CGREG    $l }
	{\*ERINFO:} { async-notif-or-resp-fixed    *ERINFO   $l }
	{\+CGEV:}   { async-notif                  +CGEREP   $l }
	{\+CIEV:}   { async-notif                  +CMER     $l }
	default     { syync-reply $l }
    }
}

proc cchan-line {c l} {
    lappend queue [list $c $l]
    check-busy
}

proc sync-subcommand {c l args} {
    global busy dchan
    if {[info exists busy]} { error "already busy $busy; want $c $l $args" }
    if {[trap-log "write device" { puts $dchan $l } dummy]} {
	

proc sync-reply {l} {
    global busy
    if {![info exists busy]} {
	bad-data $l "unexpected sync reply"
	return
    }
    eval $

proc check-busy {} {
    global busy queue
    while {![info exists busy] && [llength $queue]} {
	manyset [lindex $queue 0] c l
	set queue [lrange $queue 1 end]
	if {[trap-log "process for $c [logquote $l]"] {
	    process-client-command $c $l
	} dummy]} {
	    client-command-complete $c ERROR
	}
    }
}

proc client-command-complete {c l} {
    if {[trap-log "write to $c" { puts $c $l } dummy]} {
	client-failure $c
    }
}

proc cchan-readable {

proc sendout-async
proc logquote


try-open-our-device
