#
# Copyright (c) 2022-2024 Andrea Biscuola <a@abiscuola.com>
#
# Permission to use, copy, modify, and distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
#

package require Tk

namespace eval gui {
	namespace export displaymsg newservertab away closechannel removeuser \
	    setmode updateuser displaytopic newprivatetab newchanneltab \
	    privatechat adduser quiet showmode newreadtab init changech \
	    raisewin onexit savefont newserver clearusers setfontcolor \
	    setchatbackground

	namespace ensemble create

	set fsett [dict create]
	set appsett [dict create]

	set quiet 0
}

proc ::gui::init {} {
	variable appsett

	option add *tearOff 0

	wm title . $::progtitle
	wm iconname . $::progtitle

	bind . <Destroy> {
		if {"%W" eq "."} {gui onexit}
	}

	font create "$::progname-font" -family $::deffont \
	    -size 9 -overstrike 0 -underline 0 -weight normal \
	    -slant roman

	. configure -menu [menu .menubar]

	.menubar add cascade -menu [menu .menubar.file] -label File
	.menubar add cascade -menu [menu .menubar.edit] -label Edit
	.menubar add cascade -menu [menu .menubar.help] -label Help

	.menubar.file add command -label "Connect" -command "::gui::newserver"
	.menubar.file add separator
	.menubar.file add command -label "Exit" -command "exit 0"

	.menubar.edit add command -label "Font" -command "tk fontchooser show"
	.menubar.edit add command -label "Extensions" -command "::gui::extensions"
	.menubar.edit add command -label "Themes" -command "::gui::themes"
	.menubar.edit add separator
	.menubar.edit add checkbutton -label "Log chats" -onvalue 1 -offvalue 0 \
	    -variable ::logging -command "::gui::logging"

	.menubar.help add command -label "Website" -command "::gui::website"

	grid [ttk::frame .main] -column 0 -row 0 -sticky nswe -pady 3 \
	    -padx 3

	grid [ttk::treeview .main.servers -selectmode browse -show tree] \
	    -column 0 -row 0 -columnspan 1 -rowspan 1 -sticky nswe

	grid [ttk::scrollbar .main.srvscroll -orient vertical \
	    -command ".main.servers yview"] -column 1 \
	    -row 0 -columnspan 1 -rowspan 1 -sticky ns
	.main.servers configure -yscrollcommand ".main.srvscroll set"

	grid [ttk::notebook .main.chatbook] \
	    -column 2 -row 0 -rowspan 1 -columnspan 1 -sticky nswe

	grid columnconfigure . 0 -weight 1
	grid rowconfigure . 0 -weight 1
	grid columnconfigure .main 0 -weight 1
	grid rowconfigure .main 0 -weight 1
	grid columnconfigure .main 2 -weight 16

	#
	# Message levels as shown in the tree:
	#
	# - $::colorinfo: noise.
	# - $::colormessage: user's noise.
	# - $::colormention: users noise directed at me!
	#
	.main.servers tag configure treenotifymention \
	    -font "$::progname-font" -foreground $::colormention
	.main.servers tag configure treenotifymsg \
	    -font "$::progname-font" -foreground $::colormessage
	.main.servers tag configure treenotifyinfo \
	    -font "$::progname-font" -foreground $::colorinfo
	.main.servers tag configure treenotifynormal \
	    -font "$::progname-font"

	#
	# Channel selection on the left tree.
	#
	bind .main.servers <<TreeviewSelect>> {
		if {[llength [.main.servers selection]] > 0} {gui changech}
	}

	#
	# Force a reconnection.
	#
	bind .main.servers <ButtonRelease-2> {
		set item [.main.servers selection]
		if {[llength $item] == 0} {return}

		set parent [.main.servers parent $item]
		if {[llength $parent] > 0} {return}

		irctk killconnection $item
	}

	#
	# Say bye to the selected channel or server.
	#
	bind .main.servers <Double-ButtonRelease-2> {
		variable channel
		variable chan

		set item [.main.servers selection]
		if {[llength $item] == 0} {return}

		set parent [.main.servers parent $item]
		if {[llength $parent] == 0} {
			set chan [irctk findnetwork $item]
		} else {
			set chan [irctk findnetwork $parent]
		}

		if {[llength $chan] == 0} {return}

		set channel [.main.servers item $item -text]
		if {[llength $parent] > 0} {
			if {![regexp {^[&#!+]} $channel]} {
				irctk partch $chan $channel
			} else {
				if {[irctk ischanrd $chan $channel]} {
					irctk partch $chan $channel
				} else {
					irc part $chan {} $channel "Leaving"
				}
			}
		} else {
			gui closechannel $item
			irc quit $chan {}
			irctk quitserver $chan
		}
	}

	#
	# Change the server configuration.
	#
	bind .main.servers <Double-ButtonRelease-3> {
		set item [.main.servers selection]
		if {[llength $item] == 0} {return}

		if {[llength [.main.servers parent $item]] != 0} {return}

		gui newserver [.main.servers item $item -text]
	}

	#
	# Users like to be able to change their fonts.
	#
	bind . <<TkFontchooserFontChanged>> {
		set fsett [dict create]

		set font [tk fontchooser configure -font]
		font configure $::progname-font -family $::deffont \
		    -size 9 -overstrike 0 -underline 0 -weight normal \
		    -slant roman

		dict set fsett font $::progname-font family $::deffont
		dict set fsett font $::progname-font size 9
		dict set fsett font $::progname-font overstrike 0
		dict set fsett font $::progname-font underline 0
		dict set fsett font $::progname-font slant roman
		dict set fsett font $::progname-font weight normal

		foreach detail $font {
			switch -regexp -- $detail {
				^[0-9]+$ {
					font configure $::progname-font \
					    -size $detail
					dict set fsett font $::progname-font \
					    size $detail
				} ^overstrike$ {
					font configure $::progname-font \
					    -overstrike 1
					dict set fsett font $::progname-font \
					    overstrike 1
				} ^underline$ {
					font configure $::progname-font \
					    -underline 1
					dict set fsett font $::progname-font \
					    underline 1
				} ^bold$ {
					font configure $::progname-font \
					    -weight bold
					dict set fsett font $::progname-font \
					    weight bold
				} ^italic$ {
					font configure $::progname-font \
					    -slant italic
					dict set fsett font $::progname-font \
					    slant italic
				} default {
					font configure $::progname-font \
					    -family $detail
					dict set fsett font $::progname-font \
					    family $detail
				}
			}
		}

		gui savefont $fsett

		ttk::style configure Treeview -rowheight \
		    [expr {[font metrics $::progname-font -linespace] + 2}]
	}

	loadfontsettings
	loadappsettings
}

proc ::gui::website {} {
	plumb {} {} {} $::website
}

proc ::gui::onexit {} {
	irctk onexit
}

proc ::gui::saveappsettings {} {
	variable appsett

	file mkdir $::cfgdir

	if {![catch {set f [::open "$::cfgdir/app.conf" w 0640]} errstr]} {
		puts $f $appsett
		close $f
	} else {
		log console $errstr
	}
}

proc ::gui::savefont {sets} {
	file mkdir $::cfgdir

	if {![catch {set f [::open "$::cfgdir/font.conf" w 0640]} errstr]} {
		puts $f $sets
		close $f
	} else {
		log console $errstr
	}
}

#
# Load the application settings from file
#
proc ::gui::loadappsettings {} {
	variable appsett

	if {[catch {set fd [::open "$::cfgdir/app.conf" r]} errstr]} {
		log console $errstr

		source $::defaulttheme

		return
	}

	set appsett [gets $fd]

	close $fd

	if {[dict exists $appsett logging]} {
		set ::logging [dict get $appsett logging]
		log enable $::logging
	}

	if {[dict exists $appsett themes]} {
		set themes {}

		foreach file [dict get $appsett themes] {
			if {[catch {source $file} errstr]} {
				log console $errstr
			} else {
				lappend themes $file
			}
		}

		dict set appsett themes $themes
	}

	if {[dict exists $appsett theme]} {
		if {[catch {ttk::style theme use [dict get $appsett theme]} errstr]} {
			log console $errstr

			dict set appsett theme default
		}
	}

	source $::defaulttheme

	saveappsettings
}

#
# Load the interface settings from file.
#
proc ::gui::loadfontsettings {} {
	variable fsett

	if {[catch {set fd [::open "$::cfgdir/font.conf" r]} errstr]} {
		log console $errstr

		return
	}

	set fsett [gets $fd]

	close $fd

	set fstring "\{[dict get $fsett font $::progname-font family]\} \
	    [dict get $fsett font $::progname-font size] \
	    [dict get $fsett font $::progname-font weight] \
	    [dict get $fsett font $::progname-font slant]"

	if {[dict get $fsett font $::progname-font overstrike]} {
		set fstring "$fstring overstrike"
	}
	if {[dict get $fsett font $::progname-font underline]} {
		set fstring "$fstring underline"
	}

	tk fontchooser configure -font "$fstring"

	font configure "$::progname-font" \
	    -family "[dict get $fsett font $::progname-font family]" \
	    -size [dict get $fsett font $::progname-font size] \
	    -overstrike [dict get $fsett font $::progname-font overstrike] \
	    -underline [dict get $fsett font $::progname-font underline] \
	    -weight [dict get $fsett font $::progname-font weight] \
	    -slant [dict get $fsett font $::progname-font slant]
}

proc ::gui::newserver {{item ""}} {
	if {[winfo exists .connect]} {
		return
	}

	tk::toplevel .connect

	wm title .connect "Connect to a new server"

	grid [ttk::frame .connect.main -padding "3 3 12 12"] -column 0 \
	    -row 0 -sticky nswe
	grid [ttk::frame .connect.bottom -padding "3 3 12 12"] -column 0 \
	    -row 1 -sticky nswe

	grid [ttk::label .connect.main.netlbl -text "Network"] \
	    -column 0 -row 0 -columnspan 1 -rowspan 1 -sticky e \
	    -padx 5 -pady 5
	grid [ttk::entry .connect.main.net] -column 1 -row 0 \
	    -columnspan 3 -rowspan 1 -sticky we \
	    -padx 5 -pady 5

	grid [ttk::label .connect.main.nicklbl -text "Nick"] \
	    -column 0 -row 2 -columnspan 1 -rowspan 1 -sticky e \
	    -padx 5 -pady 5
	grid [ttk::entry .connect.main.nick] -column 1 -row 2 \
	    -columnspan 1 -rowspan 1 -sticky we \
	    -padx 5 -pady 5

	grid [ttk::label .connect.main.realnlbl -text "Real name"] \
	    -column 2 -row 2 -columnspan 1 -rowspan 1 -sticky e \
	    -padx 5 -pady 5
	grid [ttk::entry .connect.main.realn] -column 3 -row 2 \
	    -columnspan 1 -rowspan 1 -sticky we \
	    -padx 5 -pady 5

	grid [ttk::label .connect.main.userlbl -text "User"] \
	    -column 0 -row 3 -columnspan 1 -rowspan 1 -sticky e \
	    -padx 5 -pady 5
	grid [ttk::entry .connect.main.user] -column 1 -row 3 \
	    -columnspan 1 -rowspan 1 -sticky we \
	    -padx 5 -pady 5

	grid [ttk::label .connect.main.passlbl -text "Password"] \
	    -column 2 -row 3 -columnspan 1 -rowspan 1 -sticky e \
	    -padx 5 -pady 5
	grid [ttk::entry .connect.main.pass -show "*"] -column 3 -row 3 \
	    -columnspan 1 -rowspan 1 -sticky we \
	    -padx 5 -pady 5

	grid [ttk::label .connect.main.cmdlbl -text "Connection string"] \
	    -column 0 -row 4 -columnspan 1 -rowspan 1 \
	    -padx 5 -pady 5 -sticky e
	grid [ttk::entry .connect.main.cmd] \
	    -column 1 -row 4 -columnspan 3 -rowspan 1 \
	    -padx 5 -pady 5 -sticky we

	if {$item eq ""} {
		grid [ttk::button .connect.bottom.ok -text "OK" -command {::gui::connect}] \
		    -row 8 -column 0 -columnspan 1 -rowspan 1 \
		    -padx 20
	} else {
		set net [irctk getnetwork $item]

		.connect.main.net insert 0 [lindex $net 0]
		.connect.main.net configure -state disabled

		.connect.main.nick insert 0 [lindex $net 1]
		.connect.main.realn insert 0 [lindex $net 2]
		.connect.main.pass insert 0 [lindex $net 3]
		.connect.main.cmd insert 0 [lindex $net 4]
		.connect.main.user insert 0 [lindex $net 5]

		grid [ttk::button .connect.bottom.ok -text "OK" \
		    -command "::gui::modify"] \
		    -row 8 -column 1 -columnspan 1 -rowspan 1 \
		    -padx 20
	}
	grid [ttk::button .connect.bottom.cancel -text "Cancel" \
	    -command "destroy .connect" ] -row 8 -column 6 -columnspan 1 \
	    -rowspan 1 -padx 20

	grid columnconfigure .connect 0 -weight 1
	grid rowconfigure .connect 0 -weight 1

	grid columnconfigure .connect.main 0 -weight 1
	grid rowconfigure .connect.main 0 -weight 1

	grid columnconfigure .connect.main 1 -weight 1
	grid columnconfigure .connect.main 2 -weight 1
	grid columnconfigure .connect.main 3 -weight 1

	grid rowconfigure .connect.main 1 -weight 1
	grid rowconfigure .connect.main 2 -weight 1
	grid rowconfigure .connect.main 3 -weight 1
}

proc ::gui::validform {net user nick name cmd} {
	if {![regexp {[[:alnum:]]+} $net]} {return 0}
	if {[regexp {[[:space:]]+} $user]} {return 0}
	if {"$nick" eq "" || [regexp {[[:space:]]+} $nick]} {return 0}
	if {![regexp {[:alnum:]} $name]} {return 0}
	if {"$cmd" eq ""} {return 0}

	return 1
}

proc ::gui::connect {} {
	set net [.connect.main.net get]
	set user [.connect.main.user get]
	set nick [.connect.main.nick get]
	set name [.connect.main.realn get]
	set pass [.connect.main.pass get]
	set cmd [.connect.main.cmd get]

	if {![validform "$net" "$user" "$nick" "$name" "$cmd"]} {
		set str [format "%s\n\n\n%s\n\n%s\n\n%s\n\n%s" \
		    "One or more values are not valid." \
		    "* The network name must contain alphanumeric characters." \
		    "* The user can be empty or must contain alphanumeric characters." \
		    "* The nick can not be empty, or contain spaces." \
		    "* The real name must contain alphanumeric characters." \
		    "* The command to run can not be empty."]

		tk_messageBox -default "ok" -message "Invalid values" \
		    -detail $str -title "Error!" -parent .connect

		return
	}

	set fd [irctk connect $net $user $nick $name $pass "$cmd"]

	destroy .connect
}

proc ::gui::modify {} {
	set net [.connect.main.net get]
	set nick [.connect.main.nick get]
	set user [.connect.main.user get]
	set name [.connect.main.realn get]
	set pass [.connect.main.pass get]
	set cmd [.connect.main.cmd get]

	if {![validform "$net" "$user" "$nick" "$name" "$cmd"]} {
		set str [format "%s\n\n\n%s\n\n%s\n\n%s\n\n%s" \
		    "One or more values are not valid." \
		    "* The network name must contain alphanumeric characters." \
		    "* The user can be empty or must contain alphanumeric characters." \
		    "* The nick can not be empty, or contain spaces." \
		    "* The real name must contain alphanumeric characters." \
		    "* The command to run can not be empty."]

		tk_messageBox -default "ok" -message "Invalid values" \
		    -detail $str -title "Error!" -parent .connect

		return
	}

	destroy .connect

	irctk save $net $user $nick "$name" "$pass" "$cmd"
}

proc ::gui::newservertab {fd nick network id} {
	if {![winfo exists .main.chatbook.$id]} {
		.main.chatbook add [ttk::frame .main.chatbook.$id] \
		    -text "$network" -sticky nswe

		grid [ttk::frame .main.chatbook.$id.top] -column 0 -row 0 \
		    -columnspan 1 -rowspan 1 -sticky nswe -padx 3
		grid [ttk::frame .main.chatbook.$id.bottom] -column 0 -row 1 \
		    -columnspan 1 -rowspan 1 -sticky we -padx 3 -pady 3

		grid [text .main.chatbook.$id.top.chat -state disabled \
		    -font $::progname-font -exportselection 1 -cursor top_left_arrow \
		    -spacing3 5] -column 0 -row 0 -sticky nswe -columnspan 1 \
		    -rowspan 1
		.main.chatbook.$id.top.chat tag configure default -foreground $::colordefault
		.main.chatbook.$id.top.chat tag bind sel <ButtonRelease-3> \
		    "::gui::textclick $id \"$network\" \"$network\" .main.chatbook.$id.top.chat"

		grid [ttk::label .main.chatbook.$id.bottom.nick -text $nick \
		    -font $::progname-font] -column 0 -row 0 -columnspan 1 \
		    -rowspan 1 -sticky e -padx 4
		grid [ttk::entry .main.chatbook.$id.bottom.message -font $::progname-font] \
		    -column 1 -row 0 -columnspan 1 -rowspan 1 -sticky we

		grid [ttk::scrollbar .main.chatbook.$id.top.textscroll -orient vertical \
		    -command ".main.chatbook.$id.top.chat yview"] -column 1 \
		    -row 0 -columnspan 1 -rowspan 1 -sticky ns
		.main.chatbook.$id.top.chat configure -yscrollcommand \
		    ".main.chatbook.$id.top.textscroll set"

		grid columnconfigure .main.chatbook.$id 0 -weight 1
		grid rowconfigure .main.chatbook.$id 0 -weight 1

		grid rowconfigure .main.chatbook.$id.top 0 -weight 1
		grid columnconfigure .main.chatbook.$id.top 0 -weight 1

		grid columnconfigure .main.chatbook.$id.bottom 0 -weight 0
		grid columnconfigure .main.chatbook.$id.bottom 1 -weight 1

		.main.servers insert {} end -id $id -text $network
		.main.servers tag add treenotifynormal $id
	} else {
		bind .main.chatbook.$id.bottom.message <Return> ""
	}

	bind .main.chatbook.$id.bottom.message <Return> \
	    "::gui::sendmsg $fd $id"
}

proc ::gui::newchanneltab {fd nick parent id channel network} {
	if {![winfo exists .main.chatbook.$id]} {
		.main.chatbook add [ttk::frame .main.chatbook.$id] \
		    -text "$channel" -sticky nswe

		grid [ttk::frame .main.chatbook.$id.top] -column 0 -row 0 \
		    -columnspan 1 -rowspan 1 -sticky nswe -padx 3
		grid [ttk::frame .main.chatbook.$id.bottom] -column 0 -row 1 \
		    -columnspan 1 -rowspan 1 -sticky we -padx 3 -pady 3

		grid [ttk::entry .main.chatbook.$id.top.topic -state readonly \
		    -font $::progname-font -cursor top_left_arrow] \
		    -column 0 -row 0 -sticky we -columnspan 2 -rowspan 1

		grid [text .main.chatbook.$id.top.chat -state disabled \
		    -font $::progname-font -exportselection 1 -cursor top_left_arrow \
		    -spacing3 5] -column 0 -row 1 -sticky nswe -columnspan 1 \
		    -rowspan 1
		.main.chatbook.$id.top.chat tag bind sel <ButtonRelease-3> \
		    "::gui::textclick $id \"$network\" $channel .main.chatbook.$id.top.chat"
		.main.chatbook.$id.top.chat tag configure default -foreground $::colordefault
		.main.chatbook.$id.top.chat tag configure mention -foreground $::colormention

		grid [ttk::label .main.chatbook.$id.bottom.nick -text $nick \
		    -font $::progname-font] -column 0 -row 0 -columnspan 1 \
		    -rowspan 1 -sticky e -padx 4
		grid [ttk::entry .main.chatbook.$id.bottom.message \
		    -font $::progname-font] -column 1 -row 0 -columnspan 1 \
		    -rowspan 1 -sticky we

		grid [ttk::scrollbar .main.chatbook.$id.top.textscroll -orient vertical \
		    -command ".main.chatbook.$id.top.chat yview"] -column 1 \
		    -row 1 -columnspan 1 -rowspan 1 -sticky ns
		.main.chatbook.$id.top.chat configure -yscrollcommand \
		    ".main.chatbook.$id.top.textscroll set"

		grid [ttk::treeview .main.chatbook.$id.users -selectmode browse \
		    -show tree] -column 1 -row 0 -sticky nswe \
		    -columnspan 1 -rowspan 2

		.main.chatbook.$id.users tag configure founder \
		    -font "$::progname-font" -foreground $::colorfounder
		.main.chatbook.$id.users tag configure protected \
		    -font "$::progname-font" -foreground $::colorprotected
		.main.chatbook.$id.users tag configure operator \
		    -font "$::progname-font" -foreground $::coloroperator
		.main.chatbook.$id.users tag configure halfop \
		    -font "$::progname-font" -foreground $::colorhalfop
		.main.chatbook.$id.users tag configure voice \
		    -font "$::progname-font" -foreground $::colorvoice
		.main.chatbook.$id.users tag configure user \
		    -font "$::progname-font"

		grid [ttk::scrollbar .main.chatbook.$id.userscroll -orient vertical \
		    -command ".main.chatbook.$id.users yview"] -column 2 \
		    -row 0 -columnspan 1 -rowspan 2 -sticky ns
		.main.chatbook.$id.users configure -yscrollcommand \
		    ".main.chatbook.$id.userscroll set"

		grid columnconfigure .main.chatbook.$id 0 -weight 1
		grid columnconfigure .main.chatbook.$id 1 -weight 0
		grid rowconfigure .main.chatbook.$id 0 -weight 1

		grid rowconfigure .main.chatbook.$id.top 0 -weight 0
		grid rowconfigure .main.chatbook.$id.top 1 -weight 1
		grid columnconfigure .main.chatbook.$id.top 0 -weight 1

		grid columnconfigure .main.chatbook.$id.bottom 0 -weight 0
		grid columnconfigure .main.chatbook.$id.bottom 1 -weight 1

		.main.servers insert $parent end -id $id -text $channel
		.main.servers see $id
		.main.servers tag add treenotifynormal $id
	} else {
		bind .main.chatbook.$id.bottom.message <Return> ""
		bind .main.chatbook.$id.users <Double-Button-1> ""
	}

	bind .main.chatbook.$id.bottom.message <Return> \
	    "::gui::sendmsg $fd $id"

	bind .main.chatbook.$id.users <Double-Button-1> \
	    "gui privatechat $fd $id"
}

proc ::gui::newprivatetab {fd nick parent id channel network mode} {
	if {![winfo exists .main.chatbook.$id]} {
		.main.chatbook add [ttk::frame .main.chatbook.$id] \
		    -text "$channel" -sticky nswe

		grid [ttk::frame .main.chatbook.$id.top] -column 0 -row 0 \
		    -columnspan 1 -rowspan 1 -sticky nswe -padx 3
		grid [ttk::frame .main.chatbook.$id.bottom] -column 0 -row 1 \
		    -columnspan 1 -rowspan 1 -sticky we -padx 3 -pady 3

		grid [text .main.chatbook.$id.top.chat -state disabled \
		    -font $::progname-font -exportselection 1 -cursor top_left_arrow \
		    -spacing3 5] -column 0 -row 0 -sticky nswe -columnspan 1 \
		    -rowspan 1
		.main.chatbook.$id.top.chat tag configure default -foreground $::colordefault
		.main.chatbook.$id.top.chat tag bind sel <ButtonRelease-3> \
		    "::gui::textclick $id \"$network\" $channel .main.chatbook.$id.top.chat"

		grid [ttk::label .main.chatbook.$id.bottom.nick -text $nick \
		    -font $::progname-font] -column 0 -row 0 -columnspan 1 \
		    -rowspan 1 -sticky e -padx 4
		grid [ttk::entry .main.chatbook.$id.bottom.message \
		    -font $::progname-font] -column 1 -row 0 -columnspan 1 \
		    -rowspan 1 -sticky we

		grid [ttk::scrollbar .main.chatbook.$id.top.textscroll -orient vertical \
		    -command ".main.chatbook.$id.top.chat yview"] -column 1 \
		    -row 0 -columnspan 1 -rowspan 1 -sticky ns
		.main.chatbook.$id.top.chat configure -yscrollcommand \
		    ".main.chatbook.$id.top.textscroll set"

		grid columnconfigure .main.chatbook.$id 0 -weight 1
		grid columnconfigure .main.chatbook.$id 1 -weight 0
		grid rowconfigure .main.chatbook.$id 0 -weight 1

		grid rowconfigure .main.chatbook.$id.top 0 -weight 1
		grid columnconfigure .main.chatbook.$id.top 0 -weight 1

		grid columnconfigure .main.chatbook.$id.bottom 0 -weight 0
		grid columnconfigure .main.chatbook.$id.bottom 1 -weight 1

		.main.servers insert $parent end -id $id -text $channel
		.main.servers tag add treenotifynormal $id
		if {$mode eq "focus"} {
			.main.servers see $id
		}
	} else {
		bind .main.chatbook.$id.bottom.message <Return> ""
	}

	bind .main.chatbook.$id.bottom.message <Return> \
	    "::gui::sendmsg $fd $id"
}

proc ::gui::newreadtab {parent id channel network} {
	.main.chatbook add [ttk::frame .main.chatbook.$id] \
	    -text "$channel" -sticky nswe

	grid [ttk::frame .main.chatbook.$id.top] -column 0 -row 0 \
	    -columnspan 1 -rowspan 1 -sticky nswe -padx 3

	grid [text .main.chatbook.$id.top.chat -state disabled \
	    -font $::progname-font -exportselection 1 -cursor top_left_arrow \
	    -spacing3 5] -column 0 -row 0 -sticky nswe -columnspan 1 -rowspan 1
	.main.chatbook.$id.top.chat tag configure default -foreground $::colordefault
	.main.chatbook.$id.top.chat tag bind sel <ButtonRelease-3> \
	    "::gui::textclick $id \"$network\" $channel .main.chatbook.$id.top.chat"

	grid [ttk::scrollbar .main.chatbook.$id.top.textscroll -orient vertical \
	    -command ".main.chatbook.$id.top.chat yview"] -column 1 \
	    -row 0 -columnspan 1 -rowspan 1 -sticky ns

	.main.chatbook.$id.top.chat configure -yscrollcommand \
	    ".main.chatbook.$id.top.textscroll set"

	grid columnconfigure .main.chatbook.$id 0 -weight 1
	grid columnconfigure .main.chatbook.$id 1 -weight 0
	grid rowconfigure .main.chatbook.$id 0 -weight 1

	grid rowconfigure .main.chatbook.$id.top 0 -weight 1
	grid columnconfigure .main.chatbook.$id.top 0 -weight 1

	.main.servers insert $parent end -id $id -text $channel
	.main.servers see $id
	.main.servers tag add treenotifynormal $id
}

proc ::gui::privatechat {fd id} {
	set sl [.main.chatbook.$id.users selection]
	if {[llength $sl] == 0} {return}

	irctk privatechan $fd [lindex $sl 0]
}

#
# Are we sleeping?
#
proc ::gui::showmode {} {
	variable quiet

	set date [clock format [clock seconds] -format %H:%M:%S]

	if {$quiet} {
		gui displaymsg "You are in quiet mode" * -1 \
		    message $date
	} else {
		gui displaymsg "You are in normal mode" * -1 \
		    message $date
	}
}

proc ::gui::raisewin {} {
	if {![winfo ismapped .]} {
		wm deiconify .
	} else {
		raise .
	}
}

proc ::gui::quiet {} {
	variable quiet

	set date [clock format [clock seconds] -format %H:%M:%S]

	set quiet [expr ! $quiet]
	if {$quiet} {
		gui displaymsg "You are now in quiet mode" * -1 \
		    message $date
	} else {
		gui displaymsg "You are now in normal mode" * -1 \
		    message $date
	}
}

proc ::gui::displaymsg {msg nick id level datetime} {
	variable quiet

	#
	# Sssshhhhhhhh!
	#
	if {$quiet && "$level" eq "info"} {return}

	set current [.main.chatbook select]

	if {$id ne -1} {
		set pos [lindex [.main.chatbook.$id.top.chat yview] 1]
		.main.chatbook.$id.top.chat configure -state normal

		#
		# We are important, so the message is shown in purple
		#
		if {$level eq "mention"} {
			.main.chatbook.$id.top.chat insert end \
			    "$datetime $nick:\t$msg\n" mention
		} else {
			.main.chatbook.$id.top.chat insert end \
			    "$datetime $nick:\t$msg\n" default
		}

		.main.chatbook.$id.top.chat configure -state disabled

		#
		# Don't autoscroll if we are not at the end
		#
		if {$pos == 1.0} {
			.main.chatbook.$id.top.chat see end
		}

		if {$current eq ".main.chatbook.$id"} {
			return
		}

		#
		# Make the channel name with the right color, based on
		# importance.
		#
		switch -exact $level {
			mention {
				.main.servers tag add treenotifymention $id
			} message {
				.main.servers tag add treenotifymsg $id
			} info {
				.main.servers tag add treenotifyinfo $id
			}
		}
	} else {
		if {"$current" eq ""} {return}

		#
		# We are in the channel already, just show the message.
		#

		$current.top.chat configure -state normal

		set pos [lindex [$current.top.chat yview] 1]

		$current.top.chat insert end "$datetime $nick:\t$msg\n"
		$current.top.chat configure -state disabled

		if {$pos == 1.0} {$current.top.chat see end}
	}
}

#
# The user decided to say something, let the world know!
#
proc ::gui::sendmsg {fd id} {
	set text [.main.chatbook.$id.bottom.message get]
	foreach msg [split $text "\n"] {
		if {[string length $msg] == 0} {continue}

		::irctk::writemsg $fd $id {} "$msg"
	}

	.main.chatbook.$id.bottom.message delete 0 end
}

proc ::gui::displaytopic {id datetime {topic ""}} {
	.main.chatbook.$id.top.topic configure -state enabled
	.main.chatbook.$id.top.topic delete 0 end
	.main.chatbook.$id.top.topic insert 0 $topic
	.main.chatbook.$id.top.topic configure -state readonly

	displaymsg "Topic: $topic" "*" $id info $datetime
}

proc ::gui::adduser {id nick} {
	if {![winfo exists .main.chatbook.$id.users]} {return}

	set user [string trimleft $nick "~&@%+"]
	set literal [regsub -all -- {([^0-9a-zA-Z])} $user {\\\1}]

	if {[.main.chatbook.$id.users exists $user]} {return}

	.main.chatbook.$id.users insert {} end -id $user -text $user
	.main.chatbook.$id.users tag add user $literal

	#
	# We need to check if they have any kind of superpower
	#
	switch -exact -- [string index $nick 0] {
		~ {.main.chatbook.$id.users tag add founder $literal}
		& {.main.chatbook.$id.users tag add protected $literal}
		@ {.main.chatbook.$id.users tag add operator $literal}
		% {.main.chatbook.$id.users tag add halfop $literal}
		+ {.main.chatbook.$id.users tag add voice $literal}
	}
}

proc ::gui::clearusers {id} {
	set tree .main.chatbook.$id.users

	foreach item [$tree children {}] {
		$tree delete [escapestr $item]
	}
}

proc ::gui::escapestr {text} {
	regsub -all -- {\\} $text {\\\\}
}

#
# We or somebody else decided to change their identity
#
proc ::gui::updateuser {id oldnick newnick} {
	if {"[lindex [.main.chatbook.$id.bottom.nick configure -text] 4]" eq "$oldnick"} {
		.main.chatbook.$id.bottom.nick configure -text $newnick
	}

	foreach ch [.main.servers children $id] {
		if {[winfo exists .main.chatbook.$ch.bottom.nick]} {
			set cnick [lindex [.main.chatbook.$ch.bottom.nick \
			    configure -text] 4]
			if {"$cnick" eq "$oldnick"} {
				.main.chatbook.$ch.bottom.nick configure \
				    -text $newnick
			}
		}

		if {![winfo exists .main.chatbook.$ch.users]} {continue}

		if {[.main.chatbook.$ch.users exists $newnick]} {continue}

		if {![.main.chatbook.$ch.users exists $oldnick]} {continue}

		set idx [.main.chatbook.$ch.users index $oldnick]

		.main.chatbook.$ch.users insert {} $idx -id $newnick \
		    -text $newnick -tags [.main.chatbook.$ch.users \
		    item $oldnick -tags]

		.main.chatbook.$ch.users delete [escapestr $oldnick]
	}
}

#
# From great powers comes great responsibility
#
proc ::gui::setmode {id nick mode} {
	if {![winfo exists .main.chatbook.$id.users]} {
		return
	}
	if {![.main.chatbook.$id.users exists $nick]} {
		return
	}

	switch -exact -- [string index $mode 0] {
		+ {set cmd add}
		- {set cmd remove}
	}

	if {[string first q $mode] != -1} {
		.main.chatbook.$id.users tag $cmd founder $nick
	}
	if {[string first a $mode] != -1} {
		.main.chatbook.$id.users tag $cmd protected $nick
	}
	if {[string first o $mode] != -1} {
		.main.chatbook.$id.users tag $cmd operator $nick
	}
	if {[string first h $mode] != -1} {
		.main.chatbook.$id.users tag $cmd halfop $nick
	}
	if {[string first v $mode] != -1} {
		.main.chatbook.$id.users tag $cmd voice $nick
	}
}

proc ::gui::removeuser {id nick} {
	if {![winfo exists .main.chatbook.$id.users]} {return 0}

	if {[.main.chatbook.$id.users exists $nick]} {
		.main.chatbook.$id.users delete [escapestr $nick]

		return 1
	}

	return 0
}

proc ::gui::closechannel {id} {
	if {![.main.servers exists $id]} {return}

	foreach child [.main.servers children $id] {
		.main.chatbook forget .main.chatbook.$child
		destroy .main.chatbook.$child
		.main.servers delete [list $child]
	}

	if {[winfo exists .main.chatbook.$id]} {
		.main.chatbook select .main.chatbook.$id
		.main.chatbook forget .main.chatbook.$id
		destroy .main.chatbook.$id
		.main.servers delete [list $id]
	}
}

proc ::gui::currenttabid {} {
	set current [.main.chatbook select]

	return [lindex [split [.main.chatbook select] "."] 3]
}

proc ::gui::away {id status} {
	if {![.main.servers exists $id]} {return}

	switch -exact -- $status {
		1 {set state disabled}
		0 {set state {!disabled}}
		default {return}
	}

	foreach child [.main.servers children $id] {
		if {[winfo exists .main.chatbook.$child.bottom.nick]} {
			.main.chatbook.$child.bottom.nick state $state
		}
	}

	if {[winfo exists .main.chatbook.$id]} {
		if {[winfo exists .main.chatbook.$id.bottom.nick]} {
			.main.chatbook.$id.bottom.nick state $state
		}
	}
}

proc ::gui::extensions {} {
	if {[winfo exists .extensions]} {return}

	tk::toplevel .extensions

	wm title .extensions "Manage extensions"

	grid [ttk::frame .extensions.top -padding "3 3 12 12"] -column 0 \
	    -row 0 -sticky nswe
	grid [ttk::frame .extensions.bottom -padding "3 3 12 12"] -column 0 \
	    -row 1 -sticky nswe

	grid [ttk::treeview .extensions.top.view] -column 0 -row 0 \
	    -columnspan 3 -rowspan 1 -sticky nswe

	.extensions.top.view configure -columns {Version Capabilities Path}
	.extensions.top.view heading {#0} -text "Name"
	.extensions.top.view heading {Version} -text "Version"
	.extensions.top.view heading {Capabilities} -text "Capabilities"
	.extensions.top.view heading {Path} -text "Path"

	grid [ttk::button .extensions.bottom.close -text "Close" \
	    -command "destroy .extensions"] -row 1 -column 0 \
	    -sticky ne -padx 20
	grid [ttk::button .extensions.bottom.load -text "Load" \
	    -command "::gui::loadextension"] -row 1 -column 1 \
	    -sticky ne -padx 20
	grid [ttk::button .extensions.bottom.unload -text "Unload" \
	    -command "::gui::unloadextensions"] -row 1 -column 2 \
	    -sticky ne -padx 20

	grid columnconfigure .extensions 0 -weight 1
	grid rowconfigure .extensions 0 -weight 1

	grid columnconfigure .extensions.top 0 -weight 1
	grid rowconfigure .extensions.top 0 -weight 1

	foreach ext [exts list] {
		set id [lindex $ext 0]

		.extensions.top.view insert {} end -id [lindex $ext 0] -text $id
		.extensions.top.view set $id Version [lindex $ext 1]
		.extensions.top.view set $id Capabilities [concat [lindex $ext 2]]
		.extensions.top.view set $id Path [lindex $ext 3]
	}
}

proc ::gui::loadextension {} {
	set path [tk_getOpenFile]

	if {"$path" eq ""} {return}

	exts load $path "::gui::isloaded"
}

proc ::gui::isloaded {msg} {
	if {"[dict get $msg type]" ne "handshake"} {return}

	set id [dict get $msg name]

	.extensions.top.view insert {} end -id $id \
	    -text $id
	.extensions.top.view set $id Version [dict get $msg version]
	.extensions.top.view set $id Capabilities [dict get $msg caps]
	.extensions.top.view set $id Path [dict get $msg path]

	exts load [dict get $msg path] "::irctk::extmsg"
}

proc ::gui::unloadextensions {} {
	foreach ext [.extensions.top.view selection] {
		exts killbyname $ext
		.extensions.top.view delete $ext
	}
}

#
# We decided to click on a link, a name, or whatever
# with button three in a chat window.
#
# Better to let the extensions know.
#
proc ::gui::textclick {id network channel path} {
	if {[llength [$path tag ranges sel]] == 0} {return}

	set start [lindex [$path tag ranges sel] 0]
	set end [lindex [$path tag ranges sel] 1]

	plumb $id $network $channel [$path get $start $end]
}

proc ::gui::plumb {id network channel data} {
	set msg [dict create]

	dict set msg type plumb
	dict set msg cid $id
	dict set msg network $network
	dict set msg channel $channel
	dict set msg data $data

	exts writemsg $msg
}

proc ::gui::changech {{id 0}} {
	set slt 0

	if {$id > 0} {.main.servers selection set $id}

	set slt [.main.servers selection]
	.main.chatbook select .main.chatbook.$slt

	if {[winfo exists .main.chatbook.$slt.bottom.message]} {
		focus .main.chatbook.$slt.bottom.message
	}

	.main.servers tag remove treenotifyinfo $slt
	.main.servers tag remove treenotifymsg $slt
	.main.servers tag remove treenotifymention $slt
}

proc ::gui::logging {} {
	variable appsett

	log enable $::logging

	dict set appsett logging $::logging
	saveappsettings
}

proc ::gui::themes {} {
	variable appsett

	if {[winfo exists .themes]} {return}

	tk::toplevel .themes

	wm title .themes "Themes"

	grid [ttk::frame .themes.top -padding "3 3 12 12"] -column 0 \
	    -row 0 -sticky nswe
	grid [ttk::frame .themes.bottom -padding "3 3 12 12"] -column 0 \
	    -row 1 -sticky nswe

	grid [ttk::treeview .themes.top.view -selectmode browse] \
	    -column 0 -row 0 -columnspan 3 -rowspan 1 -sticky nswe
	grid [ttk::treeview .themes.top.files] -column 0 -row 1 \
	    -columnspan 3 -rowspan 1 -sticky nswe

	.themes.top.view heading {#0} -text "Name"
	.themes.top.files heading {#0} -text "Loaded theme files"

	grid [ttk::button .themes.bottom.close -text "Close" \
	    -command "destroy .themes"] -row 0 -column 0 \
	    -sticky ne -padx 20
	grid [ttk::button .themes.bottom.load -text "Load" \
	    -command "::gui::loadtheme"] -row 0 -column 1 \
	    -sticky ne -padx 20
	grid [ttk::button .themes.bottom.unload -text "Remove" \
	    -command "::gui::removetheme"] -row 0 -column 2 \
	    -sticky ne -padx 20

	grid columnconfigure .themes 0 -weight 1
	grid rowconfigure .themes 0 -weight 1

	grid columnconfigure .themes.top 0 -weight 1
	grid rowconfigure .themes.top 0 -weight 1
	grid columnconfigure .themes.top 0 -weight 1
	grid rowconfigure .themes.top 1 -weight 1

	bind .themes.top.view <<TreeviewSelect>> {
		ttk::style theme use [.themes.top.view selection]
		source $::defaulttheme

		dict set ::gui::appsett theme [ttk::style theme use]

		::gui::saveappsettings
	}

	listthemes
}

proc ::gui::listthemes {} {
	variable appsett

	foreach item [.themes.top.view children {}] {
		.themes.top.view delete $item
	}
	foreach item [.themes.top.files children {}] {
		.themes.top.files delete $item
	}

	foreach theme [ttk::style theme names] {
		.themes.top.view insert {} end -id $theme -text $theme
	}
	if {[dict exists $appsett themes]} {
		foreach file [dict get $appsett themes] {
			.themes.top.files insert {} end -id $file -text $file
		}
	}

	.themes.top.view selection set [ttk::style theme use]
}

proc ::gui::loadtheme {} {
	variable appsett

	set path [tk_getOpenFile]

	if {"$path" eq ""} {return}

	if {[catch {source $path} errstr]} {
		log console $errstr
	}

	if {![dict exists $appsett themes]} {
		dict set appsett themes {}
	}

	if {[lsearch -exact [dict get $appsett themes] $path] == -1} {
		set themes [dict get $appsett themes]
		lappend themes $path
		dict set appsett themes $themes
		saveappsettings
	}

	listthemes
}

proc ::gui::removetheme {} {
	variable appsett

	set themes [dict get $appsett themes]

	foreach file [.themes.top.files selection] {
		set id [lsearch -exact $themes $file]

		if {$id == -1} {continue}

		set themes [lreplace $themes $id $id]
	}

	dict set appsett themes $themes

	saveappsettings

	listthemes
}

proc ::gui::setfontcolor {tag color} {
	switch -exact $tag {
		mention {
			set ::colormention $color
			.main.servers tag configure treenotifymention \
			    -foreground $color
			settextchattag $tag $color
		} message {
			set ::colormessage $color
			.main.servers tag configure treenotifymsg \
			    -foreground $color
		} info {
			set ::colorinfo $color
			.main.servers tag configure treenotifyinfo \
			    -foreground $color
		} founder {
			set ::colorfounder $color
			setuserstag $tag $color
		} protected {
			set ::colorfounder $color
			setuserstag $tag $color
		} operator {
			set ::colorfounder $color
			setuserstag $tag $color
		} halfop {
			set ::colorfounder $color
			setuserstag $tag $color
		} voice {
			set ::colorfounder $color
			setuserstag $tag $color
		} default {
			set ::colordefault $color
			.main.servers tag configure treenotifynormal \
			    -foreground $color
			settextchattag default $color
			setuserstag user $color
		}
	}
}

proc ::gui::setchatbackground {color} {
	foreach tab [winfo children .main.chatbook] {
		$tab.top.chat configure -background $color
	}
}

proc ::gui::settextchattag {tag color} {
	foreach tab [winfo children .main.chatbook] {
		$tab.top.chat tag configure $tag -foreground $color
	}
}

proc ::gui::setuserstag {tag color} {
	foreach tab [winfo children .main.chatbook] {
		if {[winfo exists $tab.users]} {
			$tab.users tag configure $tag -foreground $color
		}
	}
}
