import W
import Wkeys
import struct
import string
import types
import re
from Carbon import Qd, Icn, Fm, QuickDraw
from Carbon.QuickDraw import hilitetransfermode


nullid = '\0\0'
closedid = struct.pack('h', 468)
openid = struct.pack('h', 469)
closedsolidid = struct.pack('h', 470)
opensolidid = struct.pack('h', 471)

arrows = (nullid, closedid, openid, closedsolidid, opensolidid)

has_ctlcharsRE = re.compile(r'[\000-\037\177-\377]')
def ctlcharsREsearch(str):
	if has_ctlcharsRE.search(str) is None:
		return -1
	return 1
	
def double_repr(key, value, truncvalue = 0, 
			type = type, StringType = types.StringType,
			has_ctlchars = ctlcharsREsearch, _repr = repr, str = str):
	if type(key) == StringType and has_ctlchars(key) < 0:
		key = str(key)
	else:
		key = _repr(key)
	if key == '__builtins__':
		value = "<" + type(value).__name__ + " '__builtin__'>"
	elif key == '__return__':
		# bleh, when returning from a class codeblock we get infinite recursion in repr. 
		# Use safe repr instead.
		import repr
		value = repr.repr(value)
	else:
		try:
			value = _repr(value)
			'' + value	# test to see if it is a string, in case a __repr__ method is buggy
		except:
			value = '\xa5\xa5\xa5 exception in repr()'
	if truncvalue:
		return key + '\t' + value[:255]
	return key + '\t' + value


def truncString(s, maxwid):
	if maxwid < 1:
		return 1, ""
	strlen = len(s)
	strwid = Qd.TextWidth(s, 0, strlen);
	if strwid <= maxwid:
		return 0, s
	
	Qd.TextFace(QuickDraw.condense)
	strwid = Qd.TextWidth(s, 0, strlen)
	ellipsis = Qd.StringWidth('\xc9')
	
	if strwid <= maxwid:
		Qd.TextFace(0)
		return 1, s
	if strwid < 1:
		Qd.TextFace(0)
		return 1, ""
	
	mid = int(strlen * maxwid / strwid)
	while 1:
		if mid <= 0:
			mid = 0
			break
		strwid = Qd.TextWidth(s, 0, mid) + ellipsis
		strwid2 = Qd.TextWidth(s, 0, mid + 1) + ellipsis
		if strwid <= maxwid and maxwid <= strwid2:
			if maxwid == strwid2:
				mid += 1
			break
		if strwid > maxwid:
			mid -= 1
			if mid <= 0:
				mid = 0
				break
		elif strwid2 < maxwid:
			mid += 1
	Qd.TextFace(0)
	return 1, s[:mid] + '\xc9'


def drawTextCell(text, cellRect, ascent, theList):
	l, t, r, b = cellRect
	cellwidth = r - l
	Qd.MoveTo(l + 2, t + ascent)
	condense, text = truncString(text, cellwidth - 3)
	if condense:
		Qd.TextFace(QuickDraw.condense)
	Qd.DrawText(text, 0, len(text))
	Qd.TextFace(0)


PICTWIDTH = 16


class BrowserWidget(W.CustomList):
	
	def __init__(self, possize, object = None, col = 100, closechildren = 0):
		W.List.__init__(self, possize, callback = self.listhit)
		self.object = (None,)
		self.indent = 16
		self.lastmaxindent = 0
		self.closechildren = closechildren
		self.children = []
		self.mincol = 64
		self.setcolumn(col)
		self.bind('return', self.openselection)
		self.bind('enter', self.openselection)
		if object is not None:
			self.set(object)
	
	def set(self, object):
		if self.object[0] is not object:
			self.object = object,
			self[:] = self.unpack(object, 0)
		elif self._parentwindow is not None and self._parentwindow.wid:
			self.update()
	
	def unpack(self, object, indent):
		return unpack_object(object, indent)
	
	def update(self):
		# for now...
		W.SetCursor('watch')
		self.setdrawingmode(0)
		sel = self.getselectedobjects()
		fold = self.getunfoldedobjects()
		topcell = self.gettopcell()
		self[:] = self.unpack(self.object[0], 0)
		self.unfoldobjects(fold)
		self.setselectedobjects(sel)
		self.settopcell(topcell)
		self.setdrawingmode(1)
	
	def setcolumn(self, col):
		self.col = col
		self.colstr = struct.pack('h', col)
		if self._list:
			sel = self.getselection()
			self.setitems(self.items)
			self.setselection(sel)
	
	def key(self, char, event):
		if char in (Wkeys.leftarrowkey, Wkeys.rightarrowkey):
			sel = self.getselection()
			sel.reverse()
			self.setdrawingmode(0)
			for index in sel:
				self.fold(index, char == Wkeys.rightarrowkey)
			self.setdrawingmode(1)
		else:
			W.List.key(self, char, event)
	
	def rollover(self, (x, y), onoff):
		if onoff:
			if self.incolumn((x, y)):
				W.SetCursor('hmover')
			else:
				W.SetCursor('arrow')
	
	def inarrow(self, (x, y)):
		cl, ct, cr, cb = self._list.LRect((0, 0))
		l, t, r, b = self._bounds
		if (x - cl) < 16:
			cellheight = cb - ct
			index = (y - ct) / cellheight
			if index < len(self.items):
				return 1, index
		return None, None
	
	def incolumn(self, (x, y)):
		l, t, r, b = self._list.LRect((0, 0))
		abscol = l + self.col
		return abs(abscol - x) < 3
	
	def trackcolumn(self, (x, y)):
		from Carbon import Qd, QuickDraw, Evt
		self.SetPort()
		l, t, r, b = self._bounds
		bounds = l, t, r, b = l + 1, t + 1, r - 16, b - 1
		abscol = l + self.col
		mincol = l + self.mincol
		maxcol = r - 10
		diff = abscol - x
		Qd.PenPat('\000\377\000\377\000\377\000\377')
		Qd.PenMode(QuickDraw.srcXor)
		rect = abscol - 1, t, abscol, b
		Qd.PaintRect(rect)
		lastpoint = (x, y)
		newcol = -1
		#W.SetCursor('fist')
		while Evt.Button():
			Evt.WaitNextEvent(0, 1, None)  # needed for OSX
			(x, y) = Evt.GetMouse()
			if (x, y) <> lastpoint:
				newcol = x + diff
				newcol = max(newcol, mincol)
				newcol = min(newcol, maxcol)
				Qd.PaintRect(rect)
				rect = newcol - 1, t, newcol, b
				Qd.PaintRect(rect)
				lastpoint = (x, y)
		Qd.PaintRect(rect)
		Qd.PenPat(Qd.GetQDGlobalsBlack())
		Qd.PenNormal()
		if newcol > 0 and newcol <> abscol:
			self.setcolumn(newcol - l)
	
	def click(self, point, modifiers):
		if point == (-1, -1):	# gross.
			W.List.click(self, point ,modifiers)
			return
		hit, index = self.inarrow(point)
		if hit:
			(key, value, arrow, indent) = self.items[index]
			self.fold(index, arrow == 1)
		elif self.incolumn(point):
			self.trackcolumn(point)
		else:
			W.List.click(self, point, modifiers)
	
	# for W.List.key
	def findmatch(self, tag):
		lower = string.lower
		items = self.items
		taglen = len(tag)
		match = '\377' * 100
		match_i = -1
		for i in range(len(items)):
			item = lower(str(items[i][0]))
			if tag <= item < match:
				match = item
				match_i = i
		if match_i >= 0:
			return match_i
		else:
			return len(items) - 1
	
	def close(self):
		if self.closechildren:
			for window in self.children:
				window.close()
		self.children = []
		W.List.close(self)
	
	def fold(self, index, onoff):
		(key, value, arrow, indent) = self.items[index]
		if arrow == 0 or (onoff and arrow == 2) or (not onoff and arrow == 1):
			return
		W.SetCursor('watch')
		topcell = self.gettopcell()
		if onoff:
			self[index] = (key, value, 4, indent)
			self.setdrawingmode(0)
			self[index+1:index+1] = self.unpack(value, indent + 1)
			self[index] = (key, value, 2, indent)
		else:
			self[index] = (key, value, 3, indent)
			self.setdrawingmode(0)
			count = 0
			for i in range(index + 1, len(self.items)):
				(dummy, dummy, dummy, subindent) = self.items[i]
				if subindent <= indent:
					break
				count = count + 1
			self[index+1:index+1+count] = []
			self[index] = (key, value, 1, indent)
		maxindent = self.getmaxindent()
		if maxindent <> self.lastmaxindent:
			newabsindent = self.col + (maxindent - self.lastmaxindent) * self.indent
			if newabsindent >= self.mincol:
				self.setcolumn(newabsindent)
			self.lastmaxindent = maxindent
		self.settopcell(topcell)
		self.setdrawingmode(1)
	
	def unfoldobjects(self, objects):
		for obj in objects:
			try:
				index = self.items.index(obj)
			except ValueError:
				pass
			else:
				self.fold(index, 1)
	
	def getunfoldedobjects(self):
		curindent = 0
		objects = []
		for index in range(len(self.items)):
			(key, value, arrow, indent) = self.items[index]
			if indent > curindent:
				(k, v, a, i) = self.items[index - 1]
				objects.append((k, v, 1, i))
				curindent = indent
			elif indent < curindent:
				curindent = indent
		return objects
	
	def listhit(self, isdbl):
		if isdbl:
			self.openselection()
	
	def openselection(self):
		import os
		sel = self.getselection()
		for index in sel:
			(key, value, arrow, indent) = self[index]
			if arrow:
				self.children.append(Browser(value))
			elif type(value) == types.StringType and '\0' not in value:
				editor = self._parentwindow.parent.getscript(value)
				if editor:
					editor.select()
					return
				elif os.path.exists(value) and os.path.isfile(value):
					import macfs
					fss = macfs.FSSpec(value)
					if fss.GetCreatorType()[1] == 'TEXT':
						W.getapplication().openscript(value)
	
	def itemrepr(self, (key, value, arrow, indent), str = str, double_repr = double_repr, 
			arrows = arrows, pack = struct.pack):
		arrow = arrows[arrow]
		return arrow + pack('h', self.indent * indent) + self.colstr + \
				double_repr(key, value, 1)
	
	def getmaxindent(self, max = max):
		maxindent = 0
		for item in self.items:
			maxindent = max(maxindent, item[3])
		return maxindent
	
	def domenu_copy(self, *args):
		sel = self.getselectedobjects()
		selitems = []
		for key, value, dummy, dummy in sel:
			selitems.append(double_repr(key, value))
		text = string.join(selitems, '\r')
		if text:
			from Carbon import Scrap
			if hasattr(Scrap, 'PutScrap'):
				Scrap.ZeroScrap()
				Scrap.PutScrap('TEXT', text)
			else:
				Scrap.ClearCurrentScrap()
				sc = Scrap.GetCurrentScrap()
				sc.PutScrapFlavor('TEXT', 0, text)

	def listDefDraw(self, selected, cellRect, theCell, 
			dataOffset, dataLen, theList):
		self.myDrawCell(0, selected, cellRect, theCell, 
			dataOffset, dataLen, theList)
	
	def listDefHighlight(self, selected, cellRect, theCell, 
			dataOffset, dataLen, theList):
		self.myDrawCell(1, selected, cellRect, theCell, 
			dataOffset, dataLen, theList)
	
	def myDrawCell(self, onlyHilite, selected, cellRect, theCell, 
			dataOffset, dataLen, theList):
		savedPort = Qd.GetPort()
		Qd.SetPort(theList.GetListPort())
		savedClip = Qd.NewRgn()
		Qd.GetClip(savedClip)
		Qd.ClipRect(cellRect)
		savedPenState = Qd.GetPenState()
		Qd.PenNormal()
		
		l, t, r, b = cellRect
		
		if not onlyHilite:
			Qd.EraseRect(cellRect)
			
			ascent, descent, leading, size, hm = Fm.FontMetrics()
			linefeed = ascent + descent + leading
			
			if dataLen >= 6:
				data = theList.LGetCell(dataLen, theCell)
				iconId, indent, tab = struct.unpack("hhh", data[:6])
				try:
					key, value = data[6:].split("\t", 1)
				except ValueError:
					# bogus data, at least don't crash.
					indent = 0
					tab = 0
					iconId = 0
					key = ""
					value = data[6:]
				
				if iconId:
					try:
						theIcon = Icn.GetCIcon(iconId)
					except Icn.Error:
						pass
					else:
						rect = (0, 0, 16, 16)
						rect = Qd.OffsetRect(rect, l, t)
						rect = Qd.OffsetRect(rect, 0, (theList.cellSize[1] - (rect[3] - rect[1])) / 2)
						Icn.PlotCIcon(rect, theIcon)
				
				if len(key) >= 0:
					cl, ct, cr, cb = cellRect
					vl, vt, vr, vb = self._viewbounds
					cl = vl + PICTWIDTH + indent
					cr = vl + tab
					if cr > vr:
						cr = vr
					if cl < cr:
						drawTextCell(key, (cl, ct, cr, cb), ascent, theList)
					cl = vl + tab
					cr = vr
					if cl < cr:
						drawTextCell(value, (cl, ct, cr, cb), ascent, theList)
			#elif dataLen != 0:
			#	drawTextCell("???", 3, cellRect, ascent, theList)
			else:
				return  # we have bogus data
			
			# draw nice dotted line
			l, t, r, b = cellRect
			l = self._viewbounds[0] + tab
			r = l + 1;
			if not (theList.cellSize[1] & 0x01) or (t & 0x01):
				myPat = "\xff\x00\xff\x00\xff\x00\xff\x00"
			else:
				myPat = "\x00\xff\x00\xff\x00\xff\x00\xff"
			Qd.PenPat(myPat)
			Qd.PenMode(QuickDraw.srcCopy)
			Qd.PaintRect((l, t, r, b))
			Qd.PenNormal()
		
		if selected or onlyHilite:
			l, t, r, b = cellRect
			l = self._viewbounds[0] + PICTWIDTH
			r = self._viewbounds[2]
			Qd.PenMode(hilitetransfermode)
			Qd.PaintRect((l, t, r, b))
		
		# restore graphics environment
		Qd.SetPort(savedPort)
		Qd.SetClip(savedClip)
		Qd.DisposeRgn(savedClip)
		Qd.SetPenState(savedPenState)



class Browser:
	
	def __init__(self, object = None, title = None, closechildren = 0):
		if hasattr(object, '__name__'):
			name = object.__name__
		else:
			name = ''
		if title is None:
			title = 'Object browser'
			if name:
				title = title + ': ' + name
		self.w = w = W.Window((300, 400), title, minsize = (100, 100))
		w.info = W.TextBox((18, 8, -70, 15))
		w.updatebutton = W.BevelButton((-64, 4, 50, 16), 'Update', self.update)
		w.browser = BrowserWidget((-1, 24, 1, -14), None)
		w.bind('cmdu', w.updatebutton.push)
		w.open()
		self.set(object, name)
	
	def close(self):
		if self.w.wid:
			self.w.close()
	
	def set(self, object, name = ''):
		W.SetCursor('watch')
		tp = type(object).__name__
		try:
			length = len(object)
		except:
			length = -1
		if not name and hasattr(object, '__name__'):
			name = object.__name__
		if name:
			info = name + ': ' + tp
		else:
			info = tp
		if length >= 0:
			if length == 1:
				info = info + ' (%d element)' % length
			else:
				info = info + ' (%d elements)' % length
		self.w.info.set(info)
		self.w.browser.set(object)
	
	def update(self):
		self.w.browser.update()


SIMPLE_TYPES = (
	type(None),
	int,
	long,
	float,
	complex,
	str,
	unicode,
)

def get_ivars(obj):
	"""Return a list the names of all (potential) instance variables."""
	# __mro__ recipe from Guido
	slots = {}
	# old-style C objects
	if hasattr(obj, "__members__"):
		for name in obj.__members__:
			slots[name] = None
	if hasattr(obj, "__methods__"):
		for name in obj.__methods__:
			slots[name] = None
	# generic type
	if hasattr(obj, "__dict__"):
		slots.update(obj.__dict__)
	cls = type(obj)
	if hasattr(cls, "__mro__"):
		# new-style class, use descriptors
		for base in cls.__mro__:
			for name, value in base.__dict__.items():
				# XXX using callable() is a heuristic which isn't 100%
				# foolproof.
				if hasattr(value, "__get__") and not callable(value):
					slots[name] = None
	if "__dict__" in slots:
		del slots["__dict__"]
	slots = slots.keys()
	slots.sort()
	return slots

def unpack_object(object, indent = 0):
	tp = type(object)
	if isinstance(object, SIMPLE_TYPES) and object is not None:
		raise TypeError, "can't browse simple type: %s" % tp.__name__
	elif isinstance(object, dict):
		return unpack_dict(object, indent)
	elif isinstance(object, (tuple, list)):
		return unpack_sequence(object, indent)
	elif isinstance(object, types.ModuleType):
		return unpack_dict(object.__dict__, indent)
	else:
		return unpack_other(object, indent)

def unpack_sequence(seq, indent = 0):
	return [(i, v, not isinstance(v, SIMPLE_TYPES), indent)
	         for i, v in enumerate(seq)]

def unpack_dict(dict, indent = 0):
	items = dict.items()
	return pack_items(items, indent)

def unpack_instance(inst, indent = 0):
	if hasattr(inst, '__pybrowse_unpack__'):
		return unpack_object(inst.__pybrowse_unpack__(), indent)
	else:
		items = [('__class__', inst.__class__)] + inst.__dict__.items()
		return pack_items(items, indent)

def unpack_class(clss, indent = 0):
	items = [('__bases__', clss.__bases__), ('__name__', clss.__name__)] + clss.__dict__.items()
	return pack_items(items, indent)

def unpack_other(object, indent = 0):
	attrs = get_ivars(object)
	items = []
	for attr in attrs:
		try:
			value = getattr(object, attr)
		except:
			pass
		else:
			items.append((attr, value))
	return pack_items(items, indent)

def pack_items(items, indent = 0):
	items = [(k, v, not isinstance(v, SIMPLE_TYPES), indent)
	         for k, v in items]
	return tuple_caselesssort(items)

def caselesssort(alist):
	"""Return a sorted copy of a list. If there are only strings in the list, 
	it will not consider case"""
	
	try:
		# turn ['FOO',  'aaBc', 'ABcD'] into [('foo', 'FOO'), ('aabc', 'aaBc'), ('abcd', 'ABcD')], if possible
		tupledlist = map(lambda item, lower = string.lower: (lower(item), item), alist)
	except TypeError:
		# at least one element in alist is not a string, proceed the normal way...
		alist = alist[:]
		alist.sort()
		return alist
	else:
		tupledlist.sort()
		# turn [('aabc', 'aaBc'), ('abcd', 'ABcD'), ('foo', 'FOO')] into ['aaBc', 'ABcD', 'FOO']
		return map(lambda x: x[1], tupledlist)

def tuple_caselesssort(items):
	try:
		tupledlist = map(lambda tuple, lower = string.lower: (lower(tuple[0]), tuple), items)
	except (AttributeError, TypeError):
		items = items[:]
		items.sort()
		return items
	else:
		tupledlist.sort()
		return map(lambda (low, tuple): tuple, tupledlist)

