#!/usr/bin/python
license="""
Copyright (C) Dwayne Zon 2009 <dwayne.zon@gmail.com>

You may 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.

zSync.py 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 zToDo.py.  If not, write to:
The Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor
Boston, MA  02110-1301, USA.
"""
version_long='0.6: <2009-Apr-22 6:09pm>'
version_short='0.6'

import sys
from ftplib import FTP
import ftplib
import gtk, hildon
try:
    import osso
    nokia=True
except:
    nokia=False
import pickle, os, threading, fcntl

# Application startup routine
def application_setup(text_name, sys_name, homedir='.python'):
    global program_name, logfile, root, osso_context, context_name, app, vb
    program_name=text_name
    # "context" must start with "org.maemo."
    context_name='org.maemo.'+sys_name
    osso_context=None
    app=None
    # change into data dir
    f=os.getenv('HOME') + '/' + homedir
    if not os.path.exists(f):
        os.mkdir(f)
    os.chdir(f)
    if nokia:
        # log errors to a file for post-mortem debugging w/o a console
        logfile=sys_name+'.log'
        sys.stderr=open(logfile, 'w')
        # Nokia tablet setup
        osso_context=osso.Context(context_name, version_short, False)
        app=hildon.Program()
        root=hildon.Window()
        app.add_window(root)
        root.window_in_fullscreen=False
        root.connect('key-press-event', cb_key_press)
        root.connect('window-state-event', cb_window_state_change)
        # note that this also sets the main window title in Hildon
        gtk.set_application_name(program_name)
    else:
        root=gtk.Window()
        root.resize(200, 200)
        root.set_title(program_name)

    # exit gracefully when window is closed
    root.connect('destroy', lambda wid: gtk.main_quit())
    root.connect('delete_event', lambda a1, a2: gtk.main_quit())

    # exit gracefully on shutdown
#    signal.signal(15, lambda a1: gtk.main_quit())
    vb=gtk.VBox()
    root.add(vb)
    return root, vb

def attach_menu(menu, window=None):
    if nokia:
        if window:
            window.set_menu(menu)
        else:
            root.set_menu(menu)
    else:
        # ugly as hell
        mb=gtk.MenuBar()
        i=gtk.MenuItem('Operation Menu')
        i.set_submenu(menu)
        mb.append(i)
        vb.pack_start(mb, False, False)

# create Hildonized dialog window
def dialog_window(title, modal=True):
    if nokia:
        w=hildon.Window()
        app.add_window(w)
    else:
        w=gtk.Window()
        w.resize(800, 480)
    w.set_transient_for(root)
    w.set_destroy_with_parent(True)
    if modal:
        w.set_modal(True)
    w.set_title(title)
    if nokia:
        w.window_in_fullscreen=False
        w.connect('key-press-event', cb_key_press)
        w.connect('window-state-event', cb_window_state_change)
    return w

# display logfile
class show_logfile:
    def __init__(self, *x):
        if not nokia:
            return

        # create window with scrolling text
        self.win=dialog_window('View Logs', False)
        sw=gtk.ScrolledWindow()
        sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
        self.win.add(sw)
        tv=gtk.TextView()
        tv.set_wrap_mode(gtk.WRAP_WORD)
        tv.set_editable(False)
        buf=tv.get_buffer()
        end_mark=buf.create_mark('end', buf.get_end_iter(), False)
        sw.add(tv)

        # read log file and load widget
        sys.stderr.flush()
        buf.insert_at_cursor(file(logfile, 'rb').read())

        # create menu
        self.menu=gtk.Menu()
        i=gtk.MenuItem('Clear Log')
        i.connect('activate',self.clear_log)
        self.menu.append(i)
        i=gtk.MenuItem('Close')
        i.connect('activate',self.close)
        self.menu.append(i)

        self.win.set_menu(self.menu)
        self.win.show_all()
        tv.scroll_mark_onscreen(end_mark)

    def close(self, b):
        self.win.destroy()

    def clear_log(self, b):
        sys.stderr=file(logfile, 'w')
        self.win.destroy()

###################################################################################################################
### Dialog to edit command file

class cb_edit_command_file:
    insertcmds = ('# This is for zTodo\nPUT zToDo.db /home/user/.zToDo/\nGET zTodonew.db /home/user/.zToDo/zToDo.db\nCHECKPOINT',
				  '#zCalSync not yet implemented',
				  '# This is for the Expense application\nPUT expense.db /home/user/.bramblesoft/expense/\nCHECKPOINT')
    def __init__(self, *x):
        self.win=dialog_window('Command File')
        vb=gtk.VBox()
        self.win.add(vb)
        # Create menu
        menu2=gtk.Menu()
        i=gtk.MenuItem('Insert zToDo Commands')
        i.connect('activate',self.cb_insert,0)
        menu2.append(i)
        i=gtk.MenuItem('Insert zCalSync Commands')
        i.connect('activate',self.cb_insert,1)
        menu2.append(i)
        i=gtk.MenuItem('Insert expense Commands')
        i.connect('activate',self.cb_insert,2)
        menu2.append(i)
        attach_menu(menu2,self.win)

        box2 = gtk.VBox(False, 10)
        vb.pack_start(box2, True, True, 0)
        sw = gtk.ScrolledWindow()
        sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
        textview = gtk.TextView()
        self.textbuffer = textview.get_buffer()
        sw.add(textview)
        box2.pack_start(sw)

        infile = open(parmblock.commandfile,'r')
        if infile:
            string = infile.read()
            infile.close
        else:
            string = ''
        self.textbuffer.set_text(string)
        separator = gtk.HSeparator()
        vb.pack_start(separator, True, True, 0)
        box2 = gtk.HBox(False, 10)
        vb.pack_start(box2, False, True, 0)
        cancelbutton = button_with_image('Cancel', gtk.STOCK_CANCEL, self.cb_cancel)
        box2.pack_start(cancelbutton, True, True, 10)
        savebutton = button_with_image('Save', gtk.STOCK_SAVE, self.cb_save)
        box2.pack_start(savebutton, True, True, 10)
        self.win.show_all()

    # detail dialog: cancel button
    def cb_cancel(self, b):
        self.win.destroy()

    # detail dialog: OK button
    def cb_save(self, b):
        infile = open(parmblock.commandfile,'w')
        infile.write(self.textbuffer.get_text(self.textbuffer.get_start_iter(),self.textbuffer.get_end_iter()))
        infile.close
        self.win.destroy()

    # insert menu items
    def cb_insert(self, b, block):
        self.textbuffer.insert_at_cursor(self.insertcmds[block])

###################################################################################################################
### Dialog to edit parameters

class cb_setup_parms:
    def __init__(self, *x):
        self.win=dialog_window('Setup Parameters')
        vb=gtk.VBox()
        self.win.add(vb)

        # File edit box
        hb=gtk.HBox()
        vb.pack_start(hb, False, False,10)
        txt = gtk.Label('Command File:')
        hb.pack_start(txt, True, False,10)
        self.commandfile = gtk.Entry()
        self.commandfile.set_text(parmblock.commandfile)
        hb.pack_start(self.commandfile, True, False)
        # Port edit box
        hb=gtk.HBox()
        vb.pack_start(hb, False, False,10)
        txt = gtk.Label('Port:')
        hb.pack_start(txt, True, False,10)
        self.port = gtk.Entry()
        self.port.set_text(parmblock.port)
        hb.pack_start(self.port, True, False,10)
        # Userid edit box
        hb=gtk.HBox()
        vb.pack_start(hb, False, False,10)
        txt = gtk.Label('Userid:')
        hb.pack_start(txt, True, False,10)
        self.tuserid = gtk.Entry()
        self.tuserid.set_text(parmblock.userid)
        hb.pack_start(self.tuserid, True, False,10)
        # Passwordedit box
        hb=gtk.HBox()
        vb.pack_start(hb, False, False,10)
        txt = gtk.Label('Password:')
        hb.pack_start(txt, True, False,10)
        self.tpassword = gtk.Entry()
        self.tpassword.set_text(parmblock.password)
        hb.pack_start(self.tpassword, True, False,10)
        # buttons
        btn_save=button_with_image('Save', gtk.STOCK_SAVE, self.cb_save)
        btn_cancel=button_with_image('Cancel', gtk.STOCK_CANCEL, self.cb_cancel)
        hb=gtk.HBox()
        vb.pack_start(hb, False, False)
        hb.pack_start(btn_save, False, False,10)
        hb.pack_start(btn_cancel, False, False,10)
        self.win.show_all()

    # detail dialog: cancel button
    def cb_cancel(self, b):
        self.win.destroy()

    # detail dialog: OK button
    def cb_save(self, b):
        parmblock.commandfile = self.commandfile.get_text()
        parmblock.port = self.port.get_text()
        parmblock.userid = self.tuserid.get_text()
        parmblock.password = self.tpassword.get_text()
        if parmblock.userid <> '':
            userid = parmblock.userid
        if parmblock.password <> '':
            password = parmblock.password
        self.win.destroy()

###################################################################################################################
### utility functions

# detect hardware key presses
def cb_key_press(w, event):
    if event.keyval == gtk.keysyms.F6:
        # the "full screen" hardware key
        if w.window_in_fullscreen:
            w.unfullscreen()
        else:
            w.fullscreen()

# detect when the window is toggled to full-screen
def cb_window_state_change(w, event):
    if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
        w.window_in_fullscreen=True
    else:
        w.window_in_fullscreen=False

# button with stock image but without stock label
def button_with_image(label, stock, fcn, *args):
    b=gtk.Button(label)
    i=gtk.Image()
    if label and nokia:
        # large button
        i.set_from_stock(stock, gtk.ICON_SIZE_DND)
    else:
        # small button
        i.set_from_stock(stock, gtk.ICON_SIZE_BUTTON)
    b.set_image(i)
    b.connect('clicked', fcn, *args)
    return b

# About dialog box
def cb_about(*x):
    d=gtk.AboutDialog()
    d.set_name(program_name)
    d.set_version(version_short)
    d.set_comments('Sync with PC in support of zPIMS')
    d.set_authors(['Dwayne Zon <dwayne.zon@gmail.com'])
    d.set_license(license)
    d.set_website('http://ztodo.garage.maemo.org/')
    d.connect('response', lambda d, r: d.destroy())
    d.run()

############################################################################################################
class ftpsync(threading.Thread):

# Display messages on GUI and in log
    def dispmsg(self,msg):
        gtk.gdk.threads_enter()
        statusbox.set_text(msg)
        gtk.gdk.threads_leave()
        print >> sys.stderr,msg

# Reset Sync and Cancel buttons to normal
    def restore_buttons(self):
        gtk.gdk.threads_enter()
        sync_btn.set_sensitive(True)
        gtk.gdk.threads_leave()

# Process the command file
    def run(self):
        waserrors = False
        try:
#    if True:
            print >> sys.stderr,'Connecting to "'+parmblock.address+'":',parmblock.port
            remotezsync = FTP()
            remotezsync.connect(parmblock.address.rstrip().split(' ')[0],parmblock.port)
#            print userid,password
            remotezsync.login(userid,password)
#            remotezsync.timeout = .1
        except ftplib.all_errors, e :
            self.dispmsg( 'Connection failed')
            print >> sys.stderr,e
            self.restore_buttons()
            return
# Open command file and cycle through commands
        try:
            cmdfile = open (parmblock.commandfile,'r')
        except:
            self.dispmsg('Could not open command file')
            self.restore_buttons()
            return
        errorflag = False
        for command in cmdfile:
            self.dispmsg('Processing: ' + command.rstrip())
            if errorflag:
                self.dispmsg( 'Skipping to next checkpoint')
            cmdoptions = command.rstrip().split(' ')
# CHECKPOINT - If error, all commands will be ignored until the next checkpoint
            if cmdoptions[0] == 'CHECKPOINT':
                if errorflag:
                    self.dispmsg('Error flag reset')
                errorflag = False
# PUT <filename> <source directory> - send the file name to the remote computer
            elif (not errorflag) and cmdoptions[0] == 'PUT':
                # rename file to .tmp
                try:
                    fstore = open(cmdoptions[2] + cmdoptions[1])
                    fcntl.flock(fstore,fcntl.LOCK_EX | fcntl.LOCK_NB)
                    os.rename(cmdoptions[2] + cmdoptions[1],cmdoptions[2] + cmdoptions[1] + '.tmp')
                    fcntl.flock(fstore,fcntl.LOCK_UN | fcntl.LOCK_NB)
                    fstore.close

# if fails, set errorflag = True and don't do the rest
# after success, rename the file back
                    infile = open(cmdoptions[2] + cmdoptions[1] + '.tmp',"rb")
                    try:
                        remotezsync.storbinary("STOR " + cmdoptions[1],infile)
                        os.rename(cmdoptions[2] + cmdoptions[1] + '.tmp',cmdoptions[2] + cmdoptions[1])
                    except Exception:
                        print >> sys.stderr,'Error putting file'
                        errorflag = True
                        waserrors = True
                    else:
                        self.dispmsg( 'Successful upload')
                        infile.close()
                except ftplib.all_errors, e:
                    self.dispmsg('Failed to prep file for send. In use?')
                    print >> sys.stderr,e
                    errorflag = True
                    waserrors = True

# GET <filename> <target filename> - get the filename from the remote computer and put on the local computer
            elif (not errorflag) and cmdoptions[0] == 'GET':
                try:
                    outfile = open(cmdoptions[2] + '.tmp',"wb")
                except:
                    self.dispmsg( 'Unable to create ' + cmdoptions[2] + ' for GET')
                    errorflag = True
                    waserrors = True
                    break
                try:
                    remotezsync.retrbinary("RETR " + cmdoptions[1],outfile.write)
                except ftplib.all_errors, e :
                    self.dispmsg("Error in downloading the remote file.")
                    print >> sys.stderr,e
                    errorflag = True
                    outfile.close()
                    waserrors = True
                    break
                else:
                    self.dispmsg("Successful download")
                    outfile.close()
                    if os.path.getsize(cmdoptions[2] + '.tmp') > 0:
                        try:
                            os.rename(cmdoptions[2] + '.tmp',cmdoptions[2])
                        except ftplib.all_errors, e:
                            self.dispmsg('Rename of temp file failed. In use?')
                            print >> sys.stderr,e
                            errorflag = True
                            waserrors = True
                    else:
                        self.dispmsg('File downloaded was empty. Skipping to next checkpoint')
                        errorflag = True
                        waserrors = True
# RUN <fully qualified program name>
            elif (not errorflag) and cmdoptions[0] == 'RUN':
                os.system(cmdoptions[1])
        remotezsync.close()
        cmdfile.close()
        if waserrors:
            comptype = 'with errors'
        else:
            comptype = ''
        self.dispmsg('Command file processing completed '+comptype)
        self.restore_buttons()
# Do the sync
def cb_sync(b):
# Open FTP connection
# Text changed in the IPPrompt combobox
    global ftpthread
    sync_btn.set_sensitive(False)
    parmblock.addressentry = addressprompt.get_active()
    parmblock.address = addressprompt.get_active_text()
    if parmblock.addressentry < 0 :
        addresses.insert(0,[parmblock.address])
        parmblock.addressentry = 0
    if len(addresses) > 5:
        addresses.remove(5)
    ftpthread = ftpsync()
    ftpthread.start()

# Define the saved parameters
class parmblock_model:
    address = ''
    addressentry = None
    commandfile='zSync.txt'
    port='5962'
    userid=''
    password=''

################################################################################################################
# Main code starts here
root, vb=application_setup('zSync', 'zSync','.zSync')
print >> sys.stderr,version_long

# Load the saved parms
addresses = gtk.ListStore(str)
try:
    parmfile = file("zSync.dat","rb")
    parmblock = pickle.load(parmfile)
    saveaddrs=pickle.load(parmfile)
#    print "loading",saveaddrs
    for i in saveaddrs:
        addresses.append([i])
    parmfile.close
except:
    parmblock = parmblock_model()
if parmblock.userid == '':
    userid='zSync'
else:
    userid = parmblock.userid
if parmblock.password == '':
    password='zSync1956'
else:
    password = parmblock.password

# Create the GUI
hb=gtk.HBox()
vb.pack_start(hb, False, False)

# Create menu
menu=gtk.Menu()
i=gtk.MenuItem('Setup Parms')
i.connect('activate',cb_setup_parms)
menu.append(i)
i=gtk.MenuItem('Edit Command File')
i.connect('activate',cb_edit_command_file)
menu.append(i)
i=gtk.MenuItem('View Log')
i.connect('activate',show_logfile)
menu.append(i)
i=gtk.MenuItem('About')
i.connect('activate',cb_about)
menu.append(i)
i=gtk.MenuItem('Close')
i.connect('activate',lambda x: gtk.main_quit())
menu.append(i)
attach_menu(menu)
# commandfile creates a menu item the same as the detail screen for ztodo - note
#	Also, create menu items in this dialog that inserts templates at the cursor for "ztodo", "zcal", "expense"
#  button to save
# - setup parms: command filename, port number, userid, password - save this in the dat file
#
# Title
txt = gtk.Label('zSync - Sync files/apps with PC')
hb.pack_start(txt, True, False)
# Address combo entry box
hb=gtk.HBox()
vb.pack_start(hb, False, False)
addressprompt = gtk.ComboBoxEntry(addresses,0)
cell = gtk.CellRendererText()
addressprompt.pack_start(cell,True)
if parmblock.addressentry != None:
    addressprompt.set_active(parmblock.addressentry)
#addressprompt.add_attribute(cell,'text',0)
hb.pack_start(addressprompt, True, True)

# "Sync" button
hb=gtk.HBox()
vb.pack_start(hb, False, False)
sync_btn=button_with_image('Sync', gtk.STOCK_REFRESH, cb_sync)
hb.pack_start(sync_btn, True, False)
# Statusbox
hb=gtk.HBox()
vb.pack_start(hb, False, False)
statusframe = gtk.Frame()
#statusframe.set_shadow_type(gtk.SHADOW_OUT)
#statusbox.set_line_wrap(True)
statusbox = gtk.Label()
statusbox.set_line_wrap(True)
statusframe.add(statusbox)
hb.pack_start(statusframe, True, True)
# Instructions
hb=gtk.HBox()
vb.pack_start(hb, False, False,10)
txt = gtk.Label('Enter IP address / URL of PC to sync with\nCancel from PC side if necessary')
hb.pack_start(txt, True, False)

# Set a timer and if GET is waiting for more than 3 minutes, autodisconnect
root.show_all()
#root.connect('key-press-event', cb_key_press2)

# ignore "SystemError: NULL object passed to Py_BuildValue" at poweroff
try:
    gtk.gdk.threads_init()
    gtk.main()
# Save parameters
    parmfile = open("zSync.dat","wb")
    pickle.dump(parmblock,parmfile)
    saveaddrs=[]
    for i in addresses:
        saveaddrs.append((i[0]))
#	print saveaddrs
    pickle.dump(saveaddrs, parmfile)

    parmfile.close
except:
    pass