from PresentationObject import PresentationObject
from img import img
from font import font

from qt import QColor

import math # ceil()
import os
import time

true = success = 1
false = failure = 0

class equation( img ):
	"""
	&lt;equation&gt; uses LaTeX code to insert math characters. The
	following programs must be in your $PATH: latex, dvips, gs, pnmgamma,
	pnmcrop, pnmpad, ppmchange, pnmscale, and pnmtopng.
	<p>
	Only LaTeX code will be rendered to the screen... if any tags appear
	within &lt;equation&gt;, they won't be rendered. If the LaTeX code
	contains errors, it may appear that the Presenter has frozen (if
	nothing is displayed to the screen after a minute or two). If this
	occurs, quit the Presenter and type "exit" at the shell to exit from
	LaTeX.
	<p>
	Example:
	<pre>
	  &lt;equation&gt;
	    \int_0^\infty \frac{1}{x^2} \; \mbox{d}x
	  &lt;/equation&gt;
	</pre>
	<p>
	<b>Properties:</b>
	<ul>
	  <li>
	    <i>color</i>/<i>colour</i>: color of the text, defined by name
	    (i.e., "red") or in hexidecimal (i.e., "#ff0000").
	  </li>
	  <li>
	    <i>size</i>: height of the text, defined in pixels, point size,
	    or in relation to the height of the current slide. For example:
	    <ul>
	      <li>"12"    -> 12 points</li>
	      <li>"12pt"  -> 12 points</li>
	      <li>"20px"  -> 20 pixels</li>
	      <li>"1/42"  -> 1/42 the height of the slide window</li>
	      <li>"0.05%" -> 0.05% the height of the slide window</li>
	    </ul>
	  </li>
	</ul>
	"""

	def __init__( self, *args ):
		"""
		Initiate the container, contents, and properties.

		-*args, arguments for the PresentationObject constructor.
		"""

		apply( img.__init__, (self,) + args )

	def render( self, app, x, y ):
		"""
		Draw the equation to the app's QCanvas.

		-app, SlideApplication object
		-x, x coordinate to start drawing at
		-y, y coordinate to start drawing at

		Returns x, y coordinates where the rendering left off.
		"""

		if not app.getConfig().latexEnabled:
			return x, y

		emptyPixels = self.setSize( app.getHeight() )

		#
		# Align the image in the middle of the current line.
		#
		x, y = img.render( self, app, x, y )
		self.move( self.x(), self.y() + emptyPixels/2 )
		return x, y

	def setSize( self, displayHeight=600 ):
		"""
		
		"""

		image = self.getImage()["QImageOriginal"]
		height, emptyPixels = self.getHeightInfo( displayHeight )
		width = float( height ) / image.height()
		width = int( width * image.width() )

		self.setProperty( "width", width )
		self.setProperty( "height", height )

		return emptyPixels

	def getHeightInfo( self, displayHeight ):
		"""
		Get the information needed to compute the dimensions and
		alignment of the equation image.

		-displayHeight, height of the renderer display

		Returns a tuple with the first arguments being the height
		to show the image and the second argument being the number
		of empty vertical pixels above & below the image on the
		current font line(s).
		"""

		image = self.getImage()["QImageOriginal"]

		fontHeight = font( self ).getActualFontHeight( displayHeight )
		if fontHeight == 0:
			fontHeight = self.getSlide().getFontHeight()

		#
		# If the outputted LaTeX is multiple lines, try to calculate
		# how many lines are in the image.
		#
		scale = self.getSlideshow().getConfig().latexScale
		pixelsPerLine = self.pixelsPerLine( scale )
		numLines = math.ceil( float(image.height()) / pixelsPerLine )
		height = int( float(image.height())      / \
		              (pixelsPerLine * numLines) * \
		              (fontHeight * numLines) )

		return height, fontHeight * numLines - height

	def pixelsPerLine( self, scale ):
		"""
		Find the maximum number of pixels per text line, so the number
		of lines in a LaTeX image can be computed.

		-scale, integer for the image size scale being used.

		Returns an integer for pixels.
		"""

		#
		# The pixel per line line was computed by the image height for
		# "(", which is the highest letter in the fonts, or so it
		# seems...
		#
		if scale <= 2:
			return 45
		elif scale == 3:
			return 62
		elif scale == 4:
			return 78
		elif scale == 5:
			return 96
		elif scale == 6:
			return 113
		elif scale == 7:
			return 130
		elif scale == 8:
			return 147
		elif scale == 9:
			return 165
		elif scale == 10:
			return 182
		elif scale == 11:
			return 199
		elif scale == 12:
			return 216
		elif scale == 13:
			return 233
		elif scale == 14:
			return 251
		elif scale == 15:
			return 267
		else:
			return 286

	def getHexColors( self ):
		"""
		Get the fore/background colors in hexidecimal #rrggbb.

		Returns a tuple with the first argument the foreground/text
		color and the second the background color.
		"""

		color = self.getProperty( "color" )
		bgColor = self.getProperty( "bgcolor" )

		return str(QColor(color).name()), str(QColor(bgColor).name())

	def createEquationImage( self, dirName ):
		"""
		Use the equation to attempt to take the Tex code and
		translate it to a PNG file. If any of the following programs
		exit with a non-zero return code, a PNG image will not be
		created.

		latex, dvips, gs, pnmgamma, pnmcrop, pnmpad, ppmchange,
		pnmscale, pnmtopng

		Returns a string with the path to the created image file. If no
		file was created, None is returned.
		"""

		import re

		fileName = self.getNewImageName()
		stub = "%s/%s" % ( dirName, fileName )

		#
		# Since latex won't allow me to define a destination directory
		# of the created .dvi file, they'll be created in the current
		# working directory, so don't include the xml directory which
		# is included in stub.
		#
		dviFile = "%s.dvi" % fileName
		auxFile = "%s.aux" % fileName
		logFile = "%s.log" % fileName

		#
		# ... and these files are created in the directory the .xml
		# file is located in. Marvel at the wonder.
		#
		texFile = "%s.tex" % stub
		psFile  = "%s.ps"  % stub
		ppmFile = "%s.ppm" % stub
		pngFile = "%s.png" % stub
		outFile = "%s.stdout" % stub # redirect stdout
		errFile = "%s.stderr" % stub # redirect stderr
		gsFile = "%s_gs.input" % stub
		filesToRemove = []

		regex = re.compile( r"%%BoundingBox: "            \
		                     "(?P<llx>\d*) (?P<lly>\d*) " \
		                     "(?P<urx>\d*) (?P<ury>\d*)" )

		texfp = open( texFile, "w" )
		texfp.write( "\\documentclass{amsart}\n" \
		             "\\pagestyle{empty}\n"      \
		             "\\thispagestyle{empty}\n"  \
		             "\\begin{document}\n"       \
		             "\\huge\n"                  \
		             "{\n"                       \
		             "$" )
		texfp.write( self.getEquationString() )
		texfp.write( "$\n" \
		             "}\n" \
		             "\\end{document}\n" )
		texfp.close()

		#
		# Call LaTeX to take the .tex file and create a .dvi file.
		#
		filesToRemove.append( outFile )
		filesToRemove.append( errFile )
		filesToRemove.append( texFile )
		filesToRemove.append( dviFile )
		filesToRemove.append( auxFile )
		filesToRemove.append( logFile )
		exitCode = os.system( 'echo q | latex "%s" >"%s" 2>"%s"' \
		                      % (texFile, outFile, errFile) )
		if exitCode:
			self.removeFiles( filesToRemove )
			return None

		#
		# Call DVIPS to take the .dvi file and create a .ps file.
		#
		filesToRemove.append( dviFile )
		exitCode = os.system( 'dvips -E -o "%s" "%s" >"%s" 2>"%s"' \
		                      % (psFile, dviFile, outFile, errFile) )
		if exitCode:
			self.removeFiles( filesToRemove )
			return None

		#
		# Attempt to match the above BoundingBox regular expression in
		# the .dvi file. Strip out the four parameters if it's matched.
		#
		dviArgsSet = false
		dvifp = open( psFile );
		for line in dvifp:
			match = regex.match( line );
			if match:
				llx, lly, urx, ury = match.group( "llx",
				                                  "lly",
				                                  "urx",
				                                  "ury" )
				dviArgsSet = true
				break
		dvifp.close()

		#
		# Default arguments if BoundingBox regexp wasn't matched.
		#
		if not dviArgsSet:
			self.removeFiles( filesToRemove )
			return None

		#
		# Get arguments for Ghostscript to create the .ppm file.
		#
		# 4.0
		scaleFactor = self.getSlideshow().getConfig().latexScale
		scaleFactor = float( scaleFactor )

		gsArgs = {}
		gsArgs["ppmFile"] = ppmFile
		gsArgs["width"] = int( scaleFactor * (int(urx)-int(llx)) )
		gsArgs["height"] = int( scaleFactor * (int(ury)-int(lly)) )
		gsArgs["resolv"] = "%dx%d" % ( scaleFactor*72, scaleFactor*72 )
		gsArgs["psFile"] = psFile
		gsArgs["gsFile"] = gsFile
		gsArgs["outFile"] = outFile
		gsArgs["errFile"] = errFile

		gsfp = open( gsFile, "w" )
		gsfp.write( "%s neg %s neg translate" % (llx, lly) )
		gsfp.close()

		#
		# Call Ghostscript to take the .ps file and create a .ppm file.
		#
		filesToRemove.append( gsFile )
		exitCode = os.system( 'gs -sDEVICE=ppmraw '          \
		                      '-sOutputFile="%(ppmFile)s" '  \
		                      '-g%(width)sx%(height)s '      \
		                      '-r%(resolv)s - "%(psFile)s" ' \
		                      '< "%(gsFile)s" '              \
		                      '> "%(outFile)s" '             \
		                      '2> "%(errFile)s"' % gsArgs )
		if exitCode:
			self.removeFiles( filesToRemove )
			return None

		#
		# Pass the .ppm through different programs to output a resized
		# .png file. Substitute white (.ppm background) with the
		# background color of the current slide and black (text) with
		# the slide font color.
		#
		color, bgColor = self.getHexColors()
		color = "rgb:%s/%s/%s" % ( color[1:3], color[3:5], color[5:] )
		bgColor = "rgb:%s/%s/%s" \
		          % ( bgColor[1:3], bgColor[3:5], bgColor[5:] )
		tempColor = "rgb:83/95/67"

		# reductionFactor = 1 / scaleFactor;'pnmscale %s | ' <BALEETED
		#
		# I had to change black (text) to #839567 which is then changed
		# to the requested font color because if you choose a black
		# background and white text, it all comes out transparent
		# with the two ppmchange calls. I think it's unlikely #839567
		# will be used...
		#
		filesToRemove.append( psFile )
		filesToRemove.append( ppmFile )
		exitCode = os.system( 'pnmgamma 1.0 "%s" | '                 \
		                      'pnmcrop | '                           \
		                      'pnmpad -white -l5 -r5 -t5 -b5 | '     \
		                      'ppmchange rgb:00/00/00 %s | '         \
		                      'ppmchange rgb:ff/ff/ff %s | '         \
		                      'ppmchange %s %s | '                   \
		                      'pnmtopng -interlace -transparent %s ' \
		                      '> "%s"' %                             \
		                      (ppmFile, tempColor, bgColor,
		                       tempColor, color, bgColor, pngFile) )

		if exitCode:
			self.removeFiles( filesToRemove )
			return None

		#
		# Remove temporary files used to create the .png.
		#
		self.removeFiles( filesToRemove )

		#
		# Add entry in the cache index file if caching is enabled.
		#
		if self.getSlideshow().getConfig().latexCacheEnabled:
			self.addEquationImage( "%s.png" % fileName )

		#
		# Return the path to the created .png.
		#
		return pngFile

	def getEquationString( self ):
		"""
		Get the text to be processed through LaTeX.

		Returns an "ISO 8858-1" encoded string.
		"""

		equation = ""
		for content in self.getContents():
			if not isinstance(content, PresentationObject):
				equation = "%s%s" % ( equation, content )
		try:
			equation = equation.encode( "ISO 8859-1" )
		except AttributeError:
			pass

		return equation

	def removeFiles( self, fileList=None ):
		"""
		
		"""

		for file in fileList:
			try:
				os.remove( file )
			except OSError:
				pass

	def getNewImageName( self ):
		"""
		Get a new image name based on the current time. If another
		.png image was created at the same time down to the second, a
		counter is used to avoid overwriting files. Up to 1000 files
		can be created in a second without overwriting...

		Returns a filename string without the ".png" extension.
		"""

		if self.getSlideshow().getConfig().latexCacheEnabled:
			dirName = self.getSlideshow().getConfig().latexCacheDir
		else:
			dirName = self.getSlideshow().getDirName()

		name = stub = time.strftime( "%y%m%d%H%M%S", time.localtime() )
		num = 0
		while 1:
			try:
				os.stat( "%s/%s.png" % (dirName, name) )
			except OSError:
				#
				# Another file doesn't exist so return the
				# available file name. Don't include the 
				# ".png" extension.
				#
				return name
			name = "%s_%03d" % ( stub, num )
			num = num + 1
			if num > 999:
				return "%s_1000" % stub

		return name

	def addEquationImage( self, imgName, equation=None ):
		"""
		
		"""

		cacheDir = self.getSlideshow().getConfig().latexCacheDir
		indexPath = "%s/index.dat" % cacheDir

		if equation == None:
			equation = self.getEquationString()

		try:
			fp = open( indexPath, "a" )
		except IOError:
			#
			# If the file couldn't be opened, it may be because the
			# cache directory has been created, so create it then
			# attempt to open the cache index file again.
			#
			# If it still doesn't open, obviously nothing is saved
			# in an index file, although the .png file will still
			# sit in the directory.
			#
			try:
				os.makedirs( cacheDir )
			except OSError, e:
				#
				# errno 17 means the directory already existed.
				# If this exception was thrown for another
				# error, don't try to add the image file to
				# the cache.
				#
				if e.errno != 17:
					return

				#
				# Try to open the file again
				#
				try:
					fp = open( indexPath, "a" )
				except IOError:
					return

		color, bgColor = self.getHexColors()
		fp.write( "\n[%s color=%s bgcolor=%s scale=%d]\n" \
		          % (imgName, color, bgColor,             \
		             self.getSlideshow().getConfig().latexScale) )
		fp.write( equation )
		fp.write( "\n[/%s]\n" % imgName )
		fp.close()

	def findEquationImage( self, equation=None ):