"""
integrate.py
--------------

Utilities for integrating functions over meshes surfaces.
"""

import sympy as sp
from sympy.parsing.sympy_parser import parse_expr as sympy_parse

from .constants import log
from . import util


def symbolic_barycentric(function):
    """
    Symbolically integrate a function(x,y,z) across a triangle or mesh.

    Parameters
    ----------
    function: string or sympy expression
              x, y, z will be replaced with a barycentric representation
              and the the function is integrated across the triangle.

    Returns
    ----------
    evaluator: numpy lambda function of result which takes a mesh
    expr:      sympy expression of result

    Examples
    -----------

    In [1]: function = '1'

    In [2]: integrator, expr = integrate_barycentric(function)

    In [3]: integrator
    Out[3]: <__main__.evaluator instance at 0x7f66cd2a6200>

    In [4]: expr
    Out[4]: 1/2

    In [5]: result  = integrator(mesh)

    In [6]: mesh.area
    Out[6]: 34.641016151377542

    In [7]: result.sum()
    Out[7]: 34.641016151377542
    """

    class evaluator:

        def __init__(self, expr, expr_args):
            self.lambdified = sp.lambdify(args=expr_args,
                                          expr=expr,
                                          modules=['numpy', 'sympy'])

        def __call__(self, mesh, *args):
            """
            Quickly evaluate the surface integral across a mesh

            Parameters
            ----------
            mesh: Trimesh object

            Returns
            ----------
            integrated: (len(faces),) float, integral evaluated for each face
            """
            integrated = self.lambdified(*mesh.triangles.reshape((-1, 9)).T)
            integrated *= 2 * mesh.area_faces
            return integrated

    function, symbols = substitute_barycentric(function)

    b1, b2, x1, x2, x3, y1, y2, y3, z1, z2, z3 = symbols
    # do the first integral for b1
    integrated_1 = sp.integrate(function, b1)
    integrated_1 = (integrated_1.subs({b1: 1 - b2}) -
                    integrated_1.subs({b1: 0}))

    integrated_2 = sp.integrate(integrated_1, b2)
    integrated_2 = (integrated_2.subs({b2: 1}) -
                    integrated_2.subs({b2: 0}))

    lambdified = evaluator(expr=integrated_2,
                           expr_args=[x1, y1, z1, x2, y2, z2, x3, y3, z3])

    return lambdified, integrated_2


def substitute_barycentric(function):
    if util.is_string(function):
        function = sympy_parse(function)
    # barycentric coordinates
    b1, b2 = sp.symbols('b1 b2',
                        real=True,
                        positive=True)
    # vertices of the triangles
    x1, x2, x3, y1, y2, y3, z1, z2, z3 = sp.symbols(
        'x1,x2,x3,y1,y2,y3,z1,z2,z3', real=True)

    # generate the substitution dictionary to convert from cartesian to barycentric
    # since the input could have been a sympy expression or a string
    # that we parsed substitute based on name to avoid id(x) issues
    substitutions = {}
    for symbol in function.free_symbols:
        if symbol.name == 'x':
            substitutions[symbol] = b1 * x1 + b2 * x2 + (1 - b1 - b2) * x3
        elif symbol.name == 'y':
            substitutions[symbol] = b1 * y1 + b2 * y2 + (1 - b1 - b2) * y3
        elif symbol.name == 'z':
            substitutions[symbol] = b1 * z1 + b2 * z2 + (1 - b1 - b2) * z3
    # apply the conversion to barycentric
    function = function.subs(substitutions)
    log.debug('converted function to barycentric: %s', str(function))

    symbols = (b1, b2, x1, x2, x3, y1, y2, y3, z1, z2, z3)

    return function, symbols
