"""fontemon_blender_addon.fontTools.t1Lib.py -- Tools for PostScript Type 1 fonts (Python2 only)

Functions for reading and writing raw Type 1 data:

read(path)
	reads any Type 1 font file, returns the raw data and a type indicator:
	'LWFN', 'PFB' or 'OTHER', depending on the format of the file pointed
	to by 'path'.
	Raises an error when the file does not contain valid Type 1 data.

write(path, data, kind='OTHER', dohex=False)
	writes raw Type 1 data to the file pointed to by 'path'.
	'kind' can be one of 'LWFN', 'PFB' or 'OTHER'; it defaults to 'OTHER'.
	'dohex' is a flag which determines whether the eexec encrypted
	part should be written as hexadecimal or binary, but only if kind
	is 'OTHER'.
"""
from fontemon_blender_addon.fontTools.misc.py23 import *
from fontemon_blender_addon.fontTools.misc import eexec
from fontemon_blender_addon.fontTools.misc.macCreatorType import getMacCreatorAndType
import os
import re

__author__ = "jvr"
__version__ = "1.0b2"
DEBUG = 0


try:
	try:
		from Carbon import Res
	except ImportError:
		import Res  # MacPython < 2.2
except ImportError:
	haveMacSupport = 0
else:
	haveMacSupport = 1


class T1Error(Exception): pass


class T1Font(object):

	"""Type 1 font class.

	Uses a minimal interpeter that supports just about enough PS to parse
	Type 1 fonts.
	"""

	def __init__(self, path, encoding="ascii", kind=None):
		if kind is None:
			self.data, _ = read(path)
		elif kind == "LWFN":
			self.data = readLWFN(path)
		elif kind == "PFB":
			self.data = readPFB(path)
		elif kind == "OTHER":
			self.data = readOther(path)
		else:
			raise ValueError(kind)
		self.encoding = encoding

	def saveAs(self, path, type, dohex=False):
		write(path, self.getData(), type, dohex)

	def getData(self):
		# XXX Todo: if the data has been converted to Python object,
		# recreate the PS stream
		return self.data

	def getGlyphSet(self):
		"""Return a generic GlyphSet, which is a dict-like object
		mapping glyph names to glyph objects. The returned glyph objects
		have a .draw() method that supports the Pen protocol, and will
		have an attribute named 'width', but only *after* the .draw() method
		has been called.

		In the case of Type 1, the GlyphSet is simply the CharStrings dict.
		"""
		return self["CharStrings"]

	def __getitem__(self, key):
		if not hasattr(self, "font"):
			self.parse()
		return self.font[key]

	def parse(self):
		from fontemon_blender_addon.fontTools.misc import psLib
		from fontemon_blender_addon.fontTools.misc import psCharStrings
		self.font = psLib.suckfont(self.data, self.encoding)
		charStrings = self.font["CharStrings"]
		lenIV = self.font["Private"].get("lenIV", 4)
		assert lenIV >= 0
		subrs = self.font["Private"]["Subrs"]
		for glyphName, charString in charStrings.items():
			charString, R = eexec.decrypt(charString, 4330)
			charStrings[glyphName] = psCharStrings.T1CharString(charString[lenIV:],
					subrs=subrs)
		for i in range(len(subrs)):
			charString, R = eexec.decrypt(subrs[i], 4330)
			subrs[i] = psCharStrings.T1CharString(charString[lenIV:], subrs=subrs)
		del self.data


# low level T1 data read and write functions

def read(path, onlyHeader=False):
	"""reads any Type 1 font file, returns raw data"""
	_, ext = os.path.splitext(path)
	ext = ext.lower()
	creator, typ = getMacCreatorAndType(path)
	if typ == 'LWFN':
		return readLWFN(path, onlyHeader), 'LWFN'
	if ext == '.pfb':
		return readPFB(path, onlyHeader), 'PFB'
	else:
		return readOther(path), 'OTHER'

def write(path, data, kind='OTHER', dohex=False):
	assertType1(data)
	kind = kind.upper()
	try:
		os.remove(path)
	except os.error:
		pass
	err = 1
	try:
		if kind == 'LWFN':
			writeLWFN(path, data)
		elif kind == 'PFB':
			writePFB(path, data)
		else:
			writeOther(path, data, dohex)
		err = 0
	finally:
		if err and not DEBUG:
			try:
				os.remove(path)
			except os.error:
				pass


# -- internal --

LWFNCHUNKSIZE = 2000
HEXLINELENGTH = 80


def readLWFN(path, onlyHeader=False):
	"""reads an LWFN font file, returns raw data"""
	from fontemon_blender_addon.fontTools.misc.macRes import ResourceReader
	reader = ResourceReader(path)
	try:
		data = []
		for res in reader.get('POST', []):
			code = byteord(res.data[0])
			if byteord(res.data[1]) != 0:
				raise T1Error('corrupt LWFN file')
			if code in [1, 2]:
				if onlyHeader and code == 2:
					break
				data.append(res.data[2:])
			elif code in [3, 5]:
				break
			elif code == 4:
				with open(path, "rb") as f:
					data.append(f.read())
			elif code == 0:
				pass # comment, ignore
			else:
				raise T1Error('bad chunk code: ' + repr(code))
	finally:
		reader.close()
	data = bytesjoin(data)
	assertType1(data)
	return data

def readPFB(path, onlyHeader=False):
	"""reads a PFB font file, returns raw data"""
	data = []
	with open(path, "rb") as f:
		while True:
			if f.read(1) != bytechr(128):
				raise T1Error('corrupt PFB file')
			code = byteord(f.read(1))
			if code in [1, 2]:
				chunklen = stringToLong(f.read(4))
				chunk = f.read(chunklen)
				assert len(chunk) == chunklen
				data.append(chunk)
			elif code == 3:
				break
			else:
				raise T1Error('bad chunk code: ' + repr(code))
			if onlyHeader:
				break
	data = bytesjoin(data)
	assertType1(data)
	return data

def readOther(path):
	"""reads any (font) file, returns raw data"""
	with open(path, "rb") as f:
		data = f.read()
	assertType1(data)
	chunks = findEncryptedChunks(data)
	data = []
	for isEncrypted, chunk in chunks:
		if isEncrypted and isHex(chunk[:4]):
			data.append(deHexString(chunk))
		else:
			data.append(chunk)
	return bytesjoin(data)

# file writing tools

def writeLWFN(path, data):
	# Res.FSpCreateResFile was deprecated in OS X 10.5
	Res.FSpCreateResFile(path, "just", "LWFN", 0)
	resRef = Res.FSOpenResFile(path, 2)  # write-only
	try:
		Res.UseResFile(resRef)
		resID = 501
		chunks = findEncryptedChunks(data)
		for isEncrypted, chunk in chunks:
			if isEncrypted:
				code = 2
			else:
				code = 1
			while chunk:
				res = Res.Resource(bytechr(code) + '\0' + chunk[:LWFNCHUNKSIZE - 2])
				res.AddResource('POST', resID, '')
				chunk = chunk[LWFNCHUNKSIZE - 2:]
				resID = resID + 1
		res = Res.Resource(bytechr(5) + '\0')
		res.AddResource('POST', resID, '')
	finally:
		Res.CloseResFile(resRef)

def writePFB(path, data):
	chunks = findEncryptedChunks(data)
	with open(path, "wb") as f:
		for isEncrypted, chunk in chunks:
			if isEncrypted:
				code = 2
			else:
				code = 1
			f.write(bytechr(128) + bytechr(code))
			f.write(longToString(len(chunk)))
			f.write(chunk)
		f.write(bytechr(128) + bytechr(3))

def writeOther(path, data, dohex=False):
	chunks = findEncryptedChunks(data)
	with open(path, "wb") as f:
		hexlinelen = HEXLINELENGTH // 2
		for isEncrypted, chunk in chunks:
			if isEncrypted:
				code = 2
			else:
				code = 1
			if code == 2 and dohex:
				while chunk:
					f.write(eexec.hexString(chunk[:hexlinelen]))
					f.write(b'\r')
					chunk = chunk[hexlinelen:]
			else:
				f.write(chunk)


# decryption tools

EEXECBEGIN = b"currentfile eexec"
# The spec allows for 512 ASCII zeros interrupted by arbitrary whitespace to
# follow eexec
EEXECEND = re.compile(b'(0[ \t\r\n]*){512}', flags=re.M)
EEXECINTERNALEND = b"currentfile closefile"
EEXECBEGINMARKER = b"%-- eexec start\r"
EEXECENDMARKER = b"%-- eexec end\r"

_ishexRE = re.compile(b'[0-9A-Fa-f]*$')

def isHex(text):
	return _ishexRE.match(text) is not None


def decryptType1(data):
	chunks = findEncryptedChunks(data)
	data = []
	for isEncrypted, chunk in chunks:
		if isEncrypted:
			if isHex(chunk[:4]):
				chunk = deHexString(chunk)
			decrypted, R = eexec.decrypt(chunk, 55665)
			decrypted = decrypted[4:]
			if decrypted[-len(EEXECINTERNALEND)-1:-1] != EEXECINTERNALEND \
					and decrypted[-len(EEXECINTERNALEND)-2:-2] != EEXECINTERNALEND:
				raise T1Error("invalid end of eexec part")
			decrypted = decrypted[:-len(EEXECINTERNALEND)-2] + b'\r'
			data.append(EEXECBEGINMARKER + decrypted + EEXECENDMARKER)
		else:
			if chunk[-len(EEXECBEGIN)-1:-1] == EEXECBEGIN:
				data.append(chunk[:-len(EEXECBEGIN)-1])
			else:
				data.append(chunk)
	return bytesjoin(data)

def findEncryptedChunks(data):
	chunks = []
	while True:
		eBegin = data.find(EEXECBEGIN)
		if eBegin < 0:
			break
		eBegin = eBegin + len(EEXECBEGIN) + 1
		endMatch = EEXECEND.search(data, eBegin)
		if endMatch is None:
			raise T1Error("can't find end of eexec part")
		eEnd = endMatch.start()
		cypherText = data[eBegin:eEnd + 2]
		if isHex(cypherText[:4]):
			cypherText = deHexString(cypherText)
		plainText, R = eexec.decrypt(cypherText, 55665)
		eEndLocal = plainText.find(EEXECINTERNALEND)
		if eEndLocal < 0:
			raise T1Error("can't find end of eexec part")
		chunks.append((0, data[:eBegin]))
		chunks.append((1, cypherText[:eEndLocal + len(EEXECINTERNALEND) + 1]))
		data = data[eEnd:]
	chunks.append((0, data))
	return chunks

def deHexString(hexstring):
	return eexec.deHexString(bytesjoin(hexstring.split()))


# Type 1 assertion

_fontType1RE = re.compile(br"/FontType\s+1\s+def")

def assertType1(data):
	for head in [b'%!PS-AdobeFont', b'%!FontType1']:
		if data[:len(head)] == head:
			break
	else:
		raise T1Error("not a PostScript font")
	if not _fontType1RE.search(data):
		raise T1Error("not a Type 1 font")
	if data.find(b"currentfile eexec") < 0:
		raise T1Error("not an encrypted Type 1 font")
	# XXX what else?
	return data


# pfb helpers

def longToString(long):
	s = b""
	for i in range(4):
		s += bytechr((long & (0xff << (i * 8))) >> i * 8)
	return s

def stringToLong(s):
	if len(s) != 4:
		raise ValueError('string must be 4 bytes long')
	l = 0
	for i in range(4):
		l += byteord(s[i]) << (i * 8)
	return l
