# A simplified version of Tcl expect using a pseudo-tty pair
# This could be turned into a standard module, but for now
# it is just used in the test suite

# Example usage:
#
# set p [expect::spawn {cmd pipeline}]
#
# $p timeout 5
# $p send "a command\r"
# $p expect {
#	ab.*c {
#		script
#	}
#	d[a-z] {
#		script
#	}
#   EOF { ... }
#   TIMEOUT { ... }
# }
#
# [$p before] returns data before the match
# [$p after] returns data that matches the pattern
# [$p buf] returns any data after the match that has been read
# $p close
#
# $p tty ?...?
# $p kill ?SIGNAL?
if {![exists -command namespace]} {
	# Just enough to support [namespace current]
	proc namespace {args} {
		return ""
	}
}

proc expect::spawn {cmd} {
	lassign [socket pty] m s
	# By default, turn off echo so that we can see just the output, not the input
	$m tty echo 0
	$m buffering none
	$m ndelay 1
	try {
		lappend cmd <@$s >@$s &
		set pids [exec {*}$cmd]
		$s close
		# Create a unique global variable for vwait
		set donevar ::[ref "" expect]
		set $donevar 0
		set matchinfo {
			buf {}
		}

		return [namespace current]::[lambda {cmd args} {m pids {timeout 30} donevar matchinfo {debug 0}} {
			#puts "expect::spawn cmd=$cmd, matchinfo=$matchinfo"
			# Find our own name
			set self [lindex [info level 0] 0]

			switch -exact -- $cmd {
				dputs {
					if {$debug} {
						set escapes {13 \\r 10 \\n 9 \\t 92 \\\\}
						lassign $args str
						# convert non-printable chars to printable
						set formatted {}
						binary scan $str cu* chars
						foreach c $chars {
							if {[exists escapes($c)]} {
								append formatted $escapes($c)
							} elseif {$c < 32} {
								append formatted [format \\x%02x $c]
							} elseif {$c > 127} {
								append formatted [format \\u%04x $c]
							} else {
								append formatted [format %c $c]
							}
						}
						puts $formatted
					}
				}
				kill {
					# kill the process with the given signal
					foreach i $pids {
						kill {*}$args $i
					}
				}
				pid {
					# return the process pids
					return $pids
				}
				getfd - tty {
					# pass through to the pty file descriptor
					tailcall $m $cmd {*}$args
				}
				close {
					# close the file descriptor, wait for the child process to complete
					# and return the result
					$m close
					set retopts {}
					foreach p $pids {
						lassign [wait $p] status - rc
						if {$status eq "CHILDSTATUS"} {
							# Don't treat a non-zero return code as fatal here
							if {[llength $retopts] <= 1} {
								set retopts $rc
							}
							continue
						} else {
							set msg "child killed: received signal"
						}
						set retopts [list -code error -errorcode [list $status $p $rc] $msg]
					}
					rename $self ""

					return {*}$retopts
				}
				timeout - debug {
					# set or return the variable
					if {[llength $args]} {
						set $cmd [lindex $args 0]
					} else {
						return [set $cmd]
					}
				}
				send {
					$self dputs ">>> [lindex $args 0]"
					# send to the process
					$m puts -nonewline [lindex $args 0]
					$m flush
				}
				before - after - buf {
					# return the before, after and remaining data
					return $matchinfo($cmd)
				}
				handle {
					# Internal use only
					set args [lassign $args type]
					switch -- $type {
						timeout {
							$self dputs "\[TIMEOUT patterns=$matchinfo(patterns) buf=$matchinfo(buf)\]"
							# a timeout occurred
							set matchinfo(before) $matchinfo(buf)
							set matchinfo(buf) {}
							set matchinfo(matched_pattern) TIMEOUT
							incr $donevar
							return 1
						}
						eof {
							$self dputs "\[EOF\]"
							# EOF was reached
							set matchinfo(before) $matchinfo(buf)
							set matchinfo(buf) {}
							set matchinfo(matched_pattern) EOF
							incr $donevar
							return 1
						}
						data {
							# data was received
							lassign $args data
							$self dputs "<<< $data"
							append matchinfo(buf) $data
							foreach pattern $matchinfo(patterns) {
								set result [regexp -inline -indices $pattern $matchinfo(buf)]
								if {[llength $result]} {
									$self dputs "MATCH=\[$pattern\]"
									lassign [lindex $result 0] start end
									set matchinfo(before) [string range $matchinfo(buf) 0 $start-1]
									set matchinfo(after) [string range $matchinfo(buf) $start $end]
									set matchinfo(buf) [string range $matchinfo(buf) $end+1 end]

									# Got a match, stop
									set matchinfo(matched_pattern) $pattern
									incr $donevar
									return 1
								}
							}
						}
					}
					return 0
				}
				expect {
					# Takes a list of regex-pattern, script, ... where the last script can be missing
					if {[llength $args] % 2 == 1} {
						lappend args {}
					}

					# Stash all the state in the matchinfo dict
					# Keep matchinfo(buf)
					array set matchinfo {
						before {}
						after {}
						patterns {}
						matched_pattern {}
					}

					foreach {pattern script} $args {
						lappend matchinfo(patterns) $pattern
					}

					# Handle the case where there is buffered data
					# that matches the pattern
					if {[$self handle data {}] == 0} {
						$m readable [namespace current]::[lambda {} {m self} {
							try {
								set buf [$m read]
								if {$buf eq ""} {
									$self handle eof "EOF"
								} else {
									$self handle data $buf
								}
							} on error msg {
								$self handle eof $msg
							}
						}]
						set matchinfo(afterid) [after $($timeout * 1e3) [list $self handle timeout]]

						vwait $donevar

						after cancel $matchinfo(afterid)
					}

					# Now invoke the matching script
					if {[dict exists $args $matchinfo(matched_pattern)]} {
						uplevel 1 [dict get $args $matchinfo(matched_pattern)]
					}
					# And return the data that matched the pattern
					# (is $matchinfo(before) more generally useful?)
					return $matchinfo(after)
				}
			}
		}]
	} on error {error opts} {
		catch {$m close}
		catch {$s close}
		return -code error $error
	}
}
