#!/bin/sh
# Restart with wish\
exec wish "${0}" "${@}"

# TODO:
# - Merge into OV
# - Add balloon and nameplate
#   - Make them positionable by drag'n'drop and arrow keys
#   - Show numeric coordinates in editable panel widgets
#   - Offer checkboxes to temporarily hide each
# - Make key bindings user-configurable, VILE
# - Make .aved a toplevel
# - Fix size of panel to fit a set number of frames across platforms
# - Offer a different look for Windows
# - Actually copy images to the user's images directory
# - Provide load and save capability
# - Do translations
# - Make preview background colors configurable
# WISHLIST:
# - Offer a hierarchial avatar menu
# - Offer avatar thumbnails

source ov-share.tcl
lappend auto_path [file nativename "[pwd]/BWidget-1.3.1"]
package require BWidget

set MV(homedir) "~/.OpenVerse"

# Load images
image create photo "img::up" -file "up.gif"
image create photo "img::down" -file "down.gif"
image create photo "img::new" -file "new.gif"
image create photo "img::del" -file "del.gif"
image create photo "img::browse" -file "browse.gif"
image create photo "img::hand" -file "hand.gif"
image create photo "img::yes" -file "yes.gif"
image create photo "img::no" -file "no.gif"
image create photo "img::play" -file "play.gif"
image create photo "img::pause" -file "pause.gif"
image create photo "img::file_not_found" -file "file_not_found.gif"
image create photo "img::bad_file" -file "bad_file.gif"

# TODO: make this user-configurable, VILE; do this at OV start
event add <<AvEd-Browse>> <Control-o> <Control-O>
event add <<AvEd-Prev>> <Key-Prior>
event add <<AvEd-Next>> <Key-Next>
#event add <<AvEd-First>>
#event add <<AvEd-Last>>
#event add <<AvEd-PgUp>>
#event add <<AvEd-PgDn>>
event add <<AvEd-New>> <Key-Return>
event add <<AvEd-Del>> <Control-Delete>

namespace eval aved {
variable num_frames 0	;# Number of frames in current animation
variable sel_frame 0	;# Index of currently selected frame
variable frames		;# Dynamic array of frame information
variable animated 0	;# Is this avatar animated?
variable bg_color 0	;# Index of preview color in use
variable next_image 0	;# Counter for next preview image to create
variable playback 0	;# Is the avatar being played right now?
variable playback_id	;# After id for playback process

variable balloon 0	;# Is the balloon being shown?
variable balloon_x 0	;# X coordinate of speech balloon
variable balloon_y 0	;# Y coordinate of speech balloon

variable name 0		;# Is the nameplate being shown?
variable name_x 0	;# X coordinate of nameplate
variable name_y 0	;# Y coordinate of nameplate

variable anim_frame ""	;# Frame to pack animation frames in

# Default path for loading avatar images
variable default_path [file nativename "$MV(homedir)/images"]

# List of colors to use for avatar preview background
variable colors {"gray" "black" "white" "navy"}
}

# --- ANIMATION EDITOR --------------------------------------------------------
# Highlight frame $idx with $color
proc aved::set_frame_color {idx color} {
	variable anim_frame
	foreach elem {".hand" ".num" ".img" ".dur" ".ms" ""} {
		$anim_frame.${idx}$elem configure -bg $color
	}
}

# Show selection (or lack thereof, depending on $flag) for frame $idx
proc aved::show_selection {idx flag} {
	variable sel_frame

	# This uses a dirty hack to hide or show the hand...
	set tmp $sel_frame
	if $flag {
		set sel_frame $idx
		verify_image $idx -trust-cache
		set_frame_color $idx lightblue
	} else {
		set sel_frame -1
		verify_image $idx -trust-cache
		set_frame_color $idx gray
	}
	set sel_frame $tmp
}

# Make $idx the active frame
proc aved::select_frame {idx args} {
	variable num_frames
	variable frames
	variable sel_frame
	variable anim_frame

	# Clip $idx to available frames
	if {$idx < 0} {
		set idx 0
	} elseif {$idx >= $num_frames} {
		set idx [expr $num_frames - 1]
	}

	# Adjust focus (focus handler will do the rest)
	set cur_focus [focus -displayof .]
	if [string match "$anim_frame.*" $cur_focus] {
		set part [string range $cur_focus [expr [string last .\
				$cur_focus] + 1] end]
	} else {
		set part "img"
	}
	if {"$anim_frame.$sel_frame.$part" == $cur_focus && [lindex $args 0] ==\
			"-force"} {
		handle_focus $idx
	} else {
		focus $anim_frame.$idx.$part
	}
}

# Handle focus on frame in visible slot $idx
proc aved::handle_focus {idx} {
	variable num_frames
	variable sel_frame
	variable frames
	variable anim_frame

	# Scroll to the frame if necessary
	.aved.anim see "$anim_frame.$idx"

	# Update preview image
	verify_image $idx -vcmd $frames($idx.img)

	# Adjust colors and hand icon for current and previous frames
	if {$sel_frame != $idx} {
		if {$sel_frame < $num_frames} {show_selection $sel_frame 0}
		show_selection $idx 1
		set sel_frame $idx
	}
}

# Add a new frame widget to the list
proc aved::create_frame_widget {idx} {
	variable sel_frame
	variable anim_frame

	# Create the gui elements
	set path "$anim_frame.$idx"
	frame $path -bd 1 -relief sunken
	label $path.hand -bd 0 -width 0
	entry $path.img -highlightthickness 0 -bd 0 -width 1 -validate key\
			-vcmd "aved::verify_image $idx -vcmd %P" -textvariable\
			"aved::frames($idx.img)"
	entry $path.dur -highlightthickness 0 -bd 0 -width 5 -justify right\
			-textvariable "aved::frames($idx.dur)" -vcmd\
			"aved::set_button_states %P" -validate key
	label $path.num -bd 0 -justify right -width 0 -text "[expr $idx + 1]."\
			-font [$path.img cget -font]
	label $path.ms -bd 0 -text "ms" -width 2 -font [$path.img cget -font]
	bind $path <FocusIn> "aved::handle_focus $idx"
	show_selection $idx [expr $idx == $sel_frame]
	verify_image $idx -trust-cache

	# Put together the frame
	pack $path.hand $path.num -side left
	pack $path.img -side left -fill x -expand 1
	pack $path.dur $path.ms -side left

	# Pack it
	if {$idx == 0} {
		pack $path -fill x
	} else {
		pack $path -fill x -after "$anim_frame.[expr $idx - 1]"
	}
}

# Insert a new frame immediately after active frame
# Also used to initialize the very first frame
proc aved::new_frame {args} {
	variable num_frames
	variable sel_frame
	variable frames
	variable next_image
	variable animated
	variable anim_frame

	if {$args == "-init"} {
		set idx 0
		set frames($idx.dur) 100
	} else {
		set idx [expr $sel_frame + 1]
		for {set i $num_frames} {$i > $idx} {incr i -1} {
			set j [expr $i - 1]
			foreach elem {"img" "dur" "good" "img_id"} {
				set frames($i.$elem) $frames($j.$elem)
			}
			if {$i < $num_frames} {verify_image $i -trust-cache}
		}
		set frames($idx.dur) $frames($sel_frame.dur)
	}
	set frames($idx.img) ""
	set frames($idx.good) 0
	set frames($idx.img_id) "img::aved::$next_image"
	image create photo $frames($idx.img_id)
	incr next_image
	incr num_frames

	# Create, configure, and pack the gui elements as needed
	if $animated {
		create_frame_widget [expr $num_frames - 1]
		update
		select_frame $idx
	}
}

# Delete active frame
proc aved::del_frame {} {
	variable num_frames
	variable sel_frame
	variable frames
	variable anim_frame

	if {$num_frames == 1} {return}

	# Shift frames back to cover deleted one
	set img $frames($sel_frame.img_id)
	for {set i $sel_frame} {$i < [expr $num_frames - 1]} {incr i} {
		set j [expr $i + 1]
		foreach elem {"img" "dur" "good" "img_id"} {
			set frames($i.$elem) $frames($j.$elem)
		}
		verify_image $i -trust-cache
	}

	# Destroy final frame widget and the no-longer-needed image
	destroy "$anim_frame.[expr $num_frames - 1]"
	image delete $img
	
	# Remove array entry for final frame
	incr num_frames -1
	array unset frames "$num_frames.*"

	# Highlight new current frame
	update
	select_frame $sel_frame -force
}

# Swap the active frame and its $adj'th neighbor
proc aved::move_frame {adj} {
	variable num_frames
	variable sel_frame
	variable frames

	set j [expr $sel_frame + $adj]
	if {$j < 0 || $j >= $num_frames} {return}

	foreach elem {"img" "dur" "img_id"} {
		swap frames($sel_frame.$elem) frames($j.$elem)
	}

	select_frame $j
}

# Start or stop playback of animation
proc aved::toggle_play {} {
	variable sel_frame
	variable playback
	variable playback_id
	variable frames

	if $playback {
		set playback 0
		.aved.funcs.anim.play configure -image "img::play"
		after cancel $playback_id
		.aved.preview itemconfigure img -image\
				$frames($sel_frame.img_id)
	} else {
		set playback 1
		.aved.funcs.anim.play configure -image "img::pause"
		next_frame 0
	}
}

# Go to the next frame of an animation
proc aved::next_frame {idx} {
	variable frames
	variable playback_id
	variable num_frames
	
	# Show current frame
	if {$idx >= $num_frames} {set idx 0}
	.aved.preview itemconfigure img -image $frames($idx.img_id)
	update

	# Arrage for next frame to be shown
	set playback_id [after $frames($idx.dur) "aved::next_frame\
			[expr $idx + 1]"]
}

# Create the animation panel
proc aved::create_anim_panel {} {
	variable frames
	variable anim_frame

	ScrollableFrame .aved.anim -constrainedwidth 1 -yscrollcommand\
			".aved.scroll set"
	frame .aved.funcs.anim
	frame .aved.funcs.anim.buttons
	foreach {elem func} {"up" "move_frame -1" "down" "move_frame 1" 
			"new" "new_frame" "del" "del_frame"
			"browse" "pick_image" "play" "toggle_play"} {
		button .aved.funcs.anim.$elem -image "img::$elem" -bd 1\
				-highlightthickness 0 -command "aved::$func"
		pack .aved.funcs.anim.$elem -in .aved.funcs.anim.buttons\
				-side top -fill y -expand 1
	}
	scrollbar .aved.scroll -highlightthickness 0 -bd 0 -relief flat\
			-orient vertical -command ".aved.anim yview"\
			-elementborderwidth 1
	pack .aved.scroll -in .aved.funcs.anim -side left -fill y
	pack .aved.funcs.anim.buttons -side right -fill y -expand 1
	set anim_frame [.aved.anim getframe]
	create_frame_widget 0
}

# Display the animation editor panel
proc aved::show_anim_panel {} {
	pack .aved.funcs.anim -side left -fill y
	pack .aved.anim -in .aved.edit -side left -expand 1 -fill both

	select_frame 0

	# Some key bindings...
	# TODO: make these bind .aved
	bind . <<AvEd-Prev>> {aved::select_frame [expr $aved::sel_frame - 1]}
	bind . <<AvEd-Next>> {aved::select_frame [expr $aved::sel_frame + 1]}
	bind . <<AvEd-First>> {aved::select_frame 0}
	bind . <<AvEd-Last>> {aved::select_frame [expr $aved::num_frames - 1]}
	bind . <<AvEd-PgUp>> {.aved.scroll scroll -1 pages}
	bind . <<AvEd-PgDn>> {.aved.scroll scroll 1 pages}
	bind . <<AvEd-New>> {aved::new_frame}
	bind . <<AvEd-Del>> {aved::del_frame}
	foreach ev {"Prev" "Next" "First" "Last" "PgUp" "PgDn" "New" "Del"} {
		bind . <<AvEd-$ev>> +break
	}
}

# Hide the animaion panel
proc aved::hide_anim_panel {} {
	variable num_frames
	variable playback

	if $playback {toggle_play}

	pack forget .aved.funcs.anim
	pack forget .aved.anim

	foreach ev {"Prev" "Next" "First" "Last" "PgUp" "PgDn" "New" "Del"} {
		bind . <<AvEd-$ev>>
	}
}

# --- STILL EDITOR ------------------------------------------------------------
# Create still avatar editor panel
proc aved::create_still_panel {} {
	frame .aved.still
	label .aved.still.desc -text "GIF Filename:"
	frame .aved.still.edit -bd 1 -relief sunken
	entry .aved.still.edit.img -highlightthickness 0 -bd 0\
			-textvariable "aved::frames(0.img)" -validate key\
			-vcmd "aved::verify_image 0 -vcmd %P" 
	button .aved.still.edit.browse -image "img::browse" -bd 1\
			-highlightthickness 0 -command "aved::pick_image"
	pack .aved.still.edit.img -side left -fill both -expand 1
	pack .aved.still.edit.browse -side right
	pack .aved.still.desc .aved.still.edit -fill x -expand 1
}

# Display the still panel
proc aved::show_still_panel {} {
	pack .aved.still -in .aved.edit -fill x -expand 1
	bind . <<AvEd-Browse>> {aved::pick_image; break}
	focus .aved.still.edit.img
	.aved.still.edit.img icursor end
}

# Hide the still panel
proc aved::hide_still_panel {} {
	pack forget .aved.still
	bind . <<AvEd-Browse>>
}

# --- COMMON ------------------------------------------------------------------
# Create common AvEd panel
# TODO: offer a slightly different look tailored for Windows
# (make this a global OV setting, perhaps, along with colors 'n stuff)
proc aved::create_common_panel {} {
	variable colors
	variable bg_color

	# Divider between top and bottom
	PanedWindow .aved.div -side left

	# Background colors
	frame .aved.bg
	Label .aved.bg.bg -text "BG:" -bd 0
	pack .aved.bg.bg -side left -fill x -expand 1
	set i 0
	foreach {name} $colors {
		radiobutton .aved.bg.$name -text [string totitle $name]\
				-command "aved::set_background" -value $i\
				-variable "aved::bg_color" -bd 1
		pack .aved.bg.$name -side left -fill x -expand 1
		incr i
	}

	# Preview canvas
	frame .aved.pframe -bd 1 -relief sunken
	canvas .aved.preview -highlightthickness 0 -bd 0 -width 0 -height 0
	.aved.preview create image 0 0 -tag img
	set_background
	bind .aved.preview <Configure> "aved::adjust_preview"
	pack .aved.preview -in .aved.pframe -fill both -expand 1

	# Pack top part of window
	set path [.aved.div add -weight 2]
	pack .aved.bg -in $path -side top -fill x
	pack .aved.pframe -in $path -side bottom -fill both -expand 1

	# Bottom part of window
	frame .aved.edit

	# Balloon and nameplate coordinates
	frame .aved.coords
	foreach elem {"Balloon" "Name"} {
		set var "aved::[string tolower $elem]"
		set path ".aved.coords.[string tolower $elem]"
		frame $path
		checkbutton $path.b -text $elem -variable $var -bd 1
		label $path.l -text "("
		entry $path.x -textvariable "${var}_x" -width 3 -bd 0\
				-highlightthickness 0 -justify right
		label $path.c -text ","
		entry $path.y -textvariable "${var}_y" -width 3 -bd 0\
				-highlightthickness 0 -justify right
		label $path.r -text ")"
		foreach elem2 {"b" "l" "x" "c" "y" "r"} {
			pack $path.$elem2 -side left
		}
		pack $path -side left -fill x -expand 1
	}

	# Editor buttons
	frame .aved.funcs -bd 1 -relief sunken
	frame .aved.buttons -width 70
	set rely 0.0
	foreach {type name func} {
			"button" "load" ""
			"button" "save" ""
			"button" "close" "exit"
			"checkbutton" "anim" "aved::toggle_anim"} {
		$type .aved.buttons.$name -text [string totitle $name]\
				-command $func -highlightthickness -0 -bd 1
		place .aved.buttons.$name -relx 0.0 -rely $rely -relwidth 1.0\
				-relheight 0.25
		set rely [expr $rely + 0.25]
	}
	.aved.buttons.anim configure -variable "aved::animated"
	pack .aved.buttons -in .aved.funcs -side right -fill y
	pack .aved.funcs -in .aved.edit -side right -fill y

	# Pack bottom part
	set path [.aved.div add -minsize 145 -weight 1]
	pack .aved.coords -in $path -fill x
	pack .aved.edit -in $path -fill both -expand 1
}

# Show the common parts of the avatar editor panel
proc aved::show_common_panel {} {
	variable frames
	variable sel_frame

	pack .aved.div -fill both -expand 1 -padx 3 -pady 3
	.aved.preview itemconfigure img -image $frames($sel_frame.img_id)
}

# Switch between still and animated avatar mode
proc aved::toggle_anim {} {
	variable animated
	variable frames
	if $animated {
		hide_still_panel
		show_anim_panel
	} else {
		select_frame 0
		hide_anim_panel
		show_still_panel
	}
}

# Change the color of the preview canvas
proc aved::set_background {} {
	variable bg_color
	variable colors

	.aved.preview configure -bg [lindex $colors $bg_color]
}

# Relocate the preview contents to be in the canvas center
# TODO: move balloon and nameplate
proc aved::adjust_preview {} {
	set x1 [lindex [.aved.preview coords img] 0]
	set y1 [lindex [.aved.preview coords img] 1]
	set x2 [expr [winfo width .aved.preview] / 2]
	set y2 [expr [winfo height .aved.preview] / 2]
	.aved.preview move img [expr $x2 - $x1] [expr $y2 - $y1]
}

# Allow user to pick a file from a browser; save as active frame
proc aved::pick_image {} {
	variable sel_frame
	variable frames
	variable default_path
	variable animated
	variable anim_frame

	set file [tk_getOpenFile -initialdir $default_path -filetypes {
			{"GIF Image Files"	{.gif .GIF .Gif}}
			{"GIF Image Files"	{}	"GIFF"	}
			{"All Files"		*		}}]

	if {$file == ""} return

	# Replace spaces with underscores
	set newname [string_map {" " "_"} [file tail $file]]
	
	# TODO: enable this
	# Copy the selected file to the user's images directory, if necessary
	#if {![file exists "$MV(homedir)/images/$newname"] ||
	#		[file size "$MV(homedir)/images/$newname"] !=
	#		[file size $file]} {
	#	file copy -force $file "$MV(homedir)/images/$newname"
	#}

	set default_path [file dirname $file]

	set frames($sel_frame.img) $newname
	if $animated {
		set widget "$anim_frame.$sel_frame.img"
	} else {
		set widget ".aved.still.edit.img"
	}
	focus $widget
	$widget icursor end
}

# Checks the "good" cache to determine whether to enable save and playback
proc aved::set_button_states {args} {
	variable frames
	variable num_frames
	variable sel_frame
	variable animated
	variable playback

	set save_state "normal"
	for {set idx 0} {$idx < $num_frames} {incr idx} {
		if {$frames($idx.good) != 3} {
			set save_state "disabled"
			break
		}
	}
	if $animated {
		if [llength $args] {
			if {[string is digit -strict [lindex $args 0]] &&
					[lindex $args 0] >= 10} {
				set test "\$idx != $sel_frame &&\
						\$frames(\$idx.dur) < 1"
			} else {
				set test "\$idx == $sel_frame ||\
						\$frames(\$idx.dur) < 1"
			}
		} else {
			set test "\$frames(\$idx.dur) < 1"
		}

		set anim_state "normal"
		for {set idx 0} {$idx < $num_frames} {incr idx} {
			if $test {
				set save_state "disabled"
				set anim_state "disabled"
				break
			}
		}
		if {$playback && $anim_state == "disabled"} {
			toggle_play
		}
		.aved.funcs.anim.play configure -state $anim_state
	}
	.aved.buttons.save configure -state $save_state

	if [llength $args] {return [string is digit [lindex $args 0]]}
}

# Verifies $idx's vailidity and sets its hand element to reflect
proc aved::verify_image {idx args} {
	global MV
	variable sel_frame
	variable frames
	variable playback
	variable num_frames
	variable anim_frame

	set update_cache 1
	set vcmd 0

	# Handle arguments
	if [llength $args] {
		switch -- [lindex $args 0] {
		case "-trust-cache" {
			set update_cache 0
		} case "-vcmd" {
			set vcmd 1
		}}
	}

	# Determine a "good" value to cache, or just use the old value?
	# "good" values: 0: no name; 1: file not found; 2: bad file; 3: success
	if $update_cache {
		# Determine image filename
		if $vcmd {
			set orig_name [lindex $args 1]
		} else {
			set orig_name $frames($idx.img)
		}
		set name "$MV(homedir)/images/[string_map {" " "_"} $orig_name]"

		if $vcmd {
			# Assumption: in vcmd mode, idx == sel_frame
			# Therefore, modify frame image
			set good [load_image $idx $name $orig_name]
		} else {
			# Assumption: when not in vcmd mode, idx != sel_frame
			# except to initially show the hand and frame images
			# Therefore, modify the hand
			if {[string_equal -nocase [file extension $name]\
					".gif"] && [file readable $name]} {
				set good 3
			} elseif {$orig_name == ""} {
				set good 0
			} else {
				set good 1
			}
		}
	}
	set frames($idx.good) $good
	
	# Now make use of the cached information
	if !$vcmd {
		# Nothing more to do for vcmd mode...
		if {$idx == $sel_frame} {
			set img "img::hand"
			if !$playback {
				.aved.preview itemconfigure img -image\
						$frames($idx.img_id)
			}
		} elseif {$frames($idx.good) == 3} {
			set img "img::yes"
		} else {
			set img "img::no"
		}
		$anim_frame.$idx.hand configure -image $img
	}

	# Enable or disable the save and playback buttons
	set_button_states

	# Appease the calling entry field
	if $vcmd {return 1}
}

# Attempt to load an image from disk; return a "good" value
proc aved::load_image {idx name orig_name} {
	variable frames
	if {$orig_name != ""} {
		if [file readable $name] {
			$frames($idx.img_id) blank
			if {![string_equal -nocase [file extension $name]\
					".gif"] || [catch "$frames($idx.img_id)\
					read $name -shrink"]} {
				$frames($idx.img_id) copy "img::bad_file"\
						-shrink
				return 2
			}
			return 3
		} else {
			$frames($idx.img_id) copy "img::file_not_found" -shrink
			return 1
		}
	} else {
		$frames($idx.img_id) blank
		return 0
	}
}

# Main AvEd procedure
proc aved::main {} {
	# TODO: make .aved a toplevel, not a frame
	wm title . "OpenVerse Avatar Editor"
	wm resizable . 1 1
	wm minsize . 300 330
	frame .aved

	new_frame -init
	create_common_panel
	create_still_panel
	create_anim_panel

	show_still_panel
	show_common_panel

	pack .aved -expand 1 -fill both
	update

#	balloon::do_balloon 40 40 .aved.preview balloon -text "Hello!"
}

aved::main

