#######################################################################
# RCS: @(#) $Id: client.tcl,v 5.9 2004/01/09 20:53:58 scholly Exp $
#
# client.tcl: starting rtitcl-based applications as an IPC client
#
# Copyright (C) 2004 Electronic Data Systems Corporation.  All Rights Reserved.
#
#######################################################################

# Generic application startup: set globals, call utlGenericParams

#######################################################################
# Load util.tcl (required functions)

set PVDIR [utlFullPath $PVDIR]
if {[catch {source $PVDIR/lib/util.tcl}]} {
	winmsgbox "ERROR: Cannot load util.tcl" PVEXT
}
if {[utlSysname] == "MSWindows"} {
	if {[catch {source $PVDIR/lib/tcl8.tcl}]} {
		winmsgbox "ERROR: Cannot load tcl8.tcl" PVEXT
	}
}


#######################################################################
# Initialize global variables

set VERSION "(dev)"
set PROGRAM pvext
utlInitGenericGlobals


#######################################################################
# Initialize global constants

# constants for coordinates; 72 dots per inch for PS; HPGL uses PLU == 1/40 mm
set DPI 72
set DPI2PLU [expr {25.4*40/$DPI}]

# different paper sizes in DPIs
set PAPERSIZE(A)  "[expr {8*$DPI+$DPI/2}]  [expr {11*$DPI}]"
set PAPERSIZE(B)  "[expr {11*$DPI}]        [expr {17*$DPI}]"
set PAPERSIZE(C)  "[expr {17*$DPI}]        [expr {22*$DPI}]"
set PAPERSIZE(D)  "[expr {22*$DPI}]        [expr {34*$DPI}]"
set PAPERSIZE(E)  "[expr {34*$DPI}]        [expr {44*$DPI}]"
set PAPERSIZE(J)  "[expr {34*$DPI}]        [expr {92*$DPI}]"
set PAPERSIZE(A4) "[expr {int(21/2.54*$DPI)}]   [expr {int(29.7/2.54*$DPI)}]"
set PAPERSIZE(A3) "[expr {int(29.7/2.54*$DPI)}] [expr {int(42/2.54*$DPI)}]"
set PAPERSIZE(A2) "[expr {int(42/2.54*$DPI)}]   [expr {int(59.4/2.54*$DPI)}]"
set PAPERSIZE(A1) "[expr {int(59.4/2.54*$DPI)}] [expr {int(84.1/2.54*$DPI)}]"
set PAPERSIZE(A0) "[expr {int(84.1/2.54*$DPI)}] [expr {int(129.7/2.54*$DPI)}]"

# ordered by increasing size
set PAPERSIZES {A A4 B A3 C A2 D A1 E A0 J}


#######################################################################
# Define quit procedures

proc appQuit {} {
	global PROGRAM

	if {[info proc ${PROGRAM}Quit] != ""} {
		${PROGRAM}Quit
	} else {
		mainQuit
	}
}


proc mainQuit {} {

	# Clean up
	utlQuitActions
	ipcCloseAll
	utlRMTempFiles
	
	# Give up control to ensure secondary processes can finish (Windows)
	utlSleep 1
}


#######################################################################
# External Converter commands for pvext (e.g., PreView & VisView)
#######################################################################
# Overview of the more important commands:
#  "Open Input-File"    input file name
#  "Open Output-File"   suggested name...can be overridden by 
#                       conversion process.
#  "Set Page n"         sets the page number; can be ignored and acknowledged
#                       if not appropriate. Will always start with '1'.
#  "Select Add-defined" same as that command in pvdwfout; selects 3D rotation
#  "Convert Image"      does the conversion; send after previous commands
#  "Query OutputFile"   must return the actual output file name. Allows the
#                       process to create arbitrary files
#  "Query MorePages"    Return "none" if no more, any other value means more.
#  "Close File X"       X=Input|Output
#  "Set Initial-values" resets all parameters
#  "Query Version"      return version string
#  "Halt"               close all files, delete any temporary files
#
# Notes:
# If suggested output file names aren't used be careful that the files 
# end up in the correct directory. For instance, un-tarring a tar file.
# 
# In the case of a tar file, OPEN could un-tar and capture the file 
# names, SET PAGE could just set an index, CONVERT could do nothing,
# QUERY OUTPUT-FILE could return filename based on index.
#
# Each command is mapped to a procedure with the first 2 keywords as 
# part of the name.  All procedures communicate through the global 
# status variable 'pvext'.  They return an empty string for success 
# and an error message otherwise.
#
#######################################################################


#######################################################################
# Define the input file name

proc pvext:open-input-file args {
	global pvext TMPDIR

	dbg {>pvext:open-input-file: $args}

	# Reset global state
	set pvext {}; utlSet pvext -page 1

	# Deal with path name issues, including resolving any tilde
	set infile [utlFullPath [lindex $args 0]]
	if {[string compare [file tail $infile] \
		[string trimleft [file tail $infile] "~"]] != 0} {
		set newfile [string trimleft [file tail $infile] "~"]
		if {[file exists $TMPDIR/$newfile]} {
			set newfile [utlTempFile [file rootname $newfile] \
			[file ext $infile]]
		} else {
			set newfile $TMPDIR/$newfile
			utlAddTempFile $newfile
		}
		utlCopyFile $infile $newfile
		set infile $newfile
	}

	# Determine file characteristics and save in the "pvext" global
	if { [file readable [utlNativeFileName $infile]]} {

		# informat
		set informat [prpFileType $infile]
		utlSet pvext -informat $informat
		dbg {informat: $informat}

		# Save file characteristics
		utlSet pvext -infile $infile -informat $informat
		set result "Acknowledge Open Input-File [utlNativeFileName $infile]"

	} else {
		set result "Error Open Input-File [utlNativeFileName $infile] \$file missing or not readable\$"
	}

	dbg {<pvext:open-input-file: $result}
	return $result
}


#######################################################################
# Receive suggested output name; accept only directory part of it

proc pvext:open-output-file args {
	global pvext

	dbg {>pvext:open-output-file: $args}

	# Deal with path name issues, including resolving any tilde
	set file [utlFullPath [lindex $args 0]]
	if {[string compare [file tail $file] [string trimleft [file tail $file] "~"]] != 0} {
		set newbase [file rootname [string trimleft [file tail $file] "~"]]
		if {![file exists [file dirname $file]/$newbase]} {
			set file [file dirname $file]/$newbase
		} else {
			set file [utlUniqueFile [file dirname $file]/$newbase [file ext $file]]
		}
	}

	# Set up output directory and suggested file name
	# NOTE: Only the directory has to be writable; the file itself 
	# does not exist yet.
	utlSet pvext -outfile "" -outdir ""
	if {[file isdirectory $file] && [file writable $file]} {
		utlSet pvext -outdir $file
		set result "Acknowledge Open Output-File $file"
	} elseif { [file writable [file dirname $file]]} {
		utlSet pvext -outfile $file
		utlSet pvext -outdir [file dirname $file]
		set result "Acknowledge Open Output-File $file"
	} else {
		set result "Error Open Output-File [utlNativeFileName $file] \$directory not writable\$"
	}
	dbg {outdir: [utlGet pvext -outdir]}
	dbg {outfile: [utlGet pvext -outfile]}

	dbg {<pvext:open-output-file: $result}
	return $result
}


#######################################################################
# Get the format of the given file

proc pvext:query-informat args {
	global pvext

	dbg {>pvext:query-informat: $args}

	set result "Acknowledge Query-Informat \$[utlGet pvext -informat]\$"

	dbg {<pvext:query-informat: $result}
	return $result
}


#######################################################################
# Set a conversion parameter

proc pvext:set-parameter args {
	global pvext

	dbg {>pvext:set-parameter: $args}

	utlFields $args key value

	# Command-line parameters
	if {[string index $key 0] == "-"} {
		utlFields [prpParseCommonParam $args 0 [utlGet pvext -cvp]] cvp ind
		utlSet cvp fatalerror ""
		utlSet pvext -cvp $cvp

	# Config setting
	} else {
		utlSetInternalRCValue "*" $key $value
	}

	set result "Acknowledge Set-Parameter $args"

	dbg {<pvext:set-parameter: $result}
	return $result
}


#######################################################################
# pvext:set-native-formats
# For VisView, this allows setting which formats are considered native,
# which can differ from case-to-case.
#
# The following are the possible uses for this:
# 1) Make sure no more converting is done than is needed.  Loaders may
# exist for some of formats which could reduce the number of 
# conversions pvext needs to do.  Simply specify all formats that have
# loaders as native formats and pvext will not do any extra 
# conversions so that a loader will handle the result.
# 2) Allows dynamically configuring which loaders are in place.  This
# can differ from installation to installation, so setting this info
# will allow controlling what formats pvext should produce as results.
# 3) Allows dynamically configuring which loaders are working properly.
# Apparently, VisView will try to use pvext sometimes when a loader
# fails.  Not specifying the format of the failed loader as a native
# format will cause pvext to take care of the conversion by going to 
# another format.

proc pvext:set-native-formats args {

	dbg {>pvext:set-native-formats: $args}

	prpAddNativeFormats $args

	set result "Acknowledge set-native-formats"

	dbg {>pvext:set-native-formats: $result}
	return $result
}

		
#######################################################################
# Sets the page number; can be ignored and acknowledged if not 
# appropriate. Will always start with '1'.

proc pvext:set-page args {
	global pvext

	dbg {>pvext:set-page: $args}

	set page [lindex $args 0]
	utlSet pvext -page $page
	set result "Acknowledge Set Page $page"

	dbg {<pvext:set-page: $result}
	return $result
}


#######################################################################
# This selects 3D rotation.  It is named so that it is the same as the
# command used in pvdwfout.

proc pvext:select-app-defined args {
	global pvext

	dbg {>pvext:select-app-defined: $args}

	utlSet cvp view [lindex $args 0]x[lindex $args 1]x[lindex $args 2]
	utlSet pvext -cvp $cvp
	set result "Acknowledge Select App-defined $args"

	dbg {<pvext:select-app-defined: $result}
	return $result
}


#######################################################################
# This does the conversion; send after previous commands

proc pvext:convert-image args {
	global pvext TMPDIR ARGS

	dbg {>pvext:convert-image: $args}

	# All pvext conversions occur here (or have happened before)
	if { [utlGet pvext -converted] != "yes"} {

		utlCleanUpTempSubdir
		utlSetupTempSubdir pve

		# Initializations
		utlSet cvp quiet 2
		utlSet cvp interp "file"
		utlSet cvp monocolor [utlDefaultBool Monocolor]
		utlSet cvp stamp [utlDefault stamp]
		utlSet cvp stampformat [utlDefault StampFormat]
		utlSet cvp infofile [utlDefault infofile]
		if {[utlDefaultBool FixPS]} {utlSet cvp fixps 1}
		utlSet cvp linesperpage [utlDefault LinesPerPage]
		utlSet cvp hpglextent [utlDefaultBool HpglExtent Converter]
		if {![utlDefaultBool PatchPS]} {
			utlSet cvp nopspatch 1
		}
		if {![utlDefaultBool CheckBBox]} {
			utlSet cvp nocheckbbox 1
		}
		set params_cvp [utlGet pvext -cvp]
		dbg {initial cvp: $cvp}
		dbg {params_cvp: $params_cvp}
		set cvp [utlMergeKeyedLists cvp params_cvp]
		dbg {new cvp: $cvp}

		# Parse command-line parameters
		dbg {pvext: args=$ARGS}
		for {set ind 0} {$ind < [llength $ARGS]} {incr ind} {
			set arg [lindex $ARGS $ind]
			dbg {pvext: parse $arg}

			# Remove preceding "-"
			set arg [string range $arg 1 end]

			utlFields [prpParseCommonParam $ARGS $ind $cvp] cvp ind
		}
		
		# Error check - make sure all required parameters have been supplied.
		if {[utlGet pvext -infile] == ""} {
			set result "Error Convert Image \$No input file specified\$"
			dbg {<pvext:convert-image: $result}
			return $result
		}
		if {[file size [utlNativeFileName [utlGet pvext -infile]]] <= 0} {
			set result "Error Convert Image \$Empty file\$"
			dbg {<pvext:convert-image: $result}
			return $result
		}

		# Determine the output directory
		set outdir [utlGet pvext -outdir]
		if { $outdir == "" } {
			set outdir [utlGetRootTempDir]
		}
		dbg {output directory: $outdir}

		# Some conversions generate files in the current directory, so change it.
		set curdir [utlPWD]
		cd $outdir

		# Set up formats for the drawengine
		set outformats [prpGetNativeFormats]
		if {$outformats != ""} {
			utlSet cvp outformats $outformats
		}
		dbg {outformats for visdraw: $outformats}

		# input file
		set infile [utlGet pvext -infile]
		set informat [utlGet pvext -informat]

		# output file
		set outfile [utlGet pvext -outfile]

		# outformat
		set outformat [prpFormatInfoEx $informat native]
		if {$outformat == ""} {
			set result "Error Open Input-File [utlNativeFileName $infile] \$unknown file type\$"
			dbg {<pvext:open-input-file: $result}
			return $result
		}

		# Modify outformat, if needed
		# NOTE: This is needed specially here because when the outformat is
		# ps, the code needs a special case for pvext to make it go to a
		# viewer-native format instead.
		if {$informat == "ps" || $outformat == "ps"} {
			set outformat [string tolower [utlDefault PSFormat Converter]]
			utlFields [prpParseSubformat $outformat] outformat subformat
			utlSet cvp subformat $subformat			
			if {![prpIsNativeFormat $outformat] && ![utlIn $outformat {pdf}]} {
				dbg {PSFormat value ($outformat) invalid or not set - reset to tif}
				set outformat tif
			}
		}
		if {$informat == $outformat} {
			# Use default format, since the invoking app clearly does not
			# want the same format back again.
			set outformat [lindex [prpFormatInfoEx $informat outformats] 0]
		}
		dbg {outformat: $outformat}

		# Some conversions are more stable when spawned
		# NOTE: This currently includes all formats using tcom
		if {[utlIn $informat {doc ppt vsd}]} {
			dbg {Setting spawn mode for input format=$informat}
			utlSet cvp spawn 1
		}

		# Convert the file to a viewer-native format
		utlSet cvp skiprename 1
		utlFields [prpConvert $infile $informat $outfile $outformat $cvp] _ outfiles

		# Error check - make sure that there are results
		if { [llength $outfiles] == 0} {
			global UTLLASTERROR
			if {$UTLLASTERROR != ""} {
				set result "Error Convert Image \$$UTLLASTERROR\$"
				dbg {<pvext:convert-image: $result}
				return $result
			} else {
				set result "Error Convert Image \$No files generated\$"
				dbg {<pvext:convert-image: $result}
				return $result
			}
		}

		# Do not delete output files
		set new_outfiles ""
		foreach file $outfiles {
			utlSubTempFile $file
			set dir [file dirname $file]
			if {$dir != $outdir} {
				utlMoveFile $file $outdir
				set file $outdir/[file tail $file]
			}
			lappend new_outfiles $file
		}
		set outfiles $new_outfiles
		dbg {New outfiles: $outfiles}

		# Finishing details
		utlCleanUpTempSubdir
		cd $curdir
		utlSet pvext -converted yes -outfiles $outfiles -numpages [llength $outfiles]

	} elseif {[utlGet pvext -page] > [utlGet pvext -numpages]} {
		set result "Error Convert Image \$Illegal page selected\$"
		dbg {<pvext:convert-image: $result}
		return $result
	}

	# Return results
	set result "Acknowledge Convert Image"
	dbg {<pvext:convert-image: $result}
	return $result
}


#######################################################################
# Return output filenames or viewsets to caller for the current page

proc pvext:query-outputfile args {
	global pvext

	dbg {>pvext:query-outputfile: $args}
	dbg {params: $pvext}

	if {[utlGet pvext -converted] != "yes"} {
		global UTLLASTERROR
		if {$UTLLASTERROR != ""} {
			set result "Error Query Output-File \$$UTLLASTERROR\$"
			dbg {<pvext:query-outputfile: $result}
			return $result
		} else {
			set result "Error Query Output-File \$No conversion done yet\$"
			dbg {<pvext:query-outputfile: $result}
			return $result
		}
	} elseif {[regexp -nocase {extended} $args]} {
		if {[utlGet pvext -page] == 1} {
			set vxlist [pveAssociateFiles [utlGet pvext -outfiles]]
			utlSet pvext -outfiles $vxlist -numpages [llength $vxlist]
		}
		set ind [expr {[utlGet pvext -page] - 1}]
		set outfile [lindex [utlGet pvext -outfiles] $ind]
		utlSubTempFile $outfile
		set result "Acknowledge Output-File \{[utlNativeFileName $outfile]\}"
		dbg {<pvext:query-outputfile: $result}
		return $result
	}
	if {[utlGet pvext -page] <= [utlGet pvext -numpages]} {
		set ind [expr {[utlGet pvext -page] - 1}]
		set outfile [lindex [utlGet pvext -outfiles] $ind]
		utlSubTempFile $outfile
		set result "Acknowledge Output-File [utlNativeFileName $outfile]"
	} else {
		set result "Error Query Output-File \$Illegal page selected\$"
	}

	dbg {<pvext:query-outputfile: $result}
	return $result
}


#######################################################################
# Return "none" if no more, any other value means more.

proc pvext:query-morepages args {
	global pvext

	dbg {>pvext:query-morepages: $args}

	if { [utlGet pvext -converted] != "yes"} {
		set result "Error Query MorePages \$No conversion done yet\$"
	} elseif {[utlGet pvext -page] < [utlGet pvext -numpages]} {
		set result "Acknowledge MorePages yes"
	} else {
		set result "Acknowledge MorePages none"
	}

	dbg {<pvext:query-morepages: $result}
	return $result
}


#######################################################################
# Invoke clean-up details
# param=Input|Output

proc pvext:close-file args {
	global pvext

	dbg {>pvext:close-file: $args}

	# need to use "Close File Input" as trigger for deletion of temp files
	if {[string tolower [lindex $args 0]] == "input"} {
		utlRMTempFiles
	}
	set result "Acknowledge Close File $args"

	dbg {<pvext:close-file: $result}
	return $result
}


#######################################################################
# Resets all parameters

proc pvext:set-initial-values args {
	global pvext

	# Reset the pvext object
	set pvext ""

	# Return the result
	set result "Acknowledge Set Initial-Values"
	dbg {-pvext:set-initial-values: $args -> $result}
	return $result
}


#######################################################################
# Return version string

proc pvext:query-version args {
	global PROGRAM VERSION

	set result "Acknowledge Version \$$PROGRAM\$ \$$VERSION\$"

	dbg {-pvext:set-initial-values: $args -> $result}
	return $result
}


#######################################################################
# Close all files, delete any temporary files

proc pvext:halt- args {
	global PROGRAM VERSION

	dbg {>pvext:halt-: $args}

	utlRMTempFiles
	set result "Acknowledge Halt \$$PROGRAM\$ \$$VERSION\$"

	dbg {<pvext:halt-: $result}
	return $result
}


#######################################################################
# Find out if there are overlays in list of files

proc pveHasOverlays {files} {
	dbg {>pveHasOverlays: $files}

	set result 0
	foreach file $files {
		if {[prpFileType $file] == "ovl"} {
			set result 1
			break
		}
	}

	dbg {<pveHasOverlays: $result}
	return $result
}


#######################################################################
# Associate overlays with drawings and return in following syntax:
# VIEWLIST:    '{' VIEWSET* '}'
# VIEWSET:	   '{' FILE-ASSOC '}'
# FILE-ASSOC:  BASEFILE OVERLAYLIST
# OVERLAYLIST: (SP OVERLAY ('@' PG). )+  /* No @PG implys the overlay is on page 1
# BASEFILE:    ASCII file name
# OVERLAY:	   ASCII file name
# PG:		   decimal number
# SP:		   ASCII space character
#
# Examples:
# {{Basefile.tif ovl1.v01@1 ovl2.v02@1 ovl3.v01@2}}
# {{Basefile.tif Basefile.v01} {OtherFile.mlr Otherfile.v01}}
# {{Basefile.tif ovl.v01 ovl.v02 ovl.v03}}	   /* everything goes to page 1 */

proc pveAssociateFiles {files} {
	dbg {>pveAssociateFiles: $files}

	set result ""
	set idx 0
	foreach file $files {
		if {[prpFileType $file] != "ovl"} {
			set result [concat $result [list \"$file\"]]
			incr idx
		} else {
			set pg [pveReadPageTag $file]
			if {$pg > 1} {
				set file "\"$file\"@$pg"
			} else {
				set file \"$file\"
			}
			if {$idx > 0} {
				set sidx [expr {$idx - 1}]
				set sublist [lindex $result $sidx]
				append sublist ",$file"
				set result [lreplace $result $sidx $sidx $sublist]
			} else {
				set result [concat $result [list $file]]
				incr idx
			}
		}
	}

	dbg {<pveAssociateFiles: $result}
	return $result
}


#######################################################################
# Read tag field of annotation file to keep track of page number

proc pveReadPageTag {fname} {
	dbg {>pveReadPageTag: $fname}

	set pg 1
	if {![catch {set fd [utlOpen $fname r]}]} {
		seek $fd 180
		catch {set tag [read $fd 6]; regexp {^\[([0-9]+)\]} $tag _ pg}
		catch {utlClose $fd}
	}

	dbg {<pveReadPageTag: $pg}
	return $pg
}


#######################################################################
# Client Control Loop
# Read an IPC command and execute it
# This is called for each IPC command received from a client.  On unix, 
# it is called from the unix-only loop in pveStartupand from windows it
# is called from the DLL interpreter function rtitcl_WndSockProc.

proc ipcMessage {ses} {
	global PROGRAM

	dbg {>ipcMessage: $ses}
	
	if {![catch {set cmd [ipc_read $ses]} msg]} {
		dbg {COMMAND: $cmd}
		set cmd [utlUnixFileName $cmd]
		set cmdproc [string tolower "${PROGRAM}:[lindex $cmd 0]-[lindex $cmd 1]"]
		if { [info procs $cmdproc] != "" } {
			if {[catch {set msg [eval "$cmdproc [lrange $cmd 2 end]"]} msg]} {
				set result "Error [lindex $cmd 0] [lindex $cmd 1] \$$msg\$"
			} else {
				set result $msg
			}
			if { [string tolower [lindex $cmd 0]] == "halt"} {
				dbg {<ipcMessage: 1 (ERROR)}
				return 1
			}
		} elseif { [lindex $cmd 0] == "eval" } {
			set result [eval $cmd]
		} else {
			set result "Error $cmd \$unknown command\$"
		}
		ipc_write $ses $result
		dbg {RESULT: $result}
	} elseif {[string match "* illegal session *" $msg]} {
		dbg {<ipcMessage: 1 (ERROR)}
		return 1
	} else {
		dbg {pvext: ipc_read -> $msg}
	}

	dbg {<ipcMessage: 0 (result ok)}
	return 0
}


#######################################################################
# Program start-up code
# Params are: HOST SID1 SID2; Connect to caller and interpret commands

proc pveStartup {} {
	global argv ARGS

	# Process generic parameters like -tmpdir and -pvdir
	# NOTE: utlGenericParams can pick up the PVExt cmdargs setting
	# (such as for setting the log file or debug file).  
	# utlGenericParams will also set TMPDIR correctly for the os.
	set args [concat [lrange $argv 1 end] -quiet]
	set ARGS [utlGenericParams $args]

	# Load any valid tpg from the lib directory
	utlLoadTPG

	# Try to load user TCL code, but do not issue an error message
	# if it fails since it is optional.
	if {[catch {UserTCLCode} msg]} { dbg $msg }

	# Set up format info
	prpSetExternalFormats

	# Event loop for unix
	# NOTE: For windows, the standard event loop in pvext.dll does the 
	# ipc message processing.
	if { [utlSysname] != "MSWindows" } {
		utlFields $argv _ host pid1 pid2

		if {[catch {set ses [ipc_connect_session $host $pid1 $pid2]} msg]} {
			utlError "Unable to connect to calling process"
			utlError $msg

		} else {
			# Keep reading IPC and interpreting commands unless an error
			# is detected.
			while {![ipcMessage $ses]} {}

			catch {ipc_close_connection $ses}
		}
		utlRMTempFiles
		exit
	}

}

pveStartup
