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

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

"""
Module implementing a dialog to compare two files and show the result side by side.
"""

from __future__ import generators
import os
from difflib import SequenceMatcher

from qt import *

from KdeQt import KQFileDialog, KQMessageBox

from CompareForm import CompareForm
import Utilities

def sbsdiff(a, b, linenumberwidth = 4):
    """
    Compare two sequences of lines; generate the delta for display side by side.
    
    @param a first sequence of lines (list of strings)
    @param b second sequence of lines (list of strings)
    @param linenumberwidth width (in characters) of the linenumbers (integer)
    @return a generator yielding tuples of differences. The tuple is composed
        of strings as follows.
        <ul>
            <li>opcode -- one of e, d, i, r for equal, delete, insert, replace</li>
            <li>lineno a -- linenumber of sequence a</li>
            <li>line a -- line of sequence a</li>
            <li>lineno b -- linenumber of sequence b</li>
            <li>line b -- line of sequence b</li>
        </ul>
    """
    linenumberformat = "%%%dd" % linenumberwidth
    emptylineno = ' ' * linenumberwidth
    
    for tag, i1, i2, j1, j2 in SequenceMatcher(None,a,b).get_opcodes():
        if tag == 'equal':
            for x in range(i2 - i1):
                yield ('e', str(linenumberformat % (i1+x+1)), a[i1+x],
                            str(linenumberformat % (j1+x+1)), b[j1+x])
            continue
        if tag == 'delete':
            for x in range(i2 - i1):
                yield ('d', str(linenumberformat % (i1+x+1)),
                            a[i1+x], emptylineno, '')
            continue
        if tag == 'insert':
            for x in range(j2 - j1):
                yield ('i', emptylineno, '',
                            str(linenumberformat % (j1+x+1)), b[j1+x])
            continue
        if tag == 'replace':
            for x in range(max(i2-i1, j2-j1)):
                if i1 + x < i2:
                    s1 = str(linenumberformat % (i1+x+1))
                    s2 = a[i1+x]
                else:
                    s1 = '    '
                    s2 = ''
                if j1 + x < j2:
                    s3 = str(linenumberformat % (j1+x+1))
                    s4 = b[j1+x]
                else:
                    s3 = '    '
                    s4 = ''
                yield ('r', s1, s2, s3, s4)

class CompareDialog(CompareForm):
    """
    Class implementing a dialog to compare two files and show the result side by side.
    """
    def __init__(self,parent = None):
        """
        Constructor
        """
        CompareForm.__init__(self,parent)
        
        self.vsb1 = self.contents_1.verticalScrollBar()
        self.hsb1 = self.contents_1.horizontalScrollBar()
        self.vsb2 = self.contents_2.verticalScrollBar()
        self.hsb2 = self.contents_2.horizontalScrollBar()
        
        self.handleSync(1)
        
        self.cInserted = QColor(190, 237, 190)
        self.cDeleted = QColor(237, 190, 190)
        self.cReplaced = QColor(190, 190, 237)
        self.cNormal = self.contents_1.paletteBackgroundColor()

    def handleDiff(self):
        """
        Private slot to handle the Compare button press.
        """
        filename1 = unicode(QDir.convertSeparators(self.file1Edit.text()))
        try:
            f1 = open(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(filename1),
                self.trUtf8("&Abort"),
                QString.null,
                QString.null,
                0, -1)
            return

        filename2 = unicode(QDir.convertSeparators(self.file2Edit.text()))
        try:
            f2 = open(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(filename2),
                self.trUtf8("&Abort"),
                QString.null,
                QString.null,
                0, -1)
            return
        
        self.contents_1.clear()
        self.contents_2.clear()
        
        paras = 0
        for opcode, ln1, l1, ln2, l2 in sbsdiff(lines1, lines2):
            p1 = "%s %s" % (ln1, l1)
            p2 = "%s %s" % (ln2, l2)
            self.contents_1.append(p1)
            self.contents_2.append(p2)
            if opcode == 'i':
                self.contents_2.setParagraphBackgroundColor(paras, self.cInserted)
            elif opcode == 'd':
                self.contents_1.setParagraphBackgroundColor(paras, self.cDeleted)
            elif opcode == 'r':
                if ln1.strip():
                    self.contents_1.setParagraphBackgroundColor(paras, self.cReplaced)
                else:
                    self.contents_1.setParagraphBackgroundColor(paras, self.cNormal)
                if ln2.strip():
                    self.contents_2.setParagraphBackgroundColor(paras, self.cReplaced)
                else:
                    self.contents_2.setParagraphBackgroundColor(paras, self.cNormal)
            else:
                self.contents_1.setParagraphBackgroundColor(paras, self.cNormal)
                self.contents_2.setParagraphBackgroundColor(paras, self.cNormal)
            paras += 1
        
        self.contents_1.setCursorPosition(0, 0)
        self.contents_1.ensureCursorVisible()
        self.contents_2.setCursorPosition(0, 0)
        self.contents_2.ensureCursorVisible()

    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)

    def handleSync(self, sync):
        """
        Private slot to connect or disconnect the scrollbars of the displays.
        
        @param sync flag indicating synchronisation status (boolean)
        """
        if sync:
            self.vsb2.setValue(self.vsb1.value())
            self.hsb2.setValue(self.hsb1.value())
            self.connect(self.vsb1, SIGNAL('valueChanged(int)'),
                self.vsb2, SLOT('setValue(int)'))
            self.connect(self.vsb2, SIGNAL('valueChanged(int)'),
                self.vsb1, SLOT('setValue(int)'))
            self.connect(self.hsb1, SIGNAL('valueChanged(int)'),
                self.hsb2, SLOT('setValue(int)'))
            self.connect(self.hsb2, SIGNAL('valueChanged(int)'),
                self.hsb1, SLOT('setValue(int)'))
        else:
            self.disconnect(self.vsb1, SIGNAL('valueChanged(int)'),
                self.vsb2, SLOT('setValue(int)'))
            self.disconnect(self.vsb2, SIGNAL('valueChanged(int)'),
                self.vsb1, SLOT('setValue(int)'))
            self.disconnect(self.hsb1, SIGNAL('valueChanged(int)'),
                self.hsb2, SLOT('setValue(int)'))
            self.disconnect(self.hsb2, SIGNAL('valueChanged(int)'),
                self.hsb1, SLOT('setValue(int)'))
