import numpy             as np
from   numpy import pi   as PI

import scipy.integrate   as integrate
import scipy.interpolate as interp

def _sphere_area_int(theta, r, x_vals, z_vals, metric):
    st = np.sin(theta)
    ct = np.cos(theta)

    x = r * st
    z = r * ct

    metric_val = np.empty((3, 3))
    for i in range(3):
        for j in range(3):
            metric_interp = interp.RectBivariateSpline(x_vals, z_vals, metric[i, j])
            metric_val[i, j] = metric_interp(x, z)

    transform = np.array([[st,  r * ct, 0.0],
                          [0.0, 0.0, r * st],
                          [ct, -r * st, 0.0]])

    ms = np.einsum('ki,lj,kl', transform, transform, metric_val)
    return np.sqrt(ms[1, 1] * ms[2, 2])

def mass_adm_sphere(x, z, metric, r, dr = 0.1):
    """
    Calculate an estimate of the ADM mass based on falloff of sphere area
    (equation (A.26) in [1]).

    [1] Alcubierre (2008): Introduction to 3+1 Numerical Relativity

    :param array_like x: 1D array of x coordinates at which the values of the
                         metric are provided
    :param array_like z: 1D array of z coordinates at which the values of the
                         metric are provided
    :param array_like metric: 4D array of metric components at the grid defined by x
                              and z. metric[i, j, k, l] is the ij-th component
                              at spatial point (X=x[k], Z=z[l]).
    :param float r: The radius at which the mass is to be evaluated.
    :param float dr: optional step used for calculating dA/dr with finite
                     differences.
    :return: Estimate of the ADM mass of the spacetime.
    :rtype: float
    """
    A     = 2 * 2 * PI * integrate.quad(_sphere_area_int, 0, PI / 2, (r,      x, z, metric))[0]
    Am1   = 2 * 2 * PI * integrate.quad(_sphere_area_int, 0, PI / 2, (r - dr, x, z, metric))[0]
    Ap1   = 2 * 2 * PI * integrate.quad(_sphere_area_int, 0, PI / 2, (r + dr, x, z, metric))[0]
    dA_dr = (Ap1 - Am1) / (2.0 * dr)

    X, Z      = np.meshgrid(x, z)
    R         = np.sqrt(X**2 + Z**2)
    grr       = ((X / R) ** 2) * metric[0, 0] + ((Z / R) ** 2) * metric[2, 2] + 2.0 * (X * Z / (R * R)) * metric[0, 2]
    grr       = np.where(np.isnan(grr), 0.0, grr)

    dst_theta = np.linspace(0, PI / 2, 128)
    grr_vals  = interp.RectBivariateSpline(x, z, grr)(r * np.sin(dst_theta), r * np.cos(dst_theta), grid = False)

    return (np.sqrt(A / (16.0 * PI)) *
            (1.0 - (dA_dr ** 2) / (16 * PI * A * np.average(grr_vals))))
