
import numpy as np

from pySDC.core.Errors import ParameterError
from pySDC.core.Problem import ptype
from pySDC.implementations.datatype_classes.mesh import mesh


# noinspection PyUnusedLocal
class auzinger(ptype):
    """
    Example implementing the Auzinger initial value problem
    """

    def __init__(self, problem_params, dtype_u=mesh, dtype_f=mesh):
        """
        Initialization routine

        Args:
            problem_params (dict): custom parameters for the example
            dtype_u: mesh data type (will be passed to parent class)
            dtype_f: mesh data type (will be passed to parent class)
        """

        # these parameters will be used later, so assert their existence
        essential_keys = ['newton_maxiter', 'newton_tol']
        for key in essential_keys:
            if key not in problem_params:
                msg = 'need %s to instantiate problem, only got %s' % (key, str(problem_params.keys()))
                raise ParameterError(msg)

        # invoke super init, passing dtype_u and dtype_f, plus setting number of elements to 2
        super(auzinger, self).__init__(2, dtype_u, dtype_f, problem_params)

    def u_exact(self, t):
        """
        Routine for the exact solution

        Args:
            t (float): current time
        Returns:
            dtype_u: mesh type containing the exact solution
        """

        me = self.dtype_u(self.init)
        me.values[0] = np.cos(t)
        me.values[1] = np.sin(t)
        return me

    def eval_f(self, u, t):
        """
        Routine to compute the RHS for both components simultaneously

        Args:
            u (dtype_u): the current values
            t (float): current time (not used here)
        Returns:
            RHS, 2 components
        """

        x1 = u.values[0]
        x2 = u.values[1]
        f = self.dtype_f(self.init)
        f.values[0] = -x2 + x1 * (1 - x1 ** 2 - x2 ** 2)
        f.values[1] = x1 + 3 * x2 * (1 - x1 ** 2 - x2 ** 2)
        return f

    def solve_system(self, rhs, dt, u0, t):
        """
        Simple Newton solver for the nonlinear system

        Args:
            rhs (dtype_f): right-hand side for the nonlinear system
            dt (float): abbrev. for the node-to-node stepsize (or any other factor required)
            u0 (dtype_u): initial guess for the iterative solver
            t (float): current time (e.g. for time-dependent BCs)

        Returns:
            dtype_u: solution u
        """

        # create new mesh object from u0 and set initial values for iteration
        u = self.dtype_u(u0)
        x1 = u.values[0]
        x2 = u.values[1]

        # start newton iteration
        n = 0
        while n < self.params.newton_maxiter:

            # form the function g with g(u) = 0
            g = np.array([x1 - dt * (-x2 + x1 * (1 - x1 ** 2 - x2 ** 2)) - rhs.values[0],
                          x2 - dt * (x1 + 3 * x2 * (1 - x1 ** 2 - x2 ** 2)) - rhs.values[1]])

            # if g is close to 0, then we are done
            res = np.linalg.norm(g, np.inf)

            if res < self.params.newton_tol:
                break

            # assemble dg and invert the matrix (yeah, I know)
            dg = np.array([[1 - dt * (1 - 3 * x1 ** 2 - x2 ** 2), -dt * (-1 - 2 * x1 * x2)],
                           [-dt * (1 - 6 * x1 * x2), 1 - dt * (3 - 3 * x1 ** 2 - 9 * x2 ** 2)]])

            idg = np.linalg.inv(dg)

            # newton update: u1 = u0 - g/dg
            u.values -= np.dot(idg, g)

            # set new values and increase iteration count
            x1 = u.values[0]
            x2 = u.values[1]
            n += 1

        return u

        # def eval_jacobian(self, u):
        #
        #     x1 = u.values[0]
        #     x2 = u.values[1]
        #
        #     dfdu = np.array([[1-3*x1**2-x2**2, -1-x1], [1+6*x2*x1, 3+3*x1**2-9*x2**2]])
        #
        #     return dfdu
        #
        #
        # def solve_system_jacobian(self, dfdu, rhs, factor, u0, t):
        #
        #     me = mesh(2)
        #     me.values = LA.spsolve(sp.eye(2) - factor * dfdu, rhs.values)
        #     return me
