from __future__ import print_function, division, absolute_import
from __future__ import unicode_literals
from fontTools.misc.py23 import *


def iup_segment(coords, rc1, rd1, rc2, rd2):
	# rc1 = reference coord 1
	# rd1 = reference delta 1
	out_arrays = [None, None]
	for j in 0,1:
		out_arrays[j] = out = []
		x1, x2, d1, d2 = rc1[j], rc2[j], rd1[j], rd2[j]


		if x1 == x2:
			n = len(coords)
			if d1 == d2:
				out.extend([d1]*n)
			else:
				out.extend([0]*n)
			continue

		if x1 > x2:
			x1, x2 = x2, x1
			d1, d2 = d2, d1

		# x1 < x2
		scale = (d2 - d1) / (x2 - x1)
		for pair in coords:
			x = pair[j]

			if x <= x1:
				d = d1
			elif x >= x2:
				d = d2
			else:
				# Interpolate
				d = d1 + (x - x1) * scale

			out.append(d)

	return zip(*out_arrays)

def iup_contour(delta, coords):
	assert len(delta) == len(coords)
	if None not in delta:
		return delta

	n = len(delta)
	# indices of points with explicit deltas
	indices = [i for i,v in enumerate(delta) if v is not None]
	if not indices:
		# All deltas are None.  Return 0,0 for all.
		return [(0,0)]*n

	out = []
	it = iter(indices)
	start = next(it)
	if start != 0:
		# Initial segment that wraps around
		i1, i2, ri1, ri2 = 0, start, start, indices[-1]
		out.extend(iup_segment(coords[i1:i2], coords[ri1], delta[ri1], coords[ri2], delta[ri2]))
	out.append(delta[start])
	for end in it:
		if end - start > 1:
			i1, i2, ri1, ri2 = start+1, end, start, end
			out.extend(iup_segment(coords[i1:i2], coords[ri1], delta[ri1], coords[ri2], delta[ri2]))
		out.append(delta[end])
		start = end
	if start != n-1:
		# Final segment that wraps around
		i1, i2, ri1, ri2 = start+1, n, start, indices[0]
		out.extend(iup_segment(coords[i1:i2], coords[ri1], delta[ri1], coords[ri2], delta[ri2]))

	assert len(delta) == len(out), (len(delta), len(out))
	return out

def iup_delta(delta, coords, ends):
	assert sorted(ends) == ends and len(coords) == (ends[-1]+1 if ends else 0) + 4
	n = len(coords)
	ends = ends + [n-4, n-3, n-2, n-1]
	out = []
	start = 0
	for end in ends:
		end += 1
		contour = iup_contour(delta[start:end], coords[start:end])
		out.extend(contour)
		start = end

	return out

# Optimizer

def can_iup_in_between(deltas, coords, i, j, tolerance):
	assert j - i >= 2
	interp = list(iup_segment(coords[i+1:j], coords[i], deltas[i], coords[j], deltas[j]))
	deltas = deltas[i+1:j]

	assert len(deltas) == len(interp)

	return all(abs(complex(x-p, y-q)) <= tolerance for (x,y),(p,q) in zip(deltas, interp))

def _iup_contour_bound_forced_set(delta, coords, tolerance=0):
	"""The forced set is a conservative set of points on the contour that must be encoded
	explicitly (ie. cannot be interpolated).  Calculating this set allows for significantly
	speeding up the dynamic-programming, as well as resolve circularity in DP.

	The set is precise; that is, if an index is in the returned set, then there is no way
	that IUP can generate delta for that point, given coords and delta.
	"""
	assert len(delta) == len(coords)

	forced = set()
	# Track "last" and "next" points on the contour as we sweep.
	nd, nc = delta[0], coords[0]
	ld, lc = delta[-1], coords[-1]
	for i in range(len(delta)-1, -1, -1):
		d, c = ld, lc
		ld, lc = delta[i-1], coords[i-1]

		for j in (0,1): # For X and for Y
			cj = c[j]
			dj = d[j]
			lcj = lc[j]
			ldj = ld[j]
			ncj = nc[j]
			ndj = nd[j]

			if lcj <= ncj:
				c1, c2 = lcj, ncj
				d1, d2 = ldj, ndj
			else:
				c1, c2 = ncj, lcj
				d1, d2 = ndj, ldj

			# If coordinate for current point is between coordinate of adjacent
			# points on the two sides, but the delta for current point is NOT
			# between delta for those adjacent points (considering tolerance
			# allowance), then there is no way that current point can be IUP-ed.
			# Mark it forced.
			force = False
			if c1 <= cj <= c2:
				if not (min(d1,d2)-tolerance <= dj <= max(d1,d2)+tolerance):
					force = True
			else: # cj < c1 or c2 < cj
				if c1 == c2:
					if d1 == d2:
						if abs(dj - d1) > tolerance:
							force = True
					else:
						if abs(dj) > tolerance:
							# Disabled the following because the "d1 == d2" does
							# check does not take tolerance into consideration...
							pass # force = True
				elif d1 != d2:
					if cj < c1:
						if dj != d1 and ((dj-tolerance < d1) != (d1 < d2)):
							force = True
					else: # c2 < cj
						if d2 != dj and ((d2 < dj+tolerance) != (d1 < d2)):
							force = True

			if force:
				forced.add(i)
				break

		nd, nc = d, c

	return forced

def _iup_contour_optimize_dp(delta, coords, forced={}, tolerance=0, lookback=None):
	"""Straightforward Dynamic-Programming.  For each index i, find least-costly encoding of
	points 0 to i where i is explicitly encoded.  We find this by considering all previous
	explicit points j and check whether interpolation can fill points between j and i.

	Note that solution always encodes last point explicitly.  Higher-level is responsible
	for removing that restriction.

	As major speedup, we stop looking further whenever we see a "forced" point."""

	n = len(delta)
	if lookback is None:
		lookback = n
	costs = {-1:0}
	chain = {-1:None}
	for i in range(0, n):
		best_cost = costs[i-1] + 1

		costs[i] = best_cost
		chain[i] = i - 1

		if i - 1 in forced:
			continue

		for j in range(i-2, max(i-lookback, -2), -1):

			cost = costs[j] + 1

			if cost < best_cost and can_iup_in_between(delta, coords, j, i, tolerance):
				costs[i] = best_cost = cost
				chain[i] = j

			if j in forced:
				break

	return chain, costs

def _rot_list(l, k):
	"""Rotate list by k items forward.  Ie. item at position 0 will be
	at position k in returned list.  Negative k is allowed."""
	n = len(l)
	k %= n
	if not k: return l
	return l[n-k:] + l[:n-k]

def _rot_set(s, k, n):
	k %= n
	if not k: return s
	return {(v + k) % n for v in s}

def iup_contour_optimize(delta, coords, tolerance=0.):
	n = len(delta)

	# Get the easy cases out of the way:

	# If all are within tolerance distance of 0, encode nothing:
	if all(abs(complex(*p)) <= tolerance for p in delta):
		return [None] * n

	# If there's exactly one point, return it:
	if n == 1:
		return delta

	# If all deltas are exactly the same, return just one (the first one):
	d0 = delta[0]
	if all(d0 == d for d in delta):
		return [d0] + [None] * (n-1)

	# Else, solve the general problem using Dynamic Programming.

	forced = _iup_contour_bound_forced_set(delta, coords, tolerance)
	# The _iup_contour_optimize_dp() routine returns the optimal encoding
	# solution given the constraint that the last point is always encoded.
	# To remove this constraint, we use two different methods, depending on
	# whether forced set is non-empty or not:

	if forced:
		# Forced set is non-empty: rotate the contour start point
		# such that the last point in the list is a forced point.
		k = (n-1) - max(forced)
		assert k >= 0

		delta  = _rot_list(delta, k)
		coords = _rot_list(coords, k)
		forced = _rot_set(forced, k, n)

		chain, costs = _iup_contour_optimize_dp(delta, coords, forced, tolerance)

		# Assemble solution.
		solution = set()
		i = n - 1
		while i is not None:
			solution.add(i)
			i = chain[i]
		assert forced <= solution, (forced, solution)
		delta = [delta[i] if i in solution else None for i in range(n)]

		delta = _rot_list(delta, -k)
	else:
		# Repeat the contour an extra time, solve the 2*n case, then look for solutions of the
		# circular n-length problem in the solution for 2*n linear case.  I cannot prove that
		# this always produces the optimal solution...
		chain, costs = _iup_contour_optimize_dp(delta+delta, coords+coords, forced, tolerance, n)
		best_sol, best_cost = None, n+1

		for start in range(n-1, 2*n-1):
			# Assemble solution.
			solution = set()
			i = start
			while i > start - n:
				solution.add(i % n)
				i = chain[i]
			if i == start - n:
				cost = costs[start] - costs[start - n]
				if cost <= best_cost:
					best_sol, best_cost = solution, cost

		delta = [delta[i] if i in best_sol else None for i in range(n)]


	return delta

def iup_delta_optimize(delta, coords, ends, tolerance=0.):
	assert sorted(ends) == ends and len(coords) == (ends[-1]+1 if ends else 0) + 4
	n = len(coords)
	ends = ends + [n-4, n-3, n-2, n-1]
	out = []
	start = 0
	for end in ends:
		contour = iup_contour_optimize(delta[start:end+1], coords[start:end+1], tolerance)
		assert len(contour) == end - start + 1
		out.extend(contour)
		start = end+1

	return out
