'''

All the software  contained in this library  is protected by copyright.
Permission  to use, copy, modify, and  distribute this software for any
purpose without fee is hereby granted, provided that this entire notice
is included  in all copies  of any software which is or includes a copy
or modification  of this software  and in all copies  of the supporting
documentation for such software.

THIS SOFTWARE IS BEING PROVIDED "AS IS", WITHOUT ANY EXPRESS OR IMPLIED
WARRANTY. IN NO EVENT, NEITHER  THE AUTHORS, NOR THE PUBLISHER, NOR ANY
MEMBER  OF THE EDITORIAL BOARD OF  THE JOURNAL  "NUMERICAL ALGORITHMS",
NOR ITS EDITOR-IN-CHIEF, BE  LIABLE FOR ANY ERROR  IN THE SOFTWARE, ANY
MISUSE  OF IT  OR ANY DAMAGE ARISING OUT OF ITS USE. THE ENTIRE RISK OF
USING THE SOFTWARE LIES WITH THE PARTY DOING SO.

ANY USE OF THE SOFTWARE  CONSTITUTES  ACCEPTANCE  OF THE TERMS  OF THE
ABOVE STATEMENT.


AUTHORS:
Per A Brodtkorb
Python code Based on matlab code written by:

Marco Caliari
University of Verona, Italy
E-mail: marco.caliari@univr.it

Stefano de Marchi, Alvise Sommariva, Marco Vianello
University of Padua, Italy
E-mail: demarchi@math.unipd.it, alvise@math.unipd.it,
        marcov@math.unipd.it

Reference
---------
Padua2DM: fast interpolation and cubature at the Padua points in Matlab/Octave
NUMERICAL ALGORITHMS, 56 (2011), PP. 45-60


Padua module
------------
In polynomial interpolation of two variables, the Padua points are the first
known example (and up to now the only one) of a unisolvent point set
(that is, the interpolating polynomial is unique) with minimal growth of their
Lebesgue constant, proven to be O(log2 n).
This module provides all the functions needed to perform interpolation and
cubature at the Padua points, together with the functions and the demos used
in the paper.

pdint.m                 : main function for interpolation at the Padua points
pdcub.m                 : main function for cubature at the Padua points
pdpts.m                 : function for the computation of the Padua points
padua_fit.m              : function for the computation of the interpolation
                          coefficients by FFT (recommended)
pdcfsMM.m               : function for the computation of the interpolation
                          coefficients by matrix multiplications
pdval.m                 : function for the evaluation of the interpolation
                          polynomial
pdwtsFFT.m              : function for the computation of the cubature
                          weights by FFT
pdwtsMM.m               : function for the computation of the cubature
                          weights by matrix multiplications (recommended)
funct.m                 : function containing some test functions
demo_pdint.m            : demo script for pdint
demo_cputime_pdint.m    : demo script for the computation of CPU time for
                          interpolation
demo_errors_pdint.m     : demo script for the comparison of interpolation with
                          coefficients computed by FFT or by matrix
                          multiplications
demo_pdcub              : demo script for pdcub
demo_cputime_pdcub.m    : demo script for the computation of CPU time for
                          cubature
demo_errors_pdcub.m     : demo script for the comparison of cubature with
                          weights computed by FFT or by matrix multiplications
demo_errors_pdcub_gl.m  : demo script for the comparison of different cubature
                          formulas
cubature_square.m       : function for the computation of some cubature
                          formulas for the square
omelyan_solovyan_rule.m : function for the computation of Omelyan-Solovyan
                          cubature points and weights
Contents.m              : Contents file for Matlab


'''
from __future__ import absolute_import, division
import numpy as np
from numpy.fft import fft
from .dctpack import dct
# from scipy.fftpack.realtransforms import dct


class _ExampleFunctions(object):
    '''
    Computes test function in the points (x, y)

    Parameters
    ----------
    x,y : array-like
        evaluate the function in the points (x,y)
    id : scalar int (default 0)
        id defining which test function to use. Options are
        0: franke
        1: half_sphere
        2: poly_degree20
        3: exp_fun1
        4: exp_fun100
        5: cos30
        6: constant
        7: exp_xy
        8: runge
        9: abs_cubed
        10: gauss
        11: exp_inv

    Returns
    -------
    z : array-like
        value of the function in the points (x,y)
    '''
    @staticmethod
    def franke(x, y):
        '''Franke function.

        The value of the definite integral on the square [-1,1] x [-1,1],
        obtained using a Padua Points cubature formula of degree 500, is
        2.1547794245591083e+000 with an estimated absolute error of 8.88e-016.

        The value of the definite integral on the square [0,1] x [0,1],
        obtained using a Padua Points cubature formula of degree 500, is
        4.06969589491556e-01 with an estimated absolute error of 8.88e-016.

        Maple: 0.40696958949155611906
        '''
        exp = np.exp
        return (3. / 4 * exp(-((9. * x - 2)**2 + (9. * y - 2)**2) / 4) +
                3. / 4 * exp(-(9. * x + 1)**2 / 49 - (9. * y + 1) / 10) +
                1. / 2 * exp(-((9. * x - 7)**2 + (9. * y - 3)**2) / 4) -
                1. / 5 * exp(-(9. * x - 4)**2 - (9. * y - 7)**2))

    @staticmethod
    def half_sphere(x, y):
        '''The value of the definite integral on the square [-1,1] x [-1,1],
        obtained using a Padua Points cubature formula of degree 2000, is
        3.9129044444568244e+000 with an estimated absolute error of 3.22e-010.
        '''
        return ((x - 0.5)**2 + (y - 0.5)**2)**(1. / 2)

    @staticmethod
    def poly_degree20(x, y):
        ''''Bivariate polynomial having moderate degree.
        The value of the definite integral on the square [-1,1] x
        [-1,1], up to machine precision, is 18157.16017316017 (see ref. 6).
        The value of the definite integral on the square [-1,1] x [-1,1],
        obtained using a Padua Points cubature formula of degree 500,
        is 1.8157160173160162e+004.

        2D modification of an example by L.N.Trefethen (see ref. 7), f(x)=x^20.
        '''
        return (x + y)**20

    @staticmethod
    def exp_fun1(x, y):
        ''' The value of the definite integral on the square [-1,1] x [-1,1],
        obtained using a Padua Points cubature formula of degree 2000,
        is 2.1234596326670683e+001 with an estimated absolute error of
        7.11e-015.
        '''
        return np.exp((x - 0.5)**2 + (y - 0.5)**2)

    @staticmethod
    def exp_fun100(x, y):
        '''The value of the definite integral on the square [-1,1] x [-1,1],
        obtained using a Padua Points cubature formula of degree 2000,
        is 3.1415926535849605e-002 with an estimated absolute error of
        3.47e-017.
        '''
        return np.exp(-100 * ((x - 0.5)**2 + (y - 0.5)**2))

    @staticmethod
    def cos30(x, y):
        ''' The value of the definite integral on the square [-1,1] x [-1,1],
        obtained using a Padua Points cubature formula of degree 500,
        is 4.3386955120336568e-003 with an estimated absolute error of
        2.95e-017.
        '''
        return np.cos(30 * (x + y))

    @staticmethod
    def constant(x, y):
        '''Constant.
        To test interpolation and cubature at degree 0.
        The value of the definite integral on the square [-1,1] x [-1,1]
        is 4.
        '''
        return np.ones(np.shape(x))

    @staticmethod
    def exp_xy(x, y):
        '''The value of the definite integral on the square [-1,1] x [-1,1]
        is up to machine precision is 5.524391382167263 (see ref. 6).
        2. The value of the definite integral on the square [-1,1] x [-1,1],
        obtained using a Padua Points cubature formula of degree 500,
        is 5.5243913821672628e+000 with an estimated absolute error of
        0.00e+000.
        2D modification of an example by L.N.Trefethen (see ref. 7),
        f(x)=exp(x).
        '''
        return np.exp(x + y)

    @staticmethod
    def runge(x, y):
        ''' Bivariate Runge function: as 1D complex function is analytic
        in a neighborhood of [-1; 1] but not throughout the complex plane.

        The value of the definite integral on the square [-1,1] x [-1,1],
        up to machine precision, is 0.597388947274307 (see ref. 6).
        The value of the definite integral on the square [-1,1] x [-1,1],
        obtained using a Padua Points cubature formula of degree 500,
        is 5.9738894727430725e-001 with an estimated absolute error of
        0.00e+000.

        2D modification of an example by L.N.Trefethen (see ref. 7),
        f(x)=1/(1+16*x^2).
        '''
        return 1. / (1 + 16 * (x**2 + y**2))

    @staticmethod
    def abs_cubed(x, y):
        '''Low regular function.
        The value of the definite integral on the square [-1,1] x [-1,1],
        up to machine precision, is 2.508723139534059 (see ref. 6).
        The value of the definite integral on the square [-1,1] x [-1,1],
        obtained using a Padua Points cubature formula of degree 500,
        is 2.5087231395340579e+000 with an estimated absolute error of
        0.00e+000.

        2D modification of an example by L.N.Trefethen (see ref. 7),
        f(x)=abs(x)^3.
        '''
        return (x**2 + y**2)**(3 / 2)

    @staticmethod
    def gauss(x, y):
        '''Bivariate gaussian: smooth function.
        The value of the definite integral on the square [-1,1] x [-1,1],
        up to machine precision, is 2.230985141404135 (see ref. 6).
        The value of the definite integral on the square [-1,1] x [-1,1],
        obtained using a Padua Points cubature formula of degree 500,
        is 2.2309851414041333e+000 with an estimated absolute error of
        2.66e-015.

        2D modification of an example by L.N.Trefethen (see ref. 7),
        f(x)=exp(-x^2).
        '''
        return np.exp(-x**2 - y**2)

    @staticmethod
    def exp_inv(x, y):
        '''Bivariate example stemming from a 1D C-infinity function.
        The value of the definite integral on the square [-1,1] x [-1,1],
        up to machine precision, is 0.853358758654305 (see ref. 6).
        The value of the definite integral on the square [-1,1] x [-1,1],
        obtained using a Padua Points cubature formula of degree 2000,
        is 8.5335875865430544e-001 with an estimated absolute error of
        3.11e-015.

        2D modification of an example by L.N.Trefethen (see ref. 7),
        f(x)=exp(-1/x^2).
        '''
        arg_z = (x**2 + y**2)
        # Avoid cases in which "arg_z=0", setting only in those instances
        # "arg_z=eps".
        arg_z = arg_z + (1 - np.abs(np.sign(arg_z))) * 1.e-100
        arg_z = 1. / arg_z
        return np.exp(-arg_z)

    def __call__(self, x, y, id=0):  # @ReservedAssignment
        s = self
        test_function = [s.franke, s.half_sphere, s.poly_degree20, s.exp_fun1,
                         s.exp_fun100, s.cos30, s.constant, s.exp_xy, s.runge,
                         s.abs_cubed, s.gauss, s.exp_inv]
        return test_function[id](x, y)
example_functions = _ExampleFunctions()


def _find_m(n):
    ix = np.r_[1:(n + 1) * (n + 2):2]
    if np.mod(n, 2) == 0:
        n2 = n // 2
        offset = np.array([[0, 1] * n2 + [0, ]] * (n2 + 1))
        ix = ix - offset.ravel(order='F')
    return ix


def padua_points(n, domain=(-1, 1, -1, 1)):
    ''' Return Padua points

    Parameters
    ----------
    n : scalar integer
         interpolation degree
    domain : vector [a,b,c,d]
        defining the rectangle [a,b] x [c,d]. (default domain = (-1,1,-1,1))

    Returns
    -------
    pad : array of shape (2 x (n+1)*(n+2)/2) such that
        (pad[0,:], pad[1,: ]) defines the Padua points in the domain
        rectangle [a,b] x [c,d].
    or
     X1,Y1,X2,Y2 : arrays
         Two subgrids X1,Y1 and X2,Y2 defining the Padua points
    -------------------------------------------------------------------------------
    '''
    a, b, c, d = domain
    t0 = [np.pi] if n == 0 else np.linspace(0, np.pi, n + 1)
    t1 = np.linspace(0, np.pi, n + 2)
    zn = (a + b + (b - a) * np.cos(t0)) / 2
    zn1 = (c + d + (d - c) * np.cos(t1)) / 2

    Pad1, Pad2 = np.meshgrid(zn, zn1)
    ix = _find_m(n)
    return np.vstack((Pad1.ravel(order='F')[ix],
                      Pad2.ravel(order='F')[ix]))


def error_estimate(C0f):
    ''' Return interpolation error estimate from Padua coefficients
    '''
    n = C0f.shape[1]
    C0f2 = np.fliplr(C0f)
    errest = sum(np.abs(np.diag(C0f2)))
    if (n >= 1):
        errest = errest + sum(np.abs(np.diag(C0f2, -1)))
        if (n >= 2):
            errest = errest + sum(np.abs(np.diag(C0f2, -2)))
    return 2 * errest


def padua_fit(Pad, fun, *args):
    '''
    Computes the Chebyshevs coefficients

    so that f(x, y) can be approximated by:

           f(x, y) = sum cjk*Tjk(x, y)

    Parameters
    ----------
    Pad : array-like
        Padua points, as computed  with padua_points function.
    fun : function to be interpolated in the form
        fun(x, y, *args), where *args are optional arguments for fun.

    Returns
    -------
    coefficents: coefficient matrix
    abs_err   : interpolation error estimate

    '''

    N = np.shape(Pad)[1]
    # recover the degree n from N = (n+1)(n+2)/2
    n = int(round(-3 + np.sqrt(1 + 8 * N)) / 2)
    C0f = fun(Pad[0], Pad[1], *args)
    if (n > 0):
        ix = _find_m(n)
        GfT = np.zeros((n + 2) * (n + 1))
        GfT[ix] = C0f * 2 / (n * (n + 1))
        GfT = GfT.reshape(n + 1, n + 2)
        GfT = GfT.T
        GfT[0] = GfT[0] / 2
        GfT[n + 1] = GfT[n + 1] / 2
        GfT[:, 0] = GfT[:, 0] / 2
        GfT[:, n] = GfT[:, n] / 2
        Gf = GfT.T
        # compute the interpolation coefficient matrix C0f by FFT
        Gfhat = np.real(fft(Gf, 2 * n, axis=0))
        Gfhathat = np.real(fft(Gfhat[:n + 1, :], 2 * (n + 1), axis=1))
        C0f = 2 * Gfhathat[:, 0:n + 1]
        C0f[0] = C0f[0, :] / np.sqrt(2)
        C0f[:, 0] = C0f[:, 0] / np.sqrt(2)
        C0f = np.fliplr(np.triu(np.fliplr(C0f)))
        C0f[n] = C0f[n] / 2

    return C0f, error_estimate(C0f)


def paduavals2coefs(f):
    useFFTwhenNisMoreThan = 100
    m = len(f)
    n = int(round(-1.5 + np.sqrt(.25 + 2 * m)))
    x = padua_points(n)
    idx = _find_m(n)
    w = 0 * x[0] + 1. / (n * (n + 1))
    idx1 = np.all(np.abs(x) == 1, axis=0)
    w[idx1] = .5 * w[idx1]
    idx2 = np.all(np.abs(x) != 1, axis=0)
    w[idx2] = 2 * w[idx2]

    G = np.zeros(idx.max() + 1)
    G[idx] = 4 * w * f

    if (n < useFFTwhenNisMoreThan):
        t1 = np.r_[0:n + 1].reshape(-1, 1)
        Tn1 = np.cos(t1 * t1.T * np.pi / n)
        t2 = np.r_[0:n + 2].reshape(-1, 1)
        Tn2 = np.cos(t2 * t2.T * np.pi / (n + 1))
        C = np.dot(Tn2, np.dot(G, Tn1))
    else:

        # dct = @(c) chebtech2.coeffs2vals(c);
        C = np.rot90(dct(dct(G.T).T))  # , axis=1)

    C[0] = .5 * C[0]
    C[:, 1] = .5 * C[:, 1]
    C[0, -1] = .5 * C[0, -1]
    del C[-1]

    # Take upper-left triangular part:
    return np.fliplr(np.triu(np.fliplr(C)))
    # C = triu(C(:,end:-1:1));
    # C = C(:,end:-1:1);


# TODO: padua_fit2 does not work correctly yet.
def padua_fit2(Pad, fun, *args):
    N = np.shape(Pad)[1]
    # recover the degree n from N = (n+1)(n+2)/2
    _n = int(round(-3 + np.sqrt(1 + 8 * N)) / 2)
    C0f = fun(Pad[0], Pad[1], *args)
    return paduavals2coefs(C0f)


def _compute_moments(n):
    k = np.r_[0:n:2]
    mom = 2 * np.sqrt(2) / (1 - k ** 2)
    mom[0] = 2
    return mom


def padua_cubature(coefficients, domain=(-1, 1, -1, 1)):
    '''
    Compute the integral through the coefficient matrix.
    '''
    n = coefficients.shape[1]
    mom = _compute_moments(n)
    M1, M2 = np.meshgrid(mom, mom)
    M = M1 * M2
    C0fM = coefficients[0:n:2, 0:n:2] * M
    a, b, c, d = domain
    integral = (b - a) * (d - c) * C0fM.sum() / 4
    return integral


def padua_val(X, Y, coefficients, domain=(-1, 1, -1, 1), use_meshgrid=False):
    '''
    Evaluate polynomial in padua form at X, Y.

     Evaluate the interpolation polynomial defined through its coefficient
     matrix coefficients at the target points X(:,1),X(:,2) or at the
     meshgrid(X1,X2)

    Parameters
    ----------
    X, Y: array-like
        evaluation points.
    coefficients : array-like
         coefficient matrix
    domain : a vector [a,b,c,d]
         defining the rectangle [a,b] x [c,d]
    use_meshgrid: bool
        If True interpolate at the points meshgrid(X, Y)

    Returns
    -------
     fxy : array-like
         evaluation of the interpolation polynomial at the target points
    '''
    X, Y = np.atleast_1d(X, Y)
    original_shape = X.shape
    min, max = np.minimum, np.maximum  # @ReservedAssignment
    a, b, c, d = domain
    n = np.shape(coefficients)[1]

    X1 = min(max(2 * (X.ravel() - a) / (b - a) - 1, -1), 1).reshape(1, -1)
    X2 = min(max(2 * (Y.ravel() - c) / (d - c) - 1, -1), 1).reshape(1, -1)
    tn = np.r_[0:n][:, None]
    TX1 = np.cos(tn * np.arccos(X1))
    TX2 = np.cos(tn * np.arccos(X2))
    TX1[1:n + 1] = TX1[1:n + 1] * np.sqrt(2)
    TX2[1:n + 1] = TX2[1:n + 1] * np.sqrt(2)
    if use_meshgrid:  # eval on meshgrid points
        return np.dot(TX1.T, np.dot(coefficients, TX2)).T
    # scattered points
    val = np.sum(np.dot(TX1.T, coefficients) * TX2.T, axis=1)
    return val.reshape(original_shape)
