"""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'.
"""
import fontTools
from fontTools.misc import eexec
from fontTools.misc.macCreatorType import getMacCreatorAndType
from fontTools.misc.textTools import bytechr, byteord, bytesjoin, tobytes
from fontTools.misc.psOperators import _type1_pre_eexec_order, _type1_fontinfo_order, _type1_post_eexec_order
from fontTools.encodings.StandardEncoding import StandardEncoding
import os
import re

__author__ = "jvr"
__version__ = "1.0b3"
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):
		if not hasattr(self, "data"):
			self.data = self.createData()
		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 fontTools.misc import psLib
		from 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

	def createData(self):
		sf = self.font

		eexec_began = False
		eexec_dict = {}
		lines = []
		lines.extend([self._tobytes(f"%!FontType1-1.1: {sf['FontName']}"),
					  self._tobytes(f"%t1Font: ({fontTools.version})"),
					  self._tobytes(f"%%BeginResource: font {sf['FontName']}")])
		# follow t1write.c:writeRegNameKeyedFont
		size = 3 # Headroom for new key addition
		size += 1 # FontMatrix is always counted
		size += 1 + 1 # Private, CharStings
		for key in font_dictionary_keys:
			size += int(key in sf)
		lines.append(self._tobytes(f"{size} dict dup begin"))

		for key, value in sf.items():
			if eexec_began:
				eexec_dict[key] = value
				continue

			if key == "FontInfo":
				fi = sf["FontInfo"]
				# follow t1write.c:writeFontInfoDict
				size = 3 # Headroom for new key addition
				for subkey in FontInfo_dictionary_keys:
					size += int(subkey in fi)
				lines.append(self._tobytes(f"/FontInfo {size} dict dup begin"))

				for subkey, subvalue in fi.items():
					lines.extend(self._make_lines(subkey, subvalue))
				lines.append(b"end def")
			elif key in _type1_post_eexec_order: # usually 'Private'
				eexec_dict[key] = value
				eexec_began = True
			else:
				lines.extend(self._make_lines(key, value))
		lines.append(b"end")
		eexec_portion = self.encode_eexec(eexec_dict)
		lines.append(bytesjoin([b"currentfile eexec ", eexec_portion]))

		for _ in range(8):
			lines.append(self._tobytes("0"*64))
		lines.extend([b"cleartomark",
					  b"%%EndResource",
					  b"%%EOF"])

		data = bytesjoin(lines, "\n")
		return data

	def encode_eexec(self, eexec_dict):
		lines = []

		# '-|', '|-', '|'
		RD_key, ND_key, NP_key = None, None, None

		for key, value in eexec_dict.items():
			if key == "Private":
				pr = eexec_dict["Private"]
				# follow t1write.c:writePrivateDict
				size = 3 # for RD, ND, NP
				for subkey in Private_dictionary_keys:
					size += int(subkey in pr)
				lines.append(b"dup /Private")
				lines.append(self._tobytes(f"{size} dict dup begin"))
				for subkey, subvalue in pr.items():
					if not RD_key and subvalue == RD_value:
						RD_key = subkey
					elif not ND_key and subvalue == ND_value:
						ND_key = subkey
					elif not NP_key and subvalue == PD_value:
						NP_key = subkey

					if subkey == 'OtherSubrs':
						# XXX: assert that no flex hint is used
						lines.append(self._tobytes(hintothers))
					elif subkey == "Subrs":
						# XXX: standard Subrs only
						lines.append(b"/Subrs 5 array")
						for i, subr_bin in enumerate(std_subrs):
							encrypted_subr, R = eexec.encrypt(bytesjoin([char_IV, subr_bin]), 4330)
							lines.append(bytesjoin([self._tobytes(f"dup {i} {len(encrypted_subr)} {RD_key} "), encrypted_subr, self._tobytes(f" {NP_key}")]))
						lines.append(b'def')

						lines.append(b"put")
					else:
						lines.extend(self._make_lines(subkey, subvalue))
			elif key == "CharStrings":
				lines.append(b"dup /CharStrings")
				lines.append(self._tobytes(f"{len(eexec_dict['CharStrings'])} dict dup begin"))
				for glyph_name, char_bin in eexec_dict["CharStrings"].items():
					char_bin.compile()
					encrypted_char, R = eexec.encrypt(bytesjoin([char_IV, char_bin.bytecode]), 4330)
					lines.append(bytesjoin([self._tobytes(f"/{glyph_name} {len(encrypted_char)} {RD_key} "), encrypted_char, self._tobytes(f" {ND_key}")]))
				lines.append(b"end put")
			else:
				lines.extend(self._make_lines(key, value))

		lines.extend([b"end",
					  b"dup /FontName get exch definefont pop",
					  b"mark",
					  b"currentfile closefile\n"])

		eexec_portion = bytesjoin(lines, "\n")
		encrypted_eexec, R = eexec.encrypt(bytesjoin([eexec_IV, eexec_portion]), 55665)

		return encrypted_eexec

	def _make_lines(self, key, value):
		if key == "FontName":
			return [self._tobytes(f"/{key} /{value} def")]
		if key in ["isFixedPitch", "ForceBold", "RndStemUp"]:
			return [self._tobytes(f"/{key} {'true' if value else 'false'} def")]
		elif key == "Encoding":
			if value == StandardEncoding:
				return [self._tobytes(f"/{key} StandardEncoding def")]
			else:
				# follow fontTools.misc.psOperators._type1_Encoding_repr
				lines = []
				lines.append(b"/Encoding 256 array")
				lines.append(b"0 1 255 {1 index exch /.notdef put} for")
				for i in range(256):
					name = value[i]
					if name != ".notdef":
						lines.append(self._tobytes(f"dup {i} /{name} put"))
				lines.append(b"def")
				return lines
		if isinstance(value, str):
			return [self._tobytes(f"/{key} ({value}) def")]
		elif isinstance(value, bool):
			return [self._tobytes(f"/{key} {'true' if value else 'false'} def")]
		elif isinstance(value, list):
			return [self._tobytes(f"/{key} [{' '.join(str(v) for v in value)}] def")]
		elif isinstance(value, tuple):
			return [self._tobytes(f"/{key} {{{' '.join(str(v) for v in value)}}} def")]
		else:
			return [self._tobytes(f"/{key} {value} def")]

	def _tobytes(self, s, errors="strict"):
		return tobytes(s, self.encoding, errors)


# 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 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


# PS stream helpers

font_dictionary_keys = list(_type1_pre_eexec_order)
# t1write.c:writeRegNameKeyedFont
# always counts following keys
font_dictionary_keys.remove("FontMatrix")

FontInfo_dictionary_keys = list(_type1_fontinfo_order)
# extend because AFDKO tx may use following keys
FontInfo_dictionary_keys.extend([
	"FSType",
	"Copyright",
])

Private_dictionary_keys = [
	# We don't know what names will be actually used.
	# "RD",
	# "ND",
	# "NP",
	"Subrs",
	"OtherSubrs",
	"UniqueID",
	"BlueValues",
	"OtherBlues",
	"FamilyBlues",
	"FamilyOtherBlues",
	"BlueScale",
	"BlueShift",
	"BlueFuzz",
	"StdHW",
	"StdVW",
	"StemSnapH",
	"StemSnapV",
	"ForceBold",
	"LanguageGroup",
	"password",
	"lenIV",
	"MinFeature",
	"RndStemUp",
]

# t1write_hintothers.h
hintothers = """/OtherSubrs[{}{}{}{systemdict/internaldict known not{pop 3}{1183615869
systemdict/internaldict get exec dup/startlock known{/startlock get exec}{dup
/strtlck known{/strtlck get exec}{pop 3}ifelse}ifelse}ifelse}executeonly]def"""
# t1write.c:saveStdSubrs
std_subrs = [
	# 3 0 callother pop pop setcurrentpoint return
	b"\x8e\x8b\x0c\x10\x0c\x11\x0c\x11\x0c\x21\x0b",
	# 0 1 callother return
	b"\x8b\x8c\x0c\x10\x0b",
	# 0 2 callother return
	b"\x8b\x8d\x0c\x10\x0b",
	# return
	b"\x0b",
	# 3 1 3 callother pop callsubr return
	b"\x8e\x8c\x8e\x0c\x10\x0c\x11\x0a\x0b"
]
# follow t1write.c:writeRegNameKeyedFont
eexec_IV = b"cccc"
char_IV = b"\x0c\x0c\x0c\x0c"
RD_value = ("string", "currentfile", "exch", "readstring", "pop")
ND_value = ("def",)
PD_value = ("put",)
