# Code Journal
import gtk
import gtk.glade
import pango
import glob
import os
import time
import xml.parsers.expat
from xml.sax import saxutils

PROJECT_DIR = os.path.expanduser("~/.codejournal")
JOURNAL="%s/journal.xml"%(PROJECT_DIR)
""" 
XML FORMAT:
<journal>
<entry project='someproj' date='MM-DD-YYYY'> </entry>
</journal>
"""
STYLESHEET='stylesheet.xsl'
XSL_STYLESHEET="""\
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">        <xsl:output method="xml" indent="yes" doctype-public="-//W3C//DTD XHTML 1.1//EN" doctype-system="http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"/>
        <xsl:template match="/*">
                <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
                        <head>
                                <title>Code Journal</title>
                        </head>
                        <body>
                                <xsl:for-each select="*">
                                <h1><xsl:value-of select="@project"/> - <xsl:value-of select="@date"/></h1>
                                <pre><xsl:value-of select="text"/></pre>
                                </xsl:for-each>
                        </body>
                </html>
        </xsl:template>
</xsl:stylesheet>
"""

# Constants that show where the two entries are in the combobox
PROJECTS=0
DATES=1 
# Constants for named items:
TODO='Todo'
TODAY='Today'
# Returns date in YYYY-MM-DD format
def getDate():
    t = time.localtime()
    year = t[0]
    month = t[1]
    if int(month) < 10:
        month = '0' + str(month)
    day = t[2]
    if int(day) < 10:
        day = '0' + str(day)
    return "%s-%s-%s" % (year, month, day)

class XMLPage:
    def __init__(self, page):
        self.xml = ''
        self.page = page

    def addStylesheet(self, stylesheet):
        if stylesheet is not None:
            self.xml = "<?xml-stylesheet href=\"%s\" type=\"text/xsl\"?>\n"%(stylesheet) + self.xml

    def startPage(self):
        self.xml = '<journal>\n' + self.xml

    def addEntry(self, project, date, entry):
        self.xml += "<entry project=\"%s\" date=\"%s\">\n" % (project, date)
        self.xml += "<text>\n%s\n</text>\n</entry>\n" % (entry)

    def addXml(self, xml):
        self.xml += xml

    def endPage(self):
        self.xml += '</journal>\n'

    def savePage(self, dir):
        file = open("%s/%s" % (dir, self.page), "w")
        file.write(self.xml)
        file.close()



class ExpatJournal:
    def __init__(self, entries):
        self.entries = entries
        self.__textFlag = False

    def start_element(self, name, attrs):
        if name == 'entry':
            self.__project = attrs['project']
            self.__date = attrs['date']
        if name == 'text':
            self.__textFlag = True
            self.__text = ''

    def end_element(self, name):
        if name == 'entry':
            try:
                self.entries[self.__project][self.__date] = self.__text.strip()
            except:
                self.entries[self.__project] = { self.__date : self.__text.strip() }

    def char_data(self, data):
        if self.__textFlag:
            self.__text = self.__text + data

class GladeObj(gtk.glade.XML):
    def __init__(self, data):
        gtk.glade.XML.__init__(self, data)

    def __getattr__(self, attr):
        return self.get_widget(attr)

class CodeJournalGui:
    def __init__(self):
        self.xml = GladeObj("codejournal.glade")
        self.entries = {}

        self.initVars()
        self.initWidgets()
        self.connect_handlers()
        self.readProjects(JOURNAL)
        
        self.updateProjectStore()
        self.updateDateStore()
        
        self.xml.combobox1.set_active(PROJECTS)
        self.xml.combobox2.set_active(PROJECTS)
        self.xml.textview1.grab_focus()
        self.xml.entry2.grab_focus()

    def initVars(self):
        self.editing = False
        self.date = getDate()

    def initWidgets(self):
        self.buildProjectStore()
        self.buildDateStore()
        self.buildExportStore()
        self.xml.textview1.set_wrap_mode(gtk.WRAP_WORD)
        self.textBuffer = self.xml.textview1.get_buffer()
        texttag = self.textBuffer.create_tag('largebold', scale=pango.SCALE_LARGE, weight=pango.WEIGHT_BOLD)
        self.textBuffer.create_mark('start', self.textBuffer.get_start_iter())
        self.textBuffer.create_mark('end', self.textBuffer.get_start_iter())

        self.xml.copy1.set_property('sensitive', False)
        self.xml.cut1.set_property('sensitive', False)
        self.xml.delete1.set_property('sensitive', False)
        self.xml.paste1.set_property('sensitive', False)
        display = gtk.gdk.display_manager_get().get_default_display()
        self.clipboard = gtk.Clipboard(display, 'CLIPBOARD')

        filefilter = gtk.FileFilter()
        filefilter.set_name("*.xml")
        filefilter.add_pattern("*.xml")
        self.xml.filechooserdialog1.add_filter(filefilter)
        
    def connect_handlers(self):
        self.xml.window1.connect('destroy', self.quit)
        # Combo boxes
        self.xml.combobox1.connect('changed', self.change_view)
        self.xml.combobox2.connect('changed', self.change_export_view)

        # New project Dialog
        self.xml.okbutton1.connect('clicked', self.newproj_ok_clicked)
        self.xml.cancelbutton1.connect('clicked', self.newproj_cancel_clicked)
        self.xml.entry1.connect('activate', self.newproj_ok_clicked)

        # Delete project dialog
        self.xml.okbutton2.connect('clicked', self.delproj_ok_clicked)
        self.xml.cancelbutton2.connect('clicked', self.delproj_cancel_clicked)

        # About Dialog
        self.xml.closebutton1.connect('clicked', self.destroy_decoy, self.xml.dialog4)
        # Visual dialog to show that export is finished
        self.xml.okbutton3.connect('clicked', self.destroy_decoy, self.xml.dialog3)

        # Connect regular buttons
        self.xml.button1.connect('clicked', self.del_project)
        self.xml.button2.connect('clicked', self.new_project)
        self.xml.button3.connect('clicked', self.destroy_decoy, self.xml.filechooserdialog1)
        self.xml.button4.connect('clicked', self.open_project)
        self.xml.button5.connect('clicked', self.destroy_decoy, self.xml.window2)
        self.xml.button6.connect('clicked', self.save_project)
        self.xml.entry3.connect('activate', self.save_project)
        self.xml.entry2.connect('activate', self.save_project)
        self.xml.radiobutton1.connect('toggled', self.export_file)
        self.xml.radiobutton2.connect('toggled', self.export_file)

        self.xml.textview1.connect('focus-in-event', self.textview_focused)
        self.textBuffer.connect('mark-set', self.mark_set)

        autodict = { 'on_new_activate'       : self.new_project, 
                     'on_save_activate'      : self.save_journal,
                     'on_export_activate'    : self.export_project,
                     'on_import_activate'    : self.import_project,
                     'on_delete'             : self.destroy_decoy,
                     'on_quit_activate'      : self.quit }
        self.xml.signal_autoconnect(autodict)

        # Edit Menu
        autodict = { 'on_cut_activate'       : self.edit_cut, 
                     'on_copy_activate'      : self.edit_copy,
                     'on_paste_activate'     : self.edit_paste,
                     'on_delete_activate'    : self.edit_delete }
        self.xml.signal_autoconnect(autodict)
        # Help Menu
        autodict = { 'on_about_activate'     : self.show_about }
        self.xml.signal_autoconnect(autodict)


    def buildProjectStore(self):
        self.projectStore = gtk.TreeStore(str, str)
        column = gtk.TreeViewColumn('Projects')
        self.xml.projview.append_column(column)
        cellStr = gtk.CellRendererText()
        column.pack_start(cellStr)
        column.set_attributes(cellStr, text=0)
        self.projSelection = self.xml.projview.get_selection()
        self.projSelection.set_select_function(self.proj_selected, self.projectStore)

    def buildDateStore(self):
        self.dateStore = gtk.ListStore(str)
        column = gtk.TreeViewColumn('Date')
        self.xml.dateview.append_column(column)
        cellStr = gtk.CellRendererText()
        column.pack_start(cellStr)
        column.set_attributes(cellStr, text=0)
        self.dateSelection = self.xml.dateview.get_selection()
        self.dateSelection.set_select_function(self.date_selected, self.dateStore)

    def buildExportStore(self):
        self.exportStore = gtk.ListStore(str)
        column = gtk.TreeViewColumn()
        column.set_widget(None)
        self.xml.exportview.append_column(column)
        cellStr = gtk.CellRendererText()
        column.pack_start(cellStr)
        column.set_attributes(cellStr, text=0)
        self.exportSelection = self.xml.exportview.get_selection()
        self.exportSelection.set_mode(gtk.SELECTION_MULTIPLE)
        self.exportSelection.set_select_function(self.export_selected)


    def readProjects(self, filename):
        try:
            r = ExpatJournal(self.entries)
            self.parser = xml.parsers.expat.ParserCreate()
            self.parser.StartElementHandler = r.start_element
            self.parser.EndElementHandler = r.end_element
            self.parser.CharacterDataHandler = r.char_data
            data = open(filename, "r").read()
            self.parser.Parse(data)
        except Exception, e:
            print e

    def saveProjects(self, filename, projects=None):
        if projects is None:
            projects = self.entries.keys()
        file = open(filename, "w")
        file.write('<journal>\n')
        for project in projects:
            for date in self.entries[project]:
                file.write('    <entry project="%s" date="%s">\n'%(project, date))
                file.write('        <text>\n')
                file.write('            %s\n' % saxutils.escape(self.entries[project][date]))
                file.write('        </text>\n')
                file.write('    </entry>\n')
        file.write('</journal>')
        file.close()

    def updateProjectStore(self):
        self.projectStore.clear()
        date = getDate()
        for project in self.entries:
            parent = self.projectStore.append(None, [project, project])
            self.projectStore.append(parent, [TODO, project])
            dates = self.entries[project].keys()
            dates.sort()
            dates.reverse()
            for entry in dates:
                if not entry == date and not entry == TODO:
                    self.projectStore.append(parent, [entry, project]) 
        self.xml.projview.set_model(self.projectStore)

    def updateDateStore(self):
        dates = {TODO : ''}
        self.dateStore.clear()
        for project in self.entries:
            for date in self.entries[project]:
                dates[date] = True
        dates = dates.keys()
        dates.sort()
        dates.reverse()
        today = getDate()
        for date in dates:
            if date == today:
                date = TODAY
            self.dateStore.append([date])
        self.xml.dateview.set_model(self.dateStore)

    def updateExportStore(self, view):
        self.exportStore.clear()
        if view == PROJECTS:
            for project in self.entries:
                self.exportStore.append([project])
        elif view == DATES:
            dates = {TODO: ''}
            for project in self.entries:
                for date in self.entries[project]:
                    dates[date] = True
            dates = dates.keys()
            dates.sort()
            dates.reverse()
            today = getDate()
            for date in dates:
                self.exportStore.append([date])
        self.xml.exportview.set_model(self.exportStore)

    def clearTextBuffer(self):
        self.textBuffer.delete(self.textBuffer.get_start_iter(), self.textBuffer.get_end_iter())

    def appendTextBuffer(self, text):
        self.textBuffer.insert(self.textBuffer.get_end_iter(), text)

    def applyTextTag(self, tag):
        end = self.textBuffer.get_iter_at_mark(self.textBuffer.get_mark('end'))
        start = self.textBuffer.get_iter_at_mark(self.textBuffer.get_mark('start'))
        if start.get_offset() >= end.get_offset():
            start = self.textBuffer.get_start_iter()
        self.textBuffer.apply_tag(tag, start, end)

    def applyNamedTag(self, tag):
        end = self.textBuffer.get_iter_at_mark(self.textBuffer.get_mark('end'))
        start = self.textBuffer.get_iter_at_mark(self.textBuffer.get_mark('start'))
        if start.get_offset() >= end.get_offset():
            start = self.textBuffer.get_start_iter()
        self.textBuffer.apply_tag_by_name(tag, start, end)

    def markHere(self, mark):
        end = self.textBuffer.get_end_iter()
        almost_end = self.textBuffer.get_iter_at_offset(end.get_offset()-1)
        self.textBuffer.place_cursor(almost_end)
        self.textBuffer.move_mark_by_name(mark, almost_end)

    def checkDate(self):
        date = getDate()
        if date != self.date:
            self.date = date
            self.updateProjectStore()
            self.updateDateStore()

    def display(self, entry=None, project=None, editing=False):
        self.saveEntry(editing)
        self.clearTextBuffer()
        if project is None:
            for project in self.entries:
                if self.entries[project].has_key(entry):
                    text = self.entries[project][entry].strip()
                    if text == '':
                        continue
                    self.markHere('start')
                    self.appendTextBuffer("%s\n"%(project))
                    self.markHere('end')
                    self.applyNamedTag('largebold')
                    self.appendTextBuffer("%s\n" % (text))
        elif entry is not None:
            self.markHere('start')
            if not self.editing:
                self.appendTextBuffer("%s\n"%(entry))
            self.markHere('end')
            self.applyNamedTag('largebold')
            if self.entries[project].has_key(entry):
                self.appendTextBuffer("%s" % (self.entries[project][entry]))
        else:
            print "Dunno what's going on here"

    def saveEntry(self, editing):
        try:
            if self.editing and not editing:
                text = self.textBuffer.get_text(self.textBuffer.get_start_iter(), self.textBuffer.get_end_iter())
                if text.strip != '':
                    self.entries[self.project][self.entry] = text
                self.editing = False
                self.saveProjects(JOURNAL)
        except KeyError, e:
            print "%s no longer exists" % (e)

    def show_projects(self):
        self.xml.vbox5.set_property('visible', True)
        self.xml.scrolledwindow6.set_property('visible', False)
        model, iter = self.projSelection.get_selected()
        if iter is not None:
            self.proj_selected(model.get_path(iter), model)
    def show_dates(self):
        self.xml.scrolledwindow6.set_property('visible', True)
        self.xml.vbox5.set_property('visible', False)
        model, iter = self.dateSelection.get_selected()
        if iter is not None:
            self.date_selected(model.get_path(iter), model)

    # Edit menu settings.
    def set_pastable(self, bool):
        self.xml.paste1.set_property('sensitive', bool)

    def set_copyable(self, bool):
        self.xml.copy1.set_property('sensitive', bool)

    def set_editable(self, bool): # sets cut and delete.
        self.xml.cut1.set_property('sensitive', bool)
        self.xml.delete1.set_property('sensitive', bool)

    # Signal Handlers
    def show_about(self, obj):
        self.xml.dialog4.show()

    def new_project(self, obj):
        self.xml.dialog1.present()

    def save_journal(self, obj):
        self.saveEntry(False)
        self.saveProjects(JOURNAL)

    def open_project(self, obj):
        filename = self.xml.filechooserdialog1.get_filename()
        if filename is None:
            return
        try:
            self.readProjects(filename)
            self.updateProjectStore()
            self.updateDateStore()
        except Exception, e:
            print e
        self.xml.filechooserdialog1.hide()

    def save_project(self, obj):
        if not self.xml.button6.get_property('sensitive'):
            return
        dir = self.xml.filechooserbutton1.get_filename()
        filename = self.xml.entry2.get_text()
        savename = self.xml.entry3.get_text()
        if filename.strip() == '' or self.exportSelection.count_selected_rows() == 0:
            self.xml.entry2.grab_focus()
            return
        if savename.strip() == '':
            self.xml.entry3.grab_focus()
            return
        model, selected = self.exportSelection.get_selected_rows()
        
        self.xml.okbutton3.set_property('sensitive', False)
        self.xml.dialog3.show()
        self.xml.progressbar1.set_fraction(0.0)
        
        items = []
        for item in selected:
            iter = model.get_iter(item)
            items.append(model.get_value(iter, 0))
        
        xml_pages = []
        if self.xml.radiobutton1.get_active():
            stylesheet = None
        elif self.xml.radiobutton2.get_active():
            stylesheet = STYLESHEET
        
        self.xml.progressbar1.set_fraction(0.2)
        if self.xml.combobox2.get_active() == PROJECTS:
            for item in items:
                dates = self.entries[item].keys()
                dates.sort()
                dates.reverse()
                node = XMLPage("%s.xml"%(item))
                xml_pages.append(node)
                for date in dates:
                    if self.entries[item][date].strip() == '':
                        continue
                    node.addEntry(item, date, saxutils.escape(self.entries[item][date]))
        elif self.xml.combobox2.get_active() == DATES:
            items.sort()
            items.reverse()
            for item in items:
                node = XMLPage(item)
                xml_pages.append(node)
                for project in self.entries.keys():
                    if item in self.entries[project]:
                        if self.entries[project][item].strip() == '':
                            continue
                        node.addEntry(project, item, saxutils.escape(self.entries[project][item]))

        self.xml.progressbar1.set_fraction(0.6)
        dir = "%s/%s"%(dir, filename)
        if not os.path.exists("%s" % (dir)):
            os.mkdir("%s" % (dir))
        if stylesheet:
            for page in xml_pages:
                page.startPage()
                page.addStylesheet(stylesheet)
                page.endPage()
                page.savePage("%s" % (dir))
        else:
            root = XMLPage(savename)
            root.startPage()
            root.addStylesheet(STYLESHEET)
            for page in xml_pages:
                root.addXml(page.xml)
            root.endPage()
            root.savePage(dir)
            self.xml.progressbar1.set_fraction(0.9)
        
        file = open("%s/%s"%(dir, STYLESHEET), "w")
        file.write(XSL_STYLESHEET)
        file.close()
        self.xml.progressbar1.set_fraction(1.0)
        self.xml.progressbar1.set_text('Export Finished!')
        self.xml.okbutton3.set_property('sensitive', True)
        
        self.xml.window2.hide()

    def import_project(self, obj):
        self.xml.filechooserdialog1.present()

    def export_project(self, obj):
        self.xml.entry3.set_text('journal.xml')
        self.xml.entry2.set_text('Journal')
        self.xml.entry2.select_region(0, -1)
        self.exportSelection.unselect_all()
        self.xml.window2.present()

    def del_project(self, obj):
        model, iter = self.projSelection.get_selected()
        if iter is not None:
            parent = model.get_value(iter, 1)
            entry = model.get_value(iter, 0)
            if entry == TODO:
                return
            if parent == entry:
                text = "Are you sure you want to remove %s and all its entries?" % (parent)
            else:
                text = "Are you sure you want to remove %s from %s?" % (entry, parent)
            self.xml.label6.set_text(text)
            self.xml.dialog2.present()

    def export_file(self, obj):
        self.xml.entry3.set_property('sensitive', self.xml.radiobutton1.get_active())

    def quit(self, obj):
        self.save_journal(None)
        gtk.main_quit()

    def edit_copy(self, obj):
        self.textBuffer.copy_clipboard(self.clipboard)

    def edit_cut(self, obj):
        self.textBuffer.cut_clipboard(self.clipboard, self.editing)

    def edit_delete(self, obj):
        print 'delete'

    def edit_paste(self, obj):
        self.textBuffer.paste_clipboard(self.clipboard, None, self.editing)

    def destroy_decoy(self, obj, data=None):
        try:
            data.hide()
        except:
            obj.hide()
        return True

    def mark_set(self, textbuffer, iter, mark):
        smark = self.textBuffer.get_mark('selection_bound')
        imark = self.textBuffer.get_mark('insert')
        siter = self.textBuffer.get_iter_at_mark(smark)
        iiter = self.textBuffer.get_iter_at_mark(imark)
        soff = siter.get_offset()
        ioff = iiter.get_offset()
        if ioff != soff:
            if self.editing:
                self.set_editable(True)
            else:
                self.set_editable(False)
            self.set_copyable(True)

        else:
            self.set_copyable(False)
            self.set_editable(False)

    def textview_focused(self, obj, event):
        self.set_pastable(self.editing)

    def newproj_ok_clicked(self, obj):
        date = getDate()
        name = self.xml.entry1.get_text().strip()
        if name in self.entries or name == '':
            return
        self.xml.dialog1.hide()
        self.entries[name] = {TODO:''}
        self.updateProjectStore()

    def newproj_cancel_clicked(self, obj):
        self.xml.dialog1.hide()

    def delproj_ok_clicked(self, obj):
        model, iter= self.projSelection.get_selected()
        entry = model.get_value(iter, 0)
        parent = model.get_value(iter, 1)
        if entry == parent:
            del self.entries[entry]
        else:
            del self.entries[parent][entry]
        self.updateProjectStore()
        self.updateDateStore()
        self.xml.label3.set_label('')
        self.xml.dialog2.hide()

    def delproj_cancel_clicked(self, obj):
        self.xml.dialog2.hide()

    def change_view(self, obj):
        self.checkDate()
        if obj.get_active() == PROJECTS:
            self.show_projects()
        if obj.get_active() == DATES:
            self.show_dates()

    def change_export_view(self, obj):
        self.xml.button6.set_property('sensitive', False)
        self.updateExportStore(obj.get_active())

    def date_selected(self, path, model):
        self.checkDate()
        self.xml.textview1.set_editable(False)
        self.xml.textview1.set_cursor_visible(False)
        iter = self.dateStore.get_iter(path)
        entry = model.get_value(iter, 0)
        self.xml.label3.set_label("<b><big>%s</big></b>"%entry)
        if entry == TODAY:
            entry = getDate()
        self.display(entry=entry)
        return True

    def proj_selected(self, path, model):
        self.checkDate()
        iter = self.projectStore.get_iter(path)
        parent = model.get_value(iter, 1)
        entry = model.get_value(iter, 0)
        # If it is a child, we make it uneditable.
        if len(path) > 1 and entry != TODO:
            self.xml.textview1.set_editable(False)
            self.xml.textview1.set_cursor_visible(False)
            self.xml.label3.set_label("<b><big>%s - Entry</big></b>"%(parent))
            editing = False
        else:
            self.saveEntry(False)
            self.editing = True
            if entry != TODO:
                entry = getDate()
                text = TODAY
            else:
                text = TODO
            self.project = parent
            self.entry = entry
            self.xml.textview1.set_editable(True)
            self.xml.textview1.set_cursor_visible(True)
            editing = True
            self.xml.label3.set_label("<b><big>%s - %s</big></b>"%(parent, text))
        self.display(entry=entry, project=parent, editing=editing)
        return True

    def export_selected(self, path):
        if self.exportSelection.path_is_selected(path) \
                and self.exportSelection.count_selected_rows() <= 1:
                    self.xml.button6.set_property('sensitive', False)
        else:
            self.xml.button6.set_property('sensitive', True)
        return True
    
def main():
    codeJournalGui = CodeJournalGui()
    gtk.main()

if __name__ == "__main__":
    if not os.path.exists(PROJECT_DIR):
        os.mkdir(PROJECT_DIR)
    main()

