#!/usr/bin/python
#
# KeyCounter project, v1.0e
# website: http://txzone.net/
#
# KeyCounter is the legal property of its developers, whose names are 
# too numerous to list here.  Please refer to the COPYRIGHT file 
# distributed with this source distribution.
#
# 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
#
# --- TROUBLES ---
# if you have trouble with python-libs (something with roots)
# in root, edit /usr/lib/python2.4/site-packages/Xlib/protocol/display.py
# and at line 530, replace 2048 by 4096
# ----------------


import md5, time, threading, xmlrpclib
import os, ConfigParser
from optparse import OptionParser
from sys import exit

# Basic test to include libraries
if os.name == 'posix':
	try:
		from Xlib.display import Display
		from Xlib import X
	except:
		print 'python-xlib library is missing'
		print 'python-xlib is here : http://python-xlib.sourceforge.net/'
		exit(0)
		
elif os.name == 'nt':
	try:
		import pythoncom, pyHook
	except:
		print 'pythoncom and pyHook libraries are missing.'
		print 'pythoncom is here : http://sourceforge.net/projects/pywin32/'
		print 'and pyHook here : http://sourceforge.net/projects/uncassist/'
		exit(0)

else: 
	print 'Mmmh, ask to the author to support your os :D'
	exit(0)

# Check for gtk
have_gtk = True
try:
	import pygtk; pygtk.require('2.0')
	import gtk
	import gobject
	try:
		import egg.trayicon
	except: 
		print 'WARNING: python-gnome2-extras is missing'
		have_gtk = False
except:
	print 'WARNING: pygtk (python-gtk2) is missing'
	print 'WARNING: pygtk is here : http://www.pygtk.org/'
	have_gtk = False


# -------------------------------------------------------------------
# GktTrayIcon tray icon.
# -------------------------------------------------------------------
class GtkTrayIcon(threading.Thread):

	def __init__(self, webpulse):

		self.wp = webpulse
		threading.Thread.__init__(self)

		# create tray icon
		self.icon = egg.trayicon.TrayIcon('KeyCounter')
		self.eventbox = gtk.EventBox()
		self.eventbox.connect('enter-notify-event', self.enter_notify_event)
		self.eventbox.connect('button-press-event', self.menu_popup)

		self.icon.add(self.eventbox)
		self.eventbox.add(gtk.image_new_from_icon_name('gnome-settings-keybindings', gtk.ICON_SIZE_SMALL_TOOLBAR))

		self.tips = gtk.Tooltips()
		self.tips.set_tip(self.icon, ('no stats'))

		self.icon.show_all()

		# create menu
		self.menu = gtk.Menu()
		self.menu_item_settings = gtk.ImageMenuItem('gtk-preferences')
		self.menu_item_settings.connect('button-release-event', self.menu_preferences) 
		self.menu.append(self.menu_item_settings)
		self.menu_item_quit = gtk.ImageMenuItem('gtk-quit')
		self.menu_item_quit.connect('button-release-event', self.menu_quit) 
		self.menu.append(self.menu_item_quit)

		self.menu.show_all()

		# create setting panel
		self.settings = gtk.Window(gtk.WINDOW_TOPLEVEL)
		self.settings.set_title('Settings')
		self.settings.set_position(gtk.WIN_POS_CENTER)
		self.settings.set_modal(True)
		self.settings.connect('delete-event', self.settings_hide)
		tbl = gtk.Table(3, 2, False)
		self.settings.set_border_width(5)
		#self.settings.set_row_spacings(3)
		#self.settings.set_col_spacings(10)
		self.settings.add(tbl)
		self.settings_login = gtk.Entry()
		self.settings_login.set_activates_default(True)
		tbl.attach(self.settings_login, 1, 2, 0, 1, gtk.EXPAND | gtk.FILL, 0, 0, 0)
		self.settings_password = gtk.Entry()
		self.settings_password.set_activates_default(True)
		self.settings_password.set_visibility(False)
		tbl.attach(self.settings_password, 1, 2, 1, 2, gtk.EXPAND | gtk.FILL, 0, 0, 0)
		widget = gtk.Label('Login')
		widget.set_alignment(0, 0.5)
		tbl.attach(widget, 0, 1, 0, 1, gtk.FILL, 0, 0, 0)
		widget = gtk.Label('Password')
		widget.set_alignment(0, 0.5)
		tbl.attach(widget, 0, 1, 1, 2, gtk.FILL, 0, 0, 0)
		widget = gtk.Button(stock=gtk.STOCK_SAVE)
		tbl.attach(widget, 1, 2, 2, 3, gtk.FILL, 0, 0, 0)
		widget.connect('clicked', self.settings_save)


	def settings_hide(self, widget, event):
		self.settings.hide()
		return True

	def settings_save(self, widget):
		print '> test settings before save'
		login = self.settings_login.get_text()
		password = self.settings_password.get_text()
		if self.wp.testCustomLogin(login, password):
			self.settings.hide()
			self.wp.saveConfig()
			w = gtk.MessageDialog(None, gtk.DIALOG_MODAL, gtk.MESSAGE_INFO, gtk.BUTTONS_OK, 'Settings saved')
			w.run()
			w.destroy()
			return True
		else:
			w = gtk.MessageDialog(None, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR, gtk.BUTTONS_OK, 'Invalid login/password')
			w.run()
			w.destroy()
		
	def menu_preferences(self, widget, event):
		self.settings_login.set_text(self.wp.login)
		self.settings_password.set_text(self.wp.password)
		self.settings.show_all()
		
	def menu_quit(self, widget, event):
		# argl, please, i need help...
		exit(0)

	def menu_popup(self, widget, event):
		self.menu.popup(None, None, None, 1, 0)

	def enter_notify_event(self, widget, event):
		tl = 900 - int(time.time() - self.wp.sleep_start)
		nk = self.wp.kb.counter
		self.tips.set_tip(self.icon, '%d key(s), pulse in %ds' % (nk, tl))

	def run(self):
		gtk.threads_init()
		gtk.main()


# -------------------------------------------------------------------
# KeyboardCounter asbtract class.
# -------------------------------------------------------------------
class KeyboardCounter:
	
	counter = 0
	lock = False

	def __init__(self):
		self.lock = threading.Lock()

	def bit(self, c, x):
		return (c & (1<<x))

	def increment(self, counter=1):
		self.lock.acquire()
		self.counter += counter
		self.lock.release()

	def reset(self):
		self.lock.acquire()
		self.counter = 0
		self.lock.release()

	def check(self):
		pass
	
	def run(self):
		while 1:
			self.check()
			time.sleep(0.08)


# -------------------------------------------------------------------
# XKeyboardCounter class. Implementation for Xlib 
# -------------------------------------------------------------------
class XKeyboardCounter(KeyboardCounter):

	disp = False
	keys = False

	def __init__(self):
		KeyboardCounter.__init__(self)
		self.disp = Display()

	def check(self):

		keys = self.disp.query_keymap()
		if self.keys == False:
			self.keys = keys
			return

		for i in range(0, len(keys)):
			for j in range(0, 32):
				b0 = self.bit(keys[i], j)
				b1 = self.bit(self.keys[i], j)
				if b1 == 0 and b0 != b1:
					self.increment()

		self.keys = keys


# -------------------------------------------------------------------
# MSKeyboardCounter class. Implementation for Windows 
# -------------------------------------------------------------------
class MSKeyboardCounter(KeyboardCounter):
	
	hm = None
	quit = ''
	
	def __init__(self):
		KeyboardCounter.__init__(self)
		hm = pyHook.HookManager()
		hm.KeyDown = self.check
		hm.HookKeyboard()

	def run(self):
		pythoncom.PumpMessages()

	def check(self, event):
		self.increment()


# -------------------------------------------------------------------
# WebPulse class. Pulse every 1/4h keycount on website
# -------------------------------------------------------------------
class WebPulse(threading.Thread):

	sleep_start = 0
	sleep_delay = 15 * 60
	login = ''
	password = ''
	
	def __init__(self):
		self.config_filename = os.path.expanduser('~/.keycounter')
		self.config_urlrpc = 'http://txzone.net/keycounter/RPC2'
		self.readConfig()

		self.xmlrpc = xmlrpclib.ServerProxy(self.config_urlrpc)
		threading.Thread.__init__(self)

	def testLogin(self):
		try:
			m = md5.md5(self.password).hexdigest()
			self.xmlrpc.login(self.login, m)
		except:
			print '> error at login(). wrong login/password or server error'
			return False
		return True

	def testCustomLogin(self, login, password):
		try:
			m = md5.md5(password).hexdigest()
			self.xmlrpc.login(login, m)
		except:
			print '> error at login(). wrong login/password or server error'
			return False
		return True

	def setKeyboard(self, kb):
		self.kb = kb

	def readConfig(self):
		self.config = ConfigParser.ConfigParser()
		self.config.read([self.config_filename])

		if not self.config.has_option('account', 'login') or not self.config.has_option('account', 'password'):
			print '> KeyCounter not yet configured, configure now !'
			if not self.configure():
				exit(1)

		if self.config.has_section('account'):
			self.login = self.config.get('account', 'login')
			self.password = self.config.get('account', 'password')

	def saveConfig(self):
		if not self.config.has_section('account'):
			self.config.add_section('account')
		self.config.set('account', 'login', self.login)
		self.config.set('account', 'password', self.password)
		self.config.write(open(self.config_filename, 'w'));
		print '> write config done.'

	def configure(self):
		try:
			while True:
				self.login = raw_input('Enter your login: ')
				if len(self.login) > 2:
					break
				else:
					print '> Invalid login'
			while True:
				self.password = raw_input('Enter your password (not hidden): ')
				if len(self.login) > 2:
					break
				else:
					print '> Invalid password'
			try:
				self.saveConfig()
			except:
				print '> error to save config file'
				return False
		except:
			print 'abort'
			return False
		return True
		
	def run(self):
		while True:
			self.sleep_start = time.time()
			time.sleep(self.sleep_delay)
			
			if self.kb.counter == 0:
				print '> time reached, but no key pressed. cancel'
			else:
				print '> pulse %d' % (self.kb.counter)
				try:
					m = md5.md5(self.password).hexdigest()
					self.xmlrpc.pulse(self.login, m, self.kb.counter)
				except:
					print '> error at pulse(). no reset'
					continue
				
				self.kb.reset()

# -------------------------------------------------------------------
# Main loop
# -------------------------------------------------------------------
if __name__ == '__main__':

	print 'keycounter project : http://txzone.net/keycounter/'
	print 'version 1.0e, by Mathieu Virbel <tito@bankiz.org>'

	# Init options
	op = OptionParser()
	op.add_option('-c', '--configure', action='store_true', default=False, help='configure KeyCounter')
	(options, args) = op.parse_args()

	print '> initialize pulse...'
	wp = WebPulse()
	wp.setDaemon(True)
	
	trayicon = False
	if have_gtk:
		print '> initialize tray icon'
		trayicon = GtkTrayIcon(wp)
		trayicon.setDaemon(True)
	else:
		print '> no gtk, no tray icon.'

	# check for configure
	if options.configure:
		wp.configure()
		exit(0)
	elif not wp.testLogin():
		print '> unable to login, exit'
		exit(0)
	
	print '> initialize keyboard...'
	if (os.name != 'nt'):
		kb = XKeyboardCounter()
	else:
		kb = MSKeyboardCounter()
	wp.setKeyboard(kb)

	print '> watching keyboard status...'
	try: 
		if trayicon != False:
			trayicon.start()
		wp.start()
		kb.run()
	except KeyboardInterrupt:
		pass

	print '> quit'
