from collections import namedtuple
from fontTools.cffLib import (
	maxStackLimit,
	TopDictIndex,
	buildOrder,
	topDictOperators,
	topDictOperators2,
	privateDictOperators,
	privateDictOperators2,
	FDArrayIndex,
	FontDict,
	VarStoreData
)
from io import BytesIO
from fontTools.cffLib.specializer import (
	specializeCommands, commandsToProgram)
from fontTools.ttLib import newTable
from fontTools import varLib
from fontTools.varLib.models import allEqual
from fontTools.misc.psCharStrings import T2CharString, T2OutlineExtractor
from fontTools.pens.t2CharStringPen import T2CharStringPen, t2c_round

from .errors import VarLibCFFDictMergeError, VarLibCFFPointTypeMergeError, VarLibMergeError


# Backwards compatibility
MergeDictError = VarLibCFFDictMergeError
MergeTypeError = VarLibCFFPointTypeMergeError


def addCFFVarStore(varFont, varModel, varDataList, masterSupports):
	fvarTable = varFont['fvar']
	axisKeys = [axis.axisTag for axis in fvarTable.axes]
	varTupleList = varLib.builder.buildVarRegionList(masterSupports, axisKeys)
	varStoreCFFV = varLib.builder.buildVarStore(varTupleList, varDataList)

	topDict = varFont['CFF2'].cff.topDictIndex[0]
	topDict.VarStore = VarStoreData(otVarStore=varStoreCFFV)
	if topDict.FDArray[0].vstore is None:
		fdArray = topDict.FDArray
		for fontDict in fdArray:
			if hasattr(fontDict, "Private"):
				fontDict.Private.vstore = topDict.VarStore


def lib_convertCFFToCFF2(cff, otFont):
	# This assumes a decompiled CFF table.
	cff2GetGlyphOrder = cff.otFont.getGlyphOrder
	topDictData = TopDictIndex(None, cff2GetGlyphOrder, None)
	topDictData.items = cff.topDictIndex.items
	cff.topDictIndex = topDictData
	topDict = topDictData[0]
	if hasattr(topDict, 'Private'):
		privateDict = topDict.Private
	else:
		privateDict = None
	opOrder = buildOrder(topDictOperators2)
	topDict.order = opOrder
	topDict.cff2GetGlyphOrder = cff2GetGlyphOrder
	if not hasattr(topDict, "FDArray"):
		fdArray = topDict.FDArray = FDArrayIndex()
		fdArray.strings = None
		fdArray.GlobalSubrs = topDict.GlobalSubrs
		topDict.GlobalSubrs.fdArray = fdArray
		charStrings = topDict.CharStrings
		if charStrings.charStringsAreIndexed:
			charStrings.charStringsIndex.fdArray = fdArray
		else:
			charStrings.fdArray = fdArray
		fontDict = FontDict()
		fontDict.setCFF2(True)
		fdArray.append(fontDict)
		fontDict.Private = privateDict
		privateOpOrder = buildOrder(privateDictOperators2)
		if privateDict is not None:
			for entry in privateDictOperators:
				key = entry[1]
				if key not in privateOpOrder:
					if key in privateDict.rawDict:
						# print "Removing private dict", key
						del privateDict.rawDict[key]
					if hasattr(privateDict, key):
						delattr(privateDict, key)
						# print "Removing privateDict attr", key
	else:
		# clean up the PrivateDicts in the fdArray
		fdArray = topDict.FDArray
		privateOpOrder = buildOrder(privateDictOperators2)
		for fontDict in fdArray:
			fontDict.setCFF2(True)
			for key in list(fontDict.rawDict.keys()):
				if key not in fontDict.order:
					del fontDict.rawDict[key]
					if hasattr(fontDict, key):
						delattr(fontDict, key)

			privateDict = fontDict.Private
			for entry in privateDictOperators:
				key = entry[1]
				if key not in privateOpOrder:
					if key in privateDict.rawDict:
						# print "Removing private dict", key
						del privateDict.rawDict[key]
					if hasattr(privateDict, key):
						delattr(privateDict, key)
						# print "Removing privateDict attr", key
	# Now delete up the decrecated topDict operators from CFF 1.0
	for entry in topDictOperators:
		key = entry[1]
		if key not in opOrder:
			if key in topDict.rawDict:
				del topDict.rawDict[key]
			if hasattr(topDict, key):
				delattr(topDict, key)

	# At this point, the Subrs and Charstrings are all still T2Charstring class
	# easiest to fix this by compiling, then decompiling again
	cff.major = 2
	file = BytesIO()
	cff.compile(file, otFont, isCFF2=True)
	file.seek(0)
	cff.decompile(file, otFont, isCFF2=True)


def convertCFFtoCFF2(varFont):
	# Convert base font to a single master CFF2 font.
	cffTable = varFont['CFF ']
	lib_convertCFFToCFF2(cffTable.cff, varFont)
	newCFF2 = newTable("CFF2")
	newCFF2.cff = cffTable.cff
	varFont['CFF2'] = newCFF2
	del varFont['CFF ']


def conv_to_int(num):
	if isinstance(num, float) and num.is_integer():
		return int(num)
	return num


pd_blend_fields = ("BlueValues", "OtherBlues", "FamilyBlues",
				   "FamilyOtherBlues", "BlueScale", "BlueShift",
				   "BlueFuzz", "StdHW", "StdVW", "StemSnapH",
				   "StemSnapV")


def get_private(regionFDArrays, fd_index, ri, fd_map):
	region_fdArray = regionFDArrays[ri]
	region_fd_map = fd_map[fd_index]
	if ri in region_fd_map:
		region_fdIndex = region_fd_map[ri]
		private = region_fdArray[region_fdIndex].Private
	else:
		private = None
	return private


def merge_PrivateDicts(top_dicts, vsindex_dict, var_model, fd_map):
	"""
	I step through the FontDicts in the FDArray of the varfont TopDict.
	For each varfont FontDict:
		step through each key in FontDict.Private.
		For each key, step through each relevant source font Private dict, and
		build a list of values to blend.
	The 'relevant' source fonts are selected by first getting the right
	submodel using vsindex_dict[vsindex]. The indices of the
	subModel.locations are mapped to source font list indices by
	assuming the latter order is the same as the order of the
	var_model.locations. I can then get the index of each subModel
	location in the list of var_model.locations.
	"""

	topDict = top_dicts[0]
	region_top_dicts = top_dicts[1:]
	if hasattr(region_top_dicts[0], 'FDArray'):
		regionFDArrays = [fdTopDict.FDArray for fdTopDict in region_top_dicts]
	else:
		regionFDArrays = [[fdTopDict] for fdTopDict in region_top_dicts]
	for fd_index, font_dict in enumerate(topDict.FDArray):
		private_dict = font_dict.Private
		vsindex = getattr(private_dict, 'vsindex', 0)
		# At the moment, no PrivateDict has a vsindex key, but let's support
		# how it should work. See comment at end of
		# merge_charstrings() - still need to optimize use of vsindex.
		sub_model, _ = vsindex_dict[vsindex]
		master_indices = []
		for loc in sub_model.locations[1:]:
			i = var_model.locations.index(loc) - 1
			master_indices.append(i)
		pds = [private_dict]
		last_pd = private_dict
		for ri in master_indices:
			pd = get_private(regionFDArrays, fd_index, ri, fd_map)
			# If the region font doesn't have this FontDict, just reference
			# the last one used.
			if pd is None:
				pd = last_pd
			else:
				last_pd = pd
			pds.append(pd)
		num_masters = len(pds)
		for key, value in private_dict.rawDict.items():
			dataList = []
			if key not in pd_blend_fields:
				continue
			if isinstance(value, list):
				try:
					values = [pd.rawDict[key] for pd in pds]
				except KeyError:
					print(
						"Warning: {key} in default font Private dict is "
						"missing from another font, and was "
						"discarded.".format(key=key))
					continue
				try:
					values = zip(*values)
				except IndexError:
					raise VarLibCFFDictMergeError(key, value, values)
				"""
				Row 0 contains the first  value from each master.
				Convert each row from absolute values to relative
				values from the previous row.
				e.g for three masters,	a list of values was:
				master 0 OtherBlues = [-217,-205]
				master 1 OtherBlues = [-234,-222]
				master 1 OtherBlues = [-188,-176]
				The call to zip() converts this to:
				[(-217, -234, -188), (-205, -222, -176)]
				and is converted finally to:
				OtherBlues = [[-217, 17.0, 46.0], [-205, 0.0, 0.0]]
				"""
				prev_val_list = [0] * num_masters
				any_points_differ = False
				for val_list in values:
					rel_list = [(val - prev_val_list[i]) for (
							i, val) in enumerate(val_list)]
					if (not any_points_differ) and not allEqual(rel_list):
						any_points_differ = True
					prev_val_list = val_list
					deltas = sub_model.getDeltas(rel_list)
					# For PrivateDict BlueValues, the default font
					# values are absolute, not relative to the prior value.
					deltas[0] = val_list[0]
					dataList.append(deltas)
				# If there are no blend values,then
				# we can collapse the blend lists.
				if not any_points_differ:
					dataList = [data[0] for data in dataList]
			else:
				values = [pd.rawDict[key] for pd in pds]
				if not allEqual(values):
					dataList = sub_model.getDeltas(values)
				else:
					dataList = values[0]

			# Convert numbers with no decimal part to an int
			if isinstance(dataList, list):
				for i, item in enumerate(dataList):
					if isinstance(item, list):
						for j, jtem in enumerate(item):
							dataList[i][j] = conv_to_int(jtem)
					else:
						dataList[i] = conv_to_int(item)
			else:
				dataList = conv_to_int(dataList)

			private_dict.rawDict[key] = dataList


def _cff_or_cff2(font):
	if "CFF " in font:
		return font["CFF "]
	return font["CFF2"]


def getfd_map(varFont, fonts_list):
	""" Since a subset source font may have fewer FontDicts in their
	FDArray than the default font, we have to match up the FontDicts in
	the different fonts . We do this with the FDSelect array, and by
	assuming that the same glyph will reference  matching FontDicts in
	each source font. We return a mapping from fdIndex in the default
	font to a dictionary which maps each master list index of each
	region font to the equivalent fdIndex in the region font."""
	fd_map = {}
	default_font = fonts_list[0]
	region_fonts = fonts_list[1:]
	num_regions = len(region_fonts)
	topDict = _cff_or_cff2(default_font).cff.topDictIndex[0]
	if not hasattr(topDict, 'FDSelect'):
		# All glyphs reference only one FontDict.
		# Map the FD index for regions to index 0.
		fd_map[0] = {ri:0 for ri in range(num_regions)}
		return fd_map

	gname_mapping = {}
	default_fdSelect = topDict.FDSelect
	glyphOrder = default_font.getGlyphOrder()
	for gid, fdIndex in enumerate(default_fdSelect):
		gname_mapping[glyphOrder[gid]] = fdIndex
		if fdIndex not in fd_map:
			fd_map[fdIndex] = {}
	for ri, region_font in enumerate(region_fonts):
		region_glyphOrder = region_font.getGlyphOrder()
		region_topDict = _cff_or_cff2(region_font).cff.topDictIndex[0]
		if not hasattr(region_topDict, 'FDSelect'):
			# All the glyphs share the same FontDict. Pick any glyph.
			default_fdIndex = gname_mapping[region_glyphOrder[0]]
			fd_map[default_fdIndex][ri] = 0
		else:
			region_fdSelect = region_topDict.FDSelect
			for gid, fdIndex in enumerate(region_fdSelect):
				default_fdIndex = gname_mapping[region_glyphOrder[gid]]
				region_map = fd_map[default_fdIndex]
				if ri not in region_map:
					region_map[ri] = fdIndex
	return fd_map


CVarData = namedtuple('CVarData', 'varDataList masterSupports vsindex_dict')
def merge_region_fonts(varFont, model, ordered_fonts_list, glyphOrder):
	topDict = varFont['CFF2'].cff.topDictIndex[0]
	top_dicts = [topDict] + [
					_cff_or_cff2(ttFont).cff.topDictIndex[0]
					for ttFont in ordered_fonts_list[1:]
					]
	num_masters = len(model.mapping)
	cvData = merge_charstrings(glyphOrder, num_masters, top_dicts, model)
	fd_map = getfd_map(varFont, ordered_fonts_list)
	merge_PrivateDicts(top_dicts, cvData.vsindex_dict, model, fd_map)
	addCFFVarStore(varFont, model, cvData.varDataList,
		cvData.masterSupports)


def _get_cs(charstrings, glyphName):
	if glyphName not in charstrings:
		return None
	return charstrings[glyphName]

def _add_new_vsindex(model, key, masterSupports, vsindex_dict,
		vsindex_by_key, varDataList):
	varTupleIndexes = []
	for support in model.supports[1:]:
		if support not in masterSupports:
			masterSupports.append(support)
		varTupleIndexes.append(masterSupports.index(support))
	var_data = varLib.builder.buildVarData(varTupleIndexes, None, False)
	vsindex = len(vsindex_dict)
	vsindex_by_key[key] = vsindex
	vsindex_dict[vsindex] = (model, [key])
	varDataList.append(var_data)
	return vsindex

def merge_charstrings(glyphOrder, num_masters, top_dicts, masterModel):

	vsindex_dict = {}
	vsindex_by_key = {}
	varDataList = []
	masterSupports = []
	default_charstrings = top_dicts[0].CharStrings
	for gid, gname in enumerate(glyphOrder):
		all_cs = [
				_get_cs(td.CharStrings, gname)
				for td in top_dicts]
		if len([gs for gs in all_cs if gs is not None]) == 1:
			continue
		model, model_cs = masterModel.getSubModel(all_cs)
		# create the first pass CFF2 charstring, from
		# the default charstring.
		default_charstring = model_cs[0]
		var_pen = CFF2CharStringMergePen([], gname, num_masters, 0)
		# We need to override outlineExtractor because these
		# charstrings do have widths in the 'program'; we need to drop these
		# values rather than post assertion error for them.
		default_charstring.outlineExtractor = MergeOutlineExtractor
		default_charstring.draw(var_pen)

		# Add the coordinates from all the other regions to the
		# blend lists in the CFF2 charstring.
		region_cs = model_cs[1:]
		for region_idx, region_charstring in enumerate(region_cs, start=1):
			var_pen.restart(region_idx)
			region_charstring.outlineExtractor = MergeOutlineExtractor
			region_charstring.draw(var_pen)

		# Collapse each coordinate list to a blend operator and its args.
		new_cs = var_pen.getCharString(
			private=default_charstring.private,
			globalSubrs=default_charstring.globalSubrs,
			var_model=model, optimize=True)
		default_charstrings[gname] = new_cs

		if (not var_pen.seen_moveto) or ('blend' not in new_cs.program):
			# If this is not a marking glyph, or if there are no blend
			# arguments, then we can use vsindex 0. No need to
			# check if we need a new vsindex.
			continue

		# If the charstring required a new model, create
		# a VarData table to go with, and set vsindex.
		key = tuple(v is not None for v in all_cs)
		try:
			vsindex = vsindex_by_key[key]
		except KeyError:
			vsindex = _add_new_vsindex(model, key, masterSupports, vsindex_dict,
				vsindex_by_key, varDataList)
		# We do not need to check for an existing new_cs.private.vsindex,
		# as we know it doesn't exist yet.
		if vsindex != 0:
			new_cs.program[:0] = [vsindex, 'vsindex']

	# If there is no variation in any of the charstrings, then vsindex_dict
	# never gets built. This could still be needed if there is variation
	# in the PrivatDict, so we will build the default data for vsindex = 0.
	if not vsindex_dict:
		key = (True,) * num_masters
		_add_new_vsindex(model, key, masterSupports, vsindex_dict,
			vsindex_by_key, varDataList)
	cvData = CVarData(varDataList=varDataList, masterSupports=masterSupports,
						vsindex_dict=vsindex_dict)
	# XXX To do: optimize use of vsindex between the PrivateDicts and
	# charstrings
	return cvData


def makeRoundNumberFunc(tolerance):
	if tolerance < 0:
		raise ValueError("Rounding tolerance must be positive")

	def roundNumber(val):
		return t2c_round(val, tolerance)

	return roundNumber


class CFFToCFF2OutlineExtractor(T2OutlineExtractor):
	""" This class is used to remove the initial width from the CFF
	charstring without trying to add the width to self.nominalWidthX,
	which is None. """
	def popallWidth(self, evenOdd=0):
		args = self.popall()
		if not self.gotWidth:
			if evenOdd ^ (len(args) % 2):
				args = args[1:]
			self.width = self.defaultWidthX
			self.gotWidth = 1
		return args


class MergeOutlineExtractor(CFFToCFF2OutlineExtractor):
	""" Used to extract the charstring commands - including hints - from a
	CFF charstring in order to merge it as another set of region data
	into a CFF2 variable font charstring."""

	def __init__(self, pen, localSubrs, globalSubrs,
			nominalWidthX, defaultWidthX, private=None):
		super().__init__(pen, localSubrs,
			globalSubrs, nominalWidthX, defaultWidthX, private)

	def countHints(self):
		args = self.popallWidth()
		self.hintCount = self.hintCount + len(args) // 2
		return args

	def _hint_op(self, type, args):
		self.pen.add_hint(type, args)

	def op_hstem(self, index):
		args = self.countHints()
		self._hint_op('hstem', args)

	def op_vstem(self, index):
		args = self.countHints()
		self._hint_op('vstem', args)

	def op_hstemhm(self, index):
		args = self.countHints()
		self._hint_op('hstemhm', args)

	def op_vstemhm(self, index):
		args = self.countHints()
		self._hint_op('vstemhm', args)

	def _get_hintmask(self, index):
		if not self.hintMaskBytes:
			args = self.countHints()
			if args:
				self._hint_op('vstemhm', args)
			self.hintMaskBytes = (self.hintCount + 7) // 8
		hintMaskBytes, index = self.callingStack[-1].getBytes(index,
			self.hintMaskBytes)
		return index, hintMaskBytes

	def op_hintmask(self, index):
		index, hintMaskBytes = self._get_hintmask(index)
		self.pen.add_hintmask('hintmask', [hintMaskBytes])
		return hintMaskBytes, index

	def op_cntrmask(self, index):
		index, hintMaskBytes = self._get_hintmask(index)
		self.pen.add_hintmask('cntrmask', [hintMaskBytes])
		return hintMaskBytes, index


class CFF2CharStringMergePen(T2CharStringPen):
	"""Pen to merge Type 2 CharStrings.
	"""
	def __init__(
				self, default_commands, glyphName, num_masters, master_idx,
				roundTolerance=0.5):
		super().__init__(
							width=None,
							glyphSet=None, CFF2=True,
							roundTolerance=roundTolerance)
		self.pt_index = 0
		self._commands = default_commands
		self.m_index = master_idx
		self.num_masters = num_masters
		self.prev_move_idx = 0
		self.seen_moveto = False
		self.glyphName = glyphName
		self.roundNumber = makeRoundNumberFunc(roundTolerance)

	def add_point(self, point_type, pt_coords):
		if self.m_index == 0:
			self._commands.append([point_type, [pt_coords]])
		else:
			cmd = self._commands[self.pt_index]
			if cmd[0] != point_type:
				raise VarLibCFFPointTypeMergeError(
									point_type,
									self.pt_index, len(cmd[1]),
									cmd[0], self.glyphName)
			cmd[1].append(pt_coords)
		self.pt_index += 1

	def add_hint(self, hint_type, args):
		if self.m_index == 0:
			self._commands.append([hint_type, [args]])
		else:
			cmd = self._commands[self.pt_index]
			if cmd[0] != hint_type:
				raise VarLibCFFPointTypeMergeError(hint_type, self.pt_index, len(cmd[1]),
					cmd[0], self.glyphName)
			cmd[1].append(args)
		self.pt_index += 1

	def add_hintmask(self, hint_type, abs_args):
		# For hintmask, fonttools.cffLib.specializer.py expects
		# each of these to be represented by two sequential commands:
		# first holding only the operator name, with an empty arg list,
		# second with an empty string as the op name, and  the mask arg list.
		if self.m_index == 0:
			self._commands.append([hint_type, []])
			self._commands.append(["", [abs_args]])
		else:
			cmd = self._commands[self.pt_index]
			if cmd[0] != hint_type:
				raise VarLibCFFPointTypeMergeError(hint_type, self.pt_index, len(cmd[1]),
					cmd[0], self.glyphName)
			self.pt_index += 1
			cmd = self._commands[self.pt_index]
			cmd[1].append(abs_args)
		self.pt_index += 1

	def _moveTo(self, pt):
		if not self.seen_moveto:
			self.seen_moveto = True
		pt_coords = self._p(pt)
		self.add_point('rmoveto', pt_coords)
		# I set prev_move_idx here because add_point()
		# can change self.pt_index.
		self.prev_move_idx = self.pt_index - 1

	def _lineTo(self, pt):
		pt_coords = self._p(pt)
		self.add_point('rlineto', pt_coords)

	def _curveToOne(self, pt1, pt2, pt3):
		_p = self._p
		pt_coords = _p(pt1)+_p(pt2)+_p(pt3)
		self.add_point('rrcurveto', pt_coords)

	def _closePath(self):
		pass

	def _endPath(self):
		pass

	def restart(self, region_idx):
		self.pt_index = 0
		self.m_index = region_idx
		self._p0 = (0, 0)

	def getCommands(self):
		return self._commands

	def reorder_blend_args(self, commands, get_delta_func, round_func):
		"""
		We first re-order the master coordinate values.
		For a moveto to lineto, the args are now arranged as:
			[ [master_0 x,y], [master_1 x,y], [master_2 x,y] ]
		We re-arrange this to
		[	[master_0 x, master_1 x, master_2 x],
			[master_0 y, master_1 y, master_2 y]
		]
		If the master values are all the same, we collapse the list to
		as single value instead of a list.

		We then convert this to:
		[ [master_0 x] + [x delta tuple] + [numBlends=1]
		  [master_0 y] + [y delta tuple] + [numBlends=1]
		]
		"""
		for cmd in commands:
			# arg[i] is the set of arguments for this operator from master i.
			args = cmd[1]
			m_args = zip(*args)
			# m_args[n] is now all num_master args for the i'th argument
			# for this operation.
			cmd[1] = list(m_args)
		lastOp = None
		for cmd in commands:
			op = cmd[0]
			# masks are represented by two cmd's: first has only op names,
			# second has only args.
			if lastOp in ['hintmask', 'cntrmask']:
				coord = list(cmd[1])
				if not allEqual(coord):
					raise VarLibMergeError("Hintmask values cannot differ between source fonts.")
				cmd[1] = [coord[0][0]]
			else:
				coords = cmd[1]
				new_coords = []
				for coord in coords:
					if allEqual(coord):
						new_coords.append(coord[0])
					else:
						# convert to deltas
						deltas = get_delta_func(coord)[1:]
						if round_func:
							deltas = [round_func(delta) for delta in deltas]
						coord = [coord[0]] + deltas
						new_coords.append(coord)
				cmd[1] = new_coords
			lastOp = op
		return commands

	def getCharString(
					self, private=None, globalSubrs=None,
					var_model=None, optimize=True):
		commands = self._commands
		commands = self.reorder_blend_args(commands, var_model.getDeltas,
											self.roundNumber)
		if optimize:
			commands = specializeCommands(
						commands, generalizeFirst=False,
						maxstack=maxStackLimit)
		program = commandsToProgram(commands)
		charString = T2CharString(
						program=program, private=private,
						globalSubrs=globalSubrs)
		return charString
