#!/usr/bin/python
# -*- coding: utf-8 -*-

"""
This is a phonelog GTK+ GUI, designed to use with opimd.

Written by Tom Hacohen. (tom@stosb.com), 
contributions from
Sebastian Spaeth (Sebastian@SSpaeth.de)
Sebastian Krzyszkowiak <seba.dos1@gmail.com>

Licensed by GPLv2
"""

__version__ = "0.16.1"


import time, ConfigParser
import os
import shutil
import re
from math import ceil
from datetime import datetime
import gettext
import dbus
import gobject, gtk
import sqlite3
from dbus.mainloop.glib import DBusGMainLoop
import phoneutils

#We stopped using mokoui as it's broken
use_mokoui = False

try:
	cat = gettext.Catalog("phonelog")
	_ = cat.gettext
except IOError:
	_ = lambda x: x


#constants
CONFIGURATION_DIR = os.path.join(os.path.expanduser('~'), '.phonelog')
TIME_FILE = CONFIGURATION_DIR + "time.dat"
CONFIGURATION_FILE = os.path.join(CONFIGURATION_DIR,'phonelog.conf')
DEFAULT_CONFIG_PATH = "/usr/share/phonelog/skeleton/phonelog.conf"
ICONS_PATH = "/usr/share/phonelog/icons/"

#PAGES POSITIONS
PAGE_INCOMING = 0 
PAGE_OUTGOING = 1
PAGE_MISSED = 2
PAGE_GENERAL = 3
PAGE_SETTINGS = 4

class CallsTab:
	#POSITION IN MODEL
	CONTACT_POSITION = 0
	NUMBER_POSITION = 1
	DATE_POSITION = 2
	DURATION_POSITION = 3
	VISUAL_POSITION = 4
	STATUS_POSITION = 5
	
	#ACTUAL GUI POSTITON
	CONTACT_COLUMN = 0
	DATE_COLUMN = 1
	__list = None
	__contact_show_column = CONTACT_POSITION
	__date_show_column = DATE_POSITION
	__tab = None
	__type = None
	__mark_new = False
	__manually_updated = False
        conf = None #inited with ConfigParser object once!
	
	def __init__(self, type, mark_new = False, conf=None):
                # init the ConfigParser object once, bail out if failing.
                if CallsTab.conf == None:
                  CallsTab.conf = conf
                assert CallsTab.conf != None
		self.createList()
		self.__type = type
		self.__createCallsTab()
		self.last_refresh = CallsTab.conf.getfloat('viewlog',self.__type)
		self.__mark_new = mark_new
	def getList(self):
		return self.__list
	def getTab(self):
		return self.__tab
	def shouldMarkNew(self):
		return self.__mark_new
	def getManuallyUpdated(self):
		return self.__manually_updated
	def setManuallyUpdated(self, status = True):
		self.__manually_updated = status
	def __getContactColumn(self):
		return self.CONTACT_COLUMN
	def __getDateColumn(self):
		return self.DATE_COLUMN

	def __createCallsTab(self):
		page = gtk.VBox()
		page.pack_start( self.__initScrolled() )
		align = gtk.Alignment(1,1,1,1)
		
		buttons = gtk.HBox(homogeneous=True, spacing = 3)
		align.add(buttons)
		page.pack_start(align, False, padding = 3)

		buttons_height = CallsTab.conf.getint('phonelog','buttons_height')
		
		callButton = gtk.Button(_("Call"))
		callButton.connect ("clicked", self.callButton_clicked)
		callButton.set_size_request(-1,buttons_height)

		numberButton = gtk.Button(_("Numbers"))
		numberButton.connect ("clicked", self.numberButton_clicked)
		numberButton.set_size_request(-1,buttons_height)

		contactButton = gtk.Button(_("Add"))
		contactButton.connect ("clicked", self.contactButton_clicked)
		contactButton.set_size_request(-1,buttons_height)

		durationButton = gtk.Button(_("Duration"))
		durationButton.connect ("clicked", self.durationButton_clicked)
		durationButton.set_size_request(-1,buttons_height)
		
		buttons.pack_start(callButton, True)
		buttons.pack_start(contactButton, True)
		buttons.pack_start(numberButton, True)
		buttons.pack_start(durationButton, True)
		
		self.__tab = page
	def __createTreeView(self):
		self.__list = gtk.TreeView (gtk.TreeStore( gobject.TYPE_STRING, 
					gobject.TYPE_STRING,
					gobject.TYPE_STRING,
					gobject.TYPE_STRING,
					gobject.TYPE_BOOLEAN,  #the boolean is there for coloring
					gobject.TYPE_STRING ) #should be changed to an icon?
					)
		self.__list.set_property('headers-visible', CallsTab.conf.getboolean('phonelog','lists_headers_visible'))
	def createList(self):

		self.__createTreeView()

		renderer = gtk.CellRendererText()
		#change text size as well?
		#renderer.set_property('weight', 800)
		renderer.set_property('foreground', 'red')

		contact_column = gtk.TreeViewColumn("Contact", renderer, text=self.CONTACT_POSITION, foreground_set=self.VISUAL_POSITION)
		self.__list.insert_column( contact_column, self.__getContactColumn() )
		time_column = gtk.TreeViewColumn("Time", renderer, text=self.DATE_POSITION, foreground_set=self.VISUAL_POSITION)
		self.__list.insert_column( time_column, self.__getDateColumn())
		
	def __initScrolled(self):
		tab_content = self.__list
		if use_mokoui:
			scrolled = mokoui.FingerScroll()
		else:
			scrolled = gtk.ScrolledWindow()
			scrolled.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)

		scrolled.add_with_viewport(tab_content)
		return scrolled
		
	def __getCalls(self):
		global phone_data
		type = self.__type
		calls = phone_data.getCalls(type)
		return calls
	def populateList(self, group_type = 1):
		"""
		group_type =
		0 - never group
		1/everything else - always group
		2 - group from the same type only.
		"""
		global phone_data
		
		if config.getboolean('phonelog','debug'):
			print _("Started populating list")
		self.setManuallyUpdated()
		
		list = self.__list
		call_type = self.__type
		previous_time = datetime.fromtimestamp(CallsTab.conf.getfloat('viewlog', call_type))
		visual_flag = self.shouldMarkNew()

		calls = self.__getCalls()
		if calls == None:
			return False

		#get and clear the model.
		model = list.get_model()
		model.clear()

		
		last_parent_added = None
		child_count = 0
		
		for call in calls:	
			highlight_call = False
			if call.has_key('Peer'):
				number = call['Peer']
				if call.has_key('@Contacts'):
					if type(call['@Contacts']) in (list, dbus.Array):
					    raw_contact = call['@Contacts'][0]
					else:
					    raw_contact = call['@Contacts']
					contact = phone_data.getContactDisplay(raw_contact)
				else:
					contact = number
			else:
				#don't group supressed numbers
				#and make the contact Unknown Number
				number = CallsTab.conf.get('phonelog',"suppressed_name")
				last_parent_added = None
				contact = CallsTab.conf.get('phonelog',"suppressed_name")

			timestamp = call['Timestamp']	
			call_time = datetime.fromtimestamp( float(timestamp) )
				
			if call['Direction'] == 'in' and int(call['Answered']) == 0:
				status = 2
			elif call['Direction'] == 'in':
				status = 0
			else:
				status = 1

			#FIXME: I'm marking all I get as read.
			if status == 2 and call.has_key('New') and int(call['New']) != 0:
				mark = phone_data.getMarkViewedObject (systemBus, call['Path'])
				mark.Update({'New':0})
				highlight_call = True

			#if none at least make it 0, else, numerify it.
			if not call.has_key('Duration'):
				tmp_duration = 0
			else:
				tmp_duration = ceil(float(call['Duration']))

			active_hours = int(tmp_duration / 3600)
			active_minutes = int(tmp_duration / 60 - active_hours * 60)
			active_seconds = int(tmp_duration - active_hours*3600 - active_minutes * 60)
	
			#don't show hours if not needed.
			if active_hours == 0:
				active_time = "%02d:%02d" % (active_minutes, active_seconds)
			else:
				active_time = "%02d:%02d:%02d" % (active_hours, active_minutes, active_seconds)
				
				
			#check if should be grouped
			#if grouping should stop, update the child count to the father
			parent = None
			if last_parent_added != None:
				#also counts the father as a child
				child_count += 1 
				
				if (((last_contact == contact) and (contact != "")) \
						or (last_number == number))\
					and (last_status == status):		
					parent = last_parent_added
				else:
					if child_count > 1:
						#if set in config, add the '(count)' to the contact field
						if CallsTab.conf.getboolean('phonelog','show_group_count'):
							current_value = model[last_parent_added][self.CONTACT_POSITION]
							model.set_value(last_parent_added,self.CONTACT_POSITION, "%s (%d)" % (current_value, child_count))
					child_count = 0
			else:
				child_count = 0
			

			#update the last_statuses so we won't rely on the GUI
			last_contact = contact
			last_number = number
			last_status = status
			last_missed = missed

			#We want the contact to show as the number if it's not in the phonebook.
			if contact == "":
				contact = number
			
			#style status
			if status == 0:
				status = CallsTab.conf.get('phonelog','received_mark')
			elif status == 1:
				status = CallsTab.conf.get('phonelog','made_mark')
			elif status == 2:
				status = CallsTab.conf.get('phonelog','missed_mark')
				
			
			parent_added = model.append(parent, ( contact , 
					number ,
					timeToString(call_time) ,
					active_time, 
					highlight_call ,
					status
					)
				)

			if parent == None:
				last_parent_added = parent_added
		#ugly hack for the last now showing counts bug
		if child_count > 1:
			child_count += 1
			#if set in config, add the '(count)' to the contact field
			if CallsTab.conf.getboolean('phonelog','show_group_count'):
				current_value = model[last_parent_added][self.CONTACT_POSITION]
				model.set_value(last_parent_added,self.CONTACT_POSITION, "%s (%d)" % (current_value, child_count))
				
		#Set last refresh time to now
		CallsTab.conf.set('viewlog',call_type,str(time.time()))

		if config.getboolean('phonelog','debug'):
			print _("Done populating list")
			
		return True										


	def callButton_clicked (self, button):
		global phone_data
		phoneObject = phone_data.getPhoneObject(systemBus)
		list = self.__list
		
		number = self.__getSelectedNumber()
		
		if number != None and number != CallsTab.conf.get('phonelog',"suppressed_name"):
			#TODO, once the highlevel api work, change to them.
			try:
				phoneObject.Initiate (number, 'voice')
			except:
				print _("Failed to initiate a call")
	def contactButton_clicked (self, button):
		global phone_data
		phoneObject = phone_data.getPhoneObject(systemBus)
		list = self.__list
		
		number = self.__getSelectedNumber()
		
		if number != None and number != CallsTab.conf.get('phonelog',"suppressed_name"):
			try:
				props = {'Phone':number}
				phoneui = phone_data.getPhoneuiObject(systemBus)
				phoneui.CreateContact(props)
			except:
				print _("Failed to initiate a call")
		
	def numberButton_clicked (self, button):
		list = self.__list

		get_column = self.__getContactColumn()

		contact_column = list.get_column(get_column)
		renderer = contact_column.get_cell_renderers()[0]

		
		if self.__contact_show_column == self.CONTACT_POSITION:
			self.__contact_show_column = self.NUMBER_POSITION
			button_label = _("Names")

		else:
			self.__contact_show_column = self.CONTACT_POSITION
			button_label = _("Numbers")
			
		button.set_label(button_label)
		contact_column.set_attributes(renderer, text=self.__contact_show_column)

		
		
	def durationButton_clicked (self, button):
		list = self.__list
		
		get_column = self.__getDateColumn()

		date_column = list.get_column(get_column)
		renderer = date_column.get_cell_renderers()[0]

		
		if self.__date_show_column == self.DATE_POSITION:
			self.__date_show_column = self.DURATION_POSITION
			button_label = _("Date")

		else:
			self.__date_show_column = self.DATE_POSITION
			button_label = _("Duration")
			
		button.set_label(button_label)
		date_column.set_attributes(renderer, text=self.__date_show_column)

		
		
	
	def __getSelectedNumber(self):
		list = self.__list
		selected = list.get_selection().get_selected_rows()
		selected = selected[1][0]

		row = list.get_model()[selected]

		number = row[self.NUMBER_POSITION]
		return number
	def addToTab(self, notebook):
		type = typeFromLegacyDaemonType(self.__type)
		if CallsTab.conf.get('phonelog',type + '_tab_type') == 'icon':
			tabWidget = gtk.Image()
			image_path = CallsTab.conf.get('phonelog',type + '_tab_image_file')

			#if not absolute
			if image_path[0] != '/':
				image_path = ICONS_PATH + image_path
				
			tabWidget.set_from_file(image_path)
		else:
			tabWidget = gtk.Label(CallsTab.conf.get('phonelog',type + '_tab_name') )
		notebook.insert_page(self.getTab(), tabWidget)
		notebook.set_tab_label_packing(self.getTab(), True, True, gtk.PACK_START)
class GeneralTab(CallsTab):
	#ACTUAL GUI POSTITON
	STATUS_COLUMN = 0	
	CONTACT_COLUMN = 1
	DATE_COLUMN = 2
	def __getStatusColumn(self):
		return self.STATUS_COLUMN
		
	def createList(self):
		CallsTab.createList(self)
		
		renderer = gtk.CellRendererText()
		renderer.set_property('weight', 800)
		renderer.set_property('size-points', 8)
		
		column0 = gtk.TreeViewColumn("Status", renderer, text=self.STATUS_POSITION)
		list = self.getList()
		list.insert_column( column0, self.__getStatusColumn() )
	def populateList(self, group_type = None):
		if group_type == None:
			group_type = 2
		CallsTab.populateList(self, 2)
	


class SignalMonitor:
	PHONE_BUSNAME = 'org.freesmartphone.opimd'
	PHONE_OBJECTPATH = '/org/freesmartphone/PIM/Calls'
	PHONE_INTERFACE = 'org.freesmartphone.PIM.Calls'
	__bus = None
		
	def __init__(self, bus):
		self.__bus = bus
		self.__connectCallStatus()

	def __connectCallStatus(self):
		bus = self.__bus
		proxy = bus.get_object(self.PHONE_BUSNAME,self.PHONE_OBJECTPATH)
		iface = dbus.Interface(proxy, self.PHONE_INTERFACE)
		iface.connect_to_signal("NewCall", self.cb_CallAdded)
        			
	def cb_CallAdded(self, path):
		#TODO: check, what to update.
		incoming.setManuallyUpdated(False)
		missed.setManuallyUpdated(False)
		outgoing.setManuallyUpdated(False)
		general.setManuallyUpdated(False)
		
class PhoneData:
	#OPHONEKITD related
	OPHONEKITD_DB = "/var/db/phonelog.db"
	CALL_STATUS_INCOMING = 0
	CALL_STATUS_OUTGOING = 1
	CALL_STATUS_ACTIVE = 2
	CALL_STATUS_HELD = 3
	CALL_STATUS_RELEASE = 4

	MARKVIEWED_BUSNAME = 'org.freesmartphone.opimd'
	#MARKVIEWED_OBJECTPATH = '/org/freesmartphone/PIM/Calls'
	MARKVIEWED_INTERFACE = 'org.freesmartphone.PIM.Call'

	CONTACTS_BUSNAME = 'org.freesmartphone.opimd'
	CONTACTS_OBJECTPATH = '/org/freesmartphone/PIM/Contacts'
	CONTACTS_INTERFACE = 'org.freesmartphone.PIM.Contacts'
	CONTACT_INTERFACE = 'org.freesmartphone.PIM.Contact'
	CONTACTS_INTERFACE_QUERY = 'org.freesmartphone.PIM.ContactQuery'

	CALLS_BUSNAME = 'org.freesmartphone.opimd'
	CALLS_OBJECTPATH = '/org/freesmartphone/PIM/Calls'
	CALLS_INTERFACE = 'org.freesmartphone.PIM.Calls'
	CALLS_INTERFACE_QUERY = 'org.freesmartphone.PIM.CallQuery'

	PHONE_BUSNAME = 'org.freesmartphone.ogsmd'
	PHONE_OBJECTPATH = '/org/freesmartphone/GSM/Device'
	PHONE_INTERFACE = 'org.freesmartphone.GSM.Call'
	
	PHONEUI_BUSNAME = 'org.shr.phoneui' 
	PHONEUI_OBJECTPATH = '/org/shr/phoneui/Contacts'
	PHONEUI_INTERFACE = 'org.shr.phoneui.Contacts'
	
	LEGACY_TYPE = 0
	OPHONEKITD_TYPE = 1
	__phonelog_type = OPHONEKITD_TYPE
	__database = None
	__phoneLog = None
	__contacts_cache = {}
	conf       = None # is inited before use to ConfigParser object

	def __init__(self, systemBus, config):
                # init the ConfigParser object once, bail out if failing.
                if PhoneData.conf == None:
                  PhoneData.conf = config
                assert PhoneData.conf != None
	def dbus_ok(self, *args, **kargs):
		pass
	def dbus_err(self, x, *args, **kargs):
		print str(x)
	def getContactDisplay(self, result):
		name = ''
		if 'Name' in result:
			name = result['Name']
		if 'Surname' in result:
			name += ' '+result['Surname']
		if name=='':
			if 'Nickname' in result:
				name = result['Nickname']
			else:
				name = "???"
		return name

	def clean(self):
		if self.__database != None:
			self.__database.close()
	def getCalls(self, type):

		if config.getboolean('phonelog','debug'):
			print _("Started getting ") + type
			
		interface = self.getDbusObject (systemBus, self.CALLS_BUSNAME, self.CALLS_OBJECTPATH, self.CALLS_INTERFACE)

		if type == "missed":
			props = {'Answered':0, 'Direction':'in'}
			props['_limit'] = PhoneData.conf.getint('phonelog', 'missed_limit') 
		elif type == "incoming":
			props = {'Answered':1, 'Direction':'in'}
			props['_limit'] = PhoneData.conf.getint('phonelog', 'received_limit') 
		elif type == "outgoing":
			props = {'Direction':'out'}
			props['_limit'] = PhoneData.conf.getint('phonelog', 'made_limit') 
		elif type == "all":
			props = {}
			props['_limit'] = PhoneData.conf.getint('phonelog', 'general_limit') 
		else:
			return None

		props['_sortby'] = 'Timestamp'
		props['_sortdesc'] = True
		props['_resolve_phonenumber'] = True
		props['_retrieve_full_contact'] = True

		x = interface.Query(props)

		query = self.getDbusObject (systemBus, self.CALLS_BUSNAME, x, self.CALLS_INTERFACE_QUERY)
			
		calls = query.GetMultipleResults(query.GetResultCount())

		query.Dispose() # delete query result from memory

		if config.getboolean('phonelog','debug'):
			print _("Finished getting ") + type

		return calls
	def getDbusObject (self, bus, busname , objectpath , interface):
		dbusObject = bus.get_object(busname, objectpath)
		return dbus.Interface(dbusObject, dbus_interface=interface)
	def getMarkViewedObject (self, bus, object_path):
		return self.getDbusObject (bus,
				      self.MARKVIEWED_BUSNAME,
				      object_path,
				      self.MARKVIEWED_INTERFACE
				     )

	def getContactsObject (self, bus):
		return self.getDbusObject (bus,
				      self.CONTACTS_BUSNAME,
				      self.CONTACTS_OBJECTPATH,
				      self.CONTACTS_INTERFACE
				     )
	def getPhoneuiObject (self, bus):
		return self.getDbusObject (bus,
				      self.PHONEUI_BUSNAME,
				      self.PHONEUI_OBJECTPATH,
				      self.PHONEUI_INTERFACE
				     )

	def getPhoneObject (self, bus):
		return self.getDbusObject (bus,
				      self.PHONE_BUSNAME,
				      self.PHONE_OBJECTPATH,
				      self.PHONE_INTERFACE
				     )

#LEGACY SUPPORT FUNCTIONS
def stringToTabNumber(string):
	if string.lower() == "received":
		return PAGE_INCOMING
	elif string.lower() == "made":
		return PAGE_OUTGOING
	elif string.lower() == "missed":
		return PAGE_MISSED
	elif string.lower() == "general":
		return PAGE_GENERAL
def typeFromLegacyDaemonType(type):
	if type == "incoming":
		return 'received'
	elif type == "outgoing":
		return 'made'
	elif type == "missed":
		return 'missed'
	elif type == "all":
		return 'general'
	else:
		return "received"
#END OF LEGACY FUNCTIONS

def timeToString(call_time):
	now = datetime.now().timetuple()
	time_tuple = call_time.timetuple()
	#check if it's today
	if (now[0] == time_tuple[0]) and \
	   (now[1] == time_tuple[1]) and \
	   (now[2] == time_tuple[2]):

		return "Today, " + call_time.strftime(config.get('phonelog','time_format'))
	else:
		return call_time.strftime(config.get('phonelog','date_format') + " " + config.get('phonelog','time_format') )

def quitMainloop(*dump):
	mainloop.quit()

def populateListByPageNum(incoming, outgoing, missed, general, page_num):
	if page_num == PAGE_GENERAL:
		if not general.getManuallyUpdated():
			general.populateList()
	elif page_num == PAGE_INCOMING:
		if not incoming.getManuallyUpdated():		
			incoming.populateList()
	elif page_num == PAGE_OUTGOING:
		if not outgoing.getManuallyUpdated():
			outgoing.populateList()
	elif page_num == PAGE_MISSED:
		if not missed.getManuallyUpdated():
			missed.populateList()
		
#THIS GLOBAL VAR IS PART OF THIS FUNCTION
upright = None
def cb_win_resize(window, rect, notebook):
	global upright
	#update upright, and no need to go further if
	#we are already in the same position
	tmp_upright = (rect.height > rect.width)
	if tmp_upright == upright:
		return
	else:
		upright = tmp_upright
	
	if upright:
		if config.getboolean('phonelog','debug'):
			print "Upright"
		notebook.set_property("tab-pos", gtk.POS_TOP)
	else:
		if config.getboolean('phonelog','debug'):
			print "Rotated"
		notebook.set_property("tab-pos", gtk.POS_LEFT)

def init_last_view_times(config):
	if not config.has_section('viewlog'):
		config.add_section('viewlog')

	if not config.has_option('viewlog', 'incoming'):
		config.set('viewlog','incoming', '0')
	if not config.has_option('viewlog', 'outgoing'):
		config.set('viewlog','outgoing', '0')
	if not config.has_option('viewlog', 'missed'):
		config.set('viewlog','missed', '0')
	if not config.has_option('viewlog', 'all'):
		config.set('viewlog','all', '0')

def cb_TabSwitch(notebook, page, page_num, incoming, outgoing, missed, general):
	populateListByPageNum(incoming, outgoing, missed, general, page_num)

#############################################
################# MAIN ######################
#############################################

print _("Phonelog")
#create the config dir if doesn't exist.
if not os.path.exists(CONFIGURATION_DIR):
	os.mkdir(CONFIGURATION_DIR)

DBusGMainLoop(set_as_default=True)
mainloop = gobject.MainLoop()

systemBus = dbus.SystemBus()
config = ConfigParser.ConfigParser()
config.read([DEFAULT_CONFIG_PATH, CONFIGURATION_FILE])
init_last_view_times(config)

phone_data = PhoneData(systemBus, config)
phoneutils.init()

RefreshMonitor = SignalMonitor(systemBus)

if config.getboolean('phonelog','debug'):
	print _("Initialized global vars")
	print _("Initializing gtk interface")

win = gtk.Window()
notebook= gtk.Notebook()
win.add (notebook)
win.connect('delete-event', quitMainloop )
win.set_title(_("Phone Log"))
win.set_default_size(480,640)

if config.getboolean('phonelog','change_rotate_layout'):
	win.connect('size-allocate', cb_win_resize, notebook) #redsign on resize


incoming = CallsTab('incoming', conf=config)
outgoing = CallsTab('outgoing')
missed = CallsTab('missed', True)
general = GeneralTab('all')

incoming.addToTab(notebook)
outgoing.addToTab(notebook)
missed.addToTab(notebook)
general.addToTab(notebook)

if config.getboolean('phonelog','debug'):
	print _("Showing main window")
win.show_all()

startup_page = stringToTabNumber(config.get('phonelog','startup_tab') )
notebook.set_current_page(startup_page)

notebook.connect ("switch-page", cb_TabSwitch, incoming, outgoing, missed, general)


populateListByPageNum(incoming, outgoing, missed, general, startup_page)

#end of inits.
mainloop.run()

phoneutils.deinit()

#clean and exit
phone_data.clean()
# write out config file on exit (also last viewed times)
with open(CONFIGURATION_FILE,'w+') as f:
  config.write(f)
