# $Id: chatgui.py 4757 2014-02-21 07:53:31Z nanang $
#
# pjsua Python GUI Demo
#
# Copyright (C)2013 Teluu Inc. (http://www.teluu.com)
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA 
#
import sys
if sys.version_info[0] >= 3: # Python 3
	import tkinter as tk
	from tkinter import ttk
	from tkinter import messagebox as msgbox
else:
	import Tkinter as tk
	import ttk
	import tkMessageBox as msgbox


class TextObserver:
	def onSendMessage(self, msg):
		pass
	def onStartTyping(self):
		pass
	def onStopTyping(self):
		pass
		
class TextFrame(ttk.Frame):
	def __init__(self, master, observer):
		ttk.Frame.__init__(self, master)
		self._observer = observer
		self._isTyping = False
		self._createWidgets()

	def _onSendMessage(self, event):
		send_text = self._typingBox.get("1.0", tk.END).strip()
		if send_text == '':
			return
		
		self.addMessage('me: ' + send_text)
		self._typingBox.delete("0.0", tk.END)
		self._onTyping(None)
		
		# notify app for sending message
		self._observer.onSendMessage(send_text)
		
	def _onTyping(self, event):
		# notify app for typing indication
		is_typing = self._typingBox.get("1.0", tk.END).strip() != ''
		if is_typing != self._isTyping:
			self._isTyping = is_typing
			if is_typing:
				self._observer.onStartTyping()
			else:
				self._observer.onStopTyping()
		
	def _createWidgets(self):
		self.rowconfigure(0, weight=1)
		self.rowconfigure(1, weight=0)
		self.rowconfigure(2, weight=0)
		self.columnconfigure(0, weight=1)
		self.columnconfigure(1, weight=0)
		
		self._text = tk.Text(self, width=50, height=30, font=("Arial", "10"))
		self._text.grid(row=0, column=0, sticky='nswe')
		self._text.config(state=tk.DISABLED)
		self._text.tag_config("info", foreground="darkgray", font=("Arial", "9", "italic"))
		
		scrl = ttk.Scrollbar(self, orient=tk.VERTICAL, command=self._text.yview)
		self._text.config(yscrollcommand=scrl.set)
		scrl.grid(row=0, column=1, sticky='nsw')
		
		self._typingBox = tk.Text(self, width=50, height=1, font=("Arial", "10"))
		self._typingBox.grid(row=1, columnspan=2, sticky='we', pady=0)
		
		self._statusBar = tk.Label(self, anchor='w', font=("Arial", "8", "italic"))
		self._statusBar.grid(row=2, columnspan=2, sticky='we')
		
		self._typingBox.bind('<Return>', self._onSendMessage)
		self._typingBox.bind("<Key>", self._onTyping)
		self._typingBox.focus_set()
		
	def addMessage(self, msg, is_chat = True):
		self._text.config(state=tk.NORMAL)
		if is_chat:
			self._text.insert(tk.END, msg+'\r\n')
		else:
			self._text.insert(tk.END, msg+'\r\n', 'info')
		self._text.config(state=tk.DISABLED)
		self._text.yview(tk.END)

	def setTypingIndication(self, who, is_typing):
		if is_typing:
			self._statusBar['text'] = "'%s' is typing.." % (who)
		else:
			self._statusBar['text'] = ''

class AudioState:
	NULL, INITIALIZING, CONNECTED, DISCONNECTED, FAILED = range(5)
			
class AudioObserver:
	def onHangup(self, peer_uri):
		pass
	def onHold(self, peer_uri):
		pass
	def onUnhold(self, peer_uri):
		pass
	def onRxMute(self, peer_uri, is_muted):
		pass
	def onRxVol(self, peer_uri, vol_pct):
		pass
	def onTxMute(self, peer_uri, is_muted):
		pass
			

class AudioFrame(ttk.Labelframe):
	def __init__(self, master, peer_uri, observer):
		ttk.Labelframe.__init__(self, master, text=peer_uri)
		self.peerUri = peer_uri
		self._observer = observer
		self._initFrame = None
		self._callFrame = None
		self._rxMute = False
		self._txMute = False
		self._state = AudioState.NULL
		
		self._createInitWidgets()
		self._createWidgets()
		
	def updateState(self, state):
		if self._state == state:
			return

		if state == AudioState.INITIALIZING:
			self._callFrame.pack_forget()
			self._initFrame.pack(fill=tk.BOTH)
			self._btnCancel.pack(side=tk.TOP)
			self._lblInitState['text'] = 'Intializing..'

		elif state == AudioState.CONNECTED:
			self._initFrame.pack_forget()
			self._callFrame.pack(fill=tk.BOTH)			
		else:
			self._callFrame.pack_forget()
			self._initFrame.pack(fill=tk.BOTH)
			if state == AudioState.FAILED:
				self._lblInitState['text'] = 'Failed'
			else:
				self._lblInitState['text'] = 'Normal cleared'
				self._btnCancel.pack_forget()
			
			self._btnHold['text'] = 'Hold'
			self._btnHold.config(state=tk.NORMAL)
			self._rxMute = False
			self._txMute = False
			self.btnRxMute['text'] = 'Mute'
			self.btnTxMute['text'] = 'Mute'
			self.rxVol.set(5.0)
		
		# save last state
		self._state = state
		
	def setStatsText(self, stats_str):
		self.stat.config(state=tk.NORMAL)
		self.stat.delete("0.0", tk.END)
		self.stat.insert(tk.END, stats_str)
		self.stat.config(state=tk.DISABLED)
		
	def _onHold(self):
		self._btnHold.config(state=tk.DISABLED)
		# notify app
		if self._btnHold['text'] == 'Hold':
			self._observer.onHold(self.peerUri)
			self._btnHold['text'] = 'Unhold'
		else:
			self._observer.onUnhold(self.peerUri)
			self._btnHold['text'] = 'Hold'
		self._btnHold.config(state=tk.NORMAL)

	def _onHangup(self):
		# notify app
		self._observer.onHangup(self.peerUri)

	def _onRxMute(self):
		# notify app
		self._rxMute = not self._rxMute
		self._observer.onRxMute(self.peerUri, self._rxMute)
		self.btnRxMute['text'] = 'Unmute' if self._rxMute else 'Mute'
		
	def _onRxVol(self, event):
		# notify app
		vol = self.rxVol.get()
		self._observer.onRxVol(self.peerUri, vol*10.0)

	def _onTxMute(self):
		# notify app
		self._txMute = not self._txMute
		self._observer.onTxMute(self.peerUri, self._txMute)
		self.btnTxMute['text'] = 'Unmute' if self._txMute else 'Mute'

	def _createInitWidgets(self):
		self._initFrame = ttk.Frame(self)
		#self._initFrame.pack(fill=tk.BOTH)

	
		self._lblInitState = tk.Label(self._initFrame, font=("Arial", "12"), text='')
		self._lblInitState.pack(side=tk.TOP, fill=tk.X, expand=1)
		
		# Operation: cancel/kick
		self._btnCancel = ttk.Button(self._initFrame, text = 'Cancel', command=self._onHangup)
		self._btnCancel.pack(side=tk.TOP)
				
	def _createWidgets(self):
		self._callFrame = ttk.Frame(self)
		#self._callFrame.pack(fill=tk.BOTH)
		
		# toolbar
		toolbar = ttk.Frame(self._callFrame)
		toolbar.pack(side=tk.TOP, fill=tk.X)
		self._btnHold = ttk.Button(toolbar, text='Hold', command=self._onHold)
		self._btnHold.pack(side=tk.LEFT, fill=tk.Y)
		#self._btnXfer = ttk.Button(toolbar, text='Transfer..')
		#self._btnXfer.pack(side=tk.LEFT, fill=tk.Y)
		self._btnHangUp = ttk.Button(toolbar, text='Hangup', command=self._onHangup)
		self._btnHangUp.pack(side=tk.LEFT, fill=tk.Y)

		# volume tool
		vol_frm = ttk.Frame(self._callFrame)
		vol_frm.pack(side=tk.TOP, fill=tk.X)
		
		self.rxVolFrm = ttk.Labelframe(vol_frm, text='RX volume')
		self.rxVolFrm.pack(side=tk.LEFT, fill=tk.Y)
		
		self.btnRxMute = ttk.Button(self.rxVolFrm, width=8, text='Mute', command=self._onRxMute)
		self.btnRxMute.pack(side=tk.LEFT)
		self.rxVol = tk.Scale(self.rxVolFrm, orient=tk.HORIZONTAL, from_=0.0, to=10.0, showvalue=1) #, tickinterval=10.0, showvalue=1)
		self.rxVol.set(5.0)
		self.rxVol.bind("<ButtonRelease-1>", self._onRxVol)
		self.rxVol.pack(side=tk.LEFT)
		
		self.txVolFrm = ttk.Labelframe(vol_frm, text='TX volume')
		self.txVolFrm.pack(side=tk.RIGHT, fill=tk.Y)
		
		self.btnTxMute = ttk.Button(self.txVolFrm, width=8, text='Mute', command=self._onTxMute)
		self.btnTxMute.pack(side=tk.LEFT)
		
		# stat
		self.stat = tk.Text(self._callFrame, width=10, height=2, bg='lightgray', relief=tk.FLAT, font=("Courier", "9"))
		self.stat.insert(tk.END, 'stat here')
		self.stat.pack(side=tk.BOTTOM, fill=tk.BOTH, expand=1)


class ChatObserver(TextObserver, AudioObserver):
	def onAddParticipant(self):
		pass
	def onStartAudio(self):
		pass
	def onStopAudio(self):
		pass
	def onCloseWindow(self):
		pass
		
class ChatFrame(tk.Toplevel):
	"""
	Room
	"""
	def __init__(self, observer):
		tk.Toplevel.__init__(self)
		self.protocol("WM_DELETE_WINDOW", self._onClose)
		self._observer = observer

		self._text = None
		self._text_shown = True
		
		self._audioEnabled = False
		self._audioFrames = []
		self._createWidgets()
	
	def _createWidgets(self):
		# toolbar
		self.toolbar = ttk.Frame(self)
		self.toolbar.pack(side=tk.TOP, fill=tk.BOTH)
		
		btnText = ttk.Button(self.toolbar, text='Show/hide text', command=self._onShowHideText)
		btnText.pack(side=tk.LEFT, fill=tk.Y)
		btnAudio = ttk.Button(self.toolbar, text='Start/stop audio', command=self._onStartStopAudio)
		btnAudio.pack(side=tk.LEFT, fill=tk.Y)
		
		ttk.Separator(self.toolbar, orient=tk.VERTICAL).pack(side=tk.LEFT, fill=tk.Y, padx = 4)

		btnAdd = ttk.Button(self.toolbar, text='Add participant..', command=self._onAddParticipant)
		btnAdd.pack(side=tk.LEFT, fill=tk.Y)

		# media frame
		self.media = ttk.Frame(self)
		self.media.pack(side=tk.BOTTOM, fill=tk.BOTH, expand=1)
		
		# create Text Chat frame
		self.media_left = ttk.Frame(self.media)
		self._text = TextFrame(self.media_left, self._observer)
		self._text.pack(fill=tk.BOTH, expand=1)
		self.media_left.pack(side=tk.LEFT, fill=tk.BOTH, expand=1)
		
		# create other media frame
		self.media_right = ttk.Frame(self.media)
		
	def _arrangeMediaFrames(self):
		if len(self._audioFrames) == 0:
			self.media_right.pack_forget()
			return
		
		self.media_right.pack(side=tk.RIGHT, fill=tk.BOTH, expand=1)
		MAX_ROWS = 3
		row_num = 0
		col_num = 1
		for frm in self._audioFrames:
			frm.grid(row=row_num, column=col_num, sticky='nsew', padx=5, pady=5)
			row_num += 1
			if row_num >= MAX_ROWS:
				row_num  = 0
				col_num += 1
	
	def _onShowHideText(self):
		self.textShowHide(not self._text_shown)
		
	def _onAddParticipant(self):
		self._observer.onAddParticipant()
	
	def _onStartStopAudio(self):
		self._audioEnabled = not self._audioEnabled
		if self._audioEnabled:
			self._observer.onStartAudio()
		else:
			self._observer.onStopAudio()
		self.enableAudio(self._audioEnabled)
		
	def _onClose(self):
		self._observer.onCloseWindow()
			
	# APIs
	
	def bringToFront(self):
		self.deiconify()
		self.lift()
		self._text._typingBox.focus_set()
		
	def textAddMessage(self, msg, is_chat = True):
		self._text.addMessage(msg, is_chat)
		
	def textSetTypingIndication(self, who, is_typing = True):
		self._text.setTypingIndication(who, is_typing)
		
	def addParticipant(self, participant_uri):
		aud_frm = AudioFrame(self.media_right, participant_uri, self._observer)
		self._audioFrames.append(aud_frm)
	
	def delParticipant(self, participant_uri):
		for aud_frm in self._audioFrames:
			if participant_uri == aud_frm.peerUri:
				self._audioFrames.remove(aud_frm)
				# need to delete aud_frm manually?
				aud_frm.destroy()
				return

	def textShowHide(self, show = True):
		if show:
			self.media_left.pack(side=tk.LEFT, fill=tk.BOTH, expand=1)
			self._text._typingBox.focus_set()
		else:
			self.media_left.pack_forget()
		self._text_shown = show
	
	def enableAudio(self, is_enabled = True):
		if is_enabled:
			self._arrangeMediaFrames()
		else:
			self.media_right.pack_forget()
		self._audioEnabled = is_enabled
			
	def audioUpdateState(self, participant_uri, state):
		for aud_frm in self._audioFrames:
			if participant_uri == aud_frm.peerUri:
				aud_frm.updateState(state)
				break
		if state >= AudioState.DISCONNECTED and len(self._audioFrames) == 1:
			self.enableAudio(False)
		else:
			self.enableAudio(True)
			
	def audioSetStatsText(self, participant_uri, stats_str):
		for aud_frm in self._audioFrames:
			if participant_uri == aud_frm.peerUri:
				aud_frm.setStatsText(stats_str)
				break
				
if __name__ == '__main__':
	root = tk.Tk()
	root.title("Chat")
	root.columnconfigure(0, weight=1)
	root.rowconfigure(0, weight=1)
	
	obs = ChatObserver()
	dlg = ChatFrame(obs)
	#dlg = TextFrame(root)
	#dlg = AudioFrame(root)

	#dlg.pack(fill=tk.BOTH, expand=1)
	root.mainloop()
