""" OpenDocument to HTML Converter for PubTal
	Uses ElementTree rather than SAX to better handle parsing

	Copyright (c) 2009 Colin Stewart (http://www.owlfish.com/)
	All rights reserved.
		
	Redistribution and use in source and binary forms, with or without
	modification, are permitted provided that the following conditions
	are met:
	1. Redistributions of source code must retain the above copyright
	   notice, this list of conditions and the following disclaimer.
	2. Redistributions in binary form must reproduce the above copyright
	   notice, this list of conditions and the following disclaimer in the
	   documentation and/or other materials provided with the distribution.
	3. The name of the author may not be used to endorse or promote products
	   derived from this software without specific prior written permission.
	
	THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
	IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
	OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
	IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
	INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
	NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
	DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
	THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
	(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
	THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
	
	If you make any bug fixes or feature enhancements please let me know!

"""

import zipfile, StringIO, cgi, re, os.path, itertools
import xml.etree.ElementTree as ElementTree
try:
	import logging
except:
	from pubtal import InfoLogging as logging

from pubtal import HTMLWriter

OFFICE_URI='urn:oasis:names:tc:opendocument:xmlns:office:1.0'
TEXT_URI='urn:oasis:names:tc:opendocument:xmlns:text:1.0'
STYLE_URI='urn:oasis:names:tc:opendocument:xmlns:style:1.0'
TABLE_URI='urn:oasis:names:tc:opendocument:xmlns:table:1.0'
#FORMAT_URI='http://www.w3.org/1999/XSL/Format'
FORMAT_URI='urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0'
DUBLIN_URI='http://purl.org/dc/elements/1.1/'
META_URI='urn:oasis:names:tc:opendocument:xmlns:meta:1.0'
XLINK_URI='http://www.w3.org/1999/xlink'
SVG_URI='urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0'
DRAW_URI='urn:oasis:names:tc:opendocument:xmlns:drawing:1.0'

# Element Tree path mapping
PM = {'o': "{%s}" % OFFICE_URI,
	  't': "{%s}" % TEXT_URI,
	  's': "{%s}" % STYLE_URI,
	  'ta': "{%s}" % TABLE_URI,
	  'f': "{%s}" % FORMAT_URI,
	  'dub': "{%s}" % DUBLIN_URI,
	  'm': "{%s}" % META_URI,
	  'x': "{%s}" % XLINK_URI,
	  'svg': "{%s}" % SVG_URI,
	  'd': "{%s}" % DRAW_URI}

# These are the fo styles that will be treated as CSS styles.
SUPPORTED_FO_STYLES = {'%(f)stext-align' % PM:1, '%(f)sfont-weight' % PM:1, '%(f)sfont-style' % PM:1, '%(f)smargin-left' % PM:1}

# These lists act as filters on which styles are applied to which kind of elements.
HEADING_STYLE_FILTER = ['text-align', 'margin-left']
PARAGRAPH_STYLE_FILTER = ['text-align', 'underline', 'line-through', 'overline'
						 ,'font-weight', 'font-style', 'vertical-align', 'margin-left']
SPAN_STYLE_FILTER = PARAGRAPH_STYLE_FILTER
						 
# These are the assumed defaults for paragraphs - OO setting these will be ignored.
DEFAULT_PARAGRAPH_STYLES = { 'text-align': 'start', 'font-weight': 'normal'
							,'font-style': 'normal', 'margin-left': '0cm'}

class OpenDocumentConverter:
	""" Convert OpenOffice format to HTML, XHTML or PlainText
	"""
	def __init__ (self):
		self.log = logging.getLogger ("PubTal.ODC")
		
	def convert (self, fileName, config={}):		
		archive = zipfile.ZipFile (fileName, 'r')
		self.contentParser = ODFParser (archive, config)
		archive.close()
		
	def getMetaInfo (self):
		return self.contentParser.getMetaInfo()
		
	def getContent (self):
		return self.contentParser.getContent()
		
	def getFootNotes (self):
		return self.contentParser.getFootNotes()
		
	def getPictures (self):
		return self.contentParser.getPictures()
		
class ODFParser:
	def __init__ (self, archive, configuration):
		self.log = logging.getLogger ("PubTal.ODF.ODFParser")
		
		# Register all handlers
		self.tagHandlers = {'%(t)sh' % PM: self.headingTag,
							'%(t)sp' % PM: self.paragraphTag}
		
		self.configuration = configuration
		self.metaData = {}
		self.styles = {}
		self.listStyles = {}
		self.styleStack = [DEFAULT_PARAGRAPH_STYLES]
		
		# Initialise our variables
		self.pictureList = []
		self.init_configuration()
		
		self.parseMetaData (ElementTree.parse (StringIO.StringIO (archive.read ("meta.xml"))))
		self.parseStyleData (ElementTree.parse (StringIO.StringIO (archive.read ("styles.xml"))))
		self.log.info ("Looking through content for further style information")
		contentTree = ElementTree.parse (StringIO.StringIO (archive.read ("content.xml")))
		self.parseStyleData (contentTree)
		self.parseContent (contentTree)
		
		logging.critical ("Implement picture handling here")
		# Read pictures
		#for pictureFilename, newFilename in self.drawHandler.getBundledPictures():
		#	self.pictureList.append ((newFilename, archive.read (pictureFilename)))
	
	def determineRequestedStyle (self, node):
		""" Determine the requested style including any inherited from parents. """
		self.log.debug ("Node attributes %s", node.attrib)
		styleName = node.get ("%(t)sstyle-name" % PM, None)
		self.log.debug ("Style is %s", styleName)
		curStyle = self.styles.get (styleName, {})
		self.log.debug ("Found style properties %s", curStyle)
		hierachy = [curStyle]
		parent = curStyle.get ("parent-name", None)
		while (parent):
			self.log.debug ("Getting parent style %s", parent)
			curStyle = self.styles.get (parent, {})
			hierachy.append (curStyle)
			parent = curStyle.get ("parent-name", None)
		# Reverse the hierachy so we apply the parent styles first
		hierachy.reverse()
		# Now apply each style to generate the final request
		requestedStyle = {}
		for style in hierachy:
			requestedStyle.update (style)
		self.log.debug ("Computed active style of %s", requestedStyle)
		return requestedStyle
	
	def getCSSStyle (self, styleFilter):
		""" Turns an ODF style into a CSS style.
		
			Currently in-effect styles are kept on a stack, so only new styles
			result in new CSS commands.
			
			When this is called the stack already contains the requested style.
		"""
		requestedStyle = self.styleStack[-1]
		previousStyle = self.styleStack[-2]
		self.log.debug ("Style stack current %s", requestedStyle)
		# For each style that we could apply determine whether the request has
		# changed from the previously in-effect style
		cssStyles = []
		textDecoration = []
		self.log.debug ("Checking for style changes for filter list %s", styleFilter)
		for style in styleFilter:
			self.log.debug ("Checking style %s.  Old value was %s, new value is %s", style, previousStyle.get (style, None),requestedStyle.get (style, None))
			if (requestedStyle.get (style, None) != previousStyle.get (style, None)):
				self.log.debug ("Style property %s has changed from previous in effect style", style)
				if (style in ["underline", "line-through", "overline"]):
					textDecoration.append (style)
				else:
					cssStyles.append (u"%s:%s" % (style, requestedStyle.get (style)))
		# Now build the css string
		if (len (textDecoration) > 0):
			cssStyles.append (u"text-decoration: %s" % u",".join (textDecoration))
		cssStyleList = ";".join (cssStyles)
		self.log.debug ("Built style list %s", cssStyleList)
		if (len (cssStyleList) > 0):
			return ' style="%s"' % cssStyleList
		return ''
		
	def parseContent (self, tree):
		self.log.info ("Parsing content")
		body = tree.find ("//%(o)sbody" % PM)
		# contentGenerator uses recursion to go through all tags
		self.contentGenerator (body)
				
	def contentGenerator (self, node):
		tagHandler = self.tagHandlers.get (node.tag, self.defaultTagHandler)
		requestedStyle = self.determineRequestedStyle (node)
		self.styleStack.append (requestedStyle)
		for child in tagHandler (node, requestedStyle):
			#self.log.debug ("Recursively handle child elements")
			self.contentGenerator (child)
		self.styleStack.pop()
		return
	
	def defaultTagHandler (self, node, requestedStyle):
		if (node.tag.startswith ("%(t)s" % PM)):
			unhandledText = True
			if (node.text): self.result.write (cgi.escape (node.text))
		else:
			unhandledText = False
		for child in node.getchildren():
			yield child
		if (node.tail and unhandledText): self.result.write (cgi.escape (node.tail))
		
	def headingTag (self, node, requestedStyle):
		""" Heading Tag Handler
		
			Deals with the start tag and any initial text, then
			yeilds all children, then deals with any remaining text and the
			close tag.
		"""
		headingLevel = int(node.get ("%(t)soutline-level" %PM,1))
		if (headingLevel > 6):
			self.log.warn ("Heading level of %s used, but HTML only supports up to level 6.", headingLevel)
			headingLevel = 6
		self.result.startElement ('h%s' % str (headingLevel), self.getCSSStyle (HEADING_STYLE_FILTER))
		if (node.text): self.result.write (cgi.escape (node.text))
		for child in node.getchildren():
			yield child
		if (node.tail): self.result.write (cgi.escape (node.tail))
		self.result.endElement ('h%s' % str (headingLevel))
		
	def paragraphTag (self, node, requestedStyle):
		""" Paragrah Tag Handler
		
			Deals with the start tag and any initial text, then
			yeilds all children, then deals with any remaining text and the
			close tag.
		"""
		self.result.startElement ('p', self.getCSSStyle (PARAGRAPH_STYLE_FILTER))
		if (node.text): self.result.write (cgi.escape (node.text))
		for child in node.getchildren():
			yield child
		if (node.tail): self.result.write (cgi.escape (node.tail))
		self.result.endElement ('p')

	def parseMetaData (self, tree):
		metaNode = tree.find ("%(o)smeta/" % PM)
		self.metaData ['creation-date'] = metaNode.findtext ("%(m)screation-date" % PM)
		self.metaData ['title'] = metaNode.findtext ("%(dub)stitle" % PM)
		self.metaData ['description'] = metaNode.findtext ("%(dub)sdescription" % PM)
		self.metaData ['subject'] = metaNode.findtext ("%(dub)ssubject" % PM)
		self.metaData ['language'] = metaNode.findtext ("%(dub)slanguage" % PM)
		self.metaData ['keywords'] = []
		for keywords in metaNode.findall ("%(m)skeyword" % PM):
			self.metaData ['keywords'].append (keywords.text)
		self.log.debug ("Meta information %s", self.metaData)
		
	def parseStyleData (self, tree):
		""" Parse style data is called twice, once with the tree for styles
			once with the content tree.
			
			This allows us to build a full style library covering both styles,
			and automatic styles.
		"""
		for styleElm in tree.findall ("//%(s)sstyle" % PM):
			styleInfo = {}
			styleName = styleElm.get ("%(s)sname" % PM)
			styleInfo ['family'] = styleElm.get ("%(s)sfamily" % PM)
			styleInfo ['parent'] = styleElm.get ("%(s)sparent-style-name" % PM, None)
			styleInfo ['display-name'] = styleElm.get ("%(s)sdisplay-name" % PM)
			children = styleElm.getchildren()
			if (len (children) > 0):
				# Assume the first child is the properties
				styleProp = children [0]
				for key, attValue in styleProp.items():
					if key in SUPPORTED_FO_STYLES:
						prop = key[key.find ("}")+1:]
						styleInfo [prop] = attValue
					if (key == "%(s)stext-underline-style" % PM): styleInfo ["underline"] = "underline"
					if (key == "%(s)stext-line-through-style" % PM): styleInfo ["line-through"] = "line-through"
					if (key == "%(s)stext-position" % PM):
						actualPosition =  attValue[0:attValue.find (' ')]
						styleInfo ["vertical-align"] = actualPosition
			self.log.debug ("Adding style %s with config %s", styleName, styleInfo)
			self.styles [styleName] = styleInfo
		
		logging.info ("Looking for list styles")
		for styleElm in tree.findall ("//%(t)slist-style" % PM):
			listStyleStack = []
			styleName = styleElm.get ("%(s)sname" % PM)
			for listStyleElm in styleElm.getchildren():
				orderedList = False
				if listStyleElm.tag == "%(t)slist-level-style-number" % PM:
					orderedList = True
				listStyleStack.append (orderedList)
			self.log.debug ("Adding list style %s with stack %s", styleName, listStyleStack)
			self.listStyles [styleName] = listStyleStack

	def init_configuration (self):
		outputType = self.configuration.get ('output-type', 'HTML')
		self.outputXHTML = True if outputType == "HTML" else False
		self.outputPlainText = True if outputType == "PlainText" else False
			
		if (self.outputPlainText):
			# We do not preserve spaces with &nbsp; because our output is not space clean.
			self.result = HTMLWriter.PlainTextWriter(outputStream=StringIO.StringIO(), outputXHTML=1, preserveSpaces = 0)
		else:
			self.result = HTMLWriter.HTMLWriter(outputStream=StringIO.StringIO(), outputXHTML=self.outputXHTML, preserveSpaces = 0)
		# We use this stack to re-direct output into footnotes.
		self.resultStack = []
		
		# We treat footnotes and endnotes the same.
		self.footNoteID = None
		self.footnotes = []
		
		# The effectiveStyleStack holds the effective style (e.g. paragraph) and is used to filter out
		# un-needed style changes.
		self.effectiveStyleStack = [DEFAULT_PARAGRAPH_STYLES]
			
		self.cleanSmartQuotes = self.configuration.get ('CleanSmartQuotes', 0)
		self.cleanHyphens = self.configuration.get ('CleanHyphens', 0)
		self.preserveSpaces = self.configuration.get ('preserveSpaces', 1)
		
	def getMetaInfo (self):
		return self.metaData
		
	def getContent (self):
		cnt = self.result.getOutput().getvalue()
		self.log.debug ("Content: %s", cnt)
		return cnt
		
	def getFootNotes (self):
		return 
		
	def getPictures (self):
		return self.pictureList