"""
Various utilities for running/importing a script
"""
import sys
import win32ui
import win32api
import win32con
import __main__
from pywin.mfc import dialog
from pywin.mfc.docview import TreeView
import os
import string
import traceback
import linecache
import bdb

from cmdline import ParseArgs

RS_DEBUGGER_NONE=0 # Dont run under the debugger.
RS_DEBUGGER_STEP=1 # Start stepping under the debugger
RS_DEBUGGER_GO=2 # Just run under the debugger, stopping only at break-points.
RS_DEBUGGER_PM=3 # Dont run under debugger, but do post-mortem analysis on exception.

debugging_options = """No debugging
Step-through in the debugger
Run in the debugger
Post-Mortem of unhandled exceptions""".split("\n")

byte_cr = "\r".encode("ascii")
byte_lf = "\n".encode("ascii")
byte_crlf = "\r\n".encode("ascii")

# A dialog box for the "Run Script" command.
class DlgRunScript(dialog.Dialog):
	"A class for the 'run script' dialog"
	def __init__(self, bHaveDebugger):
		dialog.Dialog.__init__(self, win32ui.IDD_RUN_SCRIPT )
		self.AddDDX(win32ui.IDC_EDIT1, "script")
		self.AddDDX(win32ui.IDC_EDIT2, "args")
		self.AddDDX(win32ui.IDC_COMBO1, "debuggingType", "i")
		self.HookCommand(self.OnBrowse, win32ui.IDC_BUTTON2)
		self.bHaveDebugger = bHaveDebugger
	def OnInitDialog(self):
		rc = dialog.Dialog.OnInitDialog(self)
		cbo = self.GetDlgItem(win32ui.IDC_COMBO1)
		for o in debugging_options:
			cbo.AddString(o)
		cbo.SetCurSel(self['debuggingType'])
		if not self.bHaveDebugger:
			cbo.EnableWindow(0)

	def OnBrowse(self, id, cmd):
		openFlags = win32con.OFN_OVERWRITEPROMPT|win32con.OFN_FILEMUSTEXIST
		dlg = win32ui.CreateFileDialog(1,None,None,openFlags, "Python Scripts (*.py)|*.py||", self)
		dlg.SetOFNTitle("Run Script")
		if dlg.DoModal()!=win32con.IDOK:
			return 0
		self['script'] = dlg.GetPathName()
		self.UpdateData(0)
		return 0

def GetDebugger():
	"""Get the default Python debugger.  Returns the debugger, or None.
	
	It is assumed the debugger has a standard "pdb" defined interface.
	Currently always returns the 'pywin.debugger' debugger, or None
	(pdb is _not_ returned as it is not effective in this GUI environment)
	"""
	try:
		import pywin.debugger
		return pywin.debugger
	except ImportError:
		return None

def IsOnPythonPath(path):
	"Given a path only, see if it is on the Pythonpath.  Assumes path is a full path spec."
	# must check that the command line arg's path is in sys.path
	for syspath in sys.path:
		try:
			# Python 1.5 and later allows an empty sys.path entry.
			if syspath and win32ui.FullPath(syspath)==path:
				return 1
		except win32ui.error, details:
			print "Warning: The sys.path entry '%s' is invalid\n%s" % (syspath, details)
	return 0

def GetPackageModuleName(fileName):
	"""Given a filename, return (module name, new path).
	   eg - given "c:\a\b\c\my.py", return ("b.c.my",None) if "c:\a" is on sys.path.
	   If no package found, will return ("my", "c:\a\b\c")
	"""
	path, fname = os.path.split(fileName)
	path=origPath=win32ui.FullPath(path)
	fname = os.path.splitext(fname)[0]
	modBits = []
	newPathReturn = None
	if not IsOnPythonPath(path):
		# Module not directly on the search path - see if under a package.
		while len(path)>3: # ie 'C:\'
			path, modBit = os.path.split(path)
			modBits.append(modBit)
			# If on path, _and_ existing package of that name loaded.
			if IsOnPythonPath(path) and modBit in sys.modules and \
				(os.path.exists(os.path.join(path, modBit, '__init__.py')) or \
				 os.path.exists(os.path.join(path, modBit, '__init__.pyc')) or \
				 os.path.exists(os.path.join(path, modBit, '__init__.pyo')) \
				):
				modBits.reverse()
				return ".".join(modBits) + "." + fname, newPathReturn
			# Not found - look a level higher
		else:
			newPathReturn = origPath
		
	return fname, newPathReturn

def GetActiveView():
	"""Gets the edit control (eg, EditView) with the focus, or None
	"""
	try:
		childFrame, bIsMaximised = win32ui.GetMainFrame().MDIGetActive()
		return childFrame.GetActiveView()
	except win32ui.error:
		return None

def GetActiveEditControl():
	view = GetActiveView()
	if view is None: return None
	if hasattr(view, "SCIAddText"): # Is it a scintilla control?
		return view
	try:
		return view.GetRichEditCtrl()
	except AttributeError:
		pass
	try:
		return view.GetEditCtrl()
	except AttributeError:
		pass

def GetActiveEditorDocument():
	"""Returns the active editor document and view, or (None,None) if no
	active document or its not an editor document.
	"""
	view = GetActiveView()
	if view is None or isinstance(view, TreeView):
		return (None, None)
	doc = view.GetDocument()
	if hasattr(doc, "MarkerAdd"): # Is it an Editor document?
		return doc, view
	return (None, None)

def GetActiveFileName(bAutoSave = 1):
	"""Gets the file name for the active frame, saving it if necessary.
	
	Returns None if it cant be found, or raises KeyboardInterrupt.
	"""
	pathName = None
	active = GetActiveView()
	if active is None:
		return None
	try:
		doc = active.GetDocument()
		pathName = doc.GetPathName()

		if bAutoSave and \
			(len(pathName)>0 or \
			doc.GetTitle()[:8]=="Untitled" or \
			doc.GetTitle()[:6]=="Script"): # if not a special purpose window
			if doc.IsModified():
				try:
					doc.OnSaveDocument(pathName)
					pathName = doc.GetPathName()
					
					# clear the linecache buffer
					linecache.clearcache()

				except win32ui.error:
					raise KeyboardInterrupt

	except (win32ui.error, AttributeError):
		pass
	if not pathName:
		return None
	return pathName

lastScript = ''
lastArgs = ''
lastDebuggingType = RS_DEBUGGER_NONE

def RunScript(defName=None, defArgs=None, bShowDialog = 1, debuggingType=None):
	global lastScript, lastArgs, lastDebuggingType
	_debugger_stop_frame_ = 1 # Magic variable so the debugger will hide me!

	# Get the debugger - may be None!
	debugger = GetDebugger()

	if defName is None:
		try:
			pathName = GetActiveFileName()
		except KeyboardInterrupt:
			return # User cancelled save.
	else:
		pathName = defName
	if not pathName:
		pathName = lastScript
	if defArgs is None:
		args = ''
		if pathName==lastScript:
			args = lastArgs
	else:
		args = defArgs
	if debuggingType is None: debuggingType = lastDebuggingType

	if not pathName or bShowDialog:
		dlg = DlgRunScript(debugger is not None)
		dlg['script'] = pathName
		dlg['args'] = args
		dlg['debuggingType'] = debuggingType
		if dlg.DoModal() != win32con.IDOK:
			return
		script=dlg['script']
		args=dlg['args']
		debuggingType = dlg['debuggingType']
		if not script: return
		if debuggingType == RS_DEBUGGER_GO and debugger is not None:
			# This may surprise users - they select "Run under debugger", but
			# it appears not to!  Only warn when they pick from the dialog!
			# First - ensure the debugger is activated to pickup any break-points
			# set in the editor.
			try:
				# Create the debugger, but _dont_ init the debugger GUI.
				rd = debugger._GetCurrentDebugger()
			except AttributeError:
				rd = None
			if rd is not None and len(rd.breaks)==0:
				msg = "There are no active break-points.\r\n\r\nSelecting this debug option without any\r\nbreak-points is unlikely to have the desired effect\r\nas the debugger is unlikely to be invoked..\r\n\r\nWould you like to step-through in the debugger instead?"
				rc = win32ui.MessageBox(msg, win32ui.LoadString(win32ui.IDR_DEBUGGER), win32con.MB_YESNOCANCEL | win32con.MB_ICONINFORMATION)
				if rc == win32con.IDCANCEL:
					return
				if rc == win32con.IDYES:
					debuggingType = RS_DEBUGGER_STEP

		lastDebuggingType = debuggingType
		lastScript = script
		lastArgs = args
	else:
		script = pathName

	# try and open the script.
	if len(os.path.splitext(script)[1])==0:	# check if no extension supplied, and give one.
			script = script + '.py'
	# If no path specified, try and locate the file
	path, fnameonly = os.path.split(script)
	if len(path)==0:
		try:
			os.stat(fnameonly) # See if it is OK as is...
			script = fnameonly
		except os.error:
			fullScript = LocatePythonFile(script)
			if fullScript is None:
				win32ui.MessageBox("The file '%s' can not be located" % script )
				return
			script = fullScript
	else:
		path = win32ui.FullPath(path)
		if not IsOnPythonPath(path): sys.path.append(path)

	# py3k fun: If we use text mode to open the file, we get \r\n
	# translated so Python allows the syntax (good!), but we get back
	# text already decoded from the default encoding (bad!) and Python
	# ignores any encoding decls (bad!).  If we use binary mode we get
	# the raw bytes and Python looks at the encoding (good!) but \r\n
	# chars stay in place so Python throws a syntax error (bad!).
	# So: so the binary thing and manually normalize \r\n.
	try:
		f = open(script, 'rb')
	except IOError, exc:
		win32ui.MessageBox("The file could not be opened - %s (%d)" % (exc.strerror, exc.errno))
		return

	# Get the source-code - as above, normalize \r\n
	code = f.read().replace(byte_crlf, byte_lf).replace(byte_cr, byte_lf) + byte_lf

	# Remember and hack sys.argv for the script.
	oldArgv = sys.argv
	sys.argv = ParseArgs(args)
	sys.argv.insert(0, script)
	# sys.path[0] is the path of the script
	oldPath0 = sys.path[0]
	newPath0 = os.path.split(script)[0]
	if not oldPath0: # if sys.path[0] is empty
		sys.path[0] = newPath0
		insertedPath0 = 0
	else:
		sys.path.insert(0, newPath0)
		insertedPath0 = 1
	bWorked = 0
	win32ui.DoWaitCursor(1)
	base = os.path.split(script)[1]
	# Allow windows to repaint before starting.
	win32ui.PumpWaitingMessages()
	win32ui.SetStatusText('Running script %s...' % base,1 )
	exitCode = 0
	from pywin.framework import interact
	# Check the debugger flags
	if debugger is None and (debuggingType != RS_DEBUGGER_NONE):
		win32ui.MessageBox("No debugger is installed.  Debugging options have been ignored!")
		debuggingType = RS_DEBUGGER_NONE

	# Get a code object - ignore the debugger for this, as it is probably a syntax error
	# at this point
	try:
		codeObject = compile(code, script, "exec")
	except:
		# Almost certainly a syntax error!
		_HandlePythonFailure("run script", script)
		# No code object which to run/debug.
		return
	__main__.__file__=script
	try:
		if debuggingType == RS_DEBUGGER_STEP:
			debugger.run(codeObject, __main__.__dict__, start_stepping=1)
		elif debuggingType == RS_DEBUGGER_GO:
			debugger.run(codeObject, __main__.__dict__, start_stepping=0)
		else:
			# Post mortem or no debugging
			exec codeObject in __main__.__dict__
		bWorked = 1
	except bdb.BdbQuit:
		# Dont print tracebacks when the debugger quit, but do print a message.
		print "Debugging session cancelled."
		exitCode = 1
		bWorked = 1
	except SystemExit, code:
		exitCode = code
		bWorked = 1
	except KeyboardInterrupt:
		# Consider this successful, as we dont want the debugger.
		# (but we do want a traceback!)
		if interact.edit and interact.edit.currentView:
			interact.edit.currentView.EnsureNoPrompt()
		traceback.print_exc()
		if interact.edit and interact.edit.currentView:
			interact.edit.currentView.AppendToPrompt([])
		bWorked = 1
	except:
		if interact.edit and interact.edit.currentView:
			interact.edit.currentView.EnsureNoPrompt()
		traceback.print_exc()
		if interact.edit and interact.edit.currentView:
			interact.edit.currentView.AppendToPrompt([])
		if debuggingType == RS_DEBUGGER_PM:
			debugger.pm()
	del __main__.__file__
	sys.argv = oldArgv
	if insertedPath0:
		del sys.path[0]
	else:
		sys.path[0] = oldPath0
	f.close()
	if bWorked:
		win32ui.SetStatusText("Script '%s' returned exit code %s" %(script, exitCode))
	else:
		win32ui.SetStatusText('Exception raised while running script  %s' % base)
	try:
		sys.stdout.flush()
	except AttributeError:
		pass

	win32ui.DoWaitCursor(0)

def ImportFile():
	""" This code looks for the current window, and determines if it can be imported.  If not,
	it will prompt for a file name, and allow it to be imported. """
	try:
		pathName = GetActiveFileName()
	except KeyboardInterrupt:
		pathName = None

	if pathName is not None:
		if os.path.splitext(pathName)[1].lower() not in ('.py','.pyw','.pyx'):
			pathName = None

	if pathName is None:
		openFlags = win32con.OFN_OVERWRITEPROMPT|win32con.OFN_FILEMUSTEXIST
		dlg = win32ui.CreateFileDialog(1,None,None,openFlags, "Python Scripts (*.py;*.pyw)|*.py;*.pyw;*.pyx||")
		dlg.SetOFNTitle("Import Script")
		if dlg.DoModal()!=win32con.IDOK:
			return 0

		pathName = dlg.GetPathName()
		
	# If already imported, dont look for package
	path, modName = os.path.split(pathName)
	modName, modExt = os.path.splitext(modName)
	newPath = None
	# note that some packages (*cough* email *cough*) use "lazy importers"
	# meaning sys.modules can change as a side-effect of looking at
	# module.__file__ - so we must take a copy (ie, items() in py2k,
	# list(items()) in py3k)
	for key, mod in sys.modules.items():
		if hasattr(mod, '__file__'):
			fname = mod.__file__
			base, ext = os.path.splitext(fname)
			if ext.lower() in ['.pyo', '.pyc']:
				ext = '.py'
			fname = base + ext
			if win32ui.ComparePath(fname, pathName):
				modName = key
				break
	else: # for not broken
		modName, newPath = GetPackageModuleName(pathName)
		if newPath: sys.path.append(newPath)

	if modName in sys.modules:
		bNeedReload = 1
		what = "reload"
	else:
		what = "import"
		bNeedReload = 0
	
	win32ui.SetStatusText(what.capitalize()+'ing module...',1)
	win32ui.DoWaitCursor(1)
#	win32ui.GetMainFrame().BeginWaitCursor()

	try:
		# always do an import, as it is cheap if it's already loaded.  This ensures
		# it is in our name space.
		codeObj = compile('import '+modName,'<auto import>','exec')
	except SyntaxError:
		win32ui.SetStatusText('Invalid filename for import: "' +modName+'"')
		return
	try:
		exec codeObj in __main__.__dict__
		mod = sys.modules.get(modName)
		if bNeedReload:
			try:
				## The interpreter sees this import as a local assignment, so Python 2.x throws
				##	UnboundLocalError: local variable 'reload' referenced before assignment
				## when you try to use reload after this fails
				from imp import reload as my_reload # py3k
			except ImportError:
				my_reload = reload # reload a builtin in py2k
			mod = my_reload(sys.modules[modName])
		win32ui.SetStatusText('Successfully ' + what + "ed module '"+modName+"': %s" % getattr(mod,'__file__',"<unkown file>"))
	except:
		_HandlePythonFailure(what)
	win32ui.DoWaitCursor(0)

def CheckFile():
	""" This code looks for the current window, and gets Python to check it
	without actually executing any code (ie, by compiling only)
	"""
	try:
		pathName = GetActiveFileName()
	except KeyboardInterrupt:
		return

	what = "check"	
	win32ui.SetStatusText(what.capitalize()+'ing module...',1)
	win32ui.DoWaitCursor(1)
	try:
		f = open(pathName)
	except IOError, details:
		print "Cant open file '%s' - %s" % (pathName, details)
		return
	try:
		code = f.read() + "\n"
	finally:
		f.close()
	try:
		codeObj = compile(code, pathName,'exec')
		if RunTabNanny(pathName):
			win32ui.SetStatusText("Python and the TabNanny successfully checked the file '"+os.path.basename(pathName)+"'")
	except SyntaxError:
		_HandlePythonFailure(what, pathName)
	except:
		traceback.print_exc()
		_HandlePythonFailure(what)
	win32ui.DoWaitCursor(0)

def RunTabNanny(filename):
	import cStringIO as io
	tabnanny = FindTabNanny()
	if tabnanny is None:
		win32ui.MessageBox("The TabNanny is not around, so the children can run amok!" )
		return
		
	# Capture the tab-nanny output
	newout = io.StringIO()
	old_out = sys.stderr, sys.stdout
	sys.stderr = sys.stdout = newout
	try:
		tabnanny.check(filename)
	finally:
		# Restore output
		sys.stderr, sys.stdout = old_out
	data = newout.getvalue()
	if data:
		try:
			lineno = data.split()[1]
			lineno = int(lineno)
			_JumpToPosition(filename, lineno)
			try: # Try and display whitespace
				GetActiveEditControl().SCISetViewWS(1)
			except:
				pass
			win32ui.SetStatusText("The TabNanny found trouble at line %d" % lineno)
		except (IndexError, TypeError, ValueError):
			print "The tab nanny complained, but I cant see where!"
			print data
		return 0
	return 1

def _JumpToPosition(fileName, lineno, col = 1):
	JumpToDocument(fileName, lineno, col)

def JumpToDocument(fileName, lineno=0, col = 1, nChars = 0, bScrollToTop = 0):
	# Jump to the position in a file.
	# If lineno is <= 0, dont move the position - just open/restore.
	# if nChars > 0, select that many characters.
	# if bScrollToTop, the specified line will be moved to the top of the window
	#  (eg, bScrollToTop should be false when jumping to an error line to retain the
	#  context, but true when jumping to a method defn, where we want the full body.
	# Return the view which is editing the file, or None on error.
	doc = win32ui.GetApp().OpenDocumentFile(fileName)
	if doc is None: return None
	frame = doc.GetFirstView().GetParentFrame()
	try:
		view = frame.GetEditorView()
		if frame.GetActiveView() != view:
			frame.SetActiveView(view)
		frame.AutoRestore()
	except AttributeError: # Not an editor frame??
		view = doc.GetFirstView()
	if lineno > 0:
		charNo = view.LineIndex(lineno-1)
		start = charNo + col - 1
		size = view.GetTextLength()
		try:
			view.EnsureCharsVisible(charNo)
		except AttributeError:
			print "Doesnt appear to be one of our views?"
		view.SetSel(min(start, size), min(start + nChars, size))
	if bScrollToTop:
		curTop = view.GetFirstVisibleLine()
		nScroll = (lineno-1) - curTop
		view.LineScroll(nScroll, 0)
	view.SetFocus()
	return view

def _HandlePythonFailure(what, syntaxErrorPathName = None):
	typ, details, tb = sys.exc_info()
	if isinstance(details, SyntaxError):
		try:
			msg, (fileName, line, col, text) = details
			if (not fileName or fileName =="<string>") and syntaxErrorPathName:
				fileName = syntaxErrorPathName
			_JumpToPosition(fileName, line, col)
		except (TypeError, ValueError):
			msg = str(details)
		win32ui.SetStatusText('Failed to ' + what + ' - syntax error - %s' % msg)
	else:	
		traceback.print_exc()
		win32ui.SetStatusText('Failed to ' + what + ' - ' + str(details) )
	tb = None # Clean up a cycle.

# Find the Python TabNanny in either the standard library or the Python Tools/Scripts directory.
def FindTabNanny():
	try:
		return __import__("tabnanny")
	except ImportError:
		pass
	# OK - not in the standard library - go looking.
	filename = "tabnanny.py"
	try:
		path = win32api.RegQueryValue(win32con.HKEY_LOCAL_MACHINE, "SOFTWARE\\Python\\PythonCore\\%s\\InstallPath" % (sys.winver))
	except win32api.error:
		print "WARNING - The Python registry does not have an 'InstallPath' setting"
		print "          The file '%s' can not be located" % (filename)
		return None
	fname = os.path.join(path, "Tools\\Scripts\\%s" % filename)
	try:
		os.stat(fname)
	except os.error:
		print "WARNING - The file '%s' can not be located in path '%s'" % (filename, path)
		return None

	tabnannyhome, tabnannybase = os.path.split(fname)
	tabnannybase = os.path.splitext(tabnannybase)[0]
	# Put tab nanny at the top of the path.
	sys.path.insert(0, tabnannyhome)
	try:
		return __import__(tabnannybase)
	finally:
		# remove the tab-nanny from the path
		del sys.path[0]
		
def LocatePythonFile( fileName, bBrowseIfDir = 1 ):
	" Given a file name, return a fully qualified file name, or None "
	# first look for the exact file as specified
	if not os.path.isfile(fileName):
		# Go looking!
		baseName = fileName
		for path in sys.path:
			fileName = os.path.abspath(os.path.join(path, baseName))
			if os.path.isdir(fileName):
				if bBrowseIfDir:
					d=win32ui.CreateFileDialog(1, "*.py", None, 0, "Python Files (*.py)|*.py|All files|*.*")
					d.SetOFNInitialDir(fileName)
					rc=d.DoModal()
					if rc==win32con.IDOK:
						fileName = d.GetPathName()
						break
					else:
						return None
			else:
				fileName = fileName + ".py"
				if os.path.isfile(fileName):
					break # Found it!

		else:	# for not broken out of
			return None
	return win32ui.FullPath(fileName)
