# "Avatar cache" -- plugin for Tkabber.
# Automatically downloads avatars announced by PEP or vcard-temp update
# Written by Jan Zachorowski <quantifier666@gmail.com>

package require base64
package require sha1

namespace eval avatarcache {
    variable vcard_photo_hashes
    variable pep_avatar_hashes
    variable assigned_hashes
    variable pending_avatars
    
    disco::register_feature "http://www.xmpp.org/extensions/xep-0084.html#ns-metadata+notify"
    disco::register_feature "urn:xmpp:avatar:metadata+notify"

    pubsub::register_event_notification_handler "urn:xmpp:avatar:metadata" \
	    [list [namespace current]::process_avatar_metadata_notification "urn:xmpp:avatar:metadata"]
    pubsub::register_event_notification_handler "http://www.xmpp.org/extensions/xep-0084.html#ns-metadata" \
	    [list [namespace current]::process_avatar_metadata_notification "http://www.xmpp.org/extensions/xep-0084.html#ns-metadata"]
    
    file mkdir [file join $::configdir avatars]

    custom::defvar avatarcache {} "Mapping of JIDs to avatar hashes." \
	    -group Hidden
    custom::defvar avatarcache_custom {} "Mapping of JIDs to custom assigned avatar hashes." \
	    -group Hidden

    custom::defgroup AvatarCache \
	[::msgcat::mc "Options for Avatar cache module,\
		       which automatically downloads announced avatars\
		       and notifies other modules on a hook."] \
	-group Plugins -tag "Avatar Cache"
	 
    custom::defvar auto_retrieve_avatars 1 \
	[::msgcat::mc "Automatically request PEP avatar nodes and vCard photos."] \
	-group AvatarCache -type boolean -command [namespace current]::toggle_auto_retrieve
    
    hook::add userinfo_hook [namespace current]::after_vCard_photo
    
    hook::add client_presence_hook [namespace current]::process_presence
    
    hook::add finload_hook [namespace current]::restore_cache

    hook::add roster_jid_popup_menu_hook [namespace current]::add_assign_menu_item 73
}

proc avatarcache::toggle_auto_retrieve {args} {
    variable auto_retrieve_avatars
    variable pending_avatars
    
    if {!$auto_retrieve_avatars} {
	return
    }
    
    foreach {id cmds} [array get pending_avatars] {
	foreach cmd $cmds {
	    eval $cmd
	}
    }
}

proc avatarcache::process_presence {xlib from type x args} {
    variable vcard_photo_hashes
    variable auto_retrieve_avatars
    variable pending_avatars
    set bjid [::xmpp::jid::stripResource $from]
    if {[chat::is_groupchat [chat::chatid $xlib $bjid]]} {
	return
    }
    switch -- $type {
	available -
	unavailable {
	    foreach xs $x {
		::xmpp::xml::split $xs tag xmlns attrs cdata subels
		if {$xmlns == "vcard-temp:x:update"} {
		    debugmsg avatarcache "got presence with vcard update: $from"
		    foreach subel $subels {
			::xmpp::xml::split $subel stag sxmlns sattrs scdata ssubels
			if {$stag == "photo"} {
			    debugmsg avatarcache "photo $from: '$scdata'"
			    if {[info exists vcard_photo_hashes($bjid)] \
				    && $vcard_photo_hashes($bjid) == $scdata} {
				#we downloaded vcard before, and announced hash matches
				return
			    }
			    debugmsg avatarcache "saving unknown photo hash $from $scdata"
			    if {$scdata != ""} {
				set filename [file join $::configdir avatars $scdata]
				if {[file exists $filename]} {
				    #we have file matching the announced hash. work is done.
				    set vcard_photo_hashes($bjid) $scdata
				    store_and_notify $bjid
				} {
				    if {[roster::is_trusted $xlib $bjid]} {
					set aftercmd [list after_vCard_photo "" $xlib $from 0]
					set dloadcmd [list ::xmpp::sendIQ $xlib get \
						-query [::xmpp::xml::create vCard -xmlns vcard-temp] \
						-to $bjid \
						-command [list userinfo::parse_vcard $from] \
					    ]
					if {$auto_retrieve_avatars && ![info exists pending_avatars($scdata)]} {
					    set pending_avatars($scdata) [list $aftercmd $dloadcmd]    
					    eval $aftercmd
					    eval $dloadcmd
					} {
					    set pending_avatars($scdata) [list $aftercmd $dloadcmd]    
					}
				    }
				}
			    } {
				set vcard_photo_hashes($bjid) ""
				store_and_notify $bjid
			    }
			}
		    }
		}
	    }
	}
    }
}

proc avatarcache::process_avatar_metadata_notification {node xlib jid items} {
    variable pep_avatar_hashes
    variable auto_retrieve_avatars
    variable pending_avatars
    set stop true

    debugmsg avatarcache "got avatar metadata notification: $jid"
    foreach item $items {
	::xmpp::xml::split $item tag xmlns attrs cdata subels

	foreach imetadata $subels {
	    ::xmpp::xml::split $imetadata stag sxmlns sattrs scdata ssubels

	    if {![string equal $stag metadata]} continue
	    if {![string equal $sxmlns $node]} continue

	    foreach i $ssubels {
		::xmpp::xml::split $i sstag ssxmlns ssattrs sscdata sssubels

		switch -- $sstag {
		    info {
			set stop false
			#TODO: check if has all needed data
			#TODO: multiple avatars
			
			set id [::xmpp::xml::getAttr $ssattrs id]
			debugmsg avatarcache "photo $jid: '$id'"
			
			break
		    }
		}
	    }
	}
    }
    switch $node {
	"http://www.xmpp.org/extensions/xep-0084.html#ns-metadata" {
	    set node "http://www.xmpp.org/extensions/xep-0084.html#ns-data"
	}
	"urn:xmpp:avatar:metadata" {
	    set node "urn:xmpp:avatar:data"
	}
    }	

    if {$stop} {
	catch {unset pep_avatar_hashes($jid)}
	store_and_notify $jid
    } {
	set filename [file join $::configdir avatars $id]
	if {[file exists $filename]} {
	    set pep_avatar_hashes($jid) $id
	    store_and_notify $jid
	} {
	    debugmsg avatarcache "saving unknown photo hash $jid $id"
	    set cmd [list ::xmpp::pubsub::retrieveItems $xlib $jid $node \
			-items $id \
			-command [list [namespace current]::process_avatar_data\
					$jid $node $filename] \
		    ]
	    if {$auto_retrieve_avatars && ![info exists pending_avatars($id)]} {
		set pending_avatars($id) [list $cmd]
		eval $cmd
	    } {
		set pending_avatars($id) [list $cmd]
	    }
	}
    }
}

proc avatarcache::process_avatar_data {jid node filename status xml} {
    if {$status != "ok"} return
    
    variable pep_avatar_hashes
    variable pending_avatars

    foreach item $xml {
	::xmpp::xml::split $item tag xmlns attrs cdata subels
	foreach idata $subels {
	    ::xmpp::xml::split $idata stag sxmlns sattrs scdata ssubels

	    if {![string equal $stag data]} continue
	    if {![string equal $sxmlns $node]} continue

	    set avatarb64 $scdata
	    set hash [file tail $filename]
	    
	    catch {unset pending_avatars($hash)}

	    #TODO: calculate real hash and check if matches ...
	    if {!([file exists $filename] && \
		  [file attributes $filename -readonly])} {
		set fileid [open $filename w]
		fconfigure $fileid -translation binary
		puts -nonewline $fileid [base64::decode $avatarb64]
		close $fileid
	    }
	    set pep_avatar_hashes($jid) $hash
	    store_and_notify $jid
	    return
	}
    }
}

proc avatarcache::after_vCard_photo {tab xlib jid editable} {
    debugmsg avatarcache [info level 0]
    if {[roster::is_trusted $xlib $jid] && !$editable} {
	set bjid [::xmpp::jid::stripResource $jid]
	trace vdelete ::userinfo::userinfo(photo_binval,$jid) w \
		[list [namespace current]::save_vCard_photo $jid $bjid]
	trace variable ::userinfo::userinfo(photo_binval,$jid) w \
		[list [namespace current]::save_vCard_photo $jid $bjid]
    }
}

proc avatarcache::save_vCard_photo {jid bjid a b c} {
    debugmsg avatarcache [info level 0]
    trace vdelete "userinfo::$a\($b\)" $c \
	    [list [namespace current]::save_vCard_photo $jid $bjid]
    variable vcard_photo_hashes
    variable pending_avatars
    set hash [sha1::sha1 $::userinfo::userinfo(photo_binval,$jid)]

    catch {unset pending_avatars($hash)}
    set filename [file join $::configdir avatars $hash]
	
    if {!([file exists $filename] && [file attributes $filename -readonly])} {
	set fileid [open $filename w]
	fconfigure $fileid -translation binary
	puts -nonewline $fileid $::userinfo::userinfo(photo_binval,$jid)
	close $fileid
    }
    set vcard_photo_hashes($bjid) $hash
    store_and_notify $bjid
    return
}

proc avatarcache::store_and_notify {jid} {
    variable pep_avatar_hashes
    variable vcard_photo_hashes
    variable avatarcache
    variable assigned_hashes

    set oldhash {}

    array set tmp $avatarcache
    if {[info exists tmp($jid)]} {
	set oldhash $tmp($jid)
    }
    array set tmp [list $jid ""]
    array set tmp [array get vcard_photo_hashes]
    array set tmp [array get pep_avatar_hashes]
    set avatarcache [array get tmp]

    if {[info exists assigned_hashes($jid)]} return
	
    set hash $tmp($jid)
    
    if {$hash != $oldhash} {
	if {$hash != ""} {
	    set filename [file join $::configdir avatars $hash]
	    if {[file exists $filename]} {
		hook::run user_avatar_notification_hook $jid $filename
	    }
	} {
	    hook::run user_avatar_notification_hook $jid ""
	}
    }
}

proc avatarcache::restore_cache {} {
    variable avatarcache
    variable avatarcache_custom
    variable assigned_hashes
    
    array set assigned_hashes $avatarcache_custom
    foreach {jid hash} $avatarcache {
	if {[info exists assigned_hashes($jid)]} {
	    continue
	}
	if {$hash != ""} {
	    set filename [file join $::configdir avatars $hash]
	    if {[file exists $filename]} {
		debugmsg avatarcache "restoring '$jid' -> '$hash'"
		hook::run user_avatar_notification_hook $jid $filename
	    }
	} {
	    debugmsg avatarcache "restoring '$jid' -> '$hash'"
	    hook::run user_avatar_notification_hook $jid ""
	}
    }
    
    foreach {jid hash} $avatarcache_custom {
	if {$hash != ""} {
	    set filename [file join $::configdir avatars $hash]
	    if {[file exists $filename]} {
		debugmsg avatarcache "restoring '$jid' -> '$hash'"
		hook::run user_avatar_notification_hook $jid $filename
	    }
	} {
	    debugmsg avatarcache "restoring '$jid' -> '$hash'"
	    hook::run user_avatar_notification_hook $jid ""
	}
    }
}

proc avatarcache::assign_by_hand {jid} {
    variable assigned_hashes
    variable avatarcache_custom
    set custom_filename [tk_getOpenFile -defaultextension .png \
					-filetypes [list [list "Images" [list ".png" ".jpg" ".jpeg" ".gif" ".bmp"]]\
							 [list "All files" [list "*.*"]]] \
					-title "Select custom avatar"]
    if {$custom_filename == ""} return
    
    set fd [open $custom_filename r]
    fconfigure $fd -translation binary
    set avatardata [read $fd [file size $custom_filename]]
    close $fd
    
    set hash [sha1::sha1 $avatardata]
    set filename [file join $::configdir avatars $hash]
    if {![file exists $filename]} {
	file copy $custom_filename $filename
    }
    
    set assigned_hashes($jid) $hash
    set avatarcache_custom [array get assigned_hashes]
    hook::run user_avatar_notification_hook $jid $filename
}

proc avatarcache::unassign_by_hand {jid} {
    variable assigned_hashes
    variable avatarcache
    variable avatarcache_custom

    catch {unset assigned_hashes($jid)}
    set avatarcache_custom [array get assigned_hashes]
    foreach {cachedjid hash} $avatarcache {
	if {$jid == $cachedjid} {
	    if {$hash != ""} {
		set filename [file join $::configdir avatars $hash]
		if {[file exists $filename]} {
		    hook::run user_avatar_notification_hook $jid $filename
		}
	    } {
		hook::run user_avatar_notification_hook $jid ""
	    }
	    break
	}
    }
}

proc avatarcache::hide_avatar {jid} {
    variable assigned_hashes
    variable avatarcache_custom
    set assigned_hashes($jid) ""
    set avatarcache_custom [array get assigned_hashes]
    hook::run user_avatar_notification_hook $jid ""
}

proc avatarcache::add_assign_menu_item {m xlib jid} {
    variable assigned_hashes
    set jid [::xmpp::jid::stripResource $jid]

    set mm [menu $m.avatars -tearoff 0]
    $m add cascade -label [::msgcat::mc "Avatar"] \
		   -menu $mm

    set ignore 1
    $mm add command -label [::msgcat::mc "Assign custom"] \
		    -command [namespace code [list assign_by_hand $jid]]
    if {[info exists assigned_hashes($jid)]} {
	if {$assigned_hashes($jid) == ""} {
	    set unassign_label [::msgcat::mc "Unignore"]
	    set ignore 0
	} {
	    set unassign_label [::msgcat::mc "Unassign custom"]
	}
	$mm add command -label $unassign_label \
			-command [namespace code [list unassign_by_hand $jid]]
    }
    if {$ignore} {
	$mm add command -label [::msgcat::mc "Ignore"] \
			-command [namespace code [list hide_avatar $jid]]
    }
}
