# edit.tcl
#	Logical operations on the HTML tags, things like selecting
#	a list or paragraph, finding HTML tags, etc.

# Call HMreset_edit when you are about to display a new page,
# (just after HMreset_win is called)
# initialize additional state needed for output (and editting)
proc Edit_Reset { win } {
    Mark_Reset $win
    Head_Reset $win		;# Reset header information
    Form_Reset $win
    Input_Reset $win
    Micro_Reset	$win
    Output_Reset $win
    Frame_Reset $win
}

# Determine the current node type, and nesting (for lists)
proc Edit_NodeType { win mark listVar } {
    global NodeMap
    upvar $listVar ltag	;# Inner-most list tag, if different than return value
    set nest -1
    catch {unset ltag}
    foreach fulltag [Mark_FullStack $win $mark all] {
	Mark_SplitTag $fulltag htag param
	if [info exists NodeMap($htag)] {
	    set ptag $fulltag
	} else {
	    if [IsList $htag list level] {
		if {$level > $nest} {
		    set nest $level
		    set ltag $fulltag
		}
	    }
	}
    }
    if ![info exists ptag] {
	if [info exists ltag] {
	    return $ltag
	} else {
	    return {}
	}
    }
    return $ptag
}
# Find the range that delimits the current paragraph.
proc Edit_SelectNode { win mark {ptag {}}} {
    if {$ptag == {}} {
	set ptag [Edit_NodeType $win $mark ltag]
    }
    if {$ptag == {}} {
	# Outside any node
	set range [list [$win index "$mark linestart"] \
			[$win index "$mark lineend"]]
    } else {
	set ptag H:$ptag
	set range [$win tag prevrange $ptag $mark]
	set end [lindex $range 1]
	if {[llength $range] == 0 || [$win compare $end < $mark]} {
	    # This occurs when the mark is at the very beginning of the node
	    set range [$win tag nextrange $ptag $mark]
	    if {[llength $range] == 0 ||
		    [$win compare $mark < [lindex $range 0]]} {
		# This occurs with null nodes indicated by a mark
		foreach m [Mark_Find $win] {
		    if {[string compare [Mark_Htag $win $m] $ptag] == 0} {
			set range [list $m $m]
		    }
		}
		if {[llength $range] == 0} {
		    # Something is bogus
		    Stderr "Broken Edit_SelectNode $mark $ptag"
		    set range [list [$win index "$mark linestart"] \
				 [$win index "$mark lineend"]]
		 }
	    }
	}
    }
    lappend range $ptag
    return $range
}

proc Edit_BeginNode { win mark ptag } {
    return [lindex [Edit_SelectNode $win $mark $ptag] 0]
}
proc Edit_EndNode { win mark ptag nest } {
    return [lindex [Edit_SelectNode $win $mark $ptag] 1]
}

proc Edit_CurrentRange { win tag mark } {
    set range [$win tag prevrange $tag $mark]
    set end [lindex $range 1]
    if {[llength $range] == 0 || [$win compare $end < $mark]} {
	# This occurs when the mark is at the very beginning of the node
	set range [$win tag nextrange $tag $mark]
	if {[llength $range] == 0 ||
		[$win compare $mark < [lindex $range 0]]} {
	    return {}
	}
    }
    return $range
}

# Like node_type, but restricted to lists
proc Edit_ListType { win mark nestVar } {
    upvar $nestVar nest
    set ltag {}
    set nest 0
    foreach h [Mark_Stack $win $mark] {
	if [IsList $h list level] {
	    if {$level > $nest} {
		set nest $level
		set ltag $h
	    }
	}
    }
    return $ltag
}
# Support for list selection - Edit_ListType has already been called
proc Edit_SelectList { win mark ltag } {
    return [Edit_SelectNode $win $mark $ltag]
}

proc EditRefreshNode {win mark} {
    lassign {m1 m2} [Edit_SelectNode $win $mark]
    Edit_RefreshRange $win $m1 $m2
} 
# Refresh Sel is called after adding/removing a style-like HTML tag
# how should be "nostyle" after adding a style tag
# how should be "force" after removing a style tag
proc Edit_RefreshStyle {win how} {
    set end [$win index sel.last]
    set beg [$win index sel.first]
    Mark_ReadTags $win "insert" $how
    upvar #0 HM$win var
    Edit_RefreshRange $win $beg $end noreadtags
    Text_TagAdd $win sel $beg $end
}

proc Edit_RefreshRange {win m1 m2 {readtags readtags}} {
    upvar #0 HM$win var
    if {! [info exists var(S_insertSaved)] &&
	    [$win compare $m1 <= insert] && [$win compare $m2 > insert]} {
	Input_SaveInsert $win
    }
    Text_MarkSet $win insert $m1 right
    set html [Output_string $win insert $m2]
    Mark_RemoveAll $win insert $m2
    Mark_RightGravity $win $m2		;# So things here get pushed along
    Text_Delete $win insert $m2
    if {[string compare $readtags "readtags"] == 0} {
	if [$win compare insert == 1.0] {
	    Mark_ResetTags $win
	} else {
	    Mark_ReadTags $win "insert" force
	}
    }
    if [$win compare insert == "insert linestart"] {
	set var(newline) 1
	set var(trimspace) 1
    } else {
	catch {unset var(newline)}
	catch {unset var(trimspace)}
    }
    Input_Html $win $html
    Mark_LeftGravity $win insert	;# Undo the right gravity
    Input_RestoreInsert $win
    Input_Update $win
}

# Cut html and return it
proc Edit_CutRange {win m1 m2} {
    set m1 [$win index $m1]
    set m2 [$win index $m2]
    set html [Output_string $win $m1 $m2]
    Mark_RemoveAll $win $m1 $m2
    Text_Delete $win $m1 $m2
    return $html
}
proc lassign {varList value} {
    if {[string length $value] == 0} {
	foreach var $varList {
	    uplevel [list set $var {}]
	}
    } else {
	uplevel [list foreach $varList $value { break }]
    }
}
proc Edit_ChangeNode {win mark htag} {
    lassign {m1 m2 ohtag} [Edit_SelectNode $win $mark]
    LogBegin $win Edit_ChangeNode $mark $htag $m1 $m2 $ohtag
    if [$win compare $m1 == $m2] {
	# Set up state to insert things of the right node type
	global HMtag_map
	catch {HMstack $win $HMtag_map($htag)}
	upvar #0 HM$win var
	set var(T,$htag) [list H:$htag]
	return
    } else {
	# Normal node labeled with a tag
	if {"$ohtag" != ""} {
	    if [IsList $ohtag x y] {
		Status $win "Use List menu operations, please"
		return
	    }
	    Text_TagRemove $win $ohtag $m1 $m2
	}
	Text_TagAdd $win H:$htag $m1 $m2
    }
    Edit_RefreshRange $win $m1 $m2
    LogEnd $win
    return
}

proc Edit_ClearNode {win mark} {
    set marks [Edit_SelectNode $win $mark]
    set m1 [lindex $marks 0]
    set m2 [lindex $marks 1]
    set ohtag [lindex $marks 2]
    if {[string length $ohtag] == 0} {
	return
    }
    LogBegin $win Edit_ClearNode
    if [$win compare $m1 == $m2] {
	# Null node indicated by a mark
	Mark_Remove $win $m1
    } else {
	# Normal node labeled with a tag
	Text_TagRemove $win $ohtag $m1 $m2
    }
    # Adjust the node-delimiting marks back and forwards to span the
    # decorative white-space that bounds the node.  This will go away.

    $win mark set m:change_node $m1
    Input_AdjustBack $win m:change_node
    $win mark set m:change_node2 "$m2 +1c"
    Input_AdjustForw $win m:change_node2
    Edit_RefreshRange $win m:change_node m:change_node2
    Input_Dirty $win
    LogEnd $win
    return
}

proc Edit_ClearTag {win tag mark} {
    lassign {m1 m2} [Edit_CurrentRange $win $tag $mark]
    if {[string length $m1] == 0} {
	return
    }
    Undo_Mark $win Edit_ClearTag
    Text_TagRemove $win $tag $m1 $m2
    Edit_RefreshRange $win $m1 $m2
    Input_Dirty $win
    Undo_Mark $win Edit_ClearEnd
    return
}
proc Edit_ChangeTag {win tag mark newtag} {
    lassign {m1 m2} [Edit_CurrentRange $win $tag $mark]
    if {[string length $m1] == 0} {
	return
    }
    Undo_Mark $win Edit_ChangeTag
    Text_TagRemove $win $tag $m1 $m2
    Text_TagAdd $win $newtag $m1 $m2
    Edit_RefreshRange $win $m1 $m2
    Input_Dirty $win
    Undo_Mark $win Edit_ChangeEnd
    return
}

# Backspace and Delete

proc Edit_Delete {win} {
    set how force
    if {[InputRemoveSel? $win]} {
	return
    }
    Undo_Mark $win Edit_Delete
    if [$win compare insert < end-1c] {
	set tmp m:Edit_Delete
	$win mark set $tmp "insert +1c"
	Input_AdjustForw $win $tmp
	$win mark gravity $tmp left
	Edit_RemoveHtml $win insert $tmp opt
	$win mark unset $tmp 
	set how cache
    }
    Input_Update $win $how
    Undo_Mark $win Edit_DeleteEnd
}
proc Edit_Backspace {win} {
    set how force
    if {[InputRemoveSel? $win]} {
	return
    }
    Undo_Mark $win Edit_Backspace
    if [$win compare insert != 1.0] {
	set tmp m:Edit_Backspace
	$win mark set $tmp insert-1c
	Input_Adjust $win $tmp
	Edit_RemoveHtml $win $tmp insert opt
	$win mark unset $tmp
	set how cache
    }
    Input_Update $win $how
    Undo_Mark $win Edit_BackspaceEnd
}

# Remove HTML from m1 to m2.

proc Edit_RemoveHtml {win m1 m2 {opt {}} } {
    set ptag1 [Edit_NodeType $win $m1 ltag1 ]
    set ptag2 [Edit_NodeType $win $m2 ltag2 ]
    $win mark set m:Edit_RemoveHtml $m1
    set action {}
    dputs p1 $ptag1 p2 $ptag2
    if {[info exists ltag1] != [info exists ltag2]} {
	# Keep list and non-list disjoint
	set action split
    } elseif {[info exists ltag1]} {
	IsList $ltag1 l1 nest1
	IsList $ltag2 l2 nest2
	dputs l1 $l1 $nest1 l2 $l2 $nest2
	if {[string compare $l1 $l2] != 0 || ($nest1 != $nest2)} {
	    # Fixup list element
	    ListSelect $win $m2
	    lassign {one two} [ListSelectItem $win $m2 H:$ptag2]
	    if {[string length $one]} {
		dputs List Item $one $two
		Text_TagRemove $win H:$ptag2 $one $two
		Text_TagAdd $win H:$ptag1 $one $two
		$win mark set m:Edit_RemoveHtml $two
	    } else {
		dputs Cannot find list item at $m2
		$win mark set m:Edit_RemoveHtml $m2
	    }
	    set refresh 1
	}
    } elseif {[string compare $ptag1 $ptag2] != 0} {
	# Merge non-list nodes together
	# Convert node2 to match node1
	set info2 [Edit_SelectNode $win $m2 $ptag2]
	set endp2 [lindex $info2 1]
	if {[$win compare $endp2 > $m2]} {
	    Text_TagRemove $win H:$ptag2 $m2 $endp2
	    Text_TagAdd $win H:$ptag1 $m2 $endp2
	    $win mark set m:Edit_RemoveHtml2 $endp2
	    set refresh 1
	}
    }
    # Do nothing if backspacing at the beginning of a list
    if {$opt == "opt" && $action == "split"} {
        $win mark unset m:Edit_RemoveHtml
	return
    }
    Mark_RemoveAll $win $m1 $m2
    Text_Delete $win $m1 $m2
    if {$action == "split"} {
	if [info exists ltag1] {
	    if [regexp dl $ltag1] {
		Input_Html $win <dt>
	    } else {
		Input_Html $win <li>
	    }
	}
	if {[$win compare m:Edit_RemoveHtml != "m:Edit_RemoveHtml linestart"]} {
	    Text_Insert $win m:Edit_RemoveHtml \n space
	}
#	List_Refresh $win	;# This trashes ul tag
    } elseif {[info exists refresh]} {
	if [info exists ltag1] {
	    List_Refresh $win m:Edit_RemoveHtml
	} else {
	    Edit_RefreshRange $win m:Edit_RemoveHtml m:Edit_RemoveHtml2
	    $win mark unset m:Edit_RemoveHtml2
	}
    }
    $win mark unset m:Edit_RemoveHtml
}


# Insert a block of HTML (e.g., paste)
# Care must be taken to ensure "balanced" input.
# The node where the insert takes place may also need to be modified.
#
# The cases are a cross-product of:
# Insert location - middle of a node or between two nodes
# Html balance - whole nodes, partial leading node, partial trailing node

proc Edit_PasteHtml {win html} {
    global NodeMap Unstyle_Map ListMap SingletonMap
    upvar #0 HM$win var

    # Scan input html for unopened and unclosed Html tags

    Feedback $win paste
    Log $win Edit_PasteHtml $html

    set state(stack) {}
    set state(opentags) {}
    set state(nodes) {}
    set state(ltags) {}
    HMparse_html $html [list EditBalance $win state] {}
    set state(has_nodes) [llength $state(nodes)]
    set state(has_lists) [llength $state(ltags)]

    # Close style tags, and record open node (e.g., <p>) tags.

    set close {}
    set unclosed ""
    foreach htag $state(stack) {
	if {[info exists SingletonMap($htag)]} {
	    # Ignore these
	    continue
	} elseif ![info exists NodeMap($htag)] {
	    append close </$htag>	;# Always close style
	} else {
	    lappend unclosed $htag	
	}
    }
    # Open style tags, remember unopened node tags.

    set open {}		;# New open tags, e.g. </b> needs a <b>
    set unopened {}	;# Close node tags that may be changed, e.g., </p>
    set nukeme {}	;# Unopened tags to delete, e.g., </a>
    foreach htag $state(opentags) {
	if [info exists NodeMap($htag)] {
	    lappend unopened $htag
	} elseif ![info exists Unstyle_Map($htag)] {
	    append open <$htag>
	} else {
	    # Nuke tags that cannot be reopened
	    Log $win delete </$htag>
	    regsub -nocase </$htag> $html {} html
	}
    }

    # Determine if insert is at
    # 0) the middle of a node,
    # 1) the begining of a node
    # 2) the end of a node
    # 3) a null node (e.g., <p></p>)

    lassign {m1 m2 ptag} [Edit_SelectNode $win insert]
    regsub ^H: $ptag {} ptag
    set insert_state 0
    set insert_begin 1
    set insert_end   2
    set insert_null  3
    if {[$win compare insert == $m1]} {
	set insert_state $insert_begin
    }
    if {[$win compare insert == $m2] ||
	(([string compare [$win get insert] "\n"] == 0) &&
	 [$win compare "insert +1 c" == $m2])} {
	set insert_state [expr {$insert_state | $insert_end}]
	set node_end_mark $m2
    }
    if {($insert_state == $insert_null) &&
	[$win compare $m1 != $m2]} {
	# Collapse marks into one point
	Text_Delete $win $m1 $m2
    }

    # Set gravity of underlying HTML tags
    Mark_AdjustGravity $win			;# do the right thing


    # Figure out how to insert nodes, including partials.

    set refresh 0
    if {$state(has_nodes)} {
	if {[llength $unopened] == 0} {
	    # Html begins with a node
	    set refresh 1
	    switch -- $insert_state {
		0 { ;# at Mid-node
		    # Break current node
		    if [string length $ptag] {
			set open </$ptag>$open
		    }
		    # This is probably wrong if pasting a list
		    # into a heading node 			FIX ME
		    Mark_ReadTags $win insert
		}
		1 { ;# at Begin node
dputs insert=[$win index insert]
		    Mark_RightGravity $win		;# Push node along
							;# Go back to the end
		    Input_AdjustBack $win insert 	;# of previous node
		    set insert_state $insert_end
dputs insert=[$win index insert]
		    Mark_LeftGravity $win		;# and leave it behind
		    Mark_ResetTags $win
		}
		2 { ;# at End node
		    Mark_LeftGravity $win
		    Mark_ResetTags $win
		}
		3 { ;# At Null node
		    if [string length $ptag] {
			Mark_Remove $win $node_end_mark
			$win mark set m:null_node insert
			Input_AdjustBack $win m:null_node
			$win delete m:null_node insert
			Mark_LeftGravity $win
    dputs null node insert=[$win index insert]
			Mark_ResetTags $win
			set refresh 1	;# Null node might be different type
		    } else {
			# Empty document
		    }
		}
	    }
	} else {
	    # HTML begins with unopened nodes.
	    # In most cases change the unopened node to match the
	    # type it is being inserted into.

	    set first [lindex $unopened 0]
	    foreach htag [lrange $unopened 1 end] {
		set open <$htag>$open
	    }
	    switch -- $insert_state {
		0 -
		1 -
		2 { ;# Begin, Mid and End node
		    if [string length $ptag] {
			Log $win convert </$first> to </$ptag>
			regsub -nocase </$first> $html </$ptag> html
		    } else {
			Log $win delete </$first>
			regsub -nocase </$first> $html {} html
		    }
		}
		3 { ;# At Null node
		    Mark_Remove $win $node_end_mark
		    $win mark set m:null_node insert
		    Input_AdjustBack $win m:null_node
		    $win delete m:null_node insert
		    Mark_LeftGravity $win
		    set open <$first>$open
		    set refresh 1	;# Null node might be different type
		}
	    }
	    Mark_ReadTags $win insert
	}
	if {[llength $unclosed] == 0} {
	    # Html ends at a node boundary
	    switch -- $insert_state {
		1 -
		0 { ;# at Mid-node or begin - re-open original node
		    append close <$ptag>
		    if [IsList $ptag z1 z2] {
			if [regexp "dl" $ptag] {
			    append close <dt>
			} else {
			    append close <li>
			}
		    }
		}
		2 -
		3 { ;# At End or Null node
		    # Do nothing
		}
	    }
	} else {
	    # HTML ends with an unclosed node.
	    # Either change the type of the node being inserted into,
	    # or just close the node.
	    set first [lindex $unclosed 0]
	    foreach htag [lrange $unclosed 1 end] {
		append close </$htag>
	    }
	    switch -- $insert_state {
		0 -
		1 { ;# at Begin or Middle of node
		    if [string length $ptag] {
			Text_TagRemove $win H:$ptag insert $m2
		    }
		    Text_TagAdd $win H:$first insert $m2
		    set refresh 1
		}
		2 -
		3 { ;# At Null node or End of node
		    append close </$first>
		}
	    }
	}
    } else {
	# There are no node-begin HTML tags, but some stray node-close tags.
	foreach htag $unopened {
	    Log $win delete </$htag>
	    regsub -nocase </$htag> $html {} html
	}
	Mark_ReadTags $win insert
    }
dputs $open$html$close

    Log $win Edit_PasteHtmlOpen $open
    Log $win Edit_PasteHtmlClose $close

    set var(pasteMode) 1		;# For special base and link handling
    Input_Html $win $open$html$close
    unset var(pasteMode)
    catch {unset var(base)}
    Input_Dirty $win
    if {$refresh} {
	if [IsList $ptag z1 z2] {
	    List_Refresh $win insert
	} else {
	    EditRefreshNode $win insert
	}
    }
    Input_Adjust $win insert
    Feedback $win ready
}
# Determine if a glob of html has a balanced set of tags.
# Return in the state variable the unopened tags (opentags)
# and the unclosed tags (stack)

proc EditBalance {win stateVar htag not param text} {
    global NodeMap
    upvar 2 $stateVar state	;# Edit_PasteHtml -> HMparse_html -> EditBalance
    if {"$not" == ""} {
	lappend state(stack) $htag 
	if [info exists NodeMap($htag)] {
	    lappend state(nodes) $htag
	}
	if [IsList $htag z1 z2] {
	    lappend state(ltags) $htag
	}
    } else {
	set ix [lsearch $state(stack) $htag]
	if {$ix < 0} {
	    #underflow - missing open.
	    lappend state(opentags) $htag
	} else {
	    # Pop the open tag
	    set state(stack) [lreplace $state(stack) $ix $ix]
	    if [regexp {^(ol|ul|menu|dir|dl)^} $htag] {
		# Pop list items off the stack, too.
		set dl [regexp {^dl$} $htag]
		while 1 {
		    set ltag [lindex $state(stack) $ix]
		    if {($dl && [regexp {^(dt|dd)$} $ltag]) ||
			(!$dl && [string compare $ltag "li"] == 0)} {
			set state(stack) [lreplace $state(stack) $ix $ix]
		    } else {
			break
		    }
		}
	    } elseif [regexp {^form$} $htag] {
		# Pop form elements off the stack, too.
		while 1 {
		    set ftag [lindex $state(stack) $ix]
		    if [regexp {^(input|select|option|textarea)$} $ftag] {
			set state(stack) [lreplace $state(stack) $ix $ix]
		    } else {
			break
		    }
		}
	    }
	}
    }
}
proc Edit_ConvertPlainText {text} {
    # Convert blank lines to <p> 
    # and map HTML special characters to escape sequences
    regsub -all "\n(\[ \t]*\n)+" [HMmap_code $text] <p> html
    return $html
}

