#
# GUI support for node types and profiles.
#

global execMode
if { $execMode == "interactive" } {
    package require Ttk
}

# these are the default node types when nodes.conf does not exist
#      index {name normal-icon tiny-icon services type metadata}
array set g_node_types_default {
	1 {router router.gif router.gif {zebra OSPFv2 OSPFv3 IPForward} \
	    netns {built-in type for routing}}
	2 {host host.gif host.gif {DefaultRoute SSH} \
	    netns {built-in type for servers}}
	3 {PC pc.gif pc.gif {DefaultRoute} \
	    netns {built-in type for end hosts}}
	4 {mdr mdr.gif mdr.gif {zebra OSPFv3MDR IPForward} \
	    netns {built-in type for wireless routers}}
	5 {prouter router_green.gif router_green.gif \
	    {zebra OSPFv2 OSPFv3 IPForward} \
	    physical {built-in type for physical nodes}}
	6 {OVS lanswitch.gif lanswitch.gif {DefaultRoute SSH OvsService} OVS {} }

}

# possible machine types for nodes
set MACHINE_TYPES "netns physical OVS"

# array populated from nodes.conf file
array set g_node_types { }

#
# Load the nodes.conf file into the g_nodes array
#
proc loadNodesConf { } {
    global CONFDIR g_node_types g_node_types_default MACHINE_TYPES
    set confname "$CONFDIR/nodes.conf"
    if { [catch { set f [open $confname r] } ] } {
	puts "Creating a default $confname"
	unset g_node_types
	array set g_node_types [array get g_node_types_default]
	writeNodesConf
	return
    }

    array unset g_nodes

    while { [ gets $f line ] >= 0 } {
	if { [string range $line 0 0] == "#" } { continue } ;# skip comments

	# fix-up 5-element list to include node type
	if { [llength $line] == 2 } {
	    set idx [lindex $line 0]; set data [lindex $line 1]
	    if { [llength $data] == 5 } {
		set data [linsert $data 4 [lindex $MACHINE_TYPES 0]]
		set line [list $idx $data]
	    }
	}

	# load into array of nodes
	if { [catch {array set g_node_types $line} e] } {
	    puts "Error reading $confname line '$node': $e"
	}
    }
    close $f
    checkNodeTypes true
}

# look for missing default node types; exit if fatal flag is true; return a
# string for the first missing type
proc checkNodeTypes { fatal } {
    global g_node_types_default
    set names [getNodeTypeNames]
    foreach i [lsort [array names g_node_types_default]] {
	set name [lindex $g_node_types_default($i) 0]
	if { [lsearch -exact $names $name] < 0 } {
	    puts "error: missing built-in node type '$name'!"
	    puts "move your ~/.core/nodes.conf file to re-create the defaults"
	    if { $fatal } {
		exit
	    } else {
		return $name
	    }

	}

    }
    return ""
}

#
# Write the nodes.conf file from the g_nodes array.
#
proc writeNodesConf { } {
    global CONFDIR g_node_types
    set confname "$CONFDIR/nodes.conf"
    if { [catch { set f [open "$confname" w] } ] } {
	puts "***Warning: could not write nodes file: $confname"
	return
    }

    set header "# nodes.conf: CORE node templates customization file."
    set header "$header\n# format: index {name normal-icon tiny-icon services"
    set header "$header type metadata}"
    puts $f $header
    foreach i [lsort -integer [array names g_node_types]] {
	puts $f "$i { [string trim $g_node_types($i)] }"
    }
    close $f
}

# return a list of names of node types
proc getNodeTypeNames {} {
    global g_node_types
    set names ""
    foreach i [lsort -integer [array names g_node_types]] {
	set node_type_data $g_node_types($i)
	set name [lindex $node_type_data 0]
	lappend names $name
    }
    return $names
}

proc isDefaultNodeType { nodetype } {
    global g_node_types_default

    foreach i [lsort [array names g_node_types_default]] {
	set name [lindex $g_node_types_default($i) 0]
	if { $nodetype == $name } { return true}
    }
    return false
}

# return the image path name for the specified node type
# size should equal "tiny" or "normal"
proc getNodeTypeImage { type size } {
    global g_node_types CORE_DATA_DIR
    foreach i [lsort -integer [array names g_node_types]] {
	set node_type_data $g_node_types($i)
	if { [lindex $node_type_data 0] == $type } {
	    if { $size == "tiny" } {
		set imgf [lindex $node_type_data 2]
	    } else {
		set imgf [lindex $node_type_data 1]
	    }
	    # if the image has no path, assume it can be
	    # found in $CORE_DATA_DIR/icons/tiny
	    if { [string first "/" $imgf] < 0 } {
		set imgf "$CORE_DATA_DIR/icons/$size/$imgf"
	    }
	    return $imgf
	}
    }
    return ""
}

# return the index in the global array for the given node type
proc getNodeTypeIndex { type } {
    global g_node_types
    foreach i [lsort -integer [array names g_node_types]] {
	set node_type_data $g_node_types($i)
	if { [lindex $node_type_data 0] == $type } {
	    return $i
	}
    }
    return -1
}

# return the default services for this node type
proc getNodeTypeServices { type } {
    global g_node_types
    foreach i [lsort -integer [array names g_node_types]] {
	set node_type_data $g_node_types($i)
	if { [lindex $node_type_data 0] == $type } {
	    return [lindex $node_type_data 3]
	}
    }
    return ""
}

# return the machine type (e.g. netns, physical) of the currently selected
# node type from the toolbar
proc getNodeTypeMachineType { type } {
    global MACHINE_TYPES g_node_types
    set default_machine_type [lindex $MACHINE_TYPES 3]
    set i [getNodeTypeIndex $type]
    if { $i < 0 } { return $default_machine_type }; # failsafe
    return [lindex $g_node_types($i) 4]
}

proc getNodeTypeProfile { type } {
    global g_node_types
    foreach i [lsort -integer [array names g_node_types]] {
	set node_type_data $g_node_types($i)
	if { [lindex $node_type_data 0] == $type } {
	    if {[llength $node_type_data] >= 7 } {
	        return [lindex $node_type_data 6]
	    }
	    break ;# profile may be empty
	}
    }
    return ""
}

# return the machine type (e.g. netns, physical) of the currently selected
# node type from the toolbar
proc getNodeTypeMachineType { type } {
    global MACHINE_TYPES g_node_types
    set default_machine_type [lindex $MACHINE_TYPES 3]
    set i [getNodeTypeIndex $type]
    if { $i < 0 } { return $default_machine_type }; # failsafe
    return [lindex $g_node_types($i) 4]
}

# Helper for add/delete button next to a list/combo box; from is the text entry
# from which the value is copied, and to is the list/combo box where the value
# is inserted upon add
proc listboxAddDelHelper { cmd from to combo } {
    set current [$from get] ;# current text from entry or combo
    if { $combo } {
	set values [$to cget -values]
	set i [lsearch -exact $values $current]
    }

    if { $cmd == "add" } {
	if { $combo } {
	    if { $i != -1 } { return } ;# item already exists
	    lappend values $current
	    $to configure -values $values
	} else {
	    $to insert end $current
	}
    } elseif { $cmd == "del" } {
	if { $combo } {
	    # search combo box values for current text
	    if { $i == -1 } { return } ;# item doesn't exist
	    set values [lreplace $values $i $i]
	    $to configure -values $values
	} else {
	    set values [$to curselection]
	    if { $values == "" } { return } ;# no current selection
	    $to delete [lindex $values 0] ;# delete only first selected item
	}
	$from delete 0 end ;# clear text entry/combo on delete
    }
}

# helper to populate a text entry when a listbox selection has changed
proc listboxSelect { lb ent } {
    set i [$lb curselection]
    $ent delete 0 end
    if { $i == "" } { return }
    $ent insert 0 [$lb get $i]
}

#
# Popup a profile configuration dialog box, using popupCapabilityConfig
#
proc popupNodeProfileConfig { channel node model types values captions bitmap possible_values groups session opaque } {
    global g_node_types

    set opaque_items [split $opaque :]
    if { [llength $opaque_items] != 2 } {
	puts "warning: received unexpected opaque data in conf message!"
	return
    }
    set nodetype [lindex $opaque_items 1]
    # check if we already have config for this profile, replacing values
    set existing_values [getNodeTypeProfile $nodetype]
    if { $existing_values != "" } {
	if { [llength $existing_values] == [llength $values] } {
	    set values $existing_values
	} else { ;# this accommodates changes to models
	    puts "warning: discarding stale profile for $model from nodes.conf"
	}
    }

    popupCapabilityConfig $channel $node $model $types $values \
				$captions $bitmap $possible_values $groups
}

proc popupNodeProfileConfigApply { vals } {
    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)
	if { [llength $typedata] < 7 } {
	    set typedata [linsert $typedata 6 $vals] ;# no profile in list
	} else {
	    set typedata [lreplace $typedata 6 6 $vals] ;# update the profile
	}
	array set g_node_types [list $idx $typedata]
    }
    # node type will be used in sendConfReplyMessage opaque data
    return "model:$type"
}

array set g_nodes_button_tooltips {
	add "add a new node type"
	save "apply changes to this node type"
	del "remove the selected node type"
	up "move the node type up in the list"
	down "move the selected node type down in the list"
}

# show the CORE Node Types configuration dialog
# this allows the user to define new node types having different names, icons,
# and default set of services
proc popupNodesConfig {} {
    global g_nodes_types g_nodes_button_tooltips MACHINE_TYPES g_machine_type
    global CORE_DATA_DIR

    set wi .nodesConfig
    catch {destroy $wi}
    toplevel $wi

    wm transient $wi .
    wm resizable $wi 0 1
    wm title $wi "CORE Node Types"

    # list of nodes
    labelframe $wi.s -borderwidth 0 -text "Node Types"
    listbox $wi.s.nodes -selectmode single -height 5 -width 15 \
	-yscrollcommand "$wi.s.nodes_scroll set" -exportselection 0
    scrollbar $wi.s.nodes_scroll -command "$wi.s.nodes yview"
    pack $wi.s.nodes $wi.s.nodes_scroll -fill y -side left
    pack $wi.s -padx 4 -pady 4 -fill both -side top -expand true

    # image button bar
    frame $wi.bbar
    set buttons "add save del"
    foreach b $buttons {
	# re-use images from the plugin dialog
	global plugin_img_$b
	button $wi.bbar.$b -image [set plugin_img_$b] \
                -command "nodesConfigHelper $wi $b"
        pack $wi.bbar.$b -side left
        balloon $wi.bbar.$b $g_nodes_button_tooltips($b)
    }
    pack $wi.bbar -padx 4 -pady 4 -fill x -side top

    # up/down buttons
    foreach b {up down} {
        set fn "$CORE_DATA_DIR/icons/tiny/arrow.${b}.gif"
        set img$b [image create photo -file $fn]
        button $wi.bbar.$b -image [set img${b}] \
                -command "nodesConfigHelper $wi $b"
	pack $wi.bbar.$b -side left
        balloon $wi.bbar.$b $g_nodes_button_tooltips($b)
    }

    # node type edit area
    frame $wi.s.edit -borderwidth 4
    frame $wi.s.edit.0
    label $wi.s.edit.0.namelab -text "Name"
    entry $wi.s.edit.0.name -bg white -width 20
    pack $wi.s.edit.0.namelab $wi.s.edit.0.name -side left

    frame $wi.s.edit.1
    label $wi.s.edit.1.iconlab -text "Icon"
    entry $wi.s.edit.1.icon -bg white -width 25
    button $wi.s.edit.1.filebtn -text "..." \
	-command "nodesConfigImgDialog $wi $wi.s.edit.1.icon normal"
    pack $wi.s.edit.1.iconlab $wi.s.edit.1.icon $wi.s.edit.1.filebtn -side left
    bind $wi.s.edit.1.icon <KeyPress> "nodesConfigImg $wi"

    canvas $wi.s.edit.0.c -width 60 -height 60
    # -bg white
    pack $wi.s.edit.0.c -side right -padx 10
    bind $wi.s.edit.0.c <Button> \
	"nodesConfigImgDialog $wi $wi.s.edit.1.icon normal"

    frame $wi.s.edit.2
    label $wi.s.edit.2.icontlab -text "Icon (small)"
    entry $wi.s.edit.2.icont -bg white -width 20
    button $wi.s.edit.2.filebtn -text "..." \
	-command "nodesConfigImgDialog $wi $wi.s.edit.2.icont tiny"
    pack $wi.s.edit.2.icontlab $wi.s.edit.2.icont $wi.s.edit.2.filebtn \
	-side left

    frame $wi.s.edit.5
    label $wi.s.edit.5.metalab -text "Meta-data  "
    entry $wi.s.edit.5.meta -bg white -width 25
    pack $wi.s.edit.5.metalab $wi.s.edit.5.meta -side left

    frame $wi.s.edit.3
    set machinetypemenu [tk_optionMenu $wi.s.edit.3.type g_machine_type \
    			 [lindex $MACHINE_TYPES 0]]
    foreach t [lrange $MACHINE_TYPES 1 end] {
	$machinetypemenu add radiobutton -label $t -value $t \
		-variable g_machine_type \
		-command "nodesConfigMachineHelper $wi"
    }
    button $wi.s.edit.3.services -text "Services..." \
	-command "nodesConfigServices $wi services"
    button $wi.s.edit.3.config -text "Profile..." \
	-command "nodesConfigServices $wi profile"
    pack $wi.s.edit.3.type $wi.s.edit.3.services $wi.s.edit.3.config -side left

    pack $wi.s.edit.0 $wi.s.edit.1 $wi.s.edit.2 $wi.s.edit.5 \
	-side top -anchor w
	#-padx 4 -pady 4
    pack $wi.s.edit.3 -side top -padx 4 -pady 4 -anchor w
    pack $wi.s.edit -fill both -side right

    # populate the list
    nodesConfigRefreshList $wi
    bind $wi.s.nodes <<ListboxSelect>> "nodesConfigSelect $wi \"\""
    $wi.s.nodes selection set 0
    nodesConfigSelect $wi ""


    # close button
    frame $wi.b -borderwidth 0
    button $wi.b.close -text "Close" -command "nodesConfigClose $wi"
    pack $wi.b.close -side right
    pack $wi.b -side bottom
}

proc nodesConfigRefreshList { wi } {
    global g_node_types

    set selected_idx [$wi.s.nodes curselection]

    $wi.s.nodes delete 0 end
    # this resets the g_node_types array so the indices match the listbox
    set idx 0
    foreach i [lsort -integer [array names g_node_types]] {
	incr idx
	set node_type_data $g_node_types($i)
	set name [lindex $node_type_data 0]
	$wi.s.nodes insert end $name
	if { $i != $idx } {
	    array unset g_node_types $i
	    array set g_node_types [list $idx $node_type_data]
	}
    }

    if { $selected_idx != "" } {
        $wi.s.nodes selection set $selected_idx
        nodesConfigSelect $wi ""
    }
}

# change a node type selection or save it to an array when cmd="save"
# this updates the edit controls with text from the array, or vice-versa
proc nodesConfigSelect { wi cmd } {
    global g_node_types g_machine_type

    set selected_idx [$wi.s.nodes curselection]
    if { $selected_idx == "" } { return }

    set idx [expr {$selected_idx + 1}]
    if { ![info exists g_node_types($idx)] } { return }

    set node_type_data $g_node_types($idx)

    if { [isDefaultNodeType [lindex $node_type_data 0]] } {
	set read_only disabled
    } else {
	set read_only normal
    }

    set i 0
    foreach item [list name icon icont meta] {
	if { $i == 3 } { incr i 2 } ;# skip services, type
	if { $cmd == "save" } { ;# save from controls
            set str [$wi.s.edit.$i.$item get]
	    set node_type_data [lreplace $node_type_data $i $i $str]
	} else { ;# write to the controls
	    $wi.s.edit.$i.$item configure -state normal
            $wi.s.edit.$i.$item delete 0 end
            $wi.s.edit.$i.$item insert 0 [lindex $node_type_data $i]
	    $wi.s.edit.$i.$item configure -state $read_only
	}
	incr i
    }

    if { $cmd == "save" } {
	set node_type_data [lreplace $node_type_data 4 4 $g_machine_type]
	array set g_node_types [list $idx $node_type_data]
	nodesConfigRefreshList $wi
    } else {
	set g_machine_type [lindex $node_type_data 4]
	nodesConfigImg $wi
    }
    nodesConfigMachineHelper $wi
}

# invoked when machine type is selected to enable/disable the profile button
proc nodesConfigMachineHelper { wi } {
    global g_machine_type g_plugins
    set cfgname "emul=$g_machine_type"
    # search plugin capabilities for support for this type of machine
    foreach p [array names g_plugins] {
	set caps [lindex $g_plugins($p) 5]
	if { [lsearch $caps $cfgname] != -1 } {
            $wi.s.edit.3.config configure -state normal
	    return
	}
    }
    $wi.s.edit.3.config configure -state disabled
}

# popup a file selection dialog for the icon filenames
proc nodesConfigImgDialog { wi ctl size } {
    global g_imageFileTypes CORE_DATA_DIR
    set dir "$CORE_DATA_DIR/icons/$size/"
    set f [tk_getOpenFile -initialdir $dir -filetypes $g_imageFileTypes ]
    if { [string first $dir $f] == 0 } {
	# chop off default path of $dir
	set f [string range $f [string length $dir] end]
    }
    if { $f != "" } {
        $ctl delete 0 end
        $ctl insert 0 $f
	if { $size == "normal" } { nodesConfigImg $wi }
    }
}

# update the node icon preview
proc nodesConfigImg { wi } {
    global CORE_DATA_DIR

    set imgf [$wi.s.edit.1.icon get]
    set dir "$CORE_DATA_DIR/icons/normal/"
    # image has no path, assume it can be found in CORE_DATA_DIR
    if { [string first "/" $imgf] < 0 } { set imgf "$dir/$imgf" }

    set c $wi.s.edit.0.c
    set cw [lindex [$c configure -width] 4]
    set ch [lindex [$c configure -height] 4]
    $wi.s.edit.0.c delete "preview"
    if { [catch { set img [image create photo -file $imgf] } e] } {
	# puts "f=$imgf err=$e"
	set pad 5
	set x1 $pad; set y2 $pad
	set x2 [expr {$cw - $pad}]; set y1 [expr {$ch - $pad}]
	$c create line $x1 $y1 $x2 $y2 -fill red -width 3 -tags "preview"
    } else {
	set x [expr {$cw / 2}]; set y [expr {$ch / 2}]
	$c create image $x $y -image $img -tags "preview"
    }

}

# helper for adding, deleting, and rearranging (up/down) node types
proc nodesConfigHelper { wi cmd } {
    global g_node_types

    set ctl $wi.s.nodes
    set idx [$ctl curselection]
    if { $idx != "" } {
	set type [$ctl get $idx]
	set arridx [getNodeTypeIndex $type]
    } elseif { $cmd != "add" } { ;# must have item selected
	return
    }
    set newsel ""

    switch -exact -- $cmd {
	add {
	    set n 1
	    set types [getNodeTypeNames]
	    while { [lsearch $types "router$n"] != -1 } { incr n }
	    set newname "router$n"
	    set arridx [expr {[array size g_node_types] + 1}]
	    set newdata $g_node_types(1) ;# copy first item
	    set newdata [lreplace $newdata 0 0 $newname]
	    set newdata [lreplace $newdata 5 5 ""] ;# zero the meta-data
	    array set g_node_types [list $arridx $newdata]
	    set newsel [expr {$arridx - 1}]
	}
	save {
	    nodesConfigSelect $wi save
	}
	del {
	    array unset g_node_types $arridx
	}
	up -
	down {
	    if {$cmd == "up" } {
		if { $arridx < 2 } { return }
		set newidx [expr {$arridx - 1}]
		set newsel [expr {$idx - 1}]
	    } else {
		if { $idx >= [expr {[$ctl size] - 1}]} { return }
		set newidx [expr {$arridx + 1}]
		set newsel [expr {$idx + 1}]
	    }
	    set newentry [lindex [array get g_node_types $arridx] 1]
	    set oldentry [lindex [array get g_node_types $newidx] 1]
	    if {$oldentry != ""} {
		array set g_node_types [list $arridx $oldentry]
	    }
	    array set g_node_types [list $newidx $newentry]
	}
    }

    nodesConfigRefreshList $wi
    if { $newsel != "" } {
	$ctl selection clear 0 end
	$ctl selection set $newsel
    }
    nodesConfigSelect $wi ""
}

# helper for services button
proc nodesConfigServices { wi services_or_profile } {
    global g_node_type_services_hint g_current_session g_machine_type
    set idx [$wi.s.nodes curselection]
    if { $idx == "" } { return }

    set g_node_type_services_hint [$wi.s.nodes get $idx]
    # use the default emulation plugin - not associated with any node
    set sock [lindex [getEmulPlugin "*"] 2]
    # node number 0 is sent, but these services are not associated with a node
    if { $services_or_profile == "profile" } {
	set services_or_profile $g_machine_type ;# address the model
	set opaque "$g_machine_type:$g_node_type_services_hint"
    } else {
	set opaque ""
    }
    sendConfRequestMessage $sock -1 $services_or_profile 0x1 -1 $opaque
}

# helper for when close button is pressed
proc nodesConfigClose { wi } {
    set missing [checkNodeTypes false]
    if { $missing != "" } {
	set msg "Missing default node type '$missing'!"
	set msg "$msg\nChanging the name of a default node type is not"
	set msg "$msg allowed."
	tk_messageBox -icon error -title "Error" -message $msg
	return
    }
    writeNodesConf
    drawToolbarSubmenu "routers" [getNodeTypeNames]
    setLeftTooltips "routers" [getNodeTypeNames]
    destroy $wi
}

# set the submenu tooltips stored in the left_tooltips array
# needs to be invoked whenever the name of a user-defined node type changes
# key = "routers", names = [getNodeTypeNames]  for node types
proc setLeftTooltips { key names } {
    global left_tooltips
    for {set i 0 } { $i < [llength $names] } { incr i } {
	if { $key != "routers" && [info exists left_tooltips($key$i)] } {
	    continue; # skip built-in buttons already defined
	}
	array set left_tooltips [list $key$i [lindex $names $i]]
    }
    if { $key == "routers" } {
	array set left_tooltips [list $key$i "edit node types"]
    }
}

# Helper for open/save buttons
# cmd is open or save; ctldata is the text control having the file data;
# ctlinitfn is the control having the initial filename
proc genericOpenSaveButtonPress { cmd ctldata ctlinitfn } {
    # get initial filename from ctlinitfn
    set fn [file tail [$ctlinitfn get]]

    if { $cmd == "save" } {
	set title "Save File Text"
	set fn [tk_getSaveFile -title $title -initialfile $fn -parent $ctldata]
	set mode "w"
	set action "writing"
    } else {
	set title "Load File Text"
	set fn [tk_getOpenFile -title $title -initialfile $fn -parent $ctldata]
	set mode "r"
	set action "loading"
    }

    # user presses cancel
    if { $fn == "" } { return }

    set r "retry"
    while { $r == "retry" } {
	if { [catch { set f [open $fn $mode] } e] } {
	    set r [tk_messageBox -type retrycancel -title "Error" \
		    -message "Error $action file $fn: $e"]
	} else {
	    set r ""
	}

    }
    if { $r == "cancel" } { return }

    if { $cmd == "save" } {
	puts $f [$ctldata get 0.0 end-1c]
    } else {
	$ctldata delete 0.0 end
	while { [gets $f line] >= 0 } {
	    $ctldata insert end "$line\n"
	}
    }
    close $f
}

#
# built-in node types
#
proc rj45.layer {}      { return LINK }
proc lanswitch.layer {} { return LINK }
proc hub.layer {}       { return LINK }
proc tunnel.layer {}    { return LINK }
proc wlan.layer {}      { return LINK }
proc OVS.layer {}       { return NETWORK }
proc router.layer {}    { return NETWORK }
proc router.shellcmd { n } { return "vtysh" }

# load the nodes.conf file when this file is loaded
loadNodesConf
