#!/usr/bin/env python
import StringIO
import sys
"""
prefer words,
accept characters.
If in doubt, stop parsing (or estimate).
"""
VERY_BIG = 999999999
wordWrapMarker = u"\u2010".encode("utf-8")
tabWidth = 8 # FIXME
# TODO scroll bar range estimation?
# TODO Silbentrennung
# TODO fix bug when clicking on the first char of a continued line (relayout)
# don't forget to call "start()", "addChunk()" (maybe multiple times) and "finish()"
class LogicalWordWrapper(object):
	def __init__(self, width, height, outputter, viewHeight = None):
		self.fOutputter = outputter # TODO remove
		self.setSize(width, height, viewHeight)
		self.fOffset = 0
		self.fTestY = -1
		self.fTestX = -1
	def setSize(self, width, height, viewHeight):
		self.fX = 0
		self.fY = 0
		self.fHeight = 0
		self.fWidth = 0
		self.fMaxWidth = width
		self.fMaxHeight = height
		self.fRunHeight = 0
		self.fBSilent = False
		self.fViewHeight = viewHeight
		self.fHyphen = wordWrapMarker
		self.fHyphenWidth = 1
		self.updateSilence()
	def calculateWordExtents(self, text, bYieldOnEach = False):
		height = 0
		width = 0
		word = StringIO.StringIO()
		for c in text.decode("utf-8"):
			word.write(c.encode("utf-8"))
			if c == "\n" or c == " " or c == "\t" or c == "\r" or bYieldOnEach:
				if c != "\n":
					width += 1 if c != "\t" else tabWidth
					height = max(height, 1)
				v = word.getvalue()
				word = StringIO.StringIO()
				yield v, width, height
				width = 0
				height = 0
			else:
				height = max(height, 1)
				width += 1
		v = word.getvalue()
		if v != "":
			yield v, width, height
	def calculateCharExtents(self, text):
		return(self.calculateWordExtents(text, True))
	def goToNextLine(self):
		if self.fRunHeight < 1: # make empty paragraphs take space.
			self.fRunHeight = 1
		assert(self.fRunHeight > 0)
		if self.fTestY != -1 and self.fY >= self.fTestY: # for mouse hit test
			raise OverflowError()
		self.fY += self.fRunHeight
		self.updateSilence()
		self.fX = 0
		#self.fHeight += self.fRunHeight
		#=if self.fHeight < self.fY: self.fHeight = self.fY
		self.fRunHeight = 0
		#self.fOutputter.write(("*" * self.fMaxWidth) + "\n")
	def wouldFitHereP(self, width, height, reservedWidth = 0):
		return(self.fX + reservedWidth + width <= self.fMaxWidth and self.fY + height <= self.fMaxHeight)
	def wouldMaybeFitNextLineP(self, width, height, reservedWidth = 0):
		return(0 + reservedWidth + width <= self.fMaxWidth and self.fY + height <= self.fMaxHeight)
	def increaseVerticalSpace(self, height):
		raise OverflowError("no space left")
	def addWithoutCheck(self, word, width, height):
		pass # override this
	def setHyphen(self, hyphen, width = 1):
		self.fHyphen = hyphen
		self.fHyphenWidth = width if hyphen else 0
	def printHyphen(self):
		if self.fHyphenWidth > 0 and self.wouldFitHereP(self.fHyphenWidth, self.fRunHeight, 0):
			self.fOutputter.write("\033[37m")
			self.addWithoutCheck(self.fHyphen, self.fHyphenWidth, self.fRunHeight)
			self.fOutputter.write("\033[m")
	def acceptCharacters(self, word):
		if word.endswith("\n"): # FIXME what is this?
			word = word[:-1]
			self.fOffset += 1
		#print("AC")
		# make room for and print the "dash".
		dashWidth = self.fHyphenWidth # FIXME
		if self.fX >= self.fMaxWidth: # this should not happen
			#assert(False) # debugging
			self.goToNextLine() # will die
			dashWidth = 0
		for c, width, height in self.calculateCharExtents(word):
			#print(c,width,height)
			if self.fTestY != -1 and self.fY >= self.fTestY and self.fX >= self.fTestX: # for mouse hit test
				raise OverflowError()
			if not self.wouldFitHereP(width, max(self.fRunHeight, height), dashWidth):
				if self.wouldMaybeFitNextLineP(width, max(self.fRunHeight, height), dashWidth):
					self.printHyphen()
					self.goToNextLine()
				if not self.wouldFitHereP(width, max(self.fRunHeight, height), dashWidth):
					if self.fY + height > self.fMaxHeight:
						self.increaseVerticalSpace(max(self.fRunHeight, height))
						assert(self.wouldFitHereP(width, max(self.fRunHeight, height), dashWidth))
					else:
						raise OverflowError("impossible to fit")
			self.addWithoutCheck(c, width, height)
			self.fOffset += len(c)
	def start(self):
		self.fHeight = 0
		self.fWidth = 0
		self.fX = 0
		self.fY = 0
		self.fRunHeight = 0
		self.fBSilent = False
		self.updateSilence()
		self.goHome()
	def startFaking(self, X, Y, Xout, Yout):
		p = self.fMaxWidth, self.fMaxHeight, self.fTestX, self.fTestY
		self.fX = X
		self.fY = Y
		self.fBSilent = True
		#self.fMaxHeight = Yout
		self.fTestX = Xout - 1
		self.fTestY = Yout - 1
		return(p)
	def stopFaking(self, p):
		self.fMaxWidth, self.fMaxHeight, self.fTestX, self.fTestY = p
	def goHome(self):
		pass
	def saveCaretPosition(self):
		pass
	def restoreCaretPosition(self):
		pass
	def clearToEnd(self):
		pass
	def clearToEndOfLine(self):
		pass
	def updateSilence(self):
		if not self.fBSilent and self.fViewHeight is not None and self.fY >= self.fViewHeight:
			self.fBSilent = True
	def finish(self):
		self.fY += self.fRunHeight
		self.fRunHeight = 0
		self.updateSilence()
		if self.fHeight < self.fY:
			self.fHeight = self.fY
		self.fOutputter.flush()
	def addChunk(self, text):
		self.fOffset = 0
		for word, width, height in self.calculateWordExtents(text):
			if not self.wouldFitHereP(width, max(self.fRunHeight, height), self.fHyphenWidth):
				if self.wouldMaybeFitNextLineP(width, max(self.fRunHeight, height), self.fHyphenWidth):
					self.printHyphen()
					self.goToNextLine()
				# it STILL doesn't have to fit. Check it.
				if not self.wouldFitHereP(width, max(self.fRunHeight, height), self.fHyphenWidth):
					# TODO increase vertical space, maybe.
					self.acceptCharacters(word)
					continue
			if self.fTestY != -1 and self.fY >= self.fTestY and self.fX + width >= self.fTestX: # for mouse hit test
				self.acceptCharacters(word) # should fail for us
				raise OverflowError() # just in case
			self.addWithoutCheck(word, width, height)
			self.fOffset += len(word)
	def setMaxHeight(self, value):
		self.fMaxHeight = value
	def setMaxWidth(self, value):
		self.fMaxWidth = value
	def setTestX(self, value):
		self.fTestX = value
	def setTestY(self, value):
		self.fTestY = value
	offset = property(lambda self: self.fOffset) # current offset within addChunk (for exception handling etc)
	height = property(lambda self: self.fHeight) # current height.
	width = property(lambda self: self.fWidth) # current width.
	X = property(lambda self: self.fX) # current X position (relative to current pane)
	Y = property(lambda self: self.fY) # current Y position (ranges over all the panes, if there are multiple)
	testX = property(lambda self: self.fTestX, setTestX) # current X position (relative to current pane)
	testY = property(lambda self: self.fTestY, setTestY) # current Y position (ranges over all the panes, if there are multiple)
class ConsoleWordWrapper(LogicalWordWrapper):
	def goToNextLine(self):
		LogicalWordWrapper.goToNextLine(self)
		if not self.fBSilent:
			self.clearToEndOfLine()
			self.fOutputter.write("\n")
		#self.fOutputter.write(("*" * self.fMaxWidth) + "\n")
	def goHome(self):
		self.goTo(0, 0)
	def goTo(self, X, Y):
		self.fOutputter.write("\033[%d;%dH" % (X + 1, Y + 1)) # row, column
	def saveCaretPosition(self):
		self.fOutputter.write("\033[s")
	def restoreCaretPosition(self):
		self.fOutputter.write("\033[u")
	def clearToEnd(self):
		self.fOutputter.write("\033[J")
	def clearToEndOfLine(self):
		self.fOutputter.write("\033[K\r") # the "\n" is IN the data
	def addWithoutCheck(self, word, width, height):
		if not self.fBSilent:
			displayString = word
			# if there are control characters at all, they will be at the end since #calculateWordExtents will yield on each of them.
			if len(displayString) > 0:
				suffix = displayString[-1]
				if suffix in ["\t", "\n", "\r"]:
					self.fOutputter.write(displayString[:-1])
					if suffix == "\t":
						self.fOutputter.write(" " * tabWidth)
					elif suffix == "\n":
						self.goToNextLine()
					elif suffix == "\r": #???
						pass
				else:
					self.fOutputter.write(displayString)
		self.fRunHeight = max(self.fRunHeight, height)
		self.fX += width
		if self.fWidth < self.fX:
			self.fWidth = self.fX
class MultiPaneWordWrapper(ConsoleWordWrapper):
	def __init__(self, panes, outputter): # panes is [(X0,Y0,X1,Y1)]. Will be traversed in order, top to bottom.
		self.fPanes = []
		self.fPaneStartYs = []
		self.fCurrentPaneIndex = 0
		self.setPanes(panes)
		ConsoleWordWrapper.__init__(self, (panes[0][2] - panes[0][0]) if len(panes) > 0 else 0, VERY_BIG, outputter, viewHeight = pageY)
	def setPanes(self, panes):
		self.fPanes = panes
		paneY = 0
		self.fPaneStartYs = []
		for pane in panes:
			height = pane[3] - pane[1]
			self.fPaneStartYs.append(pageY)
			pageY += height
	def goHome(self):
		self.fCurrentPaneIndex = 0
		self.fOutputter.write("\033[%d;%dH" % (self.fPanes[0][1] + 1, self.fPanes[0][0] + 1))
	def clearToEndOfLine(self):
		while self.fX < self.fMaxWidth:
			self.fOutputter.write(" ")
			self.fX += 1
	def clearToEnd(self):
		while self.fY < self.fViewHeight:
			self.clearToEndOfLine()
			self.goToNextLine()
	def goToNextLine(self):
		ConsoleWordWrapper.goToNextLine(self)
		if self.fCurrentPaneIndex + 1 < len(self.fPanes) and self.fY >= self.fPaneStartYs[self.fCurrentPaneIndex + 1]:
			while self.fCurrentPaneIndex + 1 < len(self.fPanes) and self.fY >= self.fPaneStartYs[self.fCurrentPaneIndex + 1]:
				self.fCurrentPaneIndex += 1
			if self.fCurrentPaneIndex < len(self.fPanes):
				pane = self.fPanes[self.fCurrentPaneIndex]
				self.fMaxWidth = pane[2] - pane[0]
			else:
				self.fMaxWidth = 0
	panes = property(lambda self: self.fPanes, setPanes) # current width.
if __name__ == "__main__":
	class TestWordWrapper(ConsoleWordWrapper):
		def goTo(self, X, Y):
			pass
		def saveCaretPosition(self):
			pass
		def restoreCaretPosition(self):
			pass
		def clearToEnd(self):
			pass
		def clearToEndOfLine(self):
			pass
		#def goToNextLine(self):
	def test_wrapper(text, expectedOutput, expectedHeight):
		IO = StringIO.StringIO()
		wrapper = TestWordWrapper(10, 10000, IO, viewHeight = 24)
		wrapper.setHyphen(None)
		wrapper.start()
		wrapper.addChunk(text)
		wrapper.finish()
		#print(wrapper.Y)
		#print(IO.getvalue())
		#print(expectedOutput)
		assert(IO.getvalue() == expectedOutput)
		#print wrapper.height
		assert(wrapper.height == expectedHeight)
	test_wrapper("A\nB", "A\nB", 2)
	test_wrapper("Es war Mann. Der Wasserpfeife.", """Es war 
Mann. Der 
Wasserpfei
fe.""", 4)
	test_wrapper("Es war einmal vor langer langer Zeit ein Mann. Der Mann hatte eine Wasserpfeife. Daher war er immer gut drauf.", """Es war 
einmal 
vor 
langer 
langer 
Zeit ein 
Mann. Der 
Mann 
hatte 
eine Wasse
rpfeife. 
Daher war 
er immer 
gut drauf.""", 14)
##########
