#!/usr/bin/env python

# Copyright (c) 2005 Fredrik Kuivinen <freku045@student.liu.se>
# Copyright (c) 2005 Mark Williamson <mark.williamson@cl.cam.ac.uk>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 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 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

from ctcore import *
import sys, math, random, qt, os, re, signal, sets
from optparse import OptionParser
from commit import CommitDialog

# Determine semantics according to executable name.  Default to git.
if os.path.basename(sys.argv[0]) == 'hgct':
    import hg as scm
else:
    import git as scm

qconnect = qt.QObject.connect
Qt = qt.Qt
#DEBUG = 1

class FileState:
    pass

class MyListItem(qt.QCheckListItem):
    def __init__(self, parent, file, commitMsg = False):
        qt.QCheckListItem.__init__(self, parent, file.text, qt.QCheckListItem.CheckBox)
        self.file = file
        self.commitMsg = commitMsg

    def compare(self, item, col, asc):
        if self.commitMsg:
            if asc:
                return -1
            else:
                return 1
        elif item.commitMsg:
            if asc:
                return 1
            else:
                return -1
        else:
            return cmp(self.file.srcName, item.file.srcName)

    def paintCell(self, p, cg, col, w, a):
        if self.commitMsg:
            qt.QListViewItem.paintCell(self, p, cg, col, w, a)
        else:
            qt.QCheckListItem.paintCell(self, p, cg, col, w, a)

    def isSelected(self):
        return self.state() == qt.QCheckListItem.On

    def setSelected(self, s):
        if s:
            self.setState(qt.QCheckListItem.On)
        else:
            self.setState(qt.QCheckListItem.Off)

class MyListView(qt.QListView):
    def __init__(self, parent=None, name=None):
        qt.QListView.__init__(self, parent, name)

    def __iter__(self):
        return ListViewIterator(self)

class ListViewIterator:
    def __init__(self, listview):
        self.it = qt.QListViewItemIterator(listview)

    def next(self):
        cur = self.it.current()
        if cur:
            self.it += 1
            if cur.commitMsg:
                return self.next()
            else:
                return cur
        else:
            raise StopIteration()

    def __iter__(self):
        return self

class MainWidget(qt.QMainWindow):
    def __init__(self, options, parent=None, name=None):
        qt.QMainWindow.__init__(self, parent, name)
        self.setCaption(applicationName)
        self.statusBar()
        
        splitter = qt.QSplitter(Qt.Vertical, self)
        self.setCentralWidget(splitter)
        self.splitter = splitter

        # The file list and file filter widgets are part of this layout widget.
        self.filesLayout = qt.QVBox(splitter)

        # The file list
        fW = MyListView(self.filesLayout)
        self.filesW = fW
        fW.setFocus()
        fW.setSelectionMode(qt.QListView.NoSelection)
        fW.addColumn('Description')
        fW.setResizeMode(qt.QListView.AllColumns)

        # The file filter
        self.filterLayout = qt.QHBox(self.filesLayout)
        self.filterClear = qt.QPushButton("&Clear", self.filterLayout)
        self.filterLabel = qt.QLabel(" File filter:  ", self.filterLayout)
        qconnect(self.filterClear, qt.SIGNAL("clicked()"), self.clearFilter)
        self.filter = qt.QLineEdit(self.filterLayout)
        self.filterLabel.setBuddy(self.filter)
        
        qconnect(self.filter, qt.SIGNAL("textChanged(const QString&)"), self.updateFilter)

        self.newCurLambda = lambda i: self.currentChange(i)
        qconnect(fW, qt.SIGNAL("currentChanged(QListViewItem*)"), self.newCurLambda)

        # The diff viewing widget
        self.text = qt.QWidgetStack(splitter)
        
        ops = qt.QPopupMenu(self)
        ops.setCheckable(True)
        ops.insertItem("Commit Selected Files", self.commit, Qt.CTRL+Qt.Key_T)
        ops.insertItem("Refresh", self.refreshFiles, Qt.CTRL+Qt.Key_R)
        ops.insertItem("(Un)select All", self.toggleSelectAll, Qt.CTRL+Qt.Key_S)
        self.showUnknownItem = ops.insertItem("Show Unkown Files",
                                              self.toggleShowUnknown,
                                              Qt.CTRL+Qt.Key_U)
        ops.insertItem("Preferences...", self.showPrefs, Qt.CTRL+Qt.Key_P)
        ops.setItemChecked(self.showUnknownItem, settings().showUnknown)
        self.operations = ops

        m = self.menuBar()
        m.insertItem("&Operations", ops)

        h = qt.QPopupMenu(self)
        h.insertItem("&About", self.about)
        m.insertItem("&Help", h)

        qconnect(fW, qt.SIGNAL("contextMenuRequested(QListViewItem*, const QPoint&, int)"),
                 self.contextMenuRequestedSlot)
        self.fileOps = qt.QPopupMenu(self)
        self.fileOps.insertItem("Toggle selection", self.toggleFile)
        self.fileOps.insertItem("Edit", self.editFile, Qt.CTRL+Qt.Key_E)
        self.fileOps.insertItem("Discard changes", self.discardFile)
        self.fileOps.insertItem("Ignore file", self.ignoreFile)

        # The following attribute is set by contextMenuRequestedSlot
        # and currentChange and used by the fileOps
        self.currentContextItem = None
        
        self.patchColors = {'std': 'black', 'new': '#009600', 'remove': '#C80000', 'head': '#C800C8'}

        self.files = scm.fileSetFactory(lambda f: self.addFile(f),
                                        lambda f: self.removeFile(f))
        f = File()
        f.text = "Commit message"
        f.textW = self.newTextEdit()
        f.textW.setTextFormat(Qt.PlainText)
        f.textW.setReadOnly(False)
        f.textW.setText(settings().signoff)
        qconnect(f.textW, qt.SIGNAL('cursorPositionChanged(int, int)'),
                 self.updateCommitCursor)
        self.cmitFile = f
        self.createCmitItem()
        self.editorProcesses = sets.Set()
        self.loadSettings()

        self.options = options

    def updateStatusBar(self):
        if not self.cmitFile.textW.isVisible():
            self.setStatusBar('')

    def setStatusBar(self, string):
        branch = scm.getCurrentBranch()
        if branch:
            prefix = '[' + branch + '] '
        else:
            prefix = ''
        self.statusBar().message(prefix + string)

    def updateCommitCursor(self, *dummy):
        [line, col] = self.cmitFile.textW.getCursorPosition()
        self.setStatusBar('Column: ' + str(col))

    def loadSettings(self):
        self.splitter.setSizes(settings().splitter)

    def closeEvent(self, e):
        s = self.size()
        settings().width = s.width()
        settings().height = s.height()
        settings().splitter = self.splitter.sizes()
        e.accept()

    def createCmitItem(self):
        self.cmitItem = MyListItem(self.filesW, self.cmitFile, True)
        self.cmitItem.setSelectable(False)
        self.filesW.insertItem(self.cmitItem)
        self.cmitFile.listViewItem = self.cmitItem
        
    def about(self, ignore):
        str = '<qt><center><h1>%(appName)s %(version)s</h1>' \
              '<p>Copyright &copy; 2005 Fredrik Kuivinen &lt;freku045@student.liu.se&gt;</p>' \
              '<p>Copyright &copy; 2005 Mark Williamson &lt;maw48@cl.cam.ac.uk&gt;</p></center>' \
              '<p>This program is free software; you can redistribute it ' \
              'and/or modify it under the terms of the GNU General Public ' \
              'License version 2 as published by the Free Software Foundation.' \
              '</p></qt>' % {'appName': applicationName, 'version': version}

        qt.QMessageBox.about(self, "About " + applicationName, str)

    def contextMenuRequestedSlot(self, item, pos, col):
        if item and not item.commitMsg:
            self.currentContextItem = item
            self.fileOps.exec_loop(qt.QCursor.pos())
        else:
            self.currentContextItem = None

    def toggleFile(self, ignored):
        it = self.currentContextItem
        if not it:
            return

        if it.isSelected():
            it.setSelected(False)
        else:
            it.setSelected(True)

    def editFile(self, ignored):
        it = self.currentContextItem
        if not it:
            return

        ed = getEditor()
        if not ed:
            qt.QMessageBox.warning(self, 'No editor found',
'''No editor found. Gct looks for an editor to execute in the environment
variable GCT_EDITOR, if that variable is not set it will use the variable
EDITOR.''')
            return

        # This piece of code is not entirely satisfactory. If the user
        # has EDITOR set to 'vi', or some other non-X application, the
        # editor will be started in the terminal which (h)gct was
        # started in. A better approach would be to close stdin and
        # stdout after the fork but before the exec, but this doesn't
        # seem to be possible with QProcess.
        p = qt.QProcess(ed)
        p.addArgument(it.file.dstName)
        p.setCommunication(0)
        qconnect(p, qt.SIGNAL('processExited()'), self.editorExited)
        if not p.launch(qt.QByteArray()):
            qt.QMessageBox.warning(self, 'Failed to launch editor',
                                   shortName + ' failed to launch the ' + \
                                   'editor. The command used was: ' + \
                                   ed + ' ' + it.file.dstName)
        else:
            self.editorProcesses.add(p)

    def editorExited(self):
        p = self.sender()
        status = p.exitStatus()
        file = unicode(p.arguments()[1])
        editor = unicode(p.arguments()[0]) + ' ' + file
        if not p.normalExit():
            qt.QMessageBox.warning(self, 'Editor failure',
                                   'The editor, ' +  editor + ', exited abnormally.')
        elif status != 0:
            qt.QMessageBox.warning(self, 'Editor failure',
                                   'The editor, ' +  editor + ', exited with exit code ' + str(status))

        self.editorProcesses.remove(p)
        scm.doUpdateCache(file)
        self.refreshFiles()

    def discardFile(self, ignored):
        it = self.currentContextItem
        if not it:
            return

        scm.discardFile(it.file)
        self.refreshFiles()

    def ignoreFile(self, ignored):
        it = self.currentContextItem
        if not it:
            return

        scm.ignoreFile(it.file)
        self.refreshFiles()

    def currentChange(self, item):
        f = item.file
        if not f.textW:
            f.textW = self.newTextEdit()
            f.textW.setReadOnly(True)
            f.textW.setTextFormat(Qt.RichText)
            f.textW.setText(formatPatchRichText(f.getPatch(), self.patchColors))

        self.text.raiseWidget(f.textW)
        self.currentContextItem = item
        if item.commitMsg:
            self.updateCommitCursor()

    def commit(self, id):
        selFileNames = []
        keepFiles = []
        commitFiles = []

        for item in self.filesW:
            debug("file: " + item.file.text)
            if item.isSelected():
                selFileNames.append(item.file.text)
                commitFiles.append(item.file)
            else:
                keepFiles.append(item.file)

        commitMsg = unicode(self.cmitItem.file.textW.text())

        if not selFileNames:
            qt.QMessageBox.information(self, "Commit - " + applicationName,
                                       "No files selected for commit.", "&Ok")
            return
        
        commitMsg = fixCommitMsgWhiteSpace(commitMsg)
        if scm.commitIsMerge():
            mergeMsg = scm.mergeMessage()
        else:
            mergeMsg = ''

        commitDialog = CommitDialog(mergeMsg, commitMsg, selFileNames)
        if commitDialog.exec_loop():
            try:
                scm.doCommit(keepFiles, commitFiles, commitMsg)
            except ProgramError, e:
                qt.QMessageBox.warning(self, "Commit Failed - " + applicationName,
                                       "Commit failed: " + str(e),
                                       '&Ok')
            else:
                if not self.options.oneshot:
                    self.cmitItem.file.textW.setText(settings().signoff)
                    self.refreshFiles()
                    self.statusBar().message('Commit done')

            if self.options.oneshot:
                self.close()

    def getFileState(self):
        ret = FileState()
        cur = self.filesW.currentItem()
        if cur and cur != self.cmitItem:
            ret.current = self.filesW.currentItem().file.text
        else:
            ret.current = None
        ret.selected = sets.Set()

        for x in self.filesW:
            if x.isSelected():
                ret.selected.add(x.file.text)

        return ret

    def restoreFileState(self, state):
        for f in self.files:
            f.listViewItem.setSelected(f.text in state.selected)

        for x in self.filesW:
            if x.file.text == state.current:
                self.filesW.setCurrentItem(x)

    def newTextEdit(self):
        ret = qt.QTextEdit()
        self.text.addWidget(ret)
        return ret    

    def addFile(self, file):
        f = file
        f.listViewItem = MyListItem(self.filesW, f)

        # The patch for this file is generated lazily in currentChange

        # Only display files that match the filter.
        f.listViewItem.setVisible(self.filterMatch(f))

        self.filesW.insertItem(f.listViewItem)


    def removeFile(self, file):
        f = file
        self.text.removeWidget(f.textW)
        self.filesW.takeItem(f.listViewItem)
        f.listViewItem = None

    def refreshFiles(self):
        state = self.getFileState()

        self.setUpdatesEnabled(False)
        scm.updateFiles(self.files)
        self.filesW.setCurrentItem(self.cmitItem)

        # For some reason the currentChanged signal isn't emitted
        # here. We call currentChange ourselves instead.
        self.currentChange(self.cmitItem)
        self.restoreFileState(state)
        self.setUpdatesEnabled(True)
        self.update()

        if settings().quitOnNoChanges and len(self.files) == 0:
            self.close()
        return len(self.files) > 0

    def filterMatch(self, file):
        return file.dstName.find(unicode(self.filter.text())) != -1
        
    def updateFilter(self, ignored=None):
        for w in self.filesW:
            w.setVisible(self.filterMatch(w.file))

    def clearFilter(self):
        self.filter.setText("")

    def toggleSelectAll(self):
        all = False
        for x in self.filesW:
            if x.isVisible():
                if not x.isSelected():
                    x.setSelected(True)
                    all = True

        if not all:
            for x in self.filesW:
                if x.isVisible():
                    x.setSelected(False)

    def toggleShowUnknown(self):
        if settings().showUnknown:
            settings().showUnknown = False
        else:
            settings().showUnknown = True

        self.operations.setItemChecked(self.showUnknownItem, settings().showUnknown)
        self.refreshFiles()

    def showPrefs(self):
        if settings().showSettings():
            self.refreshFiles()

commitMsgRE = re.compile('[ \t\r\f\v]*\n\\s*\n')
def fixCommitMsgWhiteSpace(msg):
    msg = msg.lstrip()
    msg = msg.rstrip()
    msg = re.sub(commitMsgRE, '\n\n', msg)
    msg += '\n'
    return msg

def formatPatchRichText(patch, colors):
    ret = ['<qt><pre><font color="', colors['std'], '">']
    prev = ' '
    for l in patch.split('\n'):
        if len(l) > 0:
            c = l[0]
        else:
            c = ' '
        
        if c != prev:
            if   c == '+': style = 'new'
            elif c == '-': style = 'remove'
            elif c == '@': style = 'head'
            else: style = 'std'
            ret.extend(['</font><font color="', colors[style], '">'])
            prev = c
        line = unicode(qt.QStyleSheet.escape(l))
        ret.extend([line, '\n'])
    ret.append('</pre></qt>')
    return u''.join(ret)

def getEditor():
    if os.environ.has_key('GCT_EDITOR'):
        return os.environ['GCT_EDITOR']
    elif os.environ.has_key('EDITOR'):
        return os.environ['EDITOR']
    else:
        return None

class EventFilter(qt.QObject):
    def __init__(self, parent, mainWidget):
        qt.QObject.__init__(self, parent)
        self.mw = mainWidget

    def eventFilter(self, watched, e):
        if (e.type() == qt.QEvent.KeyRelease or \
            e.type() == qt.QEvent.MouseButtonRelease):
            self.mw.updateStatusBar()

        return False

def main():
    scm.initialize()

    app = qt.QApplication(sys.argv)

    optParser = OptionParser(usage="%prog [--gui] [--one-shot]", version=applicationName + ' ' + version)
    optParser.add_option('-g', '--gui', action='store_true', dest='gui',
                         help='Unconditionally start the GUI')
    optParser.add_option('-o', '--one-shot', action='store_true', dest='oneshot',
                         help="Do (at most) one commit, then exit.")
    (options, args) = optParser.parse_args(app.argv()[1:])

    mw = MainWidget(options)
    ef = EventFilter(None, mw)
    app.installEventFilter(ef)
    
    if not mw.refreshFiles() and settings().quitOnNoChanges and not options.gui:
        print 'No outstanding changes'
        sys.exit(0)

    mw.resize(settings().width, settings().height)

    mw.show()
    app.setMainWidget(mw)

    # Handle CTRL-C appropriately
    signal.signal(signal.SIGINT, lambda s, f: app.quit())

    ret = app.exec_loop()
    settings().writeSettings()
    sys.exit(ret)

if executeMain:
    main()
