#! /usr/bin/env python

"""
Pen to calculate geometrical glyph statistics.

When this is fully fleshed out, it will be moved to a more prominent
place, like fontTools.pens.
"""

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

import sympy as sp
import math
from fontTools.pens.basePen import BasePen
from fontTools.pens.transformPen import TransformPen
from fontTools.pens.perimeterPen import PerimeterPen
from fontTools.pens.areaPen import AreaPen
from fontTools.misc.transform import Scale
from fontTools.misc.bezierTools import splitQuadraticAtT, splitCubicAtT
from functools import partial

n = 3 # Max Bezier degree; 3 for cubic, 2 for quadratic

t, x, y = sp.symbols('t x y', real=True)

P = tuple(zip(*(sp.symbols('%s:%d'%(w,n+1), real=True) for w in 'xy')))

# Cubic Bernstein basis functions
BinomialCoefficient = [(1, 0)]
for i in range(1, n+1):
	last = BinomialCoefficient[-1]
	this = tuple(last[j-1]+last[j] for j in range(len(last)))+(0,)
	BinomialCoefficient.append(this)
BinomialCoefficient = tuple(tuple(item[:-1]) for item in BinomialCoefficient)

BernsteinPolynomial = tuple(
	tuple(c * t**i * (1-t)**(n-i) for i,c in enumerate(coeffs))
	for n,coeffs in enumerate(BinomialCoefficient))

BezierCurve = tuple(
	tuple(sum(P[i][j]*bernstein for i,bernstein in enumerate(bernsteins))
		for j in range(2))
	for n,bernsteins in enumerate(BernsteinPolynomial))

def green(f, Bezier=BezierCurve[n]):
	f1 = -sp.integrate(sp.sympify(f), y)
	f2 = f1.subs({x:Bezier[0], y:Bezier[1]})
	return sp.integrate(f2 * sp.diff(Bezier[0], t), (t, 0, 1))

class BezierFuncs(object):

	def __init__(self, symfunc):
		self._symfunc = symfunc
		self._bezfuncs = {}

	def __getitem__(self, i):
		if i not in self._bezfuncs:
			args = []
			for d in range(i+1):
				args.append('x%d' % d)
				args.append('y%d' % d)
			self._bezfuncs[i] = sp.lambdify(args, green(self._symfunc, Bezier=BezierCurve[i]))
		return self._bezfuncs[i]

_BezierFuncs = {}

def getGreenBezierFuncs(func):
	funcstr = str(func)
	global _BezierFuncs
	if not funcstr in _BezierFuncs:
		_BezierFuncs[funcstr] = BezierFuncs(func)
	return _BezierFuncs[funcstr]

def printCache(func):
	funcstr = str(func)
	print("_BezierFuncs['%s'] = [" % funcstr)
	for i in range(n+1):
		print('	lambda P:', green(func, Bezier=BezierCurve[i]), ',')
	print(']')

class GreenPen(BasePen):

	def __init__(self, func, glyphset=None):
		BasePen.__init__(self, glyphset)
		self._funcs = getGreenBezierFuncs(func)
		self.value = 0

	def _moveTo(self, p0):
		self.__startPoint = p0

	def _lineTo(self, p1):
		p0 = self._getCurrentPoint()
		self.value += self._funcs[1](p0[0],p0[1],p1[0],p1[1])

	def _qCurveToOne(self, p1, p2):
		p0 = self._getCurrentPoint()
		self.value += self._funcs[2](p0[0],p0[1],p1[0],p1[1],p2[0],p2[1])

	def _curveToOne(self, p1, p2, p3):
		p0 = self._getCurrentPoint()
		self.value += self._funcs[3](p0[0],p0[1],p1[0],p1[1],p2[0],p2[1],p3[0],p3[1])

	def _closePath(self):
		p0 = self._getCurrentPoint()
		if p0 != self.__startPoint:
			p1 = self.__startPoint
			self.value += self._funcs[1](p0[0],p0[1],p1[0],p1[1])

#AreaPen = partial(GreenPen, func=1)
Moment1XPen = partial(GreenPen, func=x)
Moment1YPen = partial(GreenPen, func=y)
Moment2XXPen = partial(GreenPen, func=x*x)
Moment2YYPen = partial(GreenPen, func=y*y)
Moment2XYPen = partial(GreenPen, func=x*y)



#
# Glyph statistics object
#

class GlyphStatistics(object):

	def __init__(self, glyph, transform=None, glyphset=None):
		self._glyph = glyph
		self._glyphset = glyphset
		self._transform = transform

	def _penAttr(self, attr):
		internalName = '_'+attr
		if internalName not in self.__dict__:
			Pen = globals()[attr+'Pen']
			pen = transformer = Pen(glyphset=self._glyphset)
			if self._transform:
				transformer = TransformPen(pen, self._transform)
			self._glyph.draw(transformer)
			self.__dict__[internalName] = pen.value
		return self.__dict__[internalName]

	Area = property(partial(_penAttr, attr='Area'))
	Perimeter = property(partial(_penAttr, attr='Perimeter'))
	Moment1X = property(partial(_penAttr, attr='Moment1X'))
	Moment1Y = property(partial(_penAttr, attr='Moment1Y'))
	Moment2XX = property(partial(_penAttr, attr='Moment2XX'))
	Moment2YY = property(partial(_penAttr, attr='Moment2YY'))
	Moment2XY = property(partial(_penAttr, attr='Moment2XY'))

	# TODO Memoize properties below

	# Center of mass
	# https://en.wikipedia.org/wiki/Center_of_mass#A_continuous_volume
	@property
	def MeanX(self):
		return self.Moment1X / self.Area
	@property
	def MeanY(self):
		return self.Moment1Y / self.Area

	# https://en.wikipedia.org/wiki/Second_moment_of_area

	#  Var(X) = E[X^2] - E[X]^2
	@property
	def VarianceX(self):
		return self.Moment2XX / self.Area - self.MeanX**2
	@property
	def VarianceY(self):
		return self.Moment2YY / self.Area - self.MeanY**2
	
	@property
	def StdDevX(self):
		return self.VarianceX**.5
	@property
	def StdDevY(self):
		return self.VarianceY**.5

	#  Covariance(X,Y) = ( E[X.Y] - E[X]E[Y] )
	@property
	def Covariance(self):
		return self.Moment2XY / self.Area - self.MeanX*self.MeanY

	@property
	def _CovarianceMatrix(self):
		cov = self.Covariance
		return ((self.VarianceX, cov), (cov, self.VarianceY))

	@property
	def _Eigen(self):
		mat = self.CovarianceMatrix
		from numpy.linalg import eigh
		vals,vecs = eigh(mat)
		# Note: we return eigen-vectors row-major, unlike Matlab, et al
		return tuple(vals), tuple(tuple(row) for row in vecs)

	#  Correlation(X,Y) = Covariance(X,Y) / ( StdDev(X) * StdDev(Y)) )
	# https://en.wikipedia.org/wiki/Pearson_product-moment_correlation_coefficient
	@property
	def Correlation(self):
		corr = self.Covariance / (self.StdDevX * self.StdDevY)
		if abs(corr) < 1e-3: corr = 0
		return corr

	@property
	def Slant(self):
		slant = self.Covariance / self.VarianceY
		if abs(slant) < 1e-3: slant = 0
		return slant


def test(glyphset, upem, glyphs):
	print('upem', upem)

	for glyph_name in glyphs:
		print()
		print("glyph:", glyph_name)
		glyph = glyphset[glyph_name]
		stats = GlyphStatistics(glyph, transform=Scale(1./upem), glyphset=glyphset)
		for item in dir(stats):
			if item[0] == '_': continue
			print ("%s: %g" % (item, getattr(stats, item)))


def main(args):
	if not args:
		return
	filename, glyphs = args[0], args[1:]
	if not glyphs:
		glyphs = ['e', 'o', 'I', 'slash', 'E', 'zero', 'eight', 'minus', 'equal']
	from fontTools.ttLib import TTFont
	font = TTFont(filename)
	test(font.getGlyphSet(), font['head'].unitsPerEm, glyphs)

if __name__ == '__main__':
	import sys
	main(sys.argv[1:])
