from fontTools.misc import psCharStrings
from fontTools import ttLib
from fontTools.pens.basePen import NullPen
from fontTools.misc.roundTools import otRound
from fontTools.varLib.varStore import VarStoreInstancer

def _add_method(*clazzes):
	"""Returns a decorator function that adds a new method to one or
	more classes."""
	def wrapper(method):
		done = []
		for clazz in clazzes:
			if clazz in done: continue # Support multiple names of a clazz
			done.append(clazz)
			assert clazz.__name__ != 'DefaultTable', \
					'Oops, table class not found.'
			assert not hasattr(clazz, method.__name__), \
					"Oops, class '%s' has method '%s'." % (clazz.__name__,
									       method.__name__)
			setattr(clazz, method.__name__, method)
		return None
	return wrapper

def _uniq_sort(l):
	return sorted(set(l))

class _ClosureGlyphsT2Decompiler(psCharStrings.SimpleT2Decompiler):

	def __init__(self, components, localSubrs, globalSubrs):
		psCharStrings.SimpleT2Decompiler.__init__(self,
							  localSubrs,
							  globalSubrs)
		self.components = components

	def op_endchar(self, index):
		args = self.popall()
		if len(args) >= 4:
			from fontTools.encodings.StandardEncoding import StandardEncoding
			# endchar can do seac accent bulding; The T2 spec says it's deprecated,
			# but recent software that shall remain nameless does output it.
			adx, ady, bchar, achar = args[-4:]
			baseGlyph = StandardEncoding[bchar]
			accentGlyph = StandardEncoding[achar]
			self.components.add(baseGlyph)
			self.components.add(accentGlyph)

@_add_method(ttLib.getTableClass('CFF '))
def closure_glyphs(self, s):
	cff = self.cff
	assert len(cff) == 1
	font = cff[cff.keys()[0]]
	glyphSet = font.CharStrings

	decompose = s.glyphs
	while decompose:
		components = set()
		for g in decompose:
			if g not in glyphSet:
				continue
			gl = glyphSet[g]

			subrs = getattr(gl.private, "Subrs", [])
			decompiler = _ClosureGlyphsT2Decompiler(components, subrs, gl.globalSubrs)
			decompiler.execute(gl)
		components -= s.glyphs
		s.glyphs.update(components)
		decompose = components

def _empty_charstring(font, glyphName, isCFF2, ignoreWidth=False):
	c, fdSelectIndex = font.CharStrings.getItemAndSelector(glyphName)
	if isCFF2 or ignoreWidth:
		# CFF2 charstrings have no widths nor 'endchar' operators
		c.decompile()
		c.program = [] if isCFF2 else ['endchar']
	else:
		if hasattr(font, 'FDArray') and font.FDArray is not None:
			private = font.FDArray[fdSelectIndex].Private
		else:
			private = font.Private
		dfltWdX = private.defaultWidthX
		nmnlWdX = private.nominalWidthX
		pen = NullPen()
		c.draw(pen)  # this will set the charstring's width
		if c.width != dfltWdX:
			c.program = [c.width - nmnlWdX, 'endchar']
		else:
			c.program = ['endchar']

@_add_method(ttLib.getTableClass('CFF '))
def prune_pre_subset(self, font, options):
	cff = self.cff
	# CFF table must have one font only
	cff.fontNames = cff.fontNames[:1]

	if options.notdef_glyph and not options.notdef_outline:
		isCFF2 = cff.major > 1
		for fontname in cff.keys():
			font = cff[fontname]
			_empty_charstring(font, ".notdef", isCFF2=isCFF2)

	# Clear useless Encoding
	for fontname in cff.keys():
		font = cff[fontname]
		# https://github.com/fonttools/fonttools/issues/620
		font.Encoding = "StandardEncoding"

	return True # bool(cff.fontNames)

@_add_method(ttLib.getTableClass('CFF '))
def subset_glyphs(self, s):
	cff = self.cff
	for fontname in cff.keys():
		font = cff[fontname]
		cs = font.CharStrings

		glyphs = s.glyphs.union(s.glyphs_emptied)

		# Load all glyphs
		for g in font.charset:
			if g not in glyphs: continue
			c, _ = cs.getItemAndSelector(g)

		if cs.charStringsAreIndexed:
			indices = [i for i,g in enumerate(font.charset) if g in glyphs]
			csi = cs.charStringsIndex
			csi.items = [csi.items[i] for i in indices]
			del csi.file, csi.offsets
			if hasattr(font, "FDSelect"):
				sel = font.FDSelect
				# XXX We want to set sel.format to None, such that the
				# most compact format is selected. However, OTS was
				# broken and couldn't parse a FDSelect format 0 that
				# happened before CharStrings. As such, always force
				# format 3 until we fix cffLib to always generate
				# FDSelect after CharStrings.
				# https://github.com/khaledhosny/ots/pull/31
				#sel.format = None
				sel.format = 3
				sel.gidArray = [sel.gidArray[i] for i in indices]
			cs.charStrings = {g:indices.index(v)
					  for g,v in cs.charStrings.items()
					  if g in glyphs}
		else:
			cs.charStrings = {g:v
					  for g,v in cs.charStrings.items()
					  if g in glyphs}
		font.charset = [g for g in font.charset if g in glyphs]
		font.numGlyphs = len(font.charset)


		if s.options.retain_gids:
			isCFF2 = cff.major > 1
			for g in s.glyphs_emptied:
				_empty_charstring(font, g, isCFF2=isCFF2, ignoreWidth=True)


	return True # any(cff[fontname].numGlyphs for fontname in cff.keys())

@_add_method(psCharStrings.T2CharString)
def subset_subroutines(self, subrs, gsubrs):
	p = self.program
	for i in range(1, len(p)):
		if p[i] == 'callsubr':
			assert isinstance(p[i-1], int)
			p[i-1] = subrs._used.index(p[i-1] + subrs._old_bias) - subrs._new_bias
		elif p[i] == 'callgsubr':
			assert isinstance(p[i-1], int)
			p[i-1] = gsubrs._used.index(p[i-1] + gsubrs._old_bias) - gsubrs._new_bias

@_add_method(psCharStrings.T2CharString)
def drop_hints(self):
	hints = self._hints

	if hints.deletions:
		p = self.program
		for idx in reversed(hints.deletions):
			del p[idx-2:idx]

	if hints.has_hint:
		assert not hints.deletions or hints.last_hint <= hints.deletions[0]
		self.program = self.program[hints.last_hint:]
		if not self.program:
			# TODO CFF2 no need for endchar.
			self.program.append('endchar')
		if hasattr(self, 'width'):
			# Insert width back if needed
			if self.width != self.private.defaultWidthX:
				# For CFF2 charstrings, this should never happen
				assert self.private.defaultWidthX is not None, "CFF2 CharStrings must not have an initial width value"
				self.program.insert(0, self.width - self.private.nominalWidthX)

	if hints.has_hintmask:
		i = 0
		p = self.program
		while i < len(p):
			if p[i] in ['hintmask', 'cntrmask']:
				assert i + 1 <= len(p)
				del p[i:i+2]
				continue
			i += 1

	assert len(self.program)

	del self._hints

class _MarkingT2Decompiler(psCharStrings.SimpleT2Decompiler):

	def __init__(self, localSubrs, globalSubrs, private):
		psCharStrings.SimpleT2Decompiler.__init__(self,
							  localSubrs,
							  globalSubrs,
							  private)
		for subrs in [localSubrs, globalSubrs]:
			if subrs and not hasattr(subrs, "_used"):
				subrs._used = set()

	def op_callsubr(self, index):
		self.localSubrs._used.add(self.operandStack[-1]+self.localBias)
		psCharStrings.SimpleT2Decompiler.op_callsubr(self, index)

	def op_callgsubr(self, index):
		self.globalSubrs._used.add(self.operandStack[-1]+self.globalBias)
		psCharStrings.SimpleT2Decompiler.op_callgsubr(self, index)

class _DehintingT2Decompiler(psCharStrings.T2WidthExtractor):

	class Hints(object):
		def __init__(self):
			# Whether calling this charstring produces any hint stems
			# Note that if a charstring starts with hintmask, it will
			# have has_hint set to True, because it *might* produce an
			# implicit vstem if called under certain conditions.
			self.has_hint = False
			# Index to start at to drop all hints
			self.last_hint = 0
			# Index up to which we know more hints are possible.
			# Only relevant if status is 0 or 1.
			self.last_checked = 0
			# The status means:
			# 0: after dropping hints, this charstring is empty
			# 1: after dropping hints, there may be more hints
			#	continuing after this, or there might be
			#	other things.  Not clear yet.
			# 2: no more hints possible after this charstring
			self.status = 0
			# Has hintmask instructions; not recursive
			self.has_hintmask = False
			# List of indices of calls to empty subroutines to remove.
			self.deletions = []
		pass

	def __init__(self, css, localSubrs, globalSubrs, nominalWidthX, defaultWidthX, private=None):
		self._css = css
		psCharStrings.T2WidthExtractor.__init__(
			self, localSubrs, globalSubrs, nominalWidthX, defaultWidthX)
		self.private = private

	def execute(self, charString):
		old_hints = charString._hints if hasattr(charString, '_hints') else None
		charString._hints = self.Hints()

		psCharStrings.T2WidthExtractor.execute(self, charString)

		hints = charString._hints

		if hints.has_hint or hints.has_hintmask:
			self._css.add(charString)

		if hints.status != 2:
			# Check from last_check, make sure we didn't have any operators.
			for i in range(hints.last_checked, len(charString.program) - 1):
				if isinstance(charString.program[i], str):
					hints.status = 2
					break
				else:
					hints.status = 1 # There's *something* here
			hints.last_checked = len(charString.program)

		if old_hints:
			assert hints.__dict__ == old_hints.__dict__

	def op_callsubr(self, index):
		subr = self.localSubrs[self.operandStack[-1]+self.localBias]
		psCharStrings.T2WidthExtractor.op_callsubr(self, index)
		self.processSubr(index, subr)

	def op_callgsubr(self, index):
		subr = self.globalSubrs[self.operandStack[-1]+self.globalBias]
		psCharStrings.T2WidthExtractor.op_callgsubr(self, index)
		self.processSubr(index, subr)

	def op_hstem(self, index):
		psCharStrings.T2WidthExtractor.op_hstem(self, index)
		self.processHint(index)
	def op_vstem(self, index):
		psCharStrings.T2WidthExtractor.op_vstem(self, index)
		self.processHint(index)
	def op_hstemhm(self, index):
		psCharStrings.T2WidthExtractor.op_hstemhm(self, index)
		self.processHint(index)
	def op_vstemhm(self, index):
		psCharStrings.T2WidthExtractor.op_vstemhm(self, index)
		self.processHint(index)
	def op_hintmask(self, index):
		rv = psCharStrings.T2WidthExtractor.op_hintmask(self, index)
		self.processHintmask(index)
		return rv
	def op_cntrmask(self, index):
		rv = psCharStrings.T2WidthExtractor.op_cntrmask(self, index)
		self.processHintmask(index)
		return rv

	def processHintmask(self, index):
		cs = self.callingStack[-1]
		hints = cs._hints
		hints.has_hintmask = True
		if hints.status != 2:
			# Check from last_check, see if we may be an implicit vstem
			for i in range(hints.last_checked, index - 1):
				if isinstance(cs.program[i], str):
					hints.status = 2
					break
			else:
				# We are an implicit vstem
				hints.has_hint = True
				hints.last_hint = index + 1
				hints.status = 0
		hints.last_checked = index + 1

	def processHint(self, index):
		cs = self.callingStack[-1]
		hints = cs._hints
		hints.has_hint = True
		hints.last_hint = index
		hints.last_checked = index

	def processSubr(self, index, subr):
		cs = self.callingStack[-1]
		hints = cs._hints
		subr_hints = subr._hints

		# Check from last_check, make sure we didn't have
		# any operators.
		if hints.status != 2:
			for i in range(hints.last_checked, index - 1):
				if isinstance(cs.program[i], str):
					hints.status = 2
					break
			hints.last_checked = index

		if hints.status != 2:
			if subr_hints.has_hint:
				hints.has_hint = True

				# Decide where to chop off from
				if subr_hints.status == 0:
					hints.last_hint = index
				else:
					hints.last_hint = index - 2  # Leave the subr call in

		elif subr_hints.status == 0:
			hints.deletions.append(index)

		hints.status = max(hints.status, subr_hints.status)

class StopHintCountEvent(Exception):
	pass




class _DesubroutinizingT2Decompiler(psCharStrings.SimpleT2Decompiler):
	stop_hintcount_ops = ("op_hintmask", "op_cntrmask", "op_rmoveto", "op_hmoveto",
							"op_vmoveto")

	def __init__(self, localSubrs, globalSubrs, private=None):
		psCharStrings.SimpleT2Decompiler.__init__(self, localSubrs, globalSubrs,
												private)

	def execute(self, charString):
		self.need_hintcount = True  # until proven otherwise
		for op_name in self.stop_hintcount_ops:
			setattr(self, op_name, self.stop_hint_count)

		if hasattr(charString, '_desubroutinized'):
			# If a charstring has already been desubroutinized, we will still
			# need to execute it if we need to count hints in order to
			# compute the byte length for mask arguments, and haven't finished
			# counting hints pairs.
			if self.need_hintcount and self.callingStack:
				try:
					psCharStrings.SimpleT2Decompiler.execute(self, charString)
				except StopHintCountEvent:
					del self.callingStack[-1]
			return

		charString._patches = []
		psCharStrings.SimpleT2Decompiler.execute(self, charString)
		desubroutinized = charString.program[:]
		for idx, expansion in reversed(charString._patches):
			assert idx >= 2
			assert desubroutinized[idx - 1] in ['callsubr', 'callgsubr'], desubroutinized[idx - 1]
			assert type(desubroutinized[idx - 2]) == int
			if expansion[-1] == 'return':
				expansion = expansion[:-1]
			desubroutinized[idx-2:idx] = expansion
		if not self.private.in_cff2:
			if 'endchar' in desubroutinized:
				# Cut off after first endchar
				desubroutinized = desubroutinized[:desubroutinized.index('endchar') + 1]
			else:
				if not len(desubroutinized) or desubroutinized[-1] != 'return':
					desubroutinized.append('return')

		charString._desubroutinized = desubroutinized
		del charString._patches

	def op_callsubr(self, index):
		subr = self.localSubrs[self.operandStack[-1]+self.localBias]
		psCharStrings.SimpleT2Decompiler.op_callsubr(self, index)
		self.processSubr(index, subr)

	def op_callgsubr(self, index):
		subr = self.globalSubrs[self.operandStack[-1]+self.globalBias]
		psCharStrings.SimpleT2Decompiler.op_callgsubr(self, index)
		self.processSubr(index, subr)

	def stop_hint_count(self, *args):
		self.need_hintcount = False
		for op_name in self.stop_hintcount_ops:
			setattr(self, op_name, None)
		cs = self.callingStack[-1]
		if hasattr(cs, '_desubroutinized'):
			raise StopHintCountEvent()

	def op_hintmask(self, index):
		psCharStrings.SimpleT2Decompiler.op_hintmask(self, index)
		if self.need_hintcount:
			self.stop_hint_count()

	def processSubr(self, index, subr):
		cs = self.callingStack[-1]
		if not hasattr(cs, '_desubroutinized'):
			cs._patches.append((index, subr._desubroutinized))


@_add_method(ttLib.getTableClass('CFF '))
def prune_post_subset(self, ttfFont, options):
	cff = self.cff
	for fontname in cff.keys():
		font = cff[fontname]
		cs = font.CharStrings

		# Drop unused FontDictionaries
		if hasattr(font, "FDSelect"):
			sel = font.FDSelect
			indices = _uniq_sort(sel.gidArray)
			sel.gidArray = [indices.index (ss) for ss in sel.gidArray]
			arr = font.FDArray
			arr.items = [arr[i] for i in indices]
			del arr.file, arr.offsets

	# Desubroutinize if asked for
	if options.desubroutinize:
		self.desubroutinize()

	# Drop hints if not needed
	if not options.hinting:
		self.remove_hints()
	elif not options.desubroutinize:
		self.remove_unused_subroutines()
	return True


def _delete_empty_subrs(private_dict):
	if hasattr(private_dict, 'Subrs') and not private_dict.Subrs:
		if 'Subrs' in private_dict.rawDict:
			del private_dict.rawDict['Subrs']
		del private_dict.Subrs

@_add_method(ttLib.getTableClass('CFF '))
def desubroutinize(self):
	cff = self.cff
	for fontname in cff.keys():
		font = cff[fontname]
		cs = font.CharStrings
		for g in font.charset:
			c, _ = cs.getItemAndSelector(g)
			c.decompile()
			subrs = getattr(c.private, "Subrs", [])
			decompiler = _DesubroutinizingT2Decompiler(subrs, c.globalSubrs, c.private)
			decompiler.execute(c)
			c.program = c._desubroutinized
			del c._desubroutinized
		# Delete all the local subrs
		if hasattr(font, 'FDArray'):
			for fd in font.FDArray:
				pd = fd.Private
				if hasattr(pd, 'Subrs'):
					del pd.Subrs
				if 'Subrs' in pd.rawDict:
					del pd.rawDict['Subrs']
		else:
			pd = font.Private
			if hasattr(pd, 'Subrs'):
				del pd.Subrs
			if 'Subrs' in pd.rawDict:
				del pd.rawDict['Subrs']
	# as well as the global subrs
	cff.GlobalSubrs.clear()


@_add_method(ttLib.getTableClass('CFF '))
def remove_hints(self):
	cff = self.cff
	for fontname in cff.keys():
		font = cff[fontname]
		cs = font.CharStrings
		# This can be tricky, but doesn't have to. What we do is:
		#
		# - Run all used glyph charstrings and recurse into subroutines,
		# - For each charstring (including subroutines), if it has any
		#   of the hint stem operators, we mark it as such.
		#   Upon returning, for each charstring we note all the
		#   subroutine calls it makes that (recursively) contain a stem,
		# - Dropping hinting then consists of the following two ops:
		#   * Drop the piece of the program in each charstring before the
		#     last call to a stem op or a stem-calling subroutine,
		#   * Drop all hintmask operations.
		# - It's trickier... A hintmask right after hints and a few numbers
		#    will act as an implicit vstemhm. As such, we track whether
		#    we have seen any non-hint operators so far and do the right
		#    thing, recursively... Good luck understanding that :(
		css = set()
		for g in font.charset:
			c, _ = cs.getItemAndSelector(g)
			c.decompile()
			subrs = getattr(c.private, "Subrs", [])
			decompiler = _DehintingT2Decompiler(css, subrs, c.globalSubrs,
								c.private.nominalWidthX,
								c.private.defaultWidthX,
								c.private)
			decompiler.execute(c)
			c.width = decompiler.width
		for charstring in css:
			charstring.drop_hints()
		del css

		# Drop font-wide hinting values
		all_privs = []
		if hasattr(font, 'FDArray'):
			all_privs.extend(fd.Private for fd in font.FDArray)
		else:
			all_privs.append(font.Private)
		for priv in all_privs:
			for k in ['BlueValues', 'OtherBlues',
				  'FamilyBlues', 'FamilyOtherBlues',
				  'BlueScale', 'BlueShift', 'BlueFuzz',
				  'StemSnapH', 'StemSnapV', 'StdHW', 'StdVW',
				  'ForceBold', 'LanguageGroup', 'ExpansionFactor']:
				if hasattr(priv, k):
					setattr(priv, k, None)
	self.remove_unused_subroutines()


@_add_method(ttLib.getTableClass('CFF '))
def remove_unused_subroutines(self):
	cff = self.cff
	for fontname in cff.keys():
		font = cff[fontname]
		cs = font.CharStrings
		# Renumber subroutines to remove unused ones

		# Mark all used subroutines
		for g in font.charset:
			c, _ = cs.getItemAndSelector(g)
			subrs = getattr(c.private, "Subrs", [])
			decompiler = _MarkingT2Decompiler(subrs, c.globalSubrs, c.private)
			decompiler.execute(c)

		all_subrs = [font.GlobalSubrs]
		if hasattr(font, 'FDArray'):
			all_subrs.extend(fd.Private.Subrs for fd in font.FDArray if hasattr(fd.Private, 'Subrs') and fd.Private.Subrs)
		elif hasattr(font.Private, 'Subrs') and font.Private.Subrs:
			all_subrs.append(font.Private.Subrs)

		subrs = set(subrs) # Remove duplicates

		# Prepare
		for subrs in all_subrs:
			if not hasattr(subrs, '_used'):
				subrs._used = set()
			subrs._used = _uniq_sort(subrs._used)
			subrs._old_bias = psCharStrings.calcSubrBias(subrs)
			subrs._new_bias = psCharStrings.calcSubrBias(subrs._used)

		# Renumber glyph charstrings
		for g in font.charset:
			c, _ = cs.getItemAndSelector(g)
			subrs = getattr(c.private, "Subrs", [])
			c.subset_subroutines (subrs, font.GlobalSubrs)

		# Renumber subroutines themselves
		for subrs in all_subrs:
			if subrs == font.GlobalSubrs:
				if not hasattr(font, 'FDArray') and hasattr(font.Private, 'Subrs'):
					local_subrs = font.Private.Subrs
				else:
					local_subrs = []
			else:
				local_subrs = subrs

			subrs.items = [subrs.items[i] for i in subrs._used]
			if hasattr(subrs, 'file'):
				del subrs.file
			if hasattr(subrs, 'offsets'):
				del subrs.offsets

			for subr in subrs.items:
				subr.subset_subroutines (local_subrs, font.GlobalSubrs)

		# Delete local SubrsIndex if empty
		if hasattr(font, 'FDArray'):
			for fd in font.FDArray:
				_delete_empty_subrs(fd.Private)
		else:
			_delete_empty_subrs(font.Private)

		# Cleanup
		for subrs in all_subrs:
			del subrs._used, subrs._old_bias, subrs._new_bias
