# find.py - Find and Replace
import win32con, win32api
import win32ui
from pywin.mfc import dialog
import afxres
from pywin.framework import scriptutils

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 cant 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 = []

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 = flags | win32con.FR_WHOLEWORD
	if searchParams.matchCase: flags = 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("Cant 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, -2)
		else:
			self.editFindText.SetSel(0, -2)
		self.editFindText.SetFocus()
		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 not self.editFindText.GetWindowText():
			win32api.MessageBeep()
			return
		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, -2)
		else:
			self.editReplaceText.SetSel(0, -2)
		self.butReplace = self.GetDlgItem(110)
		self.butReplaceAll = self.GetDlgItem(111)
		self.CheckButtonStates()
		return rc

	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):
		self.DoFindNext()
		self.CheckButtonStates()

	def OnReplace(self, id, code):
		lastSearch.replaceText = self.editReplaceText.GetWindowText()
		_ReplaceIt(None)

	def OnReplaceAll(self, id, code):
		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 = num + 1

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

if __name__=='__main__':
	ShowFindDialog()
