#!/usr/bin/env python3
# -*- encoding: utf-8 -*-
# -*- coding: utf-8 -*-
# -*- tabstop: 4 -*-

'''
 This program source code file is part of pySearchTree, a text files search application.
 
 Copyright  © 2021 by LordBlick (at) gmail.com
 
 This program is free software; you can redistribute it and/or
 modify it under the terms of the GNU General Public License
 as published by the Free Software Foundation; either version 2
 of the License, or (at your option) any later version.
 
 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.
 
 You should have received a copy of the GNU General Public License
 along with this program; if not, you may find one here:
 http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
 or you may search the http://www.gnu.org website for the version 2 license,
 or you may write to the Free Software Foundation, Inc.,
 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
'''

'''
useful links for py3 version:
	https://stackoverflow.com/questions/58691019/threading-in-gtk3-python
	https://stackoverflow.com/questions/33425202/handling-gtk-objects-on-threads-in-python
'''

from os import path as ph, readlink as rlnk
from clIniFile_py3 import IniSections, _p
from uiMassFind import Gtk, Gdk, Pango, actions, switches, _d, hh
from gi.repository import GLib

class filesFind:
	def __init__(mn):
		mn.uiInit()
		GLib.idle_add(mn._iddle_init)
		mn.ui.Enter()

	def _iddle_init(mn):
		mn.appCfgLoad()
		mn.findInit()
		mn.appStartWatchingObject()

	def go_Quit(mn, *w):
		_p("Exiting...\n")
		mn.appStopWatchingObject()
		mn.appCfgStore()
		mn.ui.Quit()

	global uiFn, rFn
	uiFn = lambda fileName: fileName.replace(ph.expanduser('~/'), '~/')
	rFn = lambda fileName: fileName.replace('~/', ph.expanduser('~/'))

	def uiInit(mn):
		from uiMassFind import massFindUI
		ui = mn.ui = massFindUI()
		handlers = ui.Connections()
		for hn in actions|switches:
			handlers['do_'+hn] = getattr(mn, 'go_'+hn)
		ui.bld.connect_signals(handlers)
		ui.createTxtTags()
		global _l, _lp
		_l, _lp = ui._p, ui._lp

	def updateColors(mn):
		ui = mn.ui
		for color_cfg in(color_set for color_set in mn.cfg['UI'].keys() if color_set[0:2] in('bg', 'fg')):
			tag_name = color_cfg.replace('bg', 'tg').replace('fg', 'tg')
			if not(hasattr(ui, tag_name)):
				mn.cfg['UI'].remove(color_cfg)
				_p(f"Unknown ui.tg{color_cfg[0:2]}\n")
				continue
			try:
				color = Gdk.color_parse(mn.cfg['UI'][color_cfg])
			except ValueError:
				mn.cfg['UI'].remove(color_cfg)
				_p(f"Unknown color name:'{mn.cfg['UI'][color_cfg]}' for ui.tg{color_cfg[0:2]}\n")
				continue
			prop_name = {'bg': 'background-gdk', 'fg': 'foreground-gdk'}[color_cfg[0:2]]
			#_d(f"mn.tag_name.set_property(\"{prop_name}\", Gdk.color_parse(mn.cfg['UI'][{color_cfg}])\n")
			getattr(ui, tag_name).set_property(prop_name, color)

	def defaultColors(mn):
		for color_cfg, color_val in(
			('fgFlNm', 'yellow'),
			('fgPhrs', 'orange'),
			('bgPhrs', '#002818'),
			('fg_Err', 'red'),
			('fgEnum', '#0F0'),
			('fgWarn', '#F85'),):
			if not(mn.cfg['UI'][color_cfg]):
				mn.cfg['UI'][color_cfg] = color_val
		mn.updateColors()

	def go_ChangeFileset(mn, cb):
		idx = cb.get_active()
		if idx > -1:
			mn.cfg['Main']['fileSet'] = f"{idx}"
			mn.actionName, t_reFileset, t_findDirs = mn.ui.lsFileset[idx]
			mn.reFileset = t_reFileset.strip('"')
			tl_findDirs = t_findDirs.strip("'").split(':')
			if (len(tl_findDirs)==1) and(tl_findDirs[0]==''):
				mn.findDirs = None
				return
			mn.findDirs = tuple(map(ph.expanduser, tl_findDirs))

	def go_ChangeRoot(mn, w):
		ui = mn.ui
		if w.get_active():
			if mn.cfg['Main']['FindPath']:
				text = f"{mn.cfg['Main']['FindPath']}\t"
			else:
				fileName = ui.dialogChooseFile(parent=ui.mainWindow,
					startDir=ui.lsFileset[ui.cbFileset.get_active()][2][0],
					title="Select search root", act='dir_open', bShowHidden=True)
				if fileName:
					mn.cfg['Main']['FindPath'] = fileName
					text = f"{fileName}\t"
				else:
					w.set_active(False)
					return
		else:
			text = "Choose dir...\t"
			mn.cfg['Main']['FindPath'] = None
		if ui.toggMaskHome.get_active():
			text = uiFn(text)
		w.set_label(text)
		tooltip = text[:-1]
		w.set_tooltip_text(tooltip)

	def bCfg(mn, cfgStr):
		v = mn.cfg['Main'][cfgStr]
		if type(v) is str and(v.isdigit()):
			return int(v)
		return 0

	def findInit(mn):
		ui = mn.ui
		mn.bSearching = False
		mn.lsThreads = []
		#All search types rewritten to Glade UI
		#('Python', "mime:text/x-python", ('~/Devel/Python',)),

	go_SrchLog = lambda mn, w: mn.ui.stv.showDlgSrch(w)

	def go_SrchInfo(mn, w):
		mn.cfg['Main'].__setitem__('bSrchInfo', str(int(w.get_active())))

	def go_MaskHome(mn, w):
		tr = mn.ui.toggRoot
		bM = w.get_active()
		mn.cfg['Main']['bMaskHome'] = str(int(bM))
		if tr.get_active():
			tr.set_label((rFn, uiFn)[bM](tr.get_label()))
			#label = tr.child
			#label.set_text((rFn, uiFn)[bM](label.get_text()))

	def go_FindBreak(mn, w):
		mn.bBreak = True

	def go_Find(mn, w):
		if mn.bSearching:
			return
		mn.bSearching = True
		mn.bBreak = False
		mn.phrase =  mn.ui.txtFindPhrase.get_text()
		import _thread
		_thread.start_new_thread(mn.thrFind, ())
		mn.bEndQueueUI = False
		mn.bQueueOverwork = False
		GLib.timeout_add(200, mn.tmThreadQueueUI)
		#TODO: Why no animation?
		#GLib.timeout_add(50, mn.tmThreadSpinner)

	UI_THREAD_END   = 0
	UI_THREAD_LOG   = 1
	UI_WIDGET_SENSE = 2
	UI_MAX_TASKS    = 10

	def tmThreadSpinner(mn):
		ui = mn.ui
		ui.drs.tick()
		_d(f"tick:{ui.drs.rot}\n")
		return True

	def tmThreadQueueUI(mn):
		if mn.bQueueOverwork: # Test if previous call was not ended
			_d(f"bQueueOverwork:{mn.bQueueOverwork}\n")
			return True
		mn.bQueueOverwork = True
		ui = mn.ui
		utq = mn.uiThreadQueue
		if mn.bModQueue:
			_d(f"bModQueue:{mn.bModQueue}\n")
			mn.bQueueOverwork = False
			return True
		while utq:
			mn.bPopQueue = True
			task_type, task_p1, task_p2  = utq.pop(0)
			mn.bPopQueue = False
			if task_type==mn.UI_THREAD_END or(mn.bEndQueueUI):
				if not(utq): # Care about QueueUI tasks are all done before end
					ui.drs.rot = 0
					ui.drs.refresh()
					mn.bQueueOverwork = False
					return False
				else:
					mn.bEndQueueUI = True
			elif task_type==mn.UI_THREAD_LOG:
				if task_p2:
					#_l(task_p1, tag=task_p2, short_path=True)
					_l(task_p1, tag=task_p2)
				else:
					_l(task_p1)
			elif task_type==mn.UI_WIDGET_SENSE:
				getattr(ui, task_p1).set_sensitive(task_p2)
		mn.bQueueOverwork = False
		return True


	def thrAppendQueue(mn, *t):
		while mn.bPopQueue:
			_d(f"bPopQueue:„{mn.bPopQueue}”\n")
			pass
		mn.bModQueue = True
		mn.uiThreadQueue.append(*t)
		mn.bModQueue = False

	thrEnd = lambda mn: mn.thrAppendQueue((mn.UI_THREAD_END, '', ''))

	def thrPrint(mn, text, tag=None):
		mn.thrAppendQueue((mn.UI_THREAD_LOG, text, tag))

	def thrFind(mn):
		bFound = False
		dec = lambda t: t.decode(codepage, 'ignore')
		from os import walk
		import re
		from magic import open as fmo, MIME
		mime=fmo(MIME)
		mime.load()
		regex_nms = None
		mime_ex = ''
		phrase = mn.phrase
		if not(hasattr(mn, 'reFileset')):
			mn.reFileset = ""
		if not(hasattr(mn, 'actionName')):
			mn.actionName = ""
		_d(f"phrase:„{phrase}”\nmn.reFileset:„{mn.reFileset}”\n")
		if mn.bCfg('bSrchInfo'):
			mn.thrPrint(f"Searching for regex_nms:'{phrase}'...\n")
		if not phrase:
			mn.thrPrint("Empty regex_nms input...\n", tag='err')
			mn.thrEnd()
			mn.bSearching = False
			return
		try:
			rexFindIt = re.compile(phrase, re.U)
		except:
			mn.thrPrint(f"This isn't correct regular expression:'{phrase}'\n", tag='err')
			mn.thrEnd()
			mn.bSearching = False
			return
		mn.thrAppendQueue((mn.UI_WIDGET_SENSE, 'buttFind', False))
		if mn.reFileset:
			if mn.reFileset.startswith('mime:'):
				mime_ex = mn.reFileset[5:]
			else:
				regex_nms = re.compile(mn.reFileset, re.I | re.U)
		else:
			regex_nms = re.compile('.*', re.I | re.U)
		if mn.cfg['Main']['FindPath']:
			findDirs = (mn.cfg['Main']['FindPath'], )
		else:
			findDirs = mn.findDirs
		_d(f"findDirs:„{findDirs}”\n")
		if mn.actionName=='AVR asm':
			codepage = 'cp1250'
		else:
			codepage = 'utf-8'
		for findDir in findDirs:
			#_d(f"Loop active, findDir:„{findDir}”\n")
			for findRoot, fDir, findFiles in walk(findDir):
				#_d(f"Loop active, findFiles:„{findFiles}”\n")
				if mn.bBreak:
					break
				if not findFiles:
					continue
				for fileName in findFiles:
					#_d(f"Loop active, fileName:„{fileName}”\n")
					bFileSkip = False
					if mn.bBreak:
						break
					fullFileName = '/'.join((findRoot, fileName))
					if not(regex_nms and(regex_nms.match(fileName)) or(mime_ex and(mime.file(fullFileName).startswith(mime_ex)))):
						continue
					while ph.islink(fullFileName):
						link_target = ph.realpath(ph.expanduser(f"{findRoot}/{rlnk(fullFileName)}"))
						#_d(f"Resolving link loop, fullFileName:\n\t„{fullFileName}”→\n\t„{link_target}”\n")
						if fullFileName==link_target:
							for txtslice, cTag in(
								("Found filesystem link pointing to himself:„", 2), (fullFileName, 1), ("”, consider it's necessity, skipping…\n", 2) ):
								mn.thrPrint(txtslice, tag=(None, 'fnm', 'wrn')[cTag])
							fullFileName = '' #clear to escape Infinite loop
							break
						fullFileName = link_target
					if not(ph.isfile(fullFileName)):
						continue
					try:
						hFile = open(fullFileName, 'r')
					except PermissionError:
						for txtslice, cTag in(
							("This file permisions:„", 2), (fullFileName, 1), ("” does not allow to read it…\n", 2) ):
							mn.thrPrint(txtslice, tag=(None, 'fnm', 'wrn')[cTag])
						continue
					if not(hFile):
						continue
					try: fileData = hFile.read()
					except UnicodeDecodeError as e:
						#_d(f"With file„{hh(fullFileName)}” we caught exception:\n\t„{e}”\n")
						bFileSkip = True
					hFile.close()
					if bFileSkip:
						continue # ussualy some binary, skip
					bFnd = False
					for idx, line in enumerate(fileData.splitlines()):
						slices = []
						tmpline = line
						lmatch = rexFindIt.search(tmpline)
						if not(lmatch):
							continue
						if not(bFnd):
							if mn.bCfg('bMaskHome'):
								fullFileName = fullFileName.replace(ph.expanduser('~'), '~')
							for txtslice, cTag in(
								("In:'", 0), (hh(fullFileName), 1), ("':\n", 0) ):
								mn.thrPrint(txtslice, tag=(None, 'fnm')[cTag])
							bFnd = True
							if not(bFound):
								bFound = True
						#TODO: convert to autoformat with count lines and use F-string
						slices.append(("%3d:" % (idx+1), 1))	#	tag='num'
						while tmpline:
							srchB, srchE = lmatch.span()
							slices.append((tmpline[:srchB], 0))
							slices.append((tmpline[srchB:srchE], 2))	#	tag='phr'
							tmpline = tmpline[srchE:]
							lmatch = rexFindIt.search(tmpline)
							if not(lmatch):
								slices.append((tmpline, 0))
								break
						for txtslice, cTag in slices:
							if txtslice:
								if type(txtslice)==str:
									mn.thrPrint(txtslice, tag=(None, 'num', 'phr')[cTag])
								else:
									mn.thrPrint(dec(txtslice), tag=(None, 'num', 'phr')[cTag])
						mn.thrPrint('\n')
		_d(f"Loop end, findDir:„{findDir}”\n")
		mn.thrAppendQueue((mn.UI_WIDGET_SENSE, 'buttFind', True))
		if mn.bBreak:
			mn.thrPrint("Searching aborted...\n", tag='err')
		if not(bFound):
			mn.thrPrint("Searched phrase not found...\n", tag='err')
		mn.thrEnd()
		mn.bSearching = False

	def callWriteCmdBuff(mn, fd, condition):
		logBuff=mn.ui.textCmdline.get_buffer()
		for line in fd.readline():
			text = line.rstrip()
			mn.ui.logView.insert_end(text)

	def appStartWatchingObject(mn):
		'''
		from os import fdopen as fdo, O_NDELAY as NO_DELAY
		addWatch = GLib.io_add_watch
		ioIN, ioHUP = GLib.IO_IN, GLib.IO_HUP
		import pty, fcntl
		mn.ptyProc = None
		mn.pty_parent_fd, mn.pty_child_fd = pty.openpty()
		mn.fd = fdo(mn.pty_parent_fd, 'r')
		file_flags = fcntl.fcntl(mn.fd, fcntl.F_GETFL)
		fcntl.fcntl(mn.fd, fcntl.F_SETFL, file_flags|NO_DELAY)
		mn.watchID = addWatch(mn.fd,  ioIN | ioHUP,  mn.callWriteCmdBuff)
		'''
		mn.uiThreadQueue = []
		mn.bModQueue = False
		mn.bPopQueue = False

	def appStopWatchingObject(mn):
		'''
		import subprocess
		import time
		if type(mn.ptyProc) is subprocess.Popen:
			mn.ptyProc.kill()
			while mn.ptyProc.poll() == None:
				time.sleep(0.1)
			mn.ptyProc = None
		unWatch = GLib.source_remove
		unWatch(mn.watchID)
		mn.fd.close()
		'''
		pass

	def appCfgLoad(mn, restoreGeo=True):
		ui = mn.ui
		mn.cfg = IniSections()
		rfn = ph.expanduser(__file__)
		cfgBase = ui.runpath+'/'+ ph.basename(rfn)
		mn.old_cfg_fn = cfgBase+'.conf'
		cfg_fn = cfgBase+'.ini'
		_p(f"Config base: '{cfgBase}'\n")
		if ph.isfile(cfg_fn):
			mn.cfg.load(cfg_fn)
			_p(f"Loaded Config file: '{cfg_fn}'\n")
		if not(len(mn.cfg)) and(ph.isfile(mn.old_cfg_fn)):
			mn.cfg.load_old_cfg(mn.old_cfg_fn)
			_p(f"Config file: '{mn.old_cfg_fn}'\n")
			_p(f"→New Config file: '{cfg_fn}'\n")
		mn.cfg.filename = cfg_fn
		ui.toggRoot.set_active(bool(mn.cfg['Main']['FindPath']))
		if mn.cfg['Main']['phraseFind']:
			ui.txtFindPhrase.set_text(mn.cfg['Main']['phraseFind'])
		mn.lastcfg = mn.cfg['Main'].copy(), mn.cfg['UI'].copy()
		mn.defaultColors()
		idxFileSet, idx_t = 0, -1
		txtFileSet = mn.cfg['Main']['fileSet']
		if txtFileSet and(txtFileSet.isdigit()):
			idx_t = int(txtFileSet)
		if idx_t>-1:
			idxFileSet = idx_t
		ui.cbFileset.set_active(idxFileSet)
		ui.toggMaskHome.set_active(int(mn.bCfg('bMaskHome')))
		ui.toggSrchInfo.set_active(int(mn.bCfg('bSrchInfo')))
		if restoreGeo:
			ui.cfg = mn.cfg['UI']
			_d(f"type of ui.cfg:{type(ui.cfg)}\n")
			mnTxtGeo = ui.cfg.get('MainWindowGeometry', '')
			if mnTxtGeo:
				ui.setTxtWinGeometry(ui.mainWindow, mnTxtGeo)
			else:
				_d(f"Strange situation, no MainWindowGeometry cfg\n")

	def appCfgStore(mn, saveGeometry=True):
		ui = mn.ui
		if saveGeometry:
			ui.cfg['MainWindowGeometry'] = ui.getTxtWinGeometry(ui.mainWindow)
		mn.cfg['Main']['phraseFind'] = ui.txtFindPhrase.get_text()
		if not(mn.cfg['Main']['FindPath']):
			mn.cfg['Main'].remove('FindPath')
		if mn.lastcfg!=(mn.cfg['Main'], mn.cfg['UI']):
			mn.cfg.section_place('Main', 0)
			mn.cfg.store()

# Entry point
if __name__ == "__main__":
	filesFind()
