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

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

"""
Module implementing the search and replace dialog.
"""

from PyQt4.QtCore import *
from PyQt4.QtGui import *

from KdeQt import KQMessageBox

from Ui_SearchDialog import Ui_SearchDialog
from Ui_ReplaceDialog import Ui_ReplaceDialog

from E4Gui.E4Action import E4Action

import Preferences

class SearchReplaceDialog(QDialog):
    """
    Class implementing the search and replace dialog.
    """
    def __init__(self, replace, vm, parent = None):
        """
        Constructor
        
        @param replace flag indicating a replace dialog is called
        @param vm reference to the viewmanager object
        @param parent parent widget of this dialog (QWidget)
        """
        QDialog.__init__(self, parent)
        
        self.viewmanager = vm
        self.replace = replace
        
        self.findHistory = vm.getSRHistory('search')
        if replace:
            self.replaceHistory = vm.getSRHistory('replace')
            self.ui = Ui_ReplaceDialog()
            whatsThis = self.trUtf8(r"""
<b>Find and Replace</b>
<p>This dialog is used to find some text and replace it with another text. 
By checking the various checkboxes, the search can be made more specific. The search 
string might be a regular expression. In a regular expression, special characters 
interpreted are:</p>
"""
            )
        else:
            self.ui = Ui_SearchDialog()
            whatsThis = self.trUtf8(r"""
<b>Find</b>
<p>This dialog is used to find some text. By checking the various checkboxes, the search 
can be made more specific. The search string might be a regular expression. In a regular 
expression, special characters interpreted are:</p>
"""
            )
        self.ui.setupUi(self)
        if not replace:
            self.ui.wrapCheckBox.setChecked(True)
        
        whatsThis.append(self.trUtf8(r"""
<table border="0">
<tr><td><code>.</code></td><td>Matches any character</td></tr>
<tr><td><code>\(</code></td><td>This marks the start of a region for tagging a match.</td></tr>
<tr><td><code>\)</code></td><td>This marks the end of a tagged region.</td></tr>
<tr><td><code>\n</code></td>
<td>Where <code>n</code> is 1 through 9 refers to the first through ninth tagged region
when replacing. For example, if the search string was <code>Fred\([1-9]\)XXX</code> and
the replace string was <code>Sam\1YYY</code>, when applied to <code>Fred2XXX</code> this
would generate <code>Sam2YYY</code>.</td></tr>
<tr><td><code>\&lt;</code></td>
<td>This matches the start of a word using Scintilla's definitions of words.</td></tr>
<tr><td><code>\&gt;</code></td>
<td>This matches the end of a word using Scintilla's definition of words.</td></tr>
<tr><td><code>\x</code></td>
<td>This allows you to use a character x that would otherwise have a special meaning. For
example, \[ would be interpreted as [ and not as the start of a character set.</td></tr>
<tr><td><code>[...]</code></td>
<td>This indicates a set of characters, for example, [abc] means any of the characters a,
b or c. You can also use ranges, for example [a-z] for any lower case character.</td></tr>
<tr><td><code>[^...]</code></td>
<td>The complement of the characters in the set. For example, [^A-Za-z] means any
character except an alphabetic character.</td></tr>
<tr><td><code>^</code></td>
<td>This matches the start of a line (unless used inside a set, see above).</td></tr>
<tr><td><code>$</code></td> <td>This matches the end of a line.</td></tr>
<tr><td><code>*</code></td>
<td>This matches 0 or more times. For example, <code>Sa*m</code> matches <code>Sm</code>,
<code>Sam</code>, <code>Saam</code>, <code>Saaam</code> and so on.</td></tr>
<tr><td><code>+</code></td>
<td>This matches 1 or more times. For example, <code>Sa+m</code> matches
<code>Sam</code>, <code>Saam</code>, <code>Saaam</code> and so on.</td></tr>
</table>
"""
        ))
        self.setWhatsThis(whatsThis)
        
        self.findNextAct = E4Action(self.trUtf8('Find Next'),
                self.trUtf8('Find Next'),
                0, 0, self, 'search_dialog_find_next')
        self.connect(self.findNextAct, SIGNAL('triggered()'), 
                     self.on_findNextButton_clicked)
        self.findNextAct.setEnabled(False)
        self.ui.findtextCombo.addAction(self.findNextAct)
        
        self.findPrevAct = E4Action(self.trUtf8('Find Prev'),
                self.trUtf8('Find Prev'),
                0, 0, self, 'search_dialog_find_prev')
        self.connect(self.findPrevAct, SIGNAL('triggered()'), 
                     self.on_findPrevButton_clicked)
        self.findPrevAct.setEnabled(False)
        self.ui.findtextCombo.addAction(self.findPrevAct)
        
        self.havefound = False
        self.__pos = None
        self.__findBackwards = False
        
        self.ui.findNextButton.setDefault(True)

    def on_findtextCombo_editTextChanged(self, txt):
        """
        Private slot to enable/disable the find buttons.
        """
        if txt.isEmpty():
            self.ui.findNextButton.setEnabled(False)
            self.findNextAct.setEnabled(False)
            self.ui.findPrevButton.setEnabled(False)
            self.findPrevAct.setEnabled(False)
            if self.replace:
                self.ui.replaceButton.setEnabled(False)
                self.ui.replaceAllButton.setEnabled(False)
        else:
            self.ui.findNextButton.setEnabled(True)
            self.findNextAct.setEnabled(True)
            self.ui.findPrevButton.setEnabled(True)
            self.findPrevAct.setEnabled(True)
            if self.replace:
                self.ui.replaceButton.setEnabled(False)
                self.ui.replaceAllButton.setEnabled(True)

    @pyqtSignature("")
    def on_findNextButton_clicked(self):
        """
        Private slot to find the next occurrence of text.
        """
        self.ui.findPrevButton.setDefault(False)
        self.ui.findNextButton.setDefault(True)
        self.findNext()
        
    def findNext(self):
        """
        Public slot to find the next occurrence of text.
        """
        if not self.havefound or self.ui.findtextCombo.currentText().isEmpty():
            self.show(self.viewmanager.textForFind())
            return
        
        self.__findBackwards = False
        txt = self.ui.findtextCombo.currentText()
        
        # This moves any previous occurrence of this statement to the head
        # of the list and updates the combobox
        self.findHistory.removeAll(txt)
        self.findHistory.prepend(txt)
        self.ui.findtextCombo.clear()
        self.ui.findtextCombo.addItems(self.findHistory)
        self.emit(SIGNAL('searchListChanged'))
        
        ok = self.__findNextPrev(txt, False)
        if ok:
            if self.replace:
                self.ui.replaceButton.setEnabled(True)
        else:
            KQMessageBox.information(self, self.windowTitle(),
                self.trUtf8("'%1' was not found.").arg(txt))

    @pyqtSignature("")
    def on_findPrevButton_clicked(self):
        """
        Private slot to find the previous occurrence of text.
        """
        self.ui.findNextButton.setDefault(False)
        self.ui.findPrevButton.setDefault(True)
        self.findPrev()
        
    def findPrev(self):
        """
        Public slot to find the next previous of text.
        """
        if not self.havefound or self.ui.findtextCombo.currentText().isEmpty():
            self.show(self.viewmanager.textForFind())
            return
        
        self.__findBackwards = True
        txt = self.ui.findtextCombo.currentText()
        
        # This moves any previous occurrence of this statement to the head
        # of the list and updates the combobox
        self.findHistory.removeAll(txt)
        self.findHistory.prepend(txt)
        self.ui.findtextCombo.clear()
        self.ui.findtextCombo.addItems(self.findHistory)
        self.emit(SIGNAL('searchListChanged'))
        
        ok = self.__findNextPrev(txt, True)
        if ok:
            if self.replace:
                self.ui.replaceButton.setEnabled(True)
        else:
            KQMessageBox.information(self, self.windowTitle(),
                self.trUtf8("'%1' was not found.").arg(txt))
    
    def __markOccurrences(self, txt):
        """
        Private method to mark all occurrences of the search text.
        
        @param txt text to search for (QString)
        """
        aw = self.viewmanager.activeWindow()
        lineFrom = 0
        indexFrom = 0
        lineTo = -1
        indexTo = -1
        if self.ui.limitCheckBox.isChecked():
            lineFrom = self.ui.startLineSpinBox.value() - 1
            lineTo = min(self.ui.endLineSpinBox.value(), aw.lines()) - 1
            indexTo = aw.text(lineTo).length()
        
        aw.clearSearchIndicators()
        ok = aw.findFirstTarget(txt,
                self.ui.regexpCheckBox.isChecked(),
                self.ui.caseCheckBox.isChecked(),
                self.ui.wordCheckBox.isChecked(),
                lineFrom, indexFrom, lineTo, indexTo)
        while ok:
            tgtPos, tgtLen = aw.getFoundTarget()
            try:
                aw.setSearchIndicator(tgtPos, tgtLen)
            except AttributeError:
                self.viewmanager.setSearchIndicator(tgtPos, tgtLen)
            ok = aw.findNextTarget()
    
    def __findNextPrev(self, txt, backwards):
        """
        Private method to find the next occurrence of the search text.
        
        @param txt text to search for (QString)
        @param backwards flag indicating a backwards search (boolean)
        @return flag indicating success (boolean)
        """
        if Preferences.getEditor("SearchMarkersEnabled"):
            self.__markOccurrences(txt)
        
        aw = self.viewmanager.activeWindow()
        cline, cindex = aw.getCursorPosition()
        
        ok = True
        lineFrom, indexFrom, lineTo, indexTo = aw.getSelection()
        if backwards:
            line = lineFrom
            index = indexFrom - 1
            if index < 0:
                line -= 1
                if self.ui.limitCheckBox.isChecked():
                    if line < self.ui.startLineSpinBox.value() - 1:
                        if self.ui.wrapCheckBox.isChecked():
                            line = min(self.ui.endLineSpinBox.value(), aw.lines()) - 1
                        else:
                            ok = False
                else:
                    if line < 0:
                        if self.ui.wrapCheckBox.isChecked():
                            line = aw.lines() - 1
                        else:
                            ok = False
                if ok:
                    index = aw.lineLength(line)
        else:
            line = lineTo
            index = indexTo
        
        if ok:
            ok = aw.findFirst(txt,
                self.ui.regexpCheckBox.isChecked(),
                self.ui.caseCheckBox.isChecked(),
                self.ui.wordCheckBox.isChecked(),
                self.ui.wrapCheckBox.isChecked(),
                not backwards,
                line, index)
        
        if ok and self.ui.limitCheckBox.isChecked():
            lineFrom, indexFrom, lineTo, indexTo = aw.getSelection()
            if lineFrom >= (self.ui.startLineSpinBox.value() - 1) and \
               lineFrom <= min(self.ui.endLineSpinBox.value(), aw.lines()) - 1:
                ok = True
            else:
                if self.ui.wrapCheckBox.isChecked():
                    # try it again
                    if backwards:
                        line = min(self.ui.endLineSpinBox.value(), aw.lines()) - 1
                        index = aw.lineLength(line)
                    else:
                        line = self.ui.startLineSpinBox.value() - 1
                        index = 0
                    ok = aw.findFirst(txt,
                        self.ui.regexpCheckBox.isChecked(),
                        self.ui.caseCheckBox.isChecked(),
                        self.ui.wordCheckBox.isChecked(),
                        self.ui.wrapCheckBox.isChecked(),
                        not backwards,
                        line, index)
                    if ok:
                        lineFrom, indexFrom, lineTo, indexTo = aw.getSelection()
                        if lineFrom >= (self.ui.startLineSpinBox.value() - 1) and \
                           lineFrom <= \
                           min(self.ui.endLineSpinBox.value(), aw.lines()) - 1:
                            ok = True
                        else:
                            ok = False
                            aw.selectAll(False)
                            aw.setCursorPosition(cline, cindex)
                            aw.ensureCursorVisible()
                else:
                    ok = False
                    aw.selectAll(False)
                    aw.setCursorPosition(cline, cindex)
                    aw.ensureCursorVisible()
        
        return ok

    def __showFind(self, text=''):
        """
        Private method to display this dialog in find mode.
        
        @param text text to be shown in the findtext edit
        """
        self.replace = False
        
        self.ui.findtextCombo.clear()
        self.ui.findtextCombo.addItems(self.findHistory)
        self.ui.findtextCombo.setEditText(text)
        self.ui.findtextCombo.lineEdit().selectAll()
        self.ui.findtextCombo.setFocus()
        
        self.ui.caseCheckBox.setChecked(False)
        self.ui.wordCheckBox.setChecked(False)
        self.ui.wrapCheckBox.setChecked(True)
        self.ui.regexpCheckBox.setChecked(False)
        
        aw = self.viewmanager.activeWindow()
        if aw.hasSelectedText():
            line1, dummy1, line2, dummy2 = aw.getSelection()
            self.ui.startLineSpinBox.setValue(line1 + 1)
            self.ui.endLineSpinBox.setValue(line2 + 1)
        else:
            self.ui.endLineSpinBox.setValue(aw.lines())
        
        self.findNextAct.setShortcut(self.viewmanager.searchNextAct.shortcut())
        self.findNextAct.setAlternateShortcut(\
            self.viewmanager.searchNextAct.alternateShortcut())
        self.findPrevAct.setShortcut(self.viewmanager.searchPrevAct.shortcut())
        self.findPrevAct.setAlternateShortcut(\
            self.viewmanager.searchPrevAct.alternateShortcut())
        
        self.havefound = True

    @pyqtSignature("")
    def on_replaceButton_clicked(self):
        """
        Private slot to replace one occurrence of text.
        """
        ftxt = self.ui.findtextCombo.currentText()
        rtxt = self.ui.replacetextCombo.currentText()
        
        # This moves any previous occurrence of this statement to the head
        # of the list and updates the combobox
        self.replaceHistory.removeAll(rtxt)
        self.replaceHistory.prepend(rtxt)
        self.ui.replacetextCombo.clear()
        self.ui.replacetextCombo.addItems(self.replaceHistory)
        
        aw = self.viewmanager.activeWindow()
        aw.replace(rtxt)
        ok = self.__findNextPrev(ftxt, self.__findBackwards)
        
        if not ok:
            self.ui.replaceButton.setEnabled(False)
            KQMessageBox.information(self, self.windowTitle(),
                self.trUtf8("'%1' was not found.").arg(ftxt))
            self.ui.findNextButton.setDefault(not self.__findBackwards)
            self.ui.findPrevButton.setDefault(self.__findBackwards)
            self.ui.replaceButton.setDefault(False)
        else:
            self.ui.findNextButton.setDefault(False)
            self.ui.findPrevButton.setDefault(False)
            self.ui.replaceButton.setDefault(True)
        
    @pyqtSignature("")
    def on_replaceAllButton_clicked(self):
        """
        Private slot to replace all occurrences of text.
        """
        replacements = 0
        ftxt = self.ui.findtextCombo.currentText()
        rtxt = self.ui.replacetextCombo.currentText()
        
        # This moves any previous occurrence of this statement to the head
        # of the list and updates the combobox
        self.findHistory.removeAll(ftxt)
        self.findHistory.prepend(ftxt)
        self.ui.findtextCombo.clear()
        self.ui.findtextCombo.addItems(self.findHistory)
        
        self.replaceHistory.removeAll(rtxt)
        self.replaceHistory.prepend(rtxt)
        self.ui.replacetextCombo.clear()
        self.ui.replacetextCombo.addItems(self.replaceHistory)
        
        aw = self.viewmanager.activeWindow()
        cline, cindex = aw.getCursorPosition()
        fline = aw.firstVisibleLine()
        if self.ui.wrapCheckBox.isChecked():
            if self.ui.limitCheckBox.isChecked():
                line = self.ui.startLineSpinBox.value() - 1
                index = 0
            else:
                line = 0
                index = 0
            ok = aw.findFirst(ftxt,
                    self.ui.regexpCheckBox.isChecked(),
                    self.ui.caseCheckBox.isChecked(),
                    self.ui.wordCheckBox.isChecked(),
                    False, True, line, index)
        else:
            ok = aw.findFirst(ftxt,
                    self.ui.regexpCheckBox.isChecked(),
                    self.ui.caseCheckBox.isChecked(),
                    self.ui.wordCheckBox.isChecked(),
                    False,
                    not self.__findBackwards)
        
        if ok and self.ui.limitCheckBox.isChecked():
            lineFrom, indexFrom, lineTo, indexTo = aw.getSelection()
            if lineFrom >= (self.ui.startLineSpinBox.value() - 1) and \
               lineFrom <= min(self.ui.endLineSpinBox.value(), aw.lines()) - 1:
                ok = True
            else:
                ok = False
                aw.selectAll(False)
                aw.setCursorPosition(cline, cindex)
                aw.ensureCursorVisible()
        
        found = ok
        
        wordWrap = self.ui.wrapCheckBox.isChecked()
        self.ui.wrapCheckBox.setChecked(False)
        aw.beginUndoAction()
        while ok:
            aw.replace(rtxt)
            replacements += 1
            ok = self.__findNextPrev(ftxt, self.__findBackwards)
        aw.endUndoAction()
        if wordWrap:
            self.ui.wrapCheckBox.setChecked(True)
        self.ui.replaceButton.setEnabled(False)
        
        if found:
            KQMessageBox.information(self, self.windowTitle(),
                self.trUtf8("Replaced %1 occurrences.")
                    .arg(replacements))
        else:
            KQMessageBox.information(self, self.windowTitle(),
                self.trUtf8("Nothing replaced because '%1' was not found.")
                    .arg(ftxt))
        
        aw.setCursorPosition(cline, cindex)
        aw.ensureCursorVisible()
        
    def __showReplace(self, text=''):
        """
        Private slot to display this dialog in replace mode.
        
        @param text text to be shown in the findtext edit
        """
        self.replace = True
        
        self.ui.findtextCombo.clear()
        self.ui.findtextCombo.addItems(self.findHistory)
        self.ui.findtextCombo.setEditText(text)
        self.ui.findtextCombo.lineEdit().selectAll()
        self.ui.findtextCombo.setFocus()
        
        self.ui.replacetextCombo.clear()
        self.ui.replacetextCombo.addItems(self.replaceHistory)
        self.ui.replacetextCombo.setEditText('')
        
        self.ui.caseCheckBox.setChecked(False)
        self.ui.wordCheckBox.setChecked(False)
        self.ui.wrapCheckBox.setChecked(True)
        self.ui.regexpCheckBox.setChecked(False)
        
        self.havefound = True
        
        aw = self.viewmanager.activeWindow()
        if aw.hasSelectedText():
            line1, index1, line2, index2 = aw.getSelection()
            self.ui.startLineSpinBox.setValue(line1 + 1)
            self.ui.endLineSpinBox.setValue(line2 + 1)
            if line1 == line2:
                aw.setSelection(line1, index1, line1, index1)
                self.findNext()
        else:
            self.ui.endLineSpinBox.setValue(aw.lines())
        
        self.findNextAct.setShortcut(self.viewmanager.searchNextAct.shortcut())
        self.findNextAct.setAlternateShortcut(\
            self.viewmanager.searchNextAct.alternateShortcut())
        self.findPrevAct.setShortcut(self.viewmanager.searchPrevAct.shortcut())
        self.findPrevAct.setAlternateShortcut(\
            self.viewmanager.searchPrevAct.alternateShortcut())

    def show(self, text = ''):
        """
        Overridden slot from QDialog.
        
        @param text text to be shown in the findtext edit
        """
        if self.replace:
            self.__showReplace(text)
        else:
            self.__showFind(text)
        if self.__pos is not None:
            self.move(self.__pos)
        QDialog.show(self)
        self.activateWindow()

    def closeEvent(self, event):
        """
        Private event handler for the close event.
        
        @param event close event (QCloseEvent)
        """
        self.__pos = self.pos()
        QDialog.closeEvent(self, event)

    def hideEvent(self, event):
        """
        Private event handler for the hide event.
        
        @param event close event (QHideEvent)
        """
        self.__pos = self.pos()
        QDialog.hideEvent(self, event)
