# Code that allows Pythonwin to pretend it is IDLE
# (at least as far as most IDLE extensions are concerned)

import string
import win32api
import win32ui
import win32con
import sys

from pywin.mfc.dialog import GetSimpleInput
from pywin import default_scintilla_encoding

wordchars = string.ascii_uppercase + string.ascii_lowercase + string.digits

class TextError(Exception): # When a TclError would normally be raised.
	pass


class EmptyRange(Exception): # Internally raised.
	pass

def GetIDLEModule(module):
	try:
		# First get it from Pythonwin it is exists.
		modname = "pywin.idle." + module
		__import__(modname)
	except ImportError as details:
		msg = "The IDLE extension '%s' can not be located.\r\n\r\n" \
			  "Please correct the installation and restart the" \
			  " application.\r\n\r\n%s" % (module, details)
		win32ui.MessageBox(msg)
		return None
	mod=sys.modules[modname]
	mod.TclError = TextError # A hack that can go soon!
	return mod

# A class that is injected into the IDLE auto-indent extension.
# It allows for decent performance when opening a new file,
# as auto-indent uses the tokenizer module to determine indents.
# The default AutoIndent readline method works OK, but it goes through
# this layer of Tk index indirection for every single line.  For large files
# without indents (and even small files with indents :-) it was pretty slow!
def fast_readline(self):
	if self.finished:
		val = ""
	else:
		if "_scint_lines" not in self.__dict__:
			# XXX - note - assumes this is only called once the file is loaded!
			self._scint_lines = self.text.edit.GetTextRange().split("\n")
		sl = self._scint_lines
		i = self.i = self.i + 1
		if i >= len(sl):
			val = ""
		else:
			val = sl[i]+"\n"
	return val.encode(default_scintilla_encoding)

try:
	GetIDLEModule("AutoIndent").IndentSearcher.readline = fast_readline
except AttributeError: # GetIDLEModule may return None
	pass

# A class that attempts to emulate an IDLE editor window.
# Construct with a Pythonwin view.
class IDLEEditorWindow:
	def __init__(self, edit):
		self.edit = edit
		self.text = TkText(edit)
		self.extensions = {}
		self.extension_menus = {}

	def close(self):
		self.edit = self.text = None
		self.extension_menus = None
		try:
			for ext in self.extensions.values():
				closer = getattr(ext, "close", None)
				if closer is not None:
					closer()
		finally:
			self.extensions = {}

	def IDLEExtension(self, extension):
		ext = self.extensions.get(extension)
		if ext is not None: return ext
		mod = GetIDLEModule(extension)
		if mod is None: return None
		klass = getattr(mod, extension)
		ext = self.extensions[extension] = klass(self)
		# Find and bind all the events defined in the extension.
		events = [item for item in dir(klass) if item[-6:]=="_event"]
		for event in events:
			name = "<<%s>>" % (event[:-6].replace("_", "-"), )
			self.edit.bindings.bind(name, getattr(ext, event))
		return ext

	def GetMenuItems(self, menu_name):
		# Get all menu items for the menu name (eg, "edit")
		bindings = self.edit.bindings
		ret = []
		for ext in self.extensions.values():
			menudefs = getattr(ext, "menudefs", [])
			for name, items in menudefs:
				if name == menu_name:
					for text, event in [item for item in items if item is not None]:
						text = text.replace("&", "&&")
						text = text.replace("_", "&")
						ret.append((text, event))
		return ret

	######################################################################
	# The IDLE "Virtual UI" methods that are exposed to the IDLE extensions.
	#
	def askinteger(self, caption, prompt, parent=None, initialvalue=0, minvalue=None, maxvalue=None):
		while 1:
			rc = GetSimpleInput(prompt, str(initialvalue), caption)
			if rc is None: return 0 # Correct "cancel" semantics?
			err = None
			try:
				rc = int(rc)
			except ValueError:
				err = "Please enter an integer"
			if not err and minvalue is not None and rc < minvalue:
				err = "Please enter an integer greater then or equal to %s" % (minvalue,)
			if not err and maxvalue is not None and rc > maxvalue:
				err = "Please enter an integer less then or equal to %s" % (maxvalue,)
			if err:
				win32ui.MessageBox(err, caption, win32con.MB_OK)
				continue
			return rc
	def askyesno(self, caption, prompt, parent=None):
		return win32ui.MessageBox(prompt, caption, win32con.MB_YESNO)==win32con.IDYES

	######################################################################
	# The IDLE "Virtual Text Widget" methods that are exposed to the IDLE extensions.
	#

	# Is character at text_index in a Python string?  Return 0 for
	# "guaranteed no", true for anything else.
	def is_char_in_string(self, text_index):
		# A helper for the code analyser - we need internal knowledge of
		# the colorizer to get this information
		# This assumes the colorizer has got to this point!
		text_index = self.text._getoffset(text_index)
		c = self.text.edit._GetColorizer()
		if c and c.GetStringStyle(text_index) is None:
			return 0
		return 1

	# If a selection is defined in the text widget, return
	# (start, end) as Tkinter text indices, otherwise return
	# (None, None)
	def get_selection_indices(self):
		try:
			first = self.text.index("sel.first")
			last = self.text.index("sel.last")
			return first, last
		except TextError:
			return None, None

	def set_tabwidth(self, width ):
		self.edit.SCISetTabWidth(width)

	def get_tabwidth(self):
		return self.edit.GetTabWidth()

# A class providing the generic "Call Tips" interface
class CallTips:
	def __init__(self, edit):
		self.edit = edit
	def showtip(self, tip_text):
		self.edit.SCICallTipShow(tip_text)
	def hidetip(self):
		self.edit.SCICallTipCancel()

########################################
#
# Helpers for the TkText emulation.
def TkOffsetToIndex(offset, edit):
	lineoff = 0
	# May be 1 > actual end if we pretended there was a trailing '\n'
	offset = min(offset, edit.GetTextLength())
	line = edit.LineFromChar(offset)
	lineIndex = edit.LineIndex(line)
	return "%d.%d" % (line+1, offset-lineIndex)

def _NextTok(str, pos):
	# Returns (token, endPos)
	end = len(str)
	if pos>=end: return None, 0
	while pos < end and str[pos] in string.whitespace:
		pos = pos + 1
	# Special case for +-
	if str[pos] in '+-':
		return str[pos],pos+1
	# Digits also a special case.
	endPos = pos
	while endPos < end and str[endPos] in string.digits+".":
		endPos = endPos + 1
	if pos!=endPos: return str[pos:endPos], endPos
	endPos = pos
	while endPos < end and str[endPos] not in string.whitespace + string.digits + "+-":
		endPos = endPos + 1
	if pos!=endPos: return str[pos:endPos], endPos
	return None, 0

def TkIndexToOffset(bm, edit, marks):
	base, nextTokPos = _NextTok(bm, 0)
	if base is None: raise ValueError("Empty bookmark ID!")
	if base.find(".")>0:
		try:
			line, col = base.split(".", 2)
			if col=="first" or col=="last":
				# Tag name
				if line != "sel": raise ValueError("Tags arent here!")
				sel = edit.GetSel()
				if sel[0]==sel[1]:
					raise EmptyRange
				if col=="first":
					pos = sel[0]
				else:
					pos = sel[1]
			else:
				# Lines are 1 based for tkinter
				line = int(line)-1
				if line > edit.GetLineCount():
					pos = edit.GetTextLength()+1
				else:
					pos = edit.LineIndex(line)
					if pos==-1: pos = edit.GetTextLength()
					pos = pos + int(col)
		except (ValueError, IndexError):
			raise ValueError("Unexpected literal in '%s'" % base) 
	elif base == 'insert':
		pos = edit.GetSel()[0]
	elif base=='end':
		pos = edit.GetTextLength()
		# Pretend there is a trailing '\n' if necessary
		if pos and edit.SCIGetCharAt(pos-1) != "\n":
			pos = pos+1
	else:
		try:
			pos = marks[base]
		except KeyError:
			raise ValueError("Unsupported base offset or undefined mark '%s'" % base)

	while 1:
		word, nextTokPos = _NextTok(bm, nextTokPos)
		if word is None: break
		if word in ['+','-']:
			num, nextTokPos = _NextTok(bm, nextTokPos)
			if num is None: raise ValueError("+/- operator needs 2 args")
			what, nextTokPos = _NextTok(bm, nextTokPos)
			if what is None: raise ValueError("+/- operator needs 2 args")
			if what[0] != "c": raise ValueError("+/- only supports chars")
			if word=='+':
				pos = pos + int(num)
			else:
				pos = pos - int(num)
		elif word=='wordstart':
			while pos > 0 and edit.SCIGetCharAt(pos-1) in wordchars:
				pos = pos - 1
		elif word=='wordend':
			end = edit.GetTextLength()
			while pos < end and edit.SCIGetCharAt(pos) in wordchars:
				pos = pos + 1
		elif word=='linestart':
			while pos > 0 and edit.SCIGetCharAt(pos-1) not in '\n\r':
				pos = pos - 1
		elif word=='lineend':
			end = edit.GetTextLength()
			while pos < end and edit.SCIGetCharAt(pos) not in '\n\r':
				pos = pos + 1
		else:
			raise ValueError("Unsupported relative offset '%s'" % word)
	return max(pos, 0) # Tkinter is tollerant of -ve indexes - we aren't

# A class that resembles an IDLE (ie, a Tk) text widget.
# Construct with an edit object (eg, an editor view)
class TkText:
	def __init__(self, edit):
		self.calltips = None
		self.edit = edit
		self.marks = {}
##	def __getattr__(self, attr):
##		if attr=="tk": return self # So text.tk.call works.
##		if attr=="master": return None # ditto!
##		raise AttributeError, attr
##	def __getitem__(self, item):
##		if item=="tabs":
##			size = self.edit.GetTabWidth()
##			if size==8: return "" # Tk default
##			return size # correct semantics?
##		elif item=="font": # Used for measurements we dont need to do!
##			return "Dont know the font"
##		raise IndexError, "Invalid index '%s'" % item
	def make_calltip_window(self):
		if self.calltips is None:
			self.calltips = CallTips(self.edit)
		return self.calltips
	def _getoffset(self, index):
		return TkIndexToOffset(index, self.edit, self.marks)
	def _getindex(self, off):
		return TkOffsetToIndex(off, self.edit)
	def _fix_indexes(self, start, end):
		# first some magic to handle skipping over utf8 extended chars.
		while start > 0 and ord(self.edit.SCIGetCharAt(start)) & 0xC0 == 0x80:
			start -= 1
		while end < self.edit.GetTextLength() and ord(self.edit.SCIGetCharAt(end)) & 0xC0 == 0x80:
			end += 1
		# now handling fixing \r\n->\n disparities...
		if start>0 and self.edit.SCIGetCharAt(start)=='\n' and self.edit.SCIGetCharAt(start-1)=='\r':
			start = start - 1
		if end < self.edit.GetTextLength() and self.edit.SCIGetCharAt(end-1)=='\r' and self.edit.SCIGetCharAt(end)=='\n':
			end = end + 1
		return start, end
##	def get_tab_width(self):
##		return self.edit.GetTabWidth()
##	def call(self, *rest):
##		# Crap to support Tk measurement hacks for tab widths
##		if rest[0] != "font" or rest[1] != "measure":
##			raise ValueError, "Unsupport call type"
##		return len(rest[5])
##	def configure(self, **kw):
##		for name, val in kw.items():
##			if name=="tabs":
##				self.edit.SCISetTabWidth(int(val))
##			else:
##				raise ValueError, "Unsupported configuration item %s" % kw
	def bind(self, binding, handler):
		self.edit.bindings.bind(binding, handler)
	def get(self, start, end = None):
		try:
			start = self._getoffset(start)
			if end is None:
				end = start+1
			else:
				end = self._getoffset(end)
		except EmptyRange:
			return ""
		# Simple semantic checks to conform to the Tk text interface
		if end <= start: return ""
		max = self.edit.GetTextLength()
		checkEnd = 0
		if end > max:
			end = max
			checkEnd = 1
		start, end = self._fix_indexes(start, end)
		ret = self.edit.GetTextRange(start, end)
		# pretend a trailing '\n' exists if necessary.
		if checkEnd and (not ret or ret[-1] != '\n'): ret = ret + '\n'
		return ret.replace("\r", "")
	def index(self, spec):
		try:
			return self._getindex(self._getoffset(spec))
		except EmptyRange:
			return ""
	def insert(self, pos, text):
		try:
			pos = self._getoffset(pos)
		except EmptyRange:
			raise TextError("Empty range")
		self.edit.SetSel((pos, pos))
		# IDLE only deals with "\n" - we will be nicer

		bits = text.split('\n')
		self.edit.SCIAddText(bits[0])
		for bit in bits[1:]:
			self.edit.SCINewline()
			self.edit.SCIAddText(bit)

	def delete(self, start, end=None):
		try:
			start = self._getoffset(start)
			if end is not None: end = self._getoffset(end)
		except EmptyRange:
			raise TextError("Empty range")
		# If end is specified and == start, then we must delete nothing.
		if start==end: return
		# If end is not specified, delete one char
		if end is None:
			end = start+1
		else:
			# Tk says not to delete in this case, but our control would.
			if end<start: return
		if start==self.edit.GetTextLength(): return # Nothing to delete.
		old = self.edit.GetSel()[0] # Lose a selection
		# Hack for partial '\r\n' and UTF-8 char removal
		start, end = self._fix_indexes(start, end)
		self.edit.SetSel((start, end))
		self.edit.Clear()
		if old>=start and old<end:
			old=start
		elif old>=end:
			old = old - (end-start)
		self.edit.SetSel(old)

	def bell(self):
		win32api.MessageBeep()

	def see(self, pos):
		# Most commands we use in Scintilla actually force the selection
		# to be seen, making this unnecessary.
		pass

	def mark_set(self, name, pos):
		try:
			pos = self._getoffset(pos)
		except EmptyRange:
			raise TextError("Empty range '%s'" % pos)
		if name == "insert":
			self.edit.SetSel( pos )
		else:
			self.marks[name]=pos

	def tag_add(self, name, start, end):
		if name != "sel": raise ValueError("Only sel tag is supported")
		try:
			start = self._getoffset(start)
			end = self._getoffset(end)
		except EmptyRange:
			raise TextError("Empty range")
		self.edit.SetSel( start, end )

	def tag_remove(self, name, start, end):
		if name !="sel" or start != "1.0" or end != "end":
			raise ValueError("Cant remove this tag")
		# Turn the sel into a cursor
		self.edit.SetSel(self.edit.GetSel()[0])

	def compare(self, i1, op, i2):
		try:
			i1=self._getoffset(i1)
		except EmptyRange:
			i1 = ""
		try:
			i2=self._getoffset(i2)
		except EmptyRange:
			i2 = ""
		return eval("%d%s%d" % (i1,op,i2))

	def undo_block_start(self):
		self.edit.SCIBeginUndoAction()

	def undo_block_stop(self):
		self.edit.SCIEndUndoAction()

######################################################################
# 
# Test related code.
# 
######################################################################
def TestCheck(index, edit, expected=None):
	rc = TkIndexToOffset(index, edit, {})
	if rc != expected:
		print("ERROR: Index", index,", expected", expected, "but got", rc)

def TestGet(fr, to, t, expected):
	got = t.get(fr, to)
	if got != expected:
		print("ERROR: get(%s, %s) expected %s, but got %s" % (repr(fr), repr(to), repr(expected), repr(got)))

def test():
	import pywin.framework.editor
	d=pywin.framework.editor.editorTemplate.OpenDocumentFile(None)
	e=d.GetFirstView()
	t = TkText(e)
	e.SCIAddText("hi there how\nare you today\r\nI hope you are well")
	e.SetSel((4,4))

	skip = """
	TestCheck("insert", e, 4)
	TestCheck("insert wordstart", e, 3)
	TestCheck("insert wordend", e, 8)
	TestCheck("insert linestart", e, 0)
	TestCheck("insert lineend", e, 12)
	TestCheck("insert + 4 chars", e, 8)
	TestCheck("insert +4c", e, 8)
	TestCheck("insert - 2 chars", e, 2)
	TestCheck("insert -2c", e, 2)
	TestCheck("insert-2c", e, 2)
	TestCheck("insert-2 c", e, 2)
	TestCheck("insert- 2c", e, 2)
	TestCheck("1.1", e, 1)
	TestCheck("1.0", e, 0)
	TestCheck("2.0", e, 13)
	try:
		TestCheck("sel.first", e, 0)
		print "*** sel.first worked with an empty selection"
	except TextError:
		pass
	e.SetSel((4,5))
	TestCheck("sel.first- 2c", e, 2)
	TestCheck("sel.last- 2c", e, 3)
	"""
	# Check EOL semantics
	e.SetSel((4,4))
	TestGet("insert lineend", "insert lineend +1c", t, "\n")
	e.SetSel((20, 20))
	TestGet("insert lineend", "insert lineend +1c", t, "\n")
	e.SetSel((35, 35))
	TestGet("insert lineend", "insert lineend +1c", t, "\n")

class IDLEWrapper:
	def __init__(self, control):
		self.text = control

def IDLETest(extension):
	import sys, os
	modname = "pywin.idle." + extension
	__import__(modname)
	mod=sys.modules[modname]
	mod.TclError = TextError
	klass = getattr(mod, extension)

	# Create a new Scintilla Window.
	import pywin.framework.editor
	d=pywin.framework.editor.editorTemplate.OpenDocumentFile(None)
	v=d.GetFirstView()
	fname=os.path.splitext(__file__)[0] + ".py"
	v.SCIAddText(open(fname).read())
	d.SetModifiedFlag(0)
	r=klass( IDLEWrapper( TkText(v) ) )
	return r

if __name__=='__main__':
	test()