#
# GUI support for managing CORE node services.
#

#
# Popup a services configuration dialog box. Similar to popupCapabilityConfig
# but customized for configuring node services. This dialog has two uses:
# (1) selecting the default services for a node type (when session != ""), and
# (2) selecting/customizing services for a certain node
#
proc popupServicesConfig { channel node types values captions possible_values groups session } {
    global plugin_img_edit
    global g_service_ctls
    global g_sent_nodelink_definitions
    set wi .popupServicesConfig
    catch {destroy $wi}
    toplevel $wi

    # instead of using vals, the activated services are stored in this list
    set activated ""
    if { $session != "" } {
	global g_node_type_services_hint
	if { ![info exists g_node_type_services_hint] } {
	    set g_node_type_services_hint "router"
	}
	set title "Default services"
	set toptitle "Default services for node type $g_node_type_services_hint"
	set activated [getNodeTypeServices $g_node_type_services_hint]
	set parent .nodesConfig
    } else {
        set title "Node [getNodeName $node] ($node) services"
	set toptitle $title
	set activated [getNodeServices $node true]
	set parent .popup
    }
    if { ![winfo exists $parent] } {
	set parent "."
    }
    wm title $wi $title
    wm transient $wi $parent 

    label $wi.top -text $toptitle 
    pack $wi.top -side top -padx 4 -pady 4

    frame $wi.vals -relief raised -borderwidth 1

    set g_sent_nodelink_definitions 0 ;# only send node/link defs once
    set g_service_ctls {} ;# list of checkboxes

    set n 0
    set gn 0
    set lastgn -1
    foreach type $types {
	# group values into frames based on groups TLV
	set groupinfo [popupCapabilityConfigGroup $groups [expr {$n + 1}]]
	set gn [lindex $groupinfo 0]
	set groupcaption [lindex $groupinfo 1]
	if { $lastgn != $gn } {
	    labelframe $wi.vals.$gn -text $groupcaption \
	    		-borderwidth 1 -padx 4 -pady 4
	}
	frame $wi.vals.$gn.item$n
	if {$type != 11} { ;# boolean value
	    puts "warning: skipping service config [lindex $captions $n]"
	    incr n
	    continue
	}
	set servicename [lindex $captions $n]
	global $wi.vals.$gn.item$n.entval
	checkbutton $wi.vals.$gn.item$n.ent -width 12 -wraplength 90 \
	    -variable $wi.vals.$gn.item$n.entval -text $servicename \
	    -offrelief flat -indicatoron false -overrelief raised
	lappend g_service_ctls $wi.vals.$gn.item$n.ent

	if { [lsearch -exact $activated [lindex $captions $n]] == -1 } {
	    set value 0 ;# not in the activated list
	} else {
	    set value 1
	}
	set $wi.vals.$gn.item$n.entval $value
	if { $session == "" } {
	    set needcustom false
	    if { $n < [llength $possible_values] } {
		if { [lindex $possible_values $n] == 1 } { set needcustom true }
	    }
	    set btn $wi.vals.$gn.item$n.custom
	    button $btn -image $plugin_img_edit \
		    -command "customizeService $wi $node $servicename $btn"
	    setCustomButtonColor $btn $node $servicename $needcustom
	    pack $wi.vals.$gn.item$n.custom -side right -padx 4 -pady 4
	    # this causes the button for services that require customization to 
	    # turn yellow when the service is selected
	    $wi.vals.$gn.item$n.ent configure -command \
		"setCustomButtonColor $btn $node $servicename $needcustom"
	}
	pack $wi.vals.$gn.item$n.ent -side right -padx 4 -pady 4
	pack $wi.vals.$gn.item$n -side top -anchor e
	if { $lastgn != $gn } {
	    pack $wi.vals.$gn -side left -anchor n -fill both
	    set lastgn $gn
	}
	incr n
    }; # end foreach
    pack $wi.vals.$gn -side left -anchor n -fill both
    pack $wi.vals -side top -padx 4 -pady 4

    # Apply / Cancel buttons
    set apply_cmd "popupServicesConfigApply $wi $channel $node {$session}"
    set cancel_cmd "destroy $wi"
    frame $wi.btn
    button $wi.btn.apply -text "Apply" -command $apply_cmd
    button $wi.btn.def -text "Defaults" -command \
	"popupServicesConfigDefaults $wi $node {$types} {$captions} {$groups}"
    button $wi.btn.cancel -text "Cancel" -command $cancel_cmd
    set buttons [list $wi.btn.apply $wi.btn.cancel]
    if { $session == "" } {
	set buttons [linsert $buttons 1 $wi.btn.def]
    }
    foreach b $buttons { pack $b -side left -padx 4 -pady 4 }
    pack $wi.btn -side bottom
    bind $wi <Key-Return> $apply_cmd
    bind $wi <Key-Escape> $cancel_cmd
}

#
# Save the selection of activated services with the node or in the g_node_types
# array when configuring node type defaults.
#
proc popupServicesConfigApply { wi channel node session } {
    set vals [getSelectedServices]

    # save default services for a node type into the g_node_types array
    if { $session != "" } {
	global g_node_types g_node_type_services_hint
	set type $g_node_type_services_hint
	set idx [getNodeTypeIndex $type]
	if { $idx < 0 } { 
	    puts "warning: skipping unknown node type $type"
	} else {
	    set typedata $g_node_types($idx)
	    set typedata [lreplace $typedata 3 3 $vals]
	    array set g_node_types [list $idx $typedata]
	}
    # save the services configured for a specific node
    } else {
	setNodeServices $node $vals
    }

    destroy $wi
}

# load the default set of services for this node type
proc popupServicesConfigDefaults { wi node types captions groups } {
    set type [getNodeModel $node]
    set defaults [getNodeTypeServices $type]
    for { set n 0 } { $n < [llength $types] } { incr n } {
	set groupinfo [popupCapabilityConfigGroup $groups [expr {$n + 1}]]
	set gn [lindex $groupinfo 0]

	set val 0
	set valname [lindex $captions $n]
	if { [lsearch $defaults $valname] != -1 } { set val 1 }
	global $wi.vals.$gn.item$n.entval
	set $wi.vals.$gn.item$n.entval $val
    }
}

#
# Popup a service customization dialog for a given service on a node
# The customize/edit button next to a service has been pressed
#
proc customizeService { wi node service btn } {
    global g_sent_nodelink_definitions
    global plugin_img_add plugin_img_del plugin_img_folder
    global plugin_img_open plugin_img_save
    global eventtypes
    set selected [getSelectedServices]
    # if service is not enabled, enable it here
    if { [lsearch -exact $selected $service] == -1 } {
	set i [string last ".custom" $btn]
	set entval [string replace $btn $i end ".entval"]
	global $entval
	set $entval 1
	lappend selected $service
    }

    # inform the CORE services about all nodes and links, so it can build
    # custom configurations for services
    if { $g_sent_nodelink_definitions == 0 } {
	set g_sent_nodelink_definitions 1
        set sock [lindex [getEmulPlugin $node] 2]
	sendEventMessage $sock $eventtypes(definition_state) -1 "" "" 0
        sendNodeLinkDefinitions $sock
    }

    set w .popupServicesCustomize
    catch {destroy $w}
    toplevel $w
    wm transient $w .popupServicesConfig   
    wm title $w "$service on node [getNodeName $node] ($node)"

    ttk::frame $w.top
    ttk::label $w.top.lab -text "$service service"

    ttk::frame $w.top.meta
    ttk::label $w.top.meta.lab -text "Meta-data"
    ttk::entry $w.top.meta.ent -width 40
    pack $w.top.lab -side top
    pack $w.top.meta.lab -side left -padx 4 -pady 4
    pack $w.top.meta.ent -fill x -side left -padx 4 -pady 4
    pack $w.top.meta -side top
    pack $w.top -side top -padx 4 -pady 4

    ttk::notebook $w.note
    pack $w.note -fill both -expand true -padx 4 -pady 4
    ttk::notebook::enableTraversal $w.note

    set enableapplycmd "$w.btn.apply configure -state normal"

    ### Custom ###
    # services may define custom popup configuration dialogs invoked here
    set custom_popup "popupServiceConfig_$service"
    if { [info commands $custom_popup] == $custom_popup } {
	$custom_popup $wi $w $node $service $btn
    }

    ### Files ###
    ttk::frame $w.note.files
    set fileshelp "Config files and scripts that are generated for this"
    set fileshelp "$fileshelp service."
    ttk::label $w.note.files.help -text $fileshelp
    pack $w.note.files.help -side top -anchor w -padx 4 -pady 4
    $w.note add $w.note.files -text "Files" -underline 0

    ttk::frame $w.note.files.name
    ttk::label $w.note.files.name.lab -text "File name:"
    set combo $w.note.files.name.combo
    ttk::combobox $combo -width 30
    set helpercmd "customizeServiceFileHelper $w"
    ttk::button $w.note.files.name.add -image $plugin_img_add \
	-command "listboxAddDelHelper add $combo $combo true; $helpercmd false"
    ttk::button $w.note.files.name.del -image $plugin_img_del \
	-command "listboxAddDelHelper del $combo $combo true; $helpercmd true"
    pack $w.note.files.name.lab -side left
    pack $w.note.files.name.combo -side left -fill x -expand true
    foreach c [list add del] {
	pack $w.note.files.name.$c -side left
    }
    pack $w.note.files.name -side top -anchor w -padx 4 -pady 4 -fill x

    # copy source file
    global g_service_configs_opt
    set g_service_configs_opt "use"
    set f ${w}.note.files.copy
    ttk::frame $f
    ttk::radiobutton $f.opt -text "Copy this source file: " \
	-value "copy" -variable g_service_configs_opt \
	-command "customizeServiceFileOpt $w copy true"
    ttk::entry $f.name -width 45
    ttk::button $f.btn -image $plugin_img_open -command \
	"customizeServiceFileOpt $w copy true; fileButtonPopup $f.name {}"
    pack $f.opt $f.name $f.btn -side left -anchor w 
    pack $f -side top -anchor w -padx 4 -pady 4 -fill x
    bind $f.btn <Button> "customizeServiceFileOpt $w copy true"

    # use file text
    set f ${w}.note.files.use
    ttk::frame $f
    ttk::radiobutton $f.opt -text "Use text below for file contents:" \
	-value "use" -variable g_service_configs_opt \
	-command "customizeServiceFileOpt $w use true"
    pack $f.opt -side left
    foreach c [list open save] {
	ttk::button $f.$c -image [set plugin_img_$c] -command \
	    "customizeServiceFileOpt $w use true; genericOpenSaveButtonPress $c $w.note.files.txt $w.note.files.name.combo"
        pack $f.$c -side left
    }
    pack $f -side top -anchor w -padx 4 -pady 4 -fill x

    text $w.note.files.txt -bg white -width 80 -height 10 \
	-yscrollcommand "$w.note.files.scroll set" -undo 1
    ttk::scrollbar $w.note.files.scroll -command "$w.note.files.txt yview"

    pack $w.note.files.txt -side left -fill both -expand true
    pack $w.note.files.scroll -side right -fill y
    bind $w.note.files.txt <KeyPress> $enableapplycmd

    global g_service_configs_tmp g_service_configs_last
    if { [info exists g_service_configs_tmp ] } {
	array unset g_service_configs_tmp
    }
    array set g_service_configs_tmp {}
    set g_service_configs_last ""
    bind $w.note.files.name.combo <<ComboboxSelected>> "$helpercmd true"
    bind $w.note.files.name.combo <KeyPress> $enableapplycmd

    ### Directories ###
    ttk::frame $w.note.dirs
    $w.note add $w.note.dirs -text "Directories" -underline 0
    set helptxt "Directories required by this service that are"
    set helptxt "$helptxt unique for each node."
    ttk::label $w.note.dirs.help -text $helptxt
    pack $w.note.dirs.help -side top -anchor w -padx 4 -pady 4

    ttk::treeview $w.note.dirs.tree -height 3 -selectmode browse
    $w.note.dirs.tree heading \#0 -text "Per-node directories"
    $w.note.dirs.tree insert {} end -id root -text "/" -open true \
 	-image $plugin_img_folder
    ttk::button $w.note.dirs.add -image $plugin_img_add \
	-command "customizeServiceDirectoryHelper $w add; $enableapplycmd"
    ttk::button $w.note.dirs.del -image $plugin_img_del \
	-command "customizeServiceDirectoryHelper $w del; $enableapplycmd"

    pack $w.note.dirs.tree -side top -fill both -expand true -padx 4 -pady 4
    pack $w.note.dirs.del $w.note.dirs.add -side right

    ### Startup/shutdown ###
    ttk::frame $w.note.ss
    $w.note add $w.note.ss -text "Startup/shutdown" -underline 0

    global g_service_startup_index
    set g_service_startup_index 50
    ttk::frame $w.note.ss.si
    ttk::label $w.note.ss.si.idxlab -text "Startup index:"
    ttk::entry $w.note.ss.si.idxval -width 5 \
	-textvariable g_service_startup_index
    ttk::scale $w.note.ss.si.idx -from 0 -to 100 -orient horizontal \
	-variable g_service_startup_index \
	-command "$enableapplycmd; scaleresolution 1 g_service_startup_index"
    pack $w.note.ss.si.idxlab $w.note.ss.si.idxval -side left -padx 4 -pady 4
    pack $w.note.ss.si.idx -side left -expand true -fill x -padx 4 -pady 4
    pack $w.note.ss.si -side top -padx 4 -pady 4 -fill x

    global g_service_startup_time
    set g_service_startup_time ""
    ttk::frame $w.note.ss.st
    ttk::label $w.note.ss.st.timelab -text "Start time:"
    ttk::entry $w.note.ss.st.timeval -width 5 \
	-textvariable g_service_startup_time
    set txt "(seconds after runtime; leave empty for default)"
    ttk::label $w.note.ss.st.help -text $txt 
    pack $w.note.ss.st.timelab $w.note.ss.st.timeval $w.note.ss.st.help \
	-side left -padx 4 -pady 4
    pack $w.note.ss.st -side top -padx 4 -pady 4 -fill x
    bind $w.note.ss.st.timeval <KeyPress> $enableapplycmd


    set captions "Startup Shutdown Validate"
    foreach c "up down val" {
	set fr $w.note.ss
	set caption [lindex $captions 0]
	set captions [lreplace $captions 0 0]
        entrylistbox $fr $c "$caption Commands" $enableapplycmd
    }

    set closecmd "destroy $w; setCustomButtonColor $btn $node $service false"

    ttk::frame $w.btn
    global g_customize_service_diff_only
    set g_customize_service_diff_only 1
    ttk::checkbutton $w.btn.diff -variable g_customize_service_diff_only \
    	-text "only store values that have changed from their defaults"
    ttk::button $w.btn.apply -text "Apply" -state disabled \
	-command "customizeServiceApply $w $node $service; $closecmd"
    ttk::button $w.btn.reset -text "Defaults" \
	-command "customizeServiceReset $w $node $service {$selected}; $w.btn.close configure -text Close"
    ttk::button $w.btn.copy -text "Copy..." \
	-command "customizeServiceCopy $node"
    ttk::button $w.btn.close -text "Cancel" -command $closecmd
    pack $w.btn.diff -side top
    pack $w.btn.apply $w.btn.reset $w.btn.copy $w.btn.close -side left
    pack $w.btn -side top -padx 4 -pady 4

    # populate dialog values
    customizeServiceRefresh $service "$w $node {$selected}"
}

# popup dialog with tree view for copying customized service configuration
# parameters from other nodes
proc customizeServiceCopy { cnode } {
    global node_list plugin_img_edit plugin_img_open

    set w .popupServicesCopy
    catch {destroy $w}
    toplevel $w
    wm transient $w .popupServicesCustomize
    wm title $w "Copy services to node [getNodeName $cnode] ($cnode)"

    ttk::frame $w.nodes
    ttk::treeview $w.nodes.tree -height 3 -selectmode extended
    $w.nodes.tree heading \#0 -text "Service configuration items"

    pack $w.nodes.tree -side top -fill both -expand true -padx 4 -pady 4
    pack $w.nodes -side top -anchor w -fill both -expand true

    ttk::frame $w.btn
    ttk::button $w.btn.apply -text "Copy" \
    	-command "customizeServiceCopyApply $w $cnode"
    ttk::button $w.btn.view -text "View" \
    	-command "customizeServiceCopyView $w $cnode"
    ttk::button $w.btn.close -text "Cancel" -command "destroy $w"
    pack $w.btn.apply $w.btn.view $w.btn.close -side left
    pack $w.btn -side top -padx 4 -pady 4
    set tree $w.nodes.tree

    foreach node $node_list {
	if { $node == $cnode } { continue }
	set customCfgList [getCustomConfig $node]
	foreach element $customCfgList {
	    set id [getConfig $element "custom-config-id"]
	    set parts [split $id :]
	    if { [lindex $parts 0] != "service" } { continue }
	    set s [lindex $parts 1]
	    # insert node into tree
	    if { ![$tree exists "$node"] } {
		set img [getCustomImage $node]
		if { $img == "" } {
		    set model [getNodeModel $node]
		    set img [getNodeTypeImage $model normal]
		}
		set img [file tail $img].5
		global [set img]
		set img [set [set img]]
		$tree insert {} end -id "$node" -text $node -open true \
			-image $img
	    }
	    # insert service name
	    if { ![$tree exists "$node:$s"] } {
		$tree insert $node end -id "$node:$s" -text $s -open true
	    }
	    # insert service elements
	    if { [llength $parts] == 3 } {
		set f [lindex $parts 2]
	        $tree insert "$node:$s" end -id "$node:$s:$f" -text $f \
			-image $plugin_img_open
	    } else {
		set cfg [getConfig $element "config"]
		foreach c $cfg {
		    if { [lindex [split $c =] 0] == "files" } { continue }
	            $tree insert "$node:$s" end -id "$node:$s:$c" -text $c \
		    	-image $plugin_img_edit
		}
	    }
	} ;# end foreach element
    }
}

# copy selected service configuration items from other nodes to the current
# customize dialog
proc customizeServiceCopyApply { w node } {
    global g_service_configs_tmp g_service_configs_last
    global g_service_startup_index g_service_startup_time 
    set tgt .popupServicesCustomize

    set tree $w.nodes.tree
    set sel [$tree selection]
    destroy $w

    foreach s $sel {
	set parts [split $s :]
	set node [lindex $parts 0]; set service [lindex $parts 1]
	set item [lindex $parts 2]
	# customized file 
	set f [getCustomService $node "$service:$item"]
	if { $f != "" } {
	    set filedata [join $f "\n"]
            array set g_service_configs_tmp [list $item $filedata]
	    set files [$tgt.note.files.name.combo cget -values]
	    if { [lsearch $files $item] < 0 } {
		lappend files $item
		$tgt.note.files.name.combo configure -values $files
	    }
	    if { $g_service_configs_last == $item } {
		customizeServiceFileDataSet $tgt $filedata
	    }
	# customized parameters
	} else {
	    set kv [splitKeyValue $item]
	    set key [lindex $kv 0]
	    set value [lindex $kv 1]
	    switch -exact -- $key {
		meta {
		    $tgt.top.meta.ent delete 0 end
		    $tgt.top.meta.ent insert end $value
		}
		dirs {
		    foreach dir [tupleStringToList $value] {
			set dir [string range $dir 1 end]
			treeviewInsert $tgt.note.dirs.tree root [split $dir "/"]
		    }
		}
		startidx {
	    	    set g_service_startup_index $value
		}
		cmdup -
		cmddown -
		cmdval {
		    set name [string range $key 3 end]
		    foreach cmd [tupleStringToList $value] {
			$tgt.note.ss.$name.cmds.list insert end $cmd
		    }
		}
		starttime {
		    set g_service_startup_time $value
		}
		default {
		    puts "warning: didn't copy '$key'"
		}
	    }
	}
    }

}

# view the customization for comparison with current node
proc customizeServiceCopyView { w node } {
    set tree $w.nodes.tree
    set sel [$tree selection]
    destroy $w

    set fn ""
    set filedata ""
    foreach s $sel {
	set parts [split $s :]
	set node [lindex $parts 0]; set service [lindex $parts 1]
	set item [lindex $parts 2]
	# customized file 
	set f [getCustomService $node "$service:$item"]
	if { $f != "" } {
	    set fn [file join "/tmp" "services.tmp-$node-[file tail $item]"]
	    set filedata [join $f "\n"]
	# customized parameters
	} else {
	    set kv [splitKeyValue $item]
	    set key [lindex $kv 0]
	    set value [lindex $kv 1]
	    set fn [file join "/tmp" "services.tmp-$node-$key"]
	    set filedata $value
	}
    }

    if { $fn == "" } { return }

    if { [catch { set f [open $fn w] } e] } {
	puts "error opening file: $fn\n ($e)"
	return
    }
    puts $f $filedata
    close $f

    popupFileView $fn
}

# helper for add/delete directories from treeview
proc customizeServiceDirectoryHelper { w cmd } {
    if { $cmd == "add" } {
	set dir [tk_chooseDirectory -mustexist false -initialdir "/" \
		-parent $w -title "Add a per-node directory"]
	if { $dir == "" } { return }
	set dir [string range $dir 1 end] ;# chop off leading slash
	treeviewInsert $w.note.dirs.tree root [split $dir "/"]
    } elseif { $cmd == "del" } {
	set s [$w.note.dirs.tree selection]
	if { $s == "root" } { return } ;# may not delete root
	$w.note.dirs.tree delete $s ;# delete the current selection
	set parents [lreplace [split $s /] end end]
	# delete all parents of the selected node if they do not have children
	while {[llength $parents] > 1} {
	    set parent [join $parents "/"]
	    if { [llength [$w.note.dirs.tree children $parent]] == 0 } {
		$w.note.dirs.tree delete $parent
	    }
	    set parents [lreplace $parents end end]
	}
    }
}

# helper for switching files based on combo box selection
proc customizeServiceFileHelper { w clear } {
    global g_service_configs_tmp g_service_configs_last
    # save old config to array
    set cfg [customizeServiceFileDataGet $w]
    if { [info exists g_service_configs_last] && \
	 $g_service_configs_last != "" } {
	array set g_service_configs_tmp [list $g_service_configs_last $cfg]
    }
    set cfgname [$w.note.files.name.combo get]
    set g_service_configs_last $cfgname

    # populate with new config
    if { $clear } {
	$w.note.files.txt delete 0.0 end
	$w.note.files.copy.name delete 0 end
	customizeServiceFileOpt $w "use" false
    }
    if { ![info exists g_service_configs_tmp($cfgname)] } {
	array set g_service_configs_tmp [list $cfgname ""]
    } else {
	set cfg $g_service_configs_tmp($cfgname)
	customizeServiceFileDataSet $w $cfg
    }
}

# helper to insert file contents into the text controls
proc customizeServiceFileDataSet { w cfg } {
    $w.note.files.txt delete 0.0 end
    $w.note.files.copy.name delete 0 end
    if { [string range $cfg 0 6] == "file://" } {
	customizeServiceFileOpt $w "copy" false
	set cfglines [split $cfg "\n"]
	set cfg [lindex $cfglines 0] ;# truncate any other lines
	$w.note.files.copy.name insert 0 [string range $cfg 7 end]
    } else {
	customizeServiceFileOpt $w "use" false
	$w.note.files.txt insert 0.0 $cfg
    }
}

# helper to get file contents from the text controls
proc customizeServiceFileDataGet { w } {
    global g_service_configs_opt
    if { $g_service_configs_opt == "use" } {
	set cfg [$w.note.files.txt get 0.0 end-1c]
    } elseif { $g_service_configs_opt == "copy" } {
	set cfg [$w.note.files.copy.name get]
	set cfg "file://$cfg"
    }
    return $cfg
}

# helper to set option mode to use/copy
proc customizeServiceFileOpt { w mode enable_apply } {
    global g_service_configs_opt
    set g_service_configs_opt $mode
    if { $mode == "copy" } {
	$w.note.files.txt configure -state disabled -bg gray
	$w.note.files.copy.name configure -state normal
    } else {
	$w.note.files.txt configure -state normal -bg white
	$w.note.files.copy.name configure -state disabled
    }
    if { $enable_apply } {
	$w.btn.apply configure -state normal
    }
}

# create a listbox with a text entry above it, with add/delete buttons and
# a scrollbar
proc entrylistbox { fr name caption extracmd} {
    global plugin_img_add plugin_img_del

    set c $name
    ttk::labelframe $fr.$name -text "$caption"

    ttk::frame $fr.$c.edit
    ttk::entry $fr.$c.edit.cmd -width 40
    ttk::button $fr.$c.edit.add -image $plugin_img_add \
        -command "listboxAddDelHelper add $fr.$c.edit.cmd $fr.$c.cmds.list false; $extracmd"
    ttk::button $fr.$c.edit.del -image $plugin_img_del \
        -command "listboxAddDelHelper del $fr.$c.edit.cmd $fr.$c.cmds.list false; $extracmd"
    pack $fr.$c.edit.cmd -side left -fill x -expand true
    pack $fr.$c.edit.add $fr.$c.edit.del -side left

    ttk::frame $fr.$c.cmds
    listbox $fr.$c.cmds.list -height 5 -width 50 \
        -yscrollcommand "$fr.$c.cmds.scroll set" -exportselection 0
    bind $fr.$c.cmds.list <<ListboxSelect>> "listboxSelect $fr.$c.cmds.list $fr.$c.edit.cmd"
    ttk::scrollbar $fr.$c.cmds.scroll -command "$fr.$c.cmds.list yview"
    pack $fr.$c.cmds.list  -side left -fill both -expand true
    pack $fr.$c.cmds.scroll -side left -fill y
        pack $fr.$c.edit $fr.$c.cmds -side top -anchor w -fill x
    pack $fr.$c -side top -fill x -expand true
}

#
# color the customize/edit button adjacent to each service checkbutton
#
proc setCustomButtonColor { btn node service needcustom } {
    set color lightgray ;# default button background color

    # color button yellow if enabled and customization is needed
    if { $needcustom } {
	# button $wi.vals.$gn.item$n.custom / value $wi.vals.$gn.item$n.entval
	set i [string last ".custom" $btn]
	set entval [string replace $btn $i end ".entval"]
	global $entval
	if { [set $entval] } {
	    set color yellow
	}
    }
    if { [getCustomService $node $service] != "" } {
	set color green
    }
    $btn configure -bg $color
}

proc scaleresolution { res var val } {
    set factor [expr {1 / $res}]
    set val [expr {int($val * $factor) / $factor}]
    global $var
    set $var $val
    return $val
}

# return a list of services that have been selected (checkbox is checked)
proc getSelectedServices { } {
    global g_service_ctls
    set selected {}
    foreach c $g_service_ctls {
	global $c
	set service [$c cget -text]
	set var [$c cget -variable]
	global $var
	if { [set $var] == 1 } { lappend selected $service }
    }
    return $selected
}

# send a config request message with the opaque field set to query for all
# service parameters; the opaque field is "service:s5,s2,s3,s4", where service
# s5 is being configured (parseConfMessage will invoke customizeServiceValues)
proc customizeServiceRefresh { var args } {
    set args [lindex $args 0]
    set w [lindex $args 0]
    set node [lindex $args 1]
    set services [lindex $args 2]

    # move service to the front of the list of services
    set i [lsearch $services $var]
    if { $i < 0 } {
	puts "error: service $var not found in '$services'"
	return
    } elseif { $i > 0 } {
	set services [lreplace $services $i $i]
	set services [linsert $services 0 $var]
    }

    # request service parameters from daemon
    set svcstr [join $services ","]
    set sock [lindex [getEmulPlugin $node] 2]
    sendConfRequestMessage $sock $node services 0x1 -1 "service:$svcstr"
    update
}

# this returns a list of values for the service s on node if a custom service
# configuration exists
proc getCustomService { node s } {
    set values [getCustomConfigByID $node "service:$s"]
    return $values
}

# this helper is invoked upon receiving the reply to the message sent from
# customizeServiceRefresh; it populates the dialog box fields
proc customizeServiceValues { node values services } {
    global plugin_img_folder

    set service [lindex $services 0]

    set w .popupServicesCustomize
    if { ![winfo exists $w] } {
	# apply config update without dialog box
	# this occurs when loading from XML or reconnecting to a session
        setCustomConfig $node "service:$service" $service $values 0
	return
    }

    global g_customize_service_values_orig
    set g_customize_service_values_orig $values

    # merge any custom values with defaults from message
    set custom_values [getCustomService $node $service]
    set i 0
    set has_keys [hasKeyValues $custom_values]
    foreach val $custom_values {
	if { $has_keys } {
	    set kv [splitKeyValue $val]
	    set key [lindex $kv 0]; set value [lindex $kv 1]
	    set values [setServiceValuesItem $values $key $value]
	} else {
	    set values [lreplace $values $i $i $val]
	}
	incr i
    }

    # populate meta-data
    set meta [getServiceValuesItem $values "meta" 6]
    $w.top.meta.ent delete 0 end
    $w.top.meta.ent insert end $meta

    # populate Files tab
    set files [tupleStringToList [getServiceValuesItem $values "files" 1]]
    set chosenfile [lindex $files 0] ;# auto-display first file from list
    $w.note.files.name.combo configure -values $files
    $w.note.files.name.combo delete 0 end
    if { $chosenfile != "" } {
	$w.note.files.name.combo insert 0 $chosenfile
    }
    global g_service_configs_last
    set g_service_configs_last $chosenfile

    # file data
    foreach f $files {
	set filedata [join [getCustomService $node "$service:$f"] "\n"]
	if { $filedata != "" } {
	    # use file contents from existing config
	    customizeServiceFile $node $f "service:$service" $filedata false
	} elseif { $f !=  "" } {
	    # request the file contents
	    set svcstr [join $services ","]
	    set sock [lindex [getEmulPlugin "*"] 2]
	    set opaque "service:$svcstr:$f"
	    # this causes customizeServiceFile to be invoked upon reply
	    sendConfRequestMessage $sock $node services 0x1 -1 $opaque
	}
    }

    # populate Directories tab
    set dirs [tupleStringToList [getServiceValuesItem $values "dirs" 0]]
    $w.note.dirs.tree delete root
    $w.note.dirs.tree insert {} end -id root -text "/" -open true \
	-image $plugin_img_folder
    foreach dir $dirs {
	set dir [string range $dir 1 end] ;# chop off leading slash
	treeviewInsert $w.note.dirs.tree root [split $dir "/"]
    }

    # populate Startup/shutdown tab
    set idx [getServiceValuesItem $values "startidx" 2]
    global g_service_startup_index
    set g_service_startup_index $idx

    set valuesidx 3
    foreach c "up down val" {
	set fr $w.note.ss
	$fr.$c.edit.cmd delete 0 end
	$fr.$c.cmds.list delete 0 end
	set value [getServiceValuesItem $values "cmd$c" $valuesidx]
	foreach cmd [tupleStringToList $value] {
	    if { $cmd != "" } { $fr.$c.cmds.list insert end $cmd }
	}
	incr valuesidx
    }

    set starttime [getServiceValuesItem $values "starttime" 6]
    global g_service_startup_time
    set g_service_startup_time $starttime

    # populate any custom service tab
    set service [lindex $services 0]
    set custom_vals_callback "popupServiceConfig_${service}_vals"
    if { [info commands $custom_vals_callback] == $custom_vals_callback } {
	$custom_vals_callback $node $values $services $w
    }

    $w.btn.apply configure -state disabled
}

# extract items from a list of values
# old-style values has an ordered list of values; idx determines key
# new-style values is a list of key=value pairs
proc getServiceValuesItem { values key idx } {
    # determine how to handle values
    set has_keys [hasKeyValues $values]
    if { $has_keys } {
        return [getKeyValue $key $values ""]
    } else {
	return [lindex $values $idx]
    }
}

# replace a "key=value" pair in a list, returning the list
proc setServiceValuesItem { values key value } {
    set i 0
    foreach v $values {
	set k [lindex [splitKeyValue $v] 0]
	if { $k == $key } { break }
	incr i
    }
    if { $i == [llength $values] } {
	puts "key not found '$key' in service values"
	return $values
    }
    return [lreplace $values $i $i "$key=$value"]
}

# this helper is invoked upon receiving a File Message in reply to the Config
# Message sent from customizeServiceRefresh; it populates the config file entry
proc customizeServiceFile { node name type data generated} {
    global g_service_configs_tmp g_service_configs_tmp_orig 
    global g_service_configs_last

    set w .popupServicesCustomize
    if { ![winfo exists $w] } {
	# apply file config update without dialog box
	# this occurs when loading from XML or reconnecting to a session
	# type should be e.g. "service:zebra"
	set data [split $data "\n"]
	setCustomConfig $node "$type:$name" $name $data 0
	return
    }

    # store file data in array
    array set g_service_configs_tmp [list $name $data]
    if { $generated } {
	array set g_service_configs_tmp_orig [list $name $data]
    } else {
	array set g_service_configs_tmp_orig [list $name ""]
    }

    # display file if currently selected
    if { $g_service_configs_last == $name } {
	customizeServiceFileDataSet $w $data
    }

    # invoke any custom service callback
    set service [string range $type 8 end] ;# assume already checked "service:"
    set custom_file_callback "popupServiceConfig_${service}_file"
    if { [info commands $custom_file_callback] == $custom_file_callback } {
	$custom_file_callback $node $name $data $w
    }
}

# helper to recursively add a directory path to a treeview
proc treeviewInsert { tree parent items } {
    # pop first item
    set item [lindex $items 0]
    set items [lreplace $items 0 0]
    set img [$tree item $parent -image] ;# adopt icon from parent
    if { ![$tree exists "$parent/$item"] } {
	$tree insert $parent end -id "$parent/$item" -text $item -open true \
		-image $img
    }

    if { [llength $items] > 0 } {
	treeviewInsert $tree "$parent/$item" $items
    }
}

# return all children that are leaf nodes in a tree
proc treeviewLeaves { tree parent } {
    set leaves ""
    set children [$tree children $parent]
    if { [llength $children] == 0 } {
	return $parent
    }
    foreach child $children {
	set leaves [concat $leaves [treeviewLeaves $tree $child]]
    }
    return $leaves
}

# apply button pressed on customizeService dialog
proc customizeServiceApply { w node service } {
    global g_customize_service_diff_only

    catch { $w.btn.apply configure -state disabled }

    set values ""

    # Directories
    set dirs ""
    set dirstmp [treeviewLeaves $w.note.dirs.tree root]
    foreach dir $dirstmp {
	set dir [string replace $dir 0 3] ;# chop off "root" prefix
	if { $dir == "" } { continue }
	lappend dirs $dir
    }
    lappend values "dirs=[listToTupleString $dirs]"

    # Files
    set files [$w.note.files.name.combo cget -values]
    lappend values "files=[listToTupleString $files]"

    # Startup index
    global g_service_startup_index
    lappend values "startidx=$g_service_startup_index"

    # Startup/shutdown commands
    foreach c "up down val" {
	set cmds [$w.note.ss.$c.cmds.list get 0 end]
	lappend values "cmd$c=[listToTupleString $cmds]"
    }

    # meta
    lappend values "meta=[$w.top.meta.ent get]"

    # start time
    global g_service_startup_time
    lappend values "starttime=$g_service_startup_time"

    # remove any existing config files for this service
    #   this prevents duplicates when files are renamed/deleted
    set cfgs [getCustomConfig $node]
    foreach cfg $cfgs {
	set cid [lindex [lsearch -inline $cfg "custom-config-id *"] 1]
	set len [expr {[string length "service:$service:"] - 1}]
	if { [string range $cid 0 $len] == "service:$service:" } {
	    setCustomConfig $node $cid "" "" 1
	}
    }

    # save config files (that have changed)
    set trimmed_files {}
    global g_service_configs_tmp g_service_configs_tmp_orig
    global g_service_configs_last
    set cfg [customizeServiceFileDataGet $w]
    array set g_service_configs_tmp [list $g_service_configs_last $cfg]
    foreach cfgname $files {
	if { ![info exists g_service_configs_tmp($cfgname)] } {
	    puts "missing config for file '$cfgname'"
	    continue
	}
	if { [info exists g_service_configs_tmp_orig($cfgname)] } {
	    if { $g_service_configs_tmp_orig($cfgname) == \
		 $g_service_configs_tmp($cfgname) } {
		# file has not changed
		if { $g_customize_service_diff_only } { continue }
	    }
	}
	set cfg [split $g_service_configs_tmp($cfgname) "\n"]
	setCustomConfig $node "service:$service:$cfgname" $cfgname $cfg 0
	lappend trimmed_files $cfgname
    }


    # store only values that have changed from the defaults
    set trimmed {}
    global g_customize_service_values_orig
    for {set i 0} {$i < [llength $values]} {incr i} {
	set value_orig [lindex $g_customize_service_values_orig $i]
	set value_orig [tupleStringToList $value_orig]
	set value_new [tupleStringToList [lindex $values $i]]
	if { $i == 1 } {
	    # when a file has changed, store all filenames whether or not
	    # the name(s) have changed
	    if { [llength $trimmed_files] > 0 } {
		lappend trimmed [lindex $values 1]
	    }
	    continue
	}
	if {$value_orig != $value_new} {
	    lappend trimmed [lindex $values $i]
	}
    }
    if { $g_customize_service_diff_only } {
	set values $trimmed
    }
    unset g_customize_service_values_orig

    # save values without config file
    setCustomConfig $node "service:$service" $service $values 0

    array unset g_service_configs_tmp
    array unset g_service_configs_tmp_orig
    unset g_service_configs_last

    #  may want to apply here, if some config validation is implemented or
    #  runtime applying of service customization
    #  otherwise this is not necessary due to config being sent upon startup
    #  also more logic would be needed for using the reset button
    #set sock [lindex [getEmulPlugin $node] 2]
    #set types [string repeat "10 " [llength $values]]
    #sendConfReplyMessage $sock $node services $types $values "service:$service"
}

#
# reset button is pressed on customizeService dialog
#
proc customizeServiceReset { w node service services } {
    set cfgnames [$w.note.files.name.combo cget -values]
    setCustomConfig $node "service:$service" "" "" 1
    foreach cfgname $cfgnames {
	setCustomConfig $node "service:$service:$cfgname" "" "" 1
    }

    customizeServiceRefresh $service [list $w $node $services]
}

# check for old service configs in all nodes
proc upgradeConfigServices {} {
    global node_list
    foreach node $node_list {
	upgradeNodeConfigService $node
	upgradeCustomPostConfigCommands $node
    }
}

# provide backwards-compatibility with changes to services fields here
proc upgradeNodeConfigService { node } {
    set OLD_NUM_FIELDS 7
    set cfgs [getCustomConfig $node]
    foreach cfg $cfgs {
	set cid [lindex [lsearch -inline $cfg "custom-config-id service:*"] 1]
	# skip configs that are not a service definition ("service:name")
	if { [llength [split $cid :]] != 2 } { continue }

	set values [getConfig $cfg config]
        if { [llength $values] != [expr {$OLD_NUM_FIELDS-1}] } { continue }

	# update from 6 service fields to 7 when introducing validate commands
	set service [lindex [split $cid :] 1]
	#puts -nonewline "note: updating service $service on $node with empty "
	#puts "validation commands"
	set values [linsert $values end-1 {}]
	setCustomConfig $node "service:$service" $service $values 0
    }
}

proc upgradeCustomPostConfigCommands { node } {
    set cfg [getCustomPostConfigCommands $node]
    setCustomPostConfigCommands $node {}
    if { $cfg == "" } { return }
    set cfgname "custom-post-config-commands.sh"
    set values "{files=('$cfgname', )} startidx=35 {cmdup=('sh $cfgname', )}"
    setCustomConfig $node "service:UserDefined" "UserDefined" $values 0
    setCustomConfig $node "service:UserDefined:$cfgname" $cfgname $cfg 0
    set services [getNodeServices $node true]
    lappend services "UserDefined"
    setNodeServices $node $services
    puts "adding user-defined custom-post-config-commands.sh service for $node"

}

# populate services menu when right-clicking on a node at runtime
proc addServicesRightClickMenu { m node } {
    $m add cascade -label "Services" -menu $m.services

    set i 0
    set services [getNodeServices $node true]
    foreach s $services {
	set childmenu $m.services.s$i
	incr i
	destroy $childmenu
	menu $childmenu -tearoff 0
	$m.services add cascade -label $s -menu $childmenu
	foreach cmd "start stop restart validate" {
	    $childmenu add command -label $cmd \
		-command "sendServiceCmd $node $s $cmd"
	}
    }
}

proc sendServiceCmd { node service cmd } {
    global eventtypes

    set plugin [lindex [getEmulPlugin "*"] 0]
    set sock [pluginConnect $plugin connect true]

    if { $cmd == "validate" } { set cmd "pause" }
    set type $eventtypes(event_$cmd)
    set nodenum [string range $node 1 end]
    set name "service:$service"
    set data ""

    sendEventMessage $sock $type $nodenum $name $data 0
}