#!/usr/bin/python2.5
#
# Erminig-NG (A two-way synchronization tool for Google-Calendar and
#              "Fremantle-Calendar", the calendar of Maemo 5)
# 
# Copyright (c) 2010 Pascal Jermini <lorelei@garage.maemo.org>
# 
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public License
# version 2.1, as published by the Free Software Foundation.
# 
# 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
# Lesser General Public License for more details.
# 
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to:
# 
#   Free Software Foundation, Inc.
#   51 Franklin Street, Fifth Floor,
#   Boston, MA 02110-1301 USA
# 


# !!!!!! All times must be handled in UTC !!!!!!

import sqlite3
import random
import urllib
import time
import xml.utils.iso8601
import sys
import string
import getopt

import gdata.calendar
import gdata.calendar.service
import atom.service

from Event import Event
import erminig_conf

gd_client = gdata.calendar.service.CalendarService()
gd_client.email=erminig_conf.google_username
gd_client.password=erminig_conf.google_password
gd_client.source="Erminig ng 0.0.1"
gd_client.ProgrammaticLogin()

calConn = sqlite3.connect("/home/user/.calendar/calendardb")
calCur = calConn.cursor()

erminigConn = sqlite3.connect("/home/user/.erminig.db")
erminigCur = erminigConn.cursor()

def initErminigDB():
	erminigCur.execute("CREATE TABLE IF NOT EXISTS googleIDs (localID INTEGER, googleID TEXT)")
	erminigCur.execute("CREATE TABLE IF NOT EXISTS events (localID INTEGER, googleID TEXT)")
	erminigCur.execute("CREATE TABLE IF NOT EXISTS config (tag TEXT, value TEXT)")

def saveCurrentTimestamp():
	now = int(time.time())
	query = "SELECT COUNT(*) AS X FROM config WHERE tag=\"LastSync\""
	erminigCur.execute(query)
	count = (erminigCur.fetchone())[0]

	if count == 0:
		erminigCur.execute("INSERT INTO config (tag, value) VALUES (\"LastSync\", %s)" % (now))
	else:
		erminigCur.execute("UPDATE config SET value=%s WHERE tag=\"LastSync\"" % (now))

	erminigConn.commit()

def localCalendarExists(gcalID):
	query = "SELECT COUNT(*) AS X FROM googleIDs WHERE googleID=\"%s\"" \
		% (gcalID)
	erminigCur.execute(query)
	count = (erminigCur.fetchone())[0]

	if count > 0:
		return True
	else:
		return False

def createLocalCalendar(id, name):
	# Ask user if must create calendar:
	text = "Calendar '%s' doesn't exist locally: create it? [y/n] " % (name)
	ans = raw_input(text)

	if ans != "y" and ans != "Y":
		return False

	# create new calendar into Local DB:
	colour = random.randint(0,9)
	query = "INSERT INTO Calendars (Name, Colour, IsVisible, IsReadonly, CalendarType, CalendarVersion, CalendarProfile) VALUES (\"%s\", %s, 1,0,0,\"1.0\",0)" % (name, colour)
	calCur.execute(query)
	calConn.commit()
	lastId = calCur.lastrowid

	# Set-up correspondance table:
	query = "INSERT INTO googleIDs (localID, googleID) VALUES (%s, \"%s\")"\
			 % (lastId, id)
	erminigCur.execute(query)
	erminigConn.commit()

	return True

def createGoogleCalendar(lid, name):
	# Ask user if must create calendar:
	text = "Calendar '%s' doesn't exist in Google: create it? [y/n] " % \
			(name)
	ans = raw_input(text)

	if ans != "y" and ans != "Y":
		return False

	# create new calendar in Google:
	calendar = gdata.calendar.CalendarListEntry()
	calendar.title = atom.Title(text=name)
	calendar.hidden = gdata.calendar.Hidden(value='false')
	calendar.selected = gdata.calendar.Selected(value='true')
	new_calendar = gd_client.InsertCalendar(new_calendar=calendar)
	gid = urllib.unquote((new_calendar.id.text.rpartition("/"))[2])

	# Set-up correspondance table:
	query = "INSERT INTO googleIDs (localID, googleID) VALUES (%s, \"%s\")"\
			 % (lid, gid)
	erminigCur.execute(query)
	erminigConn.commit()

	return True
	
def getAllLocalCalendars():
	query = "SELECT CalendarId, Name FROM Calendars"
	calCur.execute(query)
	rows = calCur.fetchall()
	return rows
	
def googleCalendarExists(id):
	query = "SELECT COUNT(*) AS X FROM googleIDs WHERE localID=%s" \
		% (id)
	erminigCur.execute(query)
	count = (erminigCur.fetchone())[0]

	if count > 0:
		return True
	else:
		return False


def getAllCalendars():
	# Sync Google -> Local
	feed = gd_client.GetOwnCalendarsFeed()
	for i,cal in enumerate(feed.entry):
		title = cal.title.text
		# Get only the last part of the ID (and substitute the %'s):
		id = urllib.unquote((cal.id.text.rpartition("/"))[2])
		if not localCalendarExists(id):
			createLocalCalendar(id, title)

	# Sync Local -> Google
	for id,cal in getAllLocalCalendars():
		if not googleCalendarExists(id):
			createGoogleCalendar(id, cal)


def getUpdateableCalendars():
	query = "SELECT * from googleIDs"
	erminigCur.execute(query)
	rows = erminigCur.fetchall()
	return rows

def eventExistsLocally(id):
	query = "SELECT localID FROM events WHERE googleID=\"%s\"" % (id)
	erminigCur.execute(query)
	res = erminigCur.fetchone()
	if res:
		return res[0]
	else:
		return None

def insertGoogleEventLocally(event, cid):
	# check if event doesn't exist already (if yes, just update it)
	localID = eventExistsLocally(event.get_id())
	if localID:	
		print "Update -> %s" % (localID)
		updateGoogleEventLocally(event, cid, localID)
	else:
		print "NEW EVENT"
		insertNewGooglEventLocally(event, cid)

def updateGoogleEventLocally(event, cid, localID):
	now = time.time()
	calCur.execute("UPDATE Components SET DateStart=?, DateEnd=?, Summary=?, Location=?, Description=? WHERE CalendarId=? AND Id=?", (event.get_start(), event.get_end(), event.get_title(), event.get_where(), event.get_description(), cid, localID))
	calConn.commit()

def insertNewGooglEventLocally(event, cid):
	calCur.execute("INSERT INTO Components (CalendarId, ComponentType, Flags, DateStart, DateEnd, Summary, Location, Description, Status, Until, AllDay, CreatedTime, ModifiedTime, Tzid, TzOffset) VALUES (?, 1, 1, ?, ?, ?, ?, ?, -1, -1, ?, ?, ?, \":UTC\", 0)", (cid, event.get_start(), event.get_end(), event.get_title(), event.get_where(), event.get_description(), event.get_fullday(), event.get_cdate(), event.get_cdate()))
	calConn.commit()
	lastId = calCur.lastrowid

	query = "INSERT INTO ComponentDetails (Id, ComponentType, Priority, DateStamp, Sequence, Uid, Percent) VALUES (%s, 1, -1, \"0\", 0, -1, -1)" % (lastId)
	calCur.execute(query)
	calConn.commit()

	query = "INSERT INTO events (localID, googleID) VALUES (%s, \"%s\")" \
			% (lastId, event.get_id())
	erminigCur.execute(query)
	erminigConn.commit()

def isEventFullDay(date):
	if (string.find(date, "T") == -1):
		return 1
	else:
		return 0

def iso8601ToTimestamp(date):
	return xml.utils.iso8601.parse(date)

def timestampToIso8601(date):
	d = xml.utils.iso8601.tostring(date)
	if (len(d) == 24):
		return d
	if (string.find(d, "Z") == -1):
		return d
	else:
		if (len(d) == 20):
			return string.replace(d, "Z", ".000Z")
		else:
			return string.replace(d, "Z", ":00.000Z")

def removeCancelledEventLocally(gid):
	# get local ID from googleId:
	lid = getLocalIdFromGoogleId(gid)
	if not lid:
		return

	print "Removing local event (Deleted from Google -> %s)" % (gid)

	# purge Calendar entry:
	query = "DELETE FROM Components WHERE Id=%s" % (lid)
	calCur.execute(query)
	calConn.commit()

	# purge entry in correspondance table:
	query = "DELETE FROM events WHERE LocalId=%s" % (lid)
	erminigCur.execute(query)
	erminigConn.commit()

	
def getNewEventsFromGoogle(startDate):
	# get list of updateable calendars:
	toUpdate = getUpdateableCalendars()
	for localid, googleid in toUpdate:
		query = gdata.calendar.service.CalendarEventQuery(googleid, \
				'private', 'composite', None, {"ctz":"utc"})
#		query.start_min = xml.utils.iso8601.tostring(startDate)
		query.updated_min = xml.utils.iso8601.tostring(startDate)
		query.updated_max = xml.utils.iso8601.tostring(time.time())
		query.max_results = "100000"

		feed = gd_client.CalendarQuery(query)
		for i, e in enumerate(feed.entry):
			title = e.title.text
			if e.recurrence <> None:
				print "Warning: recurring event. Skipping for the moment!"
				print "Event was: %s" % (title)
				continue
			where = e.where[0].value_string
			description =  e.content.text
			start_time = iso8601ToTimestamp(e.when[0].start_time)
			end_time = iso8601ToTimestamp(e.when[0].end_time)
			id = urllib.unquote((e.id.text.rpartition("/"))[2])
			cdate = iso8601ToTimestamp(e.published.text)

			# An event is full-day if it doesn't have any
			# timing event (only date)
			fullday = isEventFullDay(e.when[0].start_time)

			event = Event(title, where, description, \
					start_time, end_time, fullday, id,\
					cdate)
			print id
			if e.event_status.value == "CANCELED":
				# remove event locally:
				removeCancelledEventLocally(id)
			else:
				insertGoogleEventLocally(event, localid)

		print "---------------------------------------------"

def getLastSync():
	# default time is now - 1 year:
	lastSync = int(time.time()) - 365*24*60*60
	query = "SELECT value FROM config WHERE tag=\"LastSync\""
	erminigCur.execute(query)
	res = erminigCur.fetchone()
	if res:
		lastSync = int(res[0])
		
	return lastSync

def queryNewLocalEvents(lastSync, lid):
	query = "SELECT Id, DateStart, DateEnd, Summary, Location, Description, ALlDay,TzOffset FROM Components WHERE CalendarId = %s AND CreatedTime > %s" % (lid, lastSync)
	calCur.execute(query)
	res = calCur.fetchall()

	return res

def createNewGoogleEvent(evt, googleid):

	start_time = timestampToIso8601(evt.get_start())
	end_time = timestampToIso8601(evt.get_end())

	# if it's a full-day event, then we can strip the start/end dates:
	if evt.get_fullday() == 1:
		start_time = timestampToIso8601(int(evt.get_start())+evt.get_tzOffset())
		end_time = timestampToIso8601(int(evt.get_end())+evt.get_tzOffset())
		start_time = start_time[0:10]
		end_time = end_time[0:10]

	event = gdata.calendar.CalendarEventEntry()
	event.title = atom.Title(text=evt.get_title())
	event.content = atom.Content(text=evt.get_description())
	event.where.append(gdata.calendar.Where(value_string=evt.get_where()))
	event.when.append(gdata.calendar.When(start_time=start_time, end_time=end_time))

	for i in range(0,erminig_conf.maxattempts):
		try:
			new_event = gd_client.InsertEvent(event, urllib.quote('/calendar/feeds/' + googleid + '/private/full'))
			break
		except gdata.service.RequestError, e:
			if i > erminig_conf.maxattempts:
				print "Maximum number of attempts to store the entry to Google reached; skipping entry"
				return
			if e[0]['status'] == 302:
				print "** Received spurious redirect - retrying in 2 seconds (attempt %s of %s)" % (i+1, erminig_conf.maxattempts)
				time.sleep(2)

	gid = (new_event.id.text.rpartition("/"))[2]
	# insert correspondance table entry:
	query = "INSERT INTO events (localID, googleID) VALUES (%s, \"%s\")" \
			% (evt.get_id(), gid)
	print query
	erminigCur.execute(query)
	erminigConn.commit()

def getNewEventsFromLocal(lastSync):
	toUpdate = getUpdateableCalendars()
	for localid, googleid in toUpdate:
		# Those are new events to create in Google:
		evts = queryNewLocalEvents(lastSync, localid)
		for e in evts:
			event = Event(e[3], e[4], e[5], e[1], e[2], e[6], \
					e[0], 0)
			event.set_tzOffset(e[7])
			gid = createNewGoogleEvent(event, googleid)

def queryUpdatedLocalEvents(lastSync, lid):
	query = "SELECT Id, DateStart, DateEnd, Summary, Location, Description, ALlDay, TzOffset FROM Components WHERE CalendarId = %s AND ModifiedTime > %s AND CreatedTime < %s" % (lid, lastSync, lastSync)
	calCur.execute(query)
	res = calCur.fetchall()

	return res

def getGoogleIdFromLocalId(lid):
	query = "SELECT googleId FROM events WHERE localId=%s" % (lid)
	erminigCur.execute(query)
	res = erminigCur.fetchone()
	if res:
		return res[0]
	else:
		return None
	
def getLocalIdFromGoogleId(gid):
	query = "SELECT localId FROM events WHERE googleId=\"%s\"" % (gid)
	erminigCur.execute(query)
	res = erminigCur.fetchone()
	if res:
		return res[0]
	else:
		return None

def updateGoogleEvent(evt, googleid):

	# get Googleid of event:
	gid = getGoogleIdFromLocalId(evt.get_id())
	if not gid:
		return

	print "Updating Local  ->  Google %s " % (gid)

	event = gd_client.GetCalendarEventEntry(urllib.quote("/calendar/feeds/"\
			+ googleid + "/private/full/" + gid))

	start_time = timestampToIso8601(evt.get_start())
	end_time = timestampToIso8601(evt.get_end())

	# if it's a full-day event, then we can strip the start/end dates:
	if evt.get_fullday() == 1:
		start_time = timestampToIso8601(int(evt.get_start())+evt.get_tzOffset())
		end_time = timestampToIso8601(int(evt.get_end())+evt.get_tzOffset())
		start_time = start_time[0:10]
		end_time = end_time[0:10]

	event.title = atom.Title(text=evt.get_title())
	event.content = atom.Content(text=evt.get_description())
	event.where[0] = gdata.calendar.Where(value_string=evt.get_where())
	event.when[0] = gdata.calendar.When(start_time=start_time, end_time=end_time)

	gd_client.UpdateEvent(event.GetEditLink().href, event)

def getUpdatedEventsFromLocal(lastSync):
	toUpdate = getUpdateableCalendars()
	for localid, googleid in toUpdate:
		# Those are events to update in Google:
		evts = queryUpdatedLocalEvents(lastSync, localid)
		for e in evts:
			event = Event(e[3], e[4], e[5], e[1], e[2], e[6], \
					e[0], 0)
			event.set_tzOffset(e[7])
			gid = updateGoogleEvent(event, googleid)

def getDeletedEventsFromLocal(lastSync):
	toUpdate = getUpdateableCalendars()
	for cid, googlecid in toUpdate:
		# get all events in the Trash:
		query = "SELECT ComponentId FROM Trash WHERE DeletedTime > %s AND CalendarId =%s" % (lastSync, cid)
		calCur.execute(query)
		rows = calCur.fetchall()

		for e in rows:
			gid = getGoogleIdFromLocalId(e[0])
			if not gid:
				return
			event = gd_client.GetCalendarEventEntry(urllib.quote(\
					"/calendar/feeds/"\
				+ googlecid + "/private/full/" + gid))
			print "Deleting Google event %s" % (gid)
			gd_client.DeleteEvent(event.GetEditLink().href)


def syncNewEvents():
	lastSync = getLastSync()
	
	getNewEventsFromGoogle(lastSync)
	getNewEventsFromLocal(lastSync)
	getUpdatedEventsFromLocal(lastSync)
	getDeletedEventsFromLocal(lastSync)

	# save timestamp of last update:
	saveCurrentTimestamp()

initErminigDB()

try:
	opts, args = getopt.getopt(sys.argv[1:], "c", ["sync-calendars"])
except getopt.GetoptError, err:
	print str(err)
	sys.exit(1)
for o, a in opts:
	if o in ("-c", "--sync-calendars"):
		getAllCalendars()
		sys.exit(0)

syncNewEvents()
