# find.py - Find and Replace
from __future__ import annotations

import win32api
import win32con
import win32ui
from pywin.framework import scriptutils
from pywin.mfc import afxres, dialog

FOUND_NOTHING = 0
FOUND_NORMAL = 1
FOUND_LOOPED_BACK = 2
FOUND_NEXT_FILE = 3


class SearchParams:
    def __init__(self, other=None):
        if other is None:
            self.__dict__["findText"] = ""
            self.__dict__["replaceText"] = ""
            self.__dict__["matchCase"] = 0
            self.__dict__["matchWords"] = 0
            self.__dict__["acrossFiles"] = 0
            self.__dict__["remember"] = 1
            self.__dict__["sel"] = (-1, -1)
            self.__dict__["keepDialogOpen"] = 0
        else:
            self.__dict__.update(other.__dict__)

    # Helper so we can't misspell attributes :-)
    def __setattr__(self, attr, val):
        if not hasattr(self, attr):
            raise AttributeError(attr)
        self.__dict__[attr] = val


curDialog = None
lastSearch = defaultSearch = SearchParams()
searchHistory: list[str] = []


def ShowFindDialog():
    _ShowDialog(FindDialog)


def ShowReplaceDialog():
    _ShowDialog(ReplaceDialog)


def _ShowDialog(dlgClass):
    global curDialog
    if curDialog is not None:
        if curDialog.__class__ != dlgClass:
            curDialog.DestroyWindow()
            curDialog = None
        else:
            curDialog.SetFocus()
    if curDialog is None:
        curDialog = dlgClass()
        curDialog.CreateWindow()


def FindNext():
    params = SearchParams(lastSearch)
    params.sel = (-1, -1)
    if not params.findText:
        ShowFindDialog()
    else:
        return _FindIt(None, params)


def _GetControl(control=None):
    if control is None:
        control = scriptutils.GetActiveEditControl()
    return control


def _FindIt(control, searchParams):
    global lastSearch, defaultSearch
    control = _GetControl(control)
    if control is None:
        return FOUND_NOTHING

    # Move to the next char, so we find the next one.
    flags = 0
    if searchParams.matchWords:
        flags |= win32con.FR_WHOLEWORD
    if searchParams.matchCase:
        flags |= win32con.FR_MATCHCASE
    if searchParams.sel == (-1, -1):
        sel = control.GetSel()
        # If the position is the same as we found last time,
        # then we assume it is a "FindNext"
        if sel == lastSearch.sel:
            sel = sel[0] + 1, sel[0] + 1
    else:
        sel = searchParams.sel

    if sel[0] == sel[1]:
        sel = sel[0], control.GetTextLength()

    rc = FOUND_NOTHING
    # (Old edit control will fail here!)
    posFind, foundSel = control.FindText(flags, sel, searchParams.findText)
    lastSearch = SearchParams(searchParams)
    if posFind >= 0:
        rc = FOUND_NORMAL
        lineno = control.LineFromChar(posFind)
        control.SCIEnsureVisible(lineno)
        control.SetSel(foundSel)
        control.SetFocus()
        win32ui.SetStatusText(win32ui.LoadString(afxres.AFX_IDS_IDLEMESSAGE))
    if rc == FOUND_NOTHING and lastSearch.acrossFiles:
        # Loop around all documents.  First find this document.
        try:
            try:
                doc = control.GetDocument()
            except AttributeError:
                try:
                    doc = control.GetParent().GetDocument()
                except AttributeError:
                    print("Can't find a document for the control!")
                    doc = None
            if doc is not None:
                template = doc.GetDocTemplate()
                alldocs = template.GetDocumentList()
                mypos = lookpos = alldocs.index(doc)
                while 1:
                    lookpos = (lookpos + 1) % len(alldocs)
                    if lookpos == mypos:
                        break
                    view = alldocs[lookpos].GetFirstView()
                    posFind, foundSel = view.FindText(
                        flags, (0, view.GetTextLength()), searchParams.findText
                    )
                    if posFind >= 0:
                        nChars = foundSel[1] - foundSel[0]
                        lineNo = view.LineFromChar(posFind)  # zero based.
                        lineStart = view.LineIndex(lineNo)
                        colNo = posFind - lineStart  # zero based.
                        scriptutils.JumpToDocument(
                            alldocs[lookpos].GetPathName(),
                            lineNo + 1,
                            colNo + 1,
                            nChars,
                        )
                        rc = FOUND_NEXT_FILE
                        break
        except win32ui.error:
            pass
    if rc == FOUND_NOTHING:
        # Loop around this control - attempt to find from the start of the control.
        posFind, foundSel = control.FindText(
            flags, (0, sel[0] - 1), searchParams.findText
        )
        if posFind >= 0:
            control.SCIEnsureVisible(control.LineFromChar(foundSel[0]))
            control.SetSel(foundSel)
            control.SetFocus()
            win32ui.SetStatusText("Not found! Searching from the top of the file.")
            rc = FOUND_LOOPED_BACK
        else:
            lastSearch.sel = -1, -1
            win32ui.SetStatusText("Can not find '%s'" % searchParams.findText)

    if rc != FOUND_NOTHING:
        lastSearch.sel = foundSel

    if lastSearch.remember:
        defaultSearch = lastSearch

        # track search history
        try:
            ix = searchHistory.index(searchParams.findText)
        except ValueError:
            if len(searchHistory) > 50:
                searchHistory[50:] = []
        else:
            del searchHistory[ix]
        searchHistory.insert(0, searchParams.findText)

    return rc


def _ReplaceIt(control):
    control = _GetControl(control)
    statusText = "Can not find '%s'." % lastSearch.findText
    rc = FOUND_NOTHING
    if control is not None and lastSearch.sel != (-1, -1):
        control.ReplaceSel(lastSearch.replaceText)
        rc = FindNext()
        if rc != FOUND_NOTHING:
            statusText = win32ui.LoadString(afxres.AFX_IDS_IDLEMESSAGE)
    win32ui.SetStatusText(statusText)
    return rc


class FindReplaceDialog(dialog.Dialog):
    def __init__(self):
        dialog.Dialog.__init__(self, self._GetDialogTemplate())
        self.HookCommand(self.OnFindNext, 109)

    def OnInitDialog(self):
        self.editFindText = self.GetDlgItem(102)
        self.butMatchWords = self.GetDlgItem(105)
        self.butMatchCase = self.GetDlgItem(107)
        self.butKeepDialogOpen = self.GetDlgItem(115)
        self.butAcrossFiles = self.GetDlgItem(116)
        self.butRemember = self.GetDlgItem(117)

        self.editFindText.SetWindowText(defaultSearch.findText)
        control = _GetControl()
        # _GetControl only gets normal MDI windows; if the interactive
        # window is docked and no document open, we get None.
        if control:
            # If we have a selection, default to that.
            sel = control.GetSelText()
            if len(sel) != 0:
                self.editFindText.SetWindowText(sel)
                if defaultSearch.remember:
                    defaultSearch.findText = sel
        for hist in searchHistory:
            self.editFindText.AddString(hist)

        if hasattr(self.editFindText, "SetEditSel"):
            self.editFindText.SetEditSel(0, -1)
        else:
            self.editFindText.SetSel(0, -1)
        self.butMatchWords.SetCheck(defaultSearch.matchWords)
        self.butMatchCase.SetCheck(defaultSearch.matchCase)
        self.butKeepDialogOpen.SetCheck(defaultSearch.keepDialogOpen)
        self.butAcrossFiles.SetCheck(defaultSearch.acrossFiles)
        self.butRemember.SetCheck(defaultSearch.remember)
        return dialog.Dialog.OnInitDialog(self)

    def OnDestroy(self, msg):
        global curDialog
        curDialog = None
        return dialog.Dialog.OnDestroy(self, msg)

    def DoFindNext(self):
        params = SearchParams()
        params.findText = self.editFindText.GetWindowText()
        params.matchCase = self.butMatchCase.GetCheck()
        params.matchWords = self.butMatchWords.GetCheck()
        params.acrossFiles = self.butAcrossFiles.GetCheck()
        params.remember = self.butRemember.GetCheck()
        return _FindIt(None, params)

    def OnFindNext(self, id, code):
        if code != 0:  # BN_CLICKED
            # 3d controls (python.exe + start_pythonwin.pyw) send
            # other notification codes
            return 1  #
        if not self.editFindText.GetWindowText():
            win32api.MessageBeep()
            return 1
        if self.DoFindNext() != FOUND_NOTHING:
            if not self.butKeepDialogOpen.GetCheck():
                self.DestroyWindow()


class FindDialog(FindReplaceDialog):
    def _GetDialogTemplate(self):
        style = (
            win32con.DS_MODALFRAME
            | win32con.WS_POPUP
            | win32con.WS_VISIBLE
            | win32con.WS_CAPTION
            | win32con.WS_SYSMENU
            | win32con.DS_SETFONT
        )
        visible = win32con.WS_CHILD | win32con.WS_VISIBLE
        dt = [
            ["Find", (0, 2, 240, 75), style, None, (8, "MS Sans Serif")],
            ["Static", "Fi&nd What:", 101, (5, 8, 40, 10), visible],
            [
                "ComboBox",
                "",
                102,
                (50, 7, 120, 120),
                visible
                | win32con.WS_BORDER
                | win32con.WS_TABSTOP
                | win32con.WS_VSCROLL
                | win32con.CBS_DROPDOWN
                | win32con.CBS_AUTOHSCROLL,
            ],
            [
                "Button",
                "Match &whole word only",
                105,
                (5, 23, 100, 10),
                visible | win32con.BS_AUTOCHECKBOX | win32con.WS_TABSTOP,
            ],
            [
                "Button",
                "Match &case",
                107,
                (5, 33, 100, 10),
                visible | win32con.BS_AUTOCHECKBOX | win32con.WS_TABSTOP,
            ],
            [
                "Button",
                "Keep &dialog open",
                115,
                (5, 43, 100, 10),
                visible | win32con.BS_AUTOCHECKBOX | win32con.WS_TABSTOP,
            ],
            [
                "Button",
                "Across &open files",
                116,
                (5, 52, 100, 10),
                visible | win32con.BS_AUTOCHECKBOX | win32con.WS_TABSTOP,
            ],
            [
                "Button",
                "&Remember as default search",
                117,
                (5, 61, 150, 10),
                visible | win32con.BS_AUTOCHECKBOX | win32con.WS_TABSTOP,
            ],
            [
                "Button",
                "&Find Next",
                109,
                (185, 5, 50, 14),
                visible | win32con.BS_DEFPUSHBUTTON | win32con.WS_TABSTOP,
            ],
            [
                "Button",
                "Cancel",
                win32con.IDCANCEL,
                (185, 23, 50, 14),
                visible | win32con.WS_TABSTOP,
            ],
        ]
        return dt


class ReplaceDialog(FindReplaceDialog):
    def _GetDialogTemplate(self):
        style = (
            win32con.DS_MODALFRAME
            | win32con.WS_POPUP
            | win32con.WS_VISIBLE
            | win32con.WS_CAPTION
            | win32con.WS_SYSMENU
            | win32con.DS_SETFONT
        )
        visible = win32con.WS_CHILD | win32con.WS_VISIBLE
        dt = [
            ["Replace", (0, 2, 240, 95), style, 0, (8, "MS Sans Serif")],
            ["Static", "Fi&nd What:", 101, (5, 8, 40, 10), visible],
            [
                "ComboBox",
                "",
                102,
                (60, 7, 110, 120),
                visible
                | win32con.WS_BORDER
                | win32con.WS_TABSTOP
                | win32con.WS_VSCROLL
                | win32con.CBS_DROPDOWN
                | win32con.CBS_AUTOHSCROLL,
            ],
            ["Static", "Re&place with:", 103, (5, 25, 50, 10), visible],
            [
                "ComboBox",
                "",
                104,
                (60, 24, 110, 120),
                visible
                | win32con.WS_BORDER
                | win32con.WS_TABSTOP
                | win32con.WS_VSCROLL
                | win32con.CBS_DROPDOWN
                | win32con.CBS_AUTOHSCROLL,
            ],
            [
                "Button",
                "Match &whole word only",
                105,
                (5, 42, 100, 10),
                visible | win32con.BS_AUTOCHECKBOX | win32con.WS_TABSTOP,
            ],
            [
                "Button",
                "Match &case",
                107,
                (5, 52, 100, 10),
                visible | win32con.BS_AUTOCHECKBOX | win32con.WS_TABSTOP,
            ],
            [
                "Button",
                "Keep &dialog open",
                115,
                (5, 62, 100, 10),
                visible | win32con.BS_AUTOCHECKBOX | win32con.WS_TABSTOP,
            ],
            [
                "Button",
                "Across &open files",
                116,
                (5, 72, 100, 10),
                visible | win32con.BS_AUTOCHECKBOX | win32con.WS_TABSTOP,
            ],
            [
                "Button",
                "&Remember as default search",
                117,
                (5, 81, 150, 10),
                visible | win32con.BS_AUTOCHECKBOX | win32con.WS_TABSTOP,
            ],
            [
                "Button",
                "&Find Next",
                109,
                (185, 5, 50, 14),
                visible | win32con.BS_DEFPUSHBUTTON | win32con.WS_TABSTOP,
            ],
            [
                "Button",
                "&Replace",
                110,
                (185, 23, 50, 14),
                visible | win32con.WS_TABSTOP,
            ],
            [
                "Button",
                "Replace &All",
                111,
                (185, 41, 50, 14),
                visible | win32con.WS_TABSTOP,
            ],
            [
                "Button",
                "Cancel",
                win32con.IDCANCEL,
                (185, 59, 50, 14),
                visible | win32con.WS_TABSTOP,
            ],
        ]
        return dt

    def OnInitDialog(self):
        rc = FindReplaceDialog.OnInitDialog(self)
        self.HookCommand(self.OnReplace, 110)
        self.HookCommand(self.OnReplaceAll, 111)
        self.HookMessage(self.OnActivate, win32con.WM_ACTIVATE)
        self.editReplaceText = self.GetDlgItem(104)
        self.editReplaceText.SetWindowText(lastSearch.replaceText)
        if hasattr(self.editReplaceText, "SetEditSel"):
            self.editReplaceText.SetEditSel(0, -1)
        else:
            self.editReplaceText.SetSel(0, -1)
        self.butReplace = self.GetDlgItem(110)
        self.butReplaceAll = self.GetDlgItem(111)
        self.CheckButtonStates()
        return rc  # 0 when focus set

    def CheckButtonStates(self):
        # We can do a "Replace" or "Replace All" if the current selection
        # is the same as the search text.
        ft = self.editFindText.GetWindowText()
        control = _GetControl()
        # 		bCanReplace = len(ft)>0 and control.GetSelText() == ft
        bCanReplace = control is not None and lastSearch.sel == control.GetSel()
        self.butReplace.EnableWindow(bCanReplace)

    # 		self.butReplaceAll.EnableWindow(bCanReplace)

    def OnActivate(self, msg):
        wparam = msg[2]
        fActive = win32api.LOWORD(wparam)
        if fActive != win32con.WA_INACTIVE:
            self.CheckButtonStates()

    def OnFindNext(self, id, code):
        if code != 0:
            return 1
        self.DoFindNext()
        self.CheckButtonStates()

    def OnReplace(self, id, code):
        if code != 0:
            return 1
        lastSearch.replaceText = self.editReplaceText.GetWindowText()
        _ReplaceIt(None)

    def OnReplaceAll(self, id, code):
        if code != 0:
            return 1
        control = _GetControl(None)
        if control is not None:
            control.SetSel(0)
            num = 0
            if self.DoFindNext() == FOUND_NORMAL:
                num = 1
                lastSearch.replaceText = self.editReplaceText.GetWindowText()
                while _ReplaceIt(control) == FOUND_NORMAL:
                    num += 1

            win32ui.SetStatusText("Replaced %d occurrences" % num)
            if num > 0 and not self.butKeepDialogOpen.GetCheck():
                self.DestroyWindow()


if __name__ == "__main__":
    ShowFindDialog()
