#!/usr/bin/env python3
# -*- encoding: utf-8 -*-

# Note: install Asana math font and texlive-math-extra, texlive-generic-extra, texlive-latex-extra, run mkluatexfontdb

# TODO make "%" work.
# TODO &nbsp; ENTITY nbsp "&#160;">]> 
# http://www.fileformat.info/info/unicode/block/mathematical_operators/utf8test.htm
# very cool: dead acute+4 = ⁴
# FIXME: handle empty equations.
# FIXME: handle operators: sum product
# TODO: handle space before "=".
# TODO: handle U+2062 invisible times.

r"""
pyexpat
xmlparser.ordered_attributes


TODO
\mathrm{...}  	Roman  	An equation formatted in Roman
\mathit{...} 	Italic 	An equation formatted in Italics
\mathbf{...} 	Bold 	An equation formatted in Bold
\mathsf{...} 	Sans serif 	An equation formatted in Sans serif
\mathtt{...} 	Typewriter 	An equation formatted in Typewriter
\mathcal{...} 	Calligraphy 	An equation formatted in Calligraphy

to suppress equation indices, add \nonumber command just before the end of row command (\\). 
"""

import unicodedata
import sys
#import exceptions
from xml.dom import minidom
import os
try:
	import urllib.parse as urllib_parse
except:
	import urllib as urllib_parse
import struct
import codecs
import unicodedata
import re

unicode_to_LaTeX_table = {
	# ambiguous: 0x2192: "\\to ", # FIXME?
	178: "^2",
	179: "^3",
	0x00C5: "\\angstrom ",
	0x2074: "^4",
	945: "\\alpha ",
	946: "\\beta ",
	947: "\\gamma ",
	611: "\\gamma ",
	948: "\\delta ",
	240: "\\delta ", # ?
	949: "\\varepsilon ", # FIXME
	0x3f5: "\\epsilon ",
	950: "\\zeta ",
	951: "\\eta ",
	952: "\\theta ",
	953: "\\iota ",
	954: "\\kappa ",
	955: "\\lambda ",
	956: "\\mu ",
	957: "\\nu ",
	958: "\\xi ",
	959: "\\omicron ",
	960: "\\pi ",
	961: "\\rho ",
	963: "\\sigma ",
	964: "\\tau ",
	965: "\\upsilon ",
	966: "\\varphi ",
	967: "\\chi ",
	968: "\\psi ",
	969: "\\omega ",
	# big letters:
	913: "\\Alpha ",
	914: "\\Beta ",
	915: "\\Gamma ",
	916: "\\Delta ",
	917: "\\Epsilon ",
	918: "\\Zeta ",
	919: "\\Eta ",
	920: "\\Theta ",
	921: "\\Iota ",
	922: "\\Kappa ",
	923: "\\Lambda ",
	924: "\\Mu ",
	925: "\\Nu ",
	926: "\\Xi ",
	927: "\\Omicron ",
	928: "\\Pi ",
	929: "\\Rho ",
	931: "\\Sigma ",
	932: "\\Tau ",
	933: "\\Upsilon ",
	934: "\\Phi ",
	935: "\\Chi ",
	936: "\\Psi ",
	937: "\\Omega ",
	981: "\\phi ",
	# Maths:
	8800: "\\neq ",
	172: "\\neg ",
	8745: "\\cap ",
	8743: "\\bigwedge ",
	8746: "\\cup ",
	8744: "\\bigvee ",
	8834: "\\subset ",
	8838: "\\subseteq ",
	8614: "\\mapsto ",
	8869: "\\perp ",
	8729: "\\bullet ",
	0x22C5: "\\cdot ",
	10799: "\\times ",
	8706: "\\partial ",
	0x221A: "\\sqrt ",
	0x00F7: "\\frac ",
	0x2020: "\\dagger ",
	0x2044: "\\frac ",
	0x2200: "\\forall ",
	0x2203: "\\exists ",
	0x2206: "\\nabla^2 ", # \cdot \\nabla ", # FIXME laplace. # \Delta
	0x2207: "\\nabla ",
	#	0x2207: "\\nabla_",
	0x220F: "\\prod ",
	0x222B: "\\int ",
	0x222E: "\\oint ",
	0x2026: "\\ldots ",
	0x00B1: "\\pm ",
	0x221E: "\\infty ",
	0x2190: "\\leftarrow ",
	0x2191: "\\uparrow ",
	0x2192: "\\rightarrow ",
	0x2193: "\\downarrow ",
	0x21D0: "\\Leftarrow ",
	0x21D2: "\\Rightarrow ",
	0x21D4: "\\Leftrightarrow ",
	# more arrows: http://www.alanwood.net/unicode/arrows.html
	0x2208: "\\in ",
	# circle
	0xB0: "^{\circ} ",
	0x2265: "\\geq ",
	0x2264: "\\leq ",
	0x03d1: "\\vartheta ",
	#0x03d5: "\\varphi ",
	0x03d6: "\\varpi ",
	0x03f1: "\\varrho ",
	0x2211: "\\sum\\limits ",
	#0x2329: "\\langle ",
	0x27e8: "\\langle ",
	0x27e9: "\\rangle ",
	0x2218: "\\circ ",
	0x223C: "\\sim ",
	0x2297: "\\otimes ",
	# TODO http://www.cl.cam.ac.uk/~mgk25/ucs/examples/TeX.txt
	0x0127: "\\hbar ",
	0x210F: "\\hbar ",
	0x29E0: "\\quabla ", # Box ", # glyph'003 in msam. # \\quabla.
	0x212B: "\\angstrom ", # don't use!
	0x2248: "\\approx ",
	0x2227: "\\land ", # "\\wedge ",
	0x2228: "\\lor ", #"\\vee ",
	0x2020: "\\dagger ",
	0xDF: "{\ss}",
	0x210B: "\\mathcal{H}",
	0x2112: "\\mathcal{L}",
	0x1D4B1: "\\mathcal{V}", 
	0x1D4C0: "\\mathcal{k}",
	0x1D4C5: "\\mathcal{p}", 
	0x1D4C7: "\\mathcal{r}", 
}

# TODO \underset{x \rightarrow \infty} {\lim}
# TODO \sum\limits_{i=0}^N i^2 
# TODO uplus

used_equations = set()

re_remove_diacritics = re.compile('[\u0300-\u036f\u1dc0-\u1dff\u20d0-\u20ff\ufe20-\ufe2f]', re.U)
re_hat = re.compile('(.)[\u0302]', re.U)
re_overline_2 = re.compile('(.)[\u0304]', re.U)
re_overline = re.compile('(.)[\u0305]', re.U)
re_dotdot = re.compile('(.)[\u0308]', re.U)
re_dot = re.compile('(.)[\u0307]', re.U)
re_arrow_above = re.compile('(.)[\u20D7]', re.U) # vector arrow: U+20D7
re_vector_overline = re.compile('(.)[\u20D7][\u0304]', re.U)
re_vector_overline_2 = re.compile('(.)[\u20D7][\u0305]', re.U)
re_vector_dot = re.compile('(.)[\u20D7][\u0307]', re.U)
re_vector_dotdot = re.compile('(.)[\u20D7][\u0308]', re.U)
re_angstrom_overline = re.compile('(A)[\u030A][\u0304]', re.U)
re_angstrom_overline_2 = re.compile('(A)[\u0304][\u030A]', re.U) # probably unused.

def unicode_to_LaTeX(text):
	# decompose.
	text = unicodedata.normalize("NFD", text)
	##text = text.replace("A\u030A\u0304", "XX\u00C5") # don't split angstrom.
	# find 0x3xx diacritical marks and put their corresponding TEX code on the FRONT in the result.
	text = re_hat.sub(r"\\hat \1", text)
	#q = text[text.find("e"):]
	#print hex(ord(q[0])), hex(ord(q[1])), hex(ord(q[2]))

	text = re_angstrom_overline.sub(r"{\\overline {\\angstrom}}", text)
	text = re_angstrom_overline_2.sub(r"{\\overline {\\angstrom}}", text)
	text = re_vector_overline.sub(r"{\\overline {\\vec \1}}", text)
	text = re_vector_overline_2.sub(r"{\\overline {\\vec \1}}", text)
	text = re_vector_dot.sub(r"{\\dot {\\vec \1}}", text)
	text = re_vector_dotdot.sub(r"{\\ddot {\\vec \1}}", text)
	text = re_dotdot.sub(r"{\\ddot \1}", text)
	text = re_dot.sub(r"{\\dot \1}", text)
	text = re_overline.sub(r"\\overline \1", text)
	text = re_overline_2.sub(r"\\overline \1", text)
	text = re_arrow_above.sub(r"\\vec{\1}", text)

	for code, part in unicode_to_LaTeX_table.items():
		text = text.replace(unicodedata.normalize("NFD", chr(code)), part)

	if text != re_remove_diacritics.sub("", text):
		raise Exception("unknown diacritical marks in text %r" % text)

	print(text)
	return text

import re
r_extra_braces = re.compile(r"\s*{([^}])}")
# TODO what about \bf{\dot{\phi}} ?
r_extra_braces_2_UTF_8 = re.compile(r"\s*{([" + chr(0xc0) + "-" + chr(0xe0-1) + "][" + chr(0x80) + "-" + chr(0xc0-1) + "])}") # 11xx xxxx 10xx xxxx (>= 0xc0, >= 0x80 & < 0xc0)
# TODO more UTF-8 (3 chars, 4 chars, 5 chars, 6 chars total).

diacritics = {
		"\\bf ": "¯", # combining harpoon: U+20D1; literal high horizontal line: U+0304
		"\\ddot ": "¨", # combining: U+0308
		"\\dot ": "˙", # combining: U+0307
		"\\hat ": "^", # combining: U+0302
		"\\vec ": "¯", # combining harpoon: U+20D1; literal high horizontal line: U+0304 # so sue me.
		"\\bar ": "¯", # combining: U+0304
		"\\overline ": "‾", # combining: U+0305
		"\\tilde ": "~", # combining: TODO
}

functions = set(["\\cos", "\\sin", "\\arccos", "\\arcsin", "\\tan", "\\arctan", "\\cot", "\\cosh", "\\sinh", "\\coth", "\\tanh", "\\det", "\\lim", "\\liminf", "\\limsup", "\\ln", "\\log", "\\lg", "\\artanh", "\\arsinh", "\\arcosh", "\\exp", "\\max", "\\diag", "\\conj", "\\div", "\\Tr"])

functions.add("\\field") # not exactly functions, I know

def simple_basename(text):
	text = text.strip()
	for TEX_code in functions:
		assert(TEX_code.startswith("\\"))
		text = text.replace(TEX_code + " ", TEX_code[1:] + " ")
		text = text.replace(TEX_code + "(", TEX_code[1:] + "(")
		text = text.replace(TEX_code + "{", TEX_code[1:] + " {")
		text = text.replace(TEX_code + "[", TEX_code[1:] + "[")
		text = text.replace(TEX_code + "|", TEX_code[1:] + "|")
		text = text.replace(TEX_code + "_", TEX_code[1:] + "_")
		text = text.replace(TEX_code + "\\", TEX_code[1:] + "\\")

	LATEX_items = reversed(sorted([(value, key) for key, value in unicode_to_LaTeX_table.items()]))
	for part, code in LATEX_items:
		#sorted(unicode_to_LaTeX_table.keys()):
		#part = unicode_to_LaTeX_table[code]
		UTF8_chunk = chr(code) # .encode("utf-8")
		text = text.replace(" " + part, UTF8_chunk)
		text = text.replace(part, UTF8_chunk)
		if part.endswith(" "):
			text = text.replace(part.rstrip(), UTF8_chunk)
			#if text.endswith(part.rstrip()):
			#	text = text[:-len(part.rstrip())] + UTF8_chunk


	text = text.replace("\n", ";").replace("\r", "") #.replace("} ", "}")

	for TEX_code, part in diacritics.items():
		text = text.replace(TEX_code, part)
		text = text.replace(TEX_code.rstrip(), part)


	text = text.replace("\\bf ", "¯") # TODO actually fuse with next character ("x" in "{x}").
	text = text.replace("\\bf", "¯") # TODO actually fuse with next character ("x" in "{x}").
	text = text.replace("\\ddot", "¨") # TODO actually fuse with next character ("x" in "{x}").
	text = text.replace("\\dot", " ̇") # TODO actually fuse with next character ("x" in "{x}").
	text = text.replace("{,}", ".")
	text = text.replace("\\,", ",")
	# equiv
	text = text.replace("\\equiv ", ":=")
	#text = r_extra_braces.sub(r"\1", text)
	#text = r_extra_braces_2_UTF_8.sub(r"\1", text)
	text = text.replace("\\:", " ")
	text = text.replace(" \\\\ ", ";")
	text = text.replace("\\\\ ", ";")
	text = text.replace("\\\\", ";")
	text = text.replace(" \\", "\\") # FIXME is this a good idea?
	text = text.replace("\\begin{pmatrix} ", "[")
	text = text.replace("\\begin{pmatrix}", "[")
	text = text.replace("\\end{pmatrix}", "]")
	text = text.replace("\\begin{vmatrix} ", "|")
	text = text.replace("\\begin{vmatrix}", "|")
	text = text.replace("\\end{vmatrix}", "|")
	text = text.replace("\\begin{bmatrix} ", "[")
	text = text.replace("\\begin{bmatrix}", "[")
	text = text.replace("\\end{bmatrix}", "]")
	text = text.replace("\\begin{array} ", "(")
	text = text.replace("\\begin{array}", "(")
	text = text.replace("\\end{array}", ")")
	text = text.replace("\\begin{cases} ", "{")
	text = text.replace("\\begin{cases}", "{")
	text = text.replace("\\end{cases}", "}")
	text = text.replace("\\stackrel", "") # can't represent stacking in file names.
	text = text.replace("\\left", "").replace("\\right", "") # big braces
	text = text.replace("\\mbox", "") # literal text.
	text = text.replace("\\{", "{") # unescape.
	text = text.replace("\\}", "}") # unescape.
	text = text.replace("\\|", "|") # norm.
	text = text.replace("\\to ", "→") # lim \to and \rightarrow are ambiguous.
	text = text.replace("\\to", "→") # lim \to and \rightarrow are ambiguous.
	text = text.replace("\\widehat", "^")
	text = text.replace("\\binom", "C")
	text = text.replace("\\text", "")
	text = text.replace("\\dbinom", "C")
	text = text.replace("\\limits", "")
	text = text.replace("\\backslash", "b")
	text = text.replace("\\mathcal", "C")
	if text.find("\\nonumber") == -1:
		text = text + "_numbered"
	else:
		text = text.replace("\\nonumber", "").strip()
	assert(text.find("\\") == -1)
	text = text.replace(" ", "_")
	#print text

	#text = text.decode("utf-8")
	text = unicodedata.normalize("NFD", text)[:80] # .encode("utf-8") # MacOSX standard (almost the same)
	return text

# Note: Latex package "tensor" is in Debian package "texlive-math-extra"
TEX_preamble = r'''
\documentclass{minimal} 
\usepackage{amsmath}
\usepackage{amsthm}
\usepackage{amssymb}
\usepackage{bm}
\usepackage{tensor}
\newcommand{\mx}[1]{\mathbf{\bm{#1}}} % Matrix command
\newcommand{\vc}[1]{\mathbf{\bm{#1}}} % Vector command 
\newcommand{\Transpose}{\text{T}}                % Transpose
\newcommand{\Tr}{\operatorname{Tr}}
\newcommand{\artanh}{\operatorname{artanh}}
\newcommand{\arsinh}{\operatorname{arsinh}}
\newcommand{\arcosh}{\operatorname{arcosh}}
\newcommand{\vectornorm}[1]{\left|\left|#1\right|\right|}
\newcommand{\diag}{\mathop{\mathrm{diag}}}
\newcommand{\quabla}{\raisebox{-.2em}{\Large$\Box$}}
\newcommand{\fourier}{\mathcal{F}}
\newcommand{\conj}{\mathop{\mathrm{conj}}}
\renewcommand\vec[1]{\ensuremath\mathbf{#1}}
\renewcommand{\bfdefault}{bx}
\newcommand{\field}[1]{\mathbb{#1}}
\newcommand{\N}{\field{N}} % natural numbers
\newcommand{\R}{\field{R}} % real numbers
\newcommand{\Z}{\field{Z}} % integers
\newcommand{\Q}{\field{Q}} % rationals
\newcommand{\C}{\field{C}} % complex numbers
\newcommand{\angstrom}{\textup{\AA}}
\renewcommand{\div}{\mathop{\mathrm{div}}}
\fontsize{12}{15}
\selectfont
\pagestyle{empty} 
\begin{document} 
'''

def get_PNG_size(name):
	f = open(name, "rb")
	signature = f.read(8)
	assert(signature == bytes([137, 80, 78, 71, 13, 10, 26, 10]))

	length = f.read(4)
	chunk_type = f.read(4)
	assert(chunk_type == bytes("IHDR", "ascii"))

	width_height = f.read(8)
	width, height = struct.unpack(">II", width_height)

	return width, height

def is_inline(eq):
	class_ = " " + (eq.getAttribute("class") or "") + " "
	return (class_.find(" ieq ") > -1) or (eq.parentNode.tagName == "span") # inline equation

def to_UTF_8(text):
	return text
	#if isinstance(text, unicode):
	#	return text.encode("utf-8")
	#else:
	#	return text

def create_TEX_images(queue):
	if len(queue) == 0:
		return
	try:
		try:
			os.unlink("/tmp/HTML_embed_TeX")
		except:
			pass
		os.mkdir("/tmp/HTML_embed_TeX")
	except:
		pass
	f = open("/tmp/HTML_embed_TeX/equations.TEX", "w")
	f.write(TEX_preamble)
	for eq in queue:
		# .replace(":=", "\\equiv ")
		TEX_code = to_UTF_8(unicode_to_LaTeX(eq.getAttribute("alt"))).replace("(", "\\left(").replace(")", "\\right)").replace("[", "\\left[").replace("]", "\\right]").replace("**", "^")
		# .replace("[", "\\left[").replace("]", "\\right]") doesn't work since "\sqrt[2]" arg spec isn't supposed to be "\sqrt[2\right]".
		TEX_code = TEX_code + " \\nonumber"
		if is_inline(eq):
			f.write("$%s$ \n \\newpage \n" % TEX_code.strip()) # FIXME don't hardcode this option?
		else:
			f.write("\\begin{eqnarray}")
			#f.write("\\[\n%s \n\\] \n" % TEX_code)
			f.write("%s\n" % TEX_code.strip())
			f.write("\\end{eqnarray}")
			f.write(" \\newpage \n")

	f.write('\end{document}')
	f.close()

	# compile LaTeX document. A DVI file is created
	#-no-pdf
	os.spawnvp(os.P_WAIT, "latex", ["latex", "-output-directory=/tmp/HTML_embed_TeX", "--output-format=dvi", "/tmp/HTML_embed_TeX/equations.TEX"])

	# Run dvipng on the generated DVI file. Use tight bounding box. 
	# Magnification is set to 1400 (is 1200 better?)
	dvipng_input_name = "/tmp/HTML_embed_TeX/equations.dvi"
	dvipng_output_pattern = "/tmp/HTML_embed_TeX/%d" # #src # this supports "%d".
	dvipng_args = ["dvipng", "-T", "tight", "-x", "1400", "-z", "9", "-bg", "transparent", "-o", dvipng_output_pattern, dvipng_input_name]
	#dvipng_args = ["dvipng", "-T", "tight", "-z", "9", "-bg", "transparent", "-o", dvipng_output_pattern, dvipng_input_name]
	os.spawnvp(os.P_WAIT, "dvipng", dvipng_args)

	for index, image in enumerate(queue):
		src = urllib_parse.unquote(to_UTF_8(image.getAttribute("src")))
		temp_name = "/tmp/HTML_embed_TeX/%d" % (index + 1)
		os.spawnvp(os.P_WAIT, "cp", ["cp", "--", temp_name, src])
		os.unlink(temp_name)
		#print "updated", src
		width, height = get_PNG_size(src)
		image.setAttribute("width", str(width))
		image.setAttribute("height", str(height))

	# Remove temporary files
	os.remove("/tmp/HTML_embed_TeX/equations.dvi")
	os.remove("/tmp/HTML_embed_TeX/equations.log")
	os.remove("/tmp/HTML_embed_TeX/equations.aux")
	#os.remove("/tmp/HTML_embed_TeX/equations.TEX")

def prepare_update_image(image):
	#TEX_code = image.getAttribute("alt")
	#print image.getAttribute("src")
	src = urllib_parse.unquote(to_UTF_8(image.getAttribute("src")))
	used_equations.add(src)
	try:
		src_mtime = os.path.getmtime(src)
	except OSError as e:
		src_mtime = 0

	if input_mtime > src_mtime: # need to update. This fires way too often, but ok.
		return True

input_name = "index" if len(sys.argv) < 2 else sys.argv[-1]
input_mtime = os.path.getmtime(input_name)

def eq_P(class_value):
	if class_value in ["eq", "ieq"]:
		return True

	class_value = " " + class_value + " "
	return class_value.find(" eq ") > -1 or class_value.find(" ieq ") > -1

doc = minidom.parseString("<root>%s</root>" % open(input_name, encoding = "utf-8").read()) # TODO encoding
#doc = minidom.parseString("<root>%s</root>" % open(input_name).read()) # TODO encoding
images = doc.getElementsByTagName("img")
equations = [image for image in images if eq_P(image.getAttribute("class"))]

try:
	os.mkdir("image")
except:
	pass
try:
	os.mkdir("image/equation")
	f = file("image/equation/.htaccess", "w")
	f.write("ForceType image/png\n")
	f.close()
except:
	pass

B_dirty = False
queue = []
for equation in equations:
	image = equation
	src = image.getAttribute("src")
	TEX_code = to_UTF_8(image.getAttribute("alt"))
	#if is_inline(image):
	TEX_code = TEX_code + " \\nonumber"

	image_basename = simple_basename(TEX_code.strip())
	expected_src = "image/equation/%s" % image_basename
	if not src or expected_src != src.encode("utf-8"):
		image.setAttribute("src", urllib_parse.quote_plus(expected_src).replace("%2F", "/"))
		B_dirty = True

	if prepare_update_image(image):
		queue.append(image)
		B_dirty = True

create_TEX_images(queue)


# delete left-over equations.
for name in set(["image/equation/%s" % name for name in os.listdir("image/equation")]):
	if os.path.basename(name).startswith("."):
		continue
	if name not in used_equations:
		pass # os.unlink(name)

if B_dirty:
	import sys
	f = codecs.open(input_name + ".tmp", "w", "UTF-8")
	for childNode in doc.documentElement.childNodes:
		childNode.writexml(f)
		# toprettyxml
	f.close()
	os.rename(input_name + ".tmp", input_name)
