# -*- coding: utf-8 -*-

# Copyright (c) 2004 - 2006 Detlev Offenbach <detlev@die-offenbachs.de>
#

"""
Module implementing a dialog to compare two files.
"""

from __future__ import generators
import os
import time
import sys

from qt import *

from KdeQt import KQFileDialog, KQMessageBox

from DiffForm import DiffForm
import Utilities

if Utilities.getPythonVersion() >= 0x0203:
    from difflib import SequenceMatcher as _SequenceMatcher
else:
    from difflib import SequenceMatcher
    # we have to to it ourselfs
    # This stuff is copied over from Python 2.3
    class _SequenceMatcher(SequenceMatcher):
        """
        Class to extend the Python 2.2 SequenceMatcher by a method from Python 2.3.
        """
        def get_grouped_opcodes(self, n=3):
            """
            Method to isolate change clusters by eliminating ranges with no changes.
    
            Return a generator of groups with upto n lines of context.
            Each group is in the same format as returned by get_opcodes().
    
            <pre>
                &gt;&gt;&gt; from pprint import pprint
                &gt;&gt;&gt; a = map(str, range(1,40))
                &gt;&gt;&gt; b = a[:]
                &gt;&gt;&gt; b[8:8] = ['i']     # Make an insertion
                &gt;&gt;&gt; b[20] += 'x'       # Make a replacement
                &gt;&gt;&gt; b[23:28] = []      # Make a deletion
                &gt;&gt;&gt; b[30] += 'y'       # Make another replacement
                &gt;&gt;&gt; pprint(list(SequenceMatcher(None,a,b).get_grouped_opcodes()))
                [[('equal', 5, 8, 5, 8), ('insert', 8, 8, 8, 9), ('equal', 8, 11, 9, 12)],
                 [('equal', 16, 19, 17, 20),
                  ('replace', 19, 20, 20, 21),
                  ('equal', 20, 22, 21, 23),
                  ('delete', 22, 27, 23, 23),
                  ('equal', 27, 30, 23, 26)],
                 [('equal', 31, 34, 27, 30),
                  ('replace', 34, 35, 30, 31),
                  ('equal', 35, 38, 31, 34)]]
            </pre>
              
            @param n number of lines of context (integer)
            @return a generator of groups with up to n lines of context
            """
            codes = self.get_opcodes()
            # Fixup leading and trailing groups if they show no changes.
            if codes[0][0] == 'equal':
                tag, i1, i2, j1, j2 = codes[0]
                codes[0] = tag, max(i1, i2-n), i2, max(j1, j2-n), j2
            if codes[-1][0] == 'equal':
                tag, i1, i2, j1, j2 = codes[-1]
                codes[-1] = tag, i1, min(i2, i1+n), j1, min(j2, j1+n)
    
            nn = n + n
            group = []
            for tag, i1, i2, j1, j2 in codes:
                # End the current group and start a new one whenever
                # there is a large range with no changes.
                if tag == 'equal' and i2-i1 > nn:
                    group.append((tag, i1, min(i2, i1+n), j1, min(j2, j1+n)))
                    yield group
                    group = []
                    i1, j1 = max(i1, i2-n), max(j1, j2-n)
                group.append((tag, i1, i2, j1 ,j2))
            if group and not (len(group)==1 and group[0][0] == 'equal'):
                yield group
    
# This function is copied from python 2.3 and slightly modified.
# The header lines contain a tab after the filename.
def unified_diff(a, b, fromfile='', tofile='', fromfiledate='',
                 tofiledate='', n=3, lineterm='\n'):
    """
    Compare two sequences of lines; generate the delta as a unified diff.

    Unified diffs are a compact way of showing line changes and a few
    lines of context.  The number of context lines is set by 'n' which
    defaults to three.

    By default, the diff control lines (those with ---, +++, or @@) are
    created with a trailing newline.  This is helpful so that inputs
    created from file.readlines() result in diffs that are suitable for
    file.writelines() since both the inputs and outputs have trailing
    newlines.

    For inputs that do not have trailing newlines, set the lineterm
    argument to "" so that the output will be uniformly newline free.

    The unidiff format normally has a header for filenames and modification
    times.  Any or all of these may be specified using strings for
    'fromfile', 'tofile', 'fromfiledate', and 'tofiledate'.  The modification
    times are normally expressed in the format returned by time.ctime().

    Example:

    <pre>
    &gt;&gt;&gt; for line in unified_diff('one two three four'.split(),
    ...             'zero one tree four'.split(), 'Original', 'Current',
    ...             'Sat Jan 26 23:30:50 1991', 'Fri Jun 06 10:20:52 2003',
    ...             lineterm=''):
    ...     print line
    --- Original Sat Jan 26 23:30:50 1991
    +++ Current Fri Jun 06 10:20:52 2003
    @@ -1,4 +1,4 @@
    +zero
     one
    -two
    -three
    +tree
     four
    </pre>
    
    @param a first sequence of lines (list of strings)
    @param b second sequence of lines (list of strings)
    @param fromfile filename of the first file (string)
    @param tofile filename of the second file (string)
    @param fromfiledate modification time of the first file (string)
    @param tofiledate modification time of the second file (string)
    @param n number of lines of context (integer)
    @param lineterm line termination string (string)
    @return a generator yielding lines of differences
    """
    started = False
    for group in _SequenceMatcher(None,a,b).get_grouped_opcodes(n):
        if not started:
            yield '--- %s\t%s%s' % (fromfile, fromfiledate, lineterm)
            yield '+++ %s\t%s%s' % (tofile, tofiledate, lineterm)
            started = True
        i1, i2, j1, j2 = group[0][1], group[-1][2], group[0][3], group[-1][4]
        yield "@@ -%d,%d +%d,%d @@%s" % (i1+1, i2-i1, j1+1, j2-j1, lineterm)
        for tag, i1, i2, j1, j2 in group:
            if tag == 'equal':
                for line in a[i1:i2]:
                    yield ' ' + line
                continue
            if tag == 'replace' or tag == 'delete':
                for line in a[i1:i2]:
                    yield '-' + line
            if tag == 'replace' or tag == 'insert':
                for line in b[j1:j2]:
                    yield '+' + line

# This function is copied from python 2.3 and slightly modified.
# The header lines contain a tab after the filename.
def context_diff(a, b, fromfile='', tofile='',
                 fromfiledate='', tofiledate='', n=3, lineterm='\n'):
    """
    Compare two sequences of lines; generate the delta as a context diff.

    Context diffs are a compact way of showing line changes and a few
    lines of context.  The number of context lines is set by 'n' which
    defaults to three.

    By default, the diff control lines (those with *** or ---) are
    created with a trailing newline.  This is helpful so that inputs
    created from file.readlines() result in diffs that are suitable for
    file.writelines() since both the inputs and outputs have trailing
    newlines.

    For inputs that do not have trailing newlines, set the lineterm
    argument to "" so that the output will be uniformly newline free.

    The context diff format normally has a header for filenames and
    modification times.  Any or all of these may be specified using
    strings for 'fromfile', 'tofile', 'fromfiledate', and 'tofiledate'.
    The modification times are normally expressed in the format returned
    by time.ctime().  If not specified, the strings default to blanks.

    Example:

    <pre>
    &gt;&gt;&gt; print ''.join(context_diff('one\ntwo\nthree\nfour\n'.splitlines(1),
    ...       'zero\none\ntree\nfour\n'.splitlines(1), 'Original', 'Current',
    ...       'Sat Jan 26 23:30:50 1991', 'Fri Jun 06 10:22:46 2003')),
    *** Original Sat Jan 26 23:30:50 1991
    --- Current Fri Jun 06 10:22:46 2003
    ***************
    *** 1,4 ****
      one
    ! two
    ! three
      four
    --- 1,4 ----
    + zero
      one
    ! tree
      four
    </pre>
    
    @param a first sequence of lines (list of strings)
    @param b second sequence of lines (list of strings)
    @param fromfile filename of the first file (string)
    @param tofile filename of the second file (string)
    @param fromfiledate modification time of the first file (string)
    @param tofiledate modification time of the second file (string)
    @param n number of lines of context (integer)
    @param lineterm line termination string (string)
    @return a generator yielding lines of differences
    """

    started = False
    prefixmap = {'insert':'+ ', 'delete':'- ', 'replace':'! ', 'equal':'  '}
    for group in _SequenceMatcher(None,a,b).get_grouped_opcodes(n):
        if not started:
            yield '*** %s\t%s%s' % (fromfile, fromfiledate, lineterm)
            yield '--- %s\t%s%s' % (tofile, tofiledate, lineterm)
            started = True

        yield '***************%s' % (lineterm,)
        if group[-1][2] - group[0][1] >= 2:
            yield '*** %d,%d ****%s' % (group[0][1]+1, group[-1][2], lineterm)
        else:
            yield '*** %d ****%s' % (group[-1][2], lineterm)
        visiblechanges = [e for e in group if e[0] in ('replace', 'delete')]
        if visiblechanges:
            for tag, i1, i2, _, _ in group:
                if tag != 'insert':
                    for line in a[i1:i2]:
                        yield prefixmap[tag] + line

        if group[-1][4] - group[0][3] >= 2:
            yield '--- %d,%d ----%s' % (group[0][3]+1, group[-1][4], lineterm)
        else:
            yield '--- %d ----%s' % (group[-1][4], lineterm)
        visiblechanges = [e for e in group if e[0] in ('replace', 'insert')]
        if visiblechanges:
            for tag, _, _, j1, j2 in group:
                if tag != 'delete':
                    for line in b[j1:j2]:
                        yield prefixmap[tag] + line

class DiffDialog(DiffForm):
    """
    Class implementing a dialog to compare two files.
    """
    def __init__(self,parent = None):
        """
        Constructor
        """
        DiffForm.__init__(self,parent)
        
        font = self.contents.currentFont()
        if sys.platform == "win32":
            font.setFamily("Lucida Console")
        else:
            font.setFamily("Monospace")
        self.contents.setFont(font)
        
        self.filename1 = ''
        self.filename2 = ''
        
        self.cAdded = QColor(190, 237, 190)
        self.cRemoved = QColor(237, 190, 190)
        self.cReplaced = QColor(190, 190, 237)
        self.cLineNo = QColor(255, 220, 168)
        self.cNormal = self.contents.paletteBackgroundColor()

    def handleSave(self):
        """
        Private slot to handle the Save button press.
        
        It saves the diff shown in the dialog to a file in the local
        filesystem.
        """
        dname, fname = Utilities.splitPath(self.filename2)
        if fname != '.':
            fname = "%s.diff" % self.filename2
        else:
            fname = dname
            
        selectedFilter = QString('')
        fname = KQFileDialog.getSaveFileName(\
            fname,
            self.trUtf8("Patch Files (*.diff)"),
            self, None,
            self.trUtf8("Save Diff"),
            selectedFilter, 0)
        
        if fname.isEmpty():
            return
            
        ext = QFileInfo(fname).extension()
        if ext.isEmpty():
            ex = selectedFilter.section('(*',1,1).section(')',0,0)
            if not ex.isEmpty():
                fname.append(ex)
        if QFileInfo(fname).exists():
            abort = KQMessageBox.warning(self,
                self.trUtf8("Save Diff"),
                self.trUtf8("<p>The patch file <b>%1</b> already exists.</p>")
                    .arg(fname),
                self.trUtf8("&Overwrite"),
                self.trUtf8("&Abort"), QString.null, 1)
            if abort:
                return
        fname = unicode(QDir.convertSeparators(fname))
        
        try:
            f = open(fname, "wb")
            paras = self.contents.paragraphs()
            for i in range(paras):
                txt = self.contents.text(i)
                try:
                    f.write("%s%s" % (unicode(txt)[:-1], os.linesep))
                except UnicodeError:
                    pass
            f.close()
        except IOError, why:
            KQMessageBox.critical(self, self.trUtf8('Save Diff'),
                self.trUtf8('<p>The patch file <b>%1</b> could not be saved.<br>Reason: %2</p>')
                    .arg(unicode(fname)).arg(str(why)))

    def handleDiff(self):
        """
        Private slot to handle the Compare button press.
        """
        self.filename1 = unicode(QDir.convertSeparators(self.file1Edit.text()))
        try:
            filemtime1 = time.ctime(os.stat(self.filename1).st_mtime)
        except IOError:
            filemtime1 = ""
        try:
            f1 = open(self.filename1, "rb")
            lines1 = f1.readlines()
            f1.close()
        except IOError:
            KQMessageBox.critical(self,
                self.trUtf8("Compare Files"),
                self.trUtf8("""<p>The file <b>%1</b> could not be read.</p>""")
                    .arg(self.filename1),
                self.trUtf8("&Abort"),
                QString.null,
                QString.null,
                0, -1)
            return

        self.filename2 = unicode(QDir.convertSeparators(self.file2Edit.text()))
        try:
            filemtime2 = time.ctime(os.stat(self.filename2).st_mtime)
        except IOError:
            filemtime2 = ""
        try:
            f2 = open(self.filename2, "rb")
            lines2 = f2.readlines()
            f2.close()
        except IOError:
            KQMessageBox.critical(self,
                self.trUtf8("Compare Files"),
                self.trUtf8("""<p>The file <b>%1</b> could not be read.</p>""")
                    .arg(self.filename2),
                self.trUtf8("&Abort"),
                QString.null,
                QString.null,
                0, -1)
            return
        
        self.contents.clear()
        self.saveButton.setEnabled(0)
        
        if self.unifiedRadioButton.isChecked():
            self.generateUnifiedDiff(lines1, lines2, self.filename1, self.filename2,
                                filemtime1, filemtime2)
        else:
            self.generateContextDiff(lines1, lines2, self.filename1, self.filename2,
                                filemtime1, filemtime2)
        
        self.contents.setCursorPosition(0, 0)
        self.contents.ensureCursorVisible()
        
        self.saveButton.setEnabled(1)

    def generateUnifiedDiff(self, a, b, fromfile, tofile,
                            fromfiledate, tofiledate):
        """
        Private slot to generate a unified diff output.
        
        @param a first sequence of lines (list of strings)
        @param b second sequence of lines (list of strings)
        @param fromfile filename of the first file (string)
        @param tofile filename of the second file (string)
        @param fromfiledate modification time of the first file (string)
        @param tofiledate modification time of the second file (string)
        """
        paras = 0
        for line in unified_diff(a, b, fromfile, tofile,
                            fromfiledate, tofiledate):
            self.contents.append(line)
            if line.startswith('+') or line.startswith('>'):
                self.contents.setParagraphBackgroundColor(paras, self.cAdded)
            elif line.startswith('-') or line.startswith('<'):
                self.contents.setParagraphBackgroundColor(paras, self.cRemoved)
            elif line.startswith('@@'):
                self.contents.setParagraphBackgroundColor(paras, self.cLineNo)
            else:
                self.contents.setParagraphBackgroundColor(paras, self.cNormal)
            paras += 1
            
        if paras == 0:
            self.contents.append(\
                self.trUtf8('There is no difference.'))

    def generateContextDiff(self, a, b, fromfile, tofile,
                            fromfiledate, tofiledate):
        """
        Private slot to generate a context diff output.
        
        @param a first sequence of lines (list of strings)
        @param b second sequence of lines (list of strings)
        @param fromfile filename of the first file (string)
        @param tofile filename of the second file (string)
        @param fromfiledate modification time of the first file (string)
        @param tofiledate modification time of the second file (string)
        """
        paras = 0
        for line in context_diff(a, b, fromfile, tofile,
                            fromfiledate, tofiledate):
            self.contents.append(line)
            if line.startswith('+ '):
                self.contents.setParagraphBackgroundColor(paras, self.cAdded)
            elif line.startswith('- '):
                self.contents.setParagraphBackgroundColor(paras, self.cRemoved)
            elif line.startswith('! '):
                self.contents.setParagraphBackgroundColor(paras, self.cReplaced)
            elif (line.startswith('*** ') or line.startswith('--- ')) and paras > 1:
                self.contents.setParagraphBackgroundColor(paras, self.cLineNo)
            else:
                self.contents.setParagraphBackgroundColor(paras, self.cNormal)
            paras += 1
            
        if paras == 0:
            self.contents.append(\
                self.trUtf8('There is no difference.'))

    def handleFileChanged(self):
        """
        Private slot to enable/disable the Compare button.
        """
        if self.file1Edit.text().isEmpty() or \
           self.file2Edit.text().isEmpty():
            self.diffButton.setEnabled(0)
        else:
            self.diffButton.setEnabled(1)

    def handleSelectFile(self, lineEdit):
        """
        Private slot to display a file selection dialog.
        
        @param lineEdit field for the display of the selected filename
                (QLineEdit)
        """
        filename = KQFileDialog.getOpenFileName(\
            lineEdit.text(),
            QString.null,
            self, None,
            self.trUtf8("Select file to compare"),
            None, 1)
            
        if not filename.isEmpty():
            lineEdit.setText(QDir.convertSeparators(filename))

    def handleSelectFile1(self):
        """
        Private slot to handle the file 1 file selection button press.
        """
        self.handleSelectFile(self.file1Edit)

    def handleSelectFile2(self):
        """
        Private slot to handle the file 2 file selection button press.
        """
        self.handleSelectFile(self.file2Edit)
