"""
Created on 17. mai 2015

@author: pab
"""

from __future__ import absolute_import, division

import numpy as np
import scipy.special as special
from numpy import (
    arccos,
    arccosh,
    arcsin,
    arcsinh,
    arctan,
    arctanh,
    cos,
    cosh,
    exp,
    exp2,
    expm1,
    log,
    log1p,
    sin,
    sinh,
    sqrt,
    square,
    tan,
    tanh,
)

function_names = [
    "cos",
    "sin",
    "tan",
    "cosh",
    "sinh",
    "tanh",
    "arcsinh",
    "exp",
    "expm1",
    "exp2",
    "square",
    "sqrt",
    "log",
    "log1p",
    "log10",
    "log2",
    "arccos",
    "arcsin",
    "arctan",
]


def darcsin(x):
    return 1.0 / sqrt(1 - x**2)


def ddarcsin(x):
    return x * darcsin(x) ** 3


def dddarcsin(x):
    y = darcsin(x)
    return y**3 * (1 + 3 * (x * y) ** 2)


def darccos(x):
    return -darcsin(x)


def ddarccos(x):
    return -ddarcsin(x)


def dddarccos(x):
    return -dddarcsin(x)


def derivative_arcsin(n):
    return (arcsin, darcsin, ddarcsin, dddarcsin, None)[min(n, 4)]


def derivative_arccos(n):
    return (arccos, darccos, ddarccos, dddarccos, None)[min(n, 4)]


def derivative_arctan(n):
    def darctan(x):
        return 1.0 / (1 + x**2)

    def ddarctan(x):
        return -2 * x * darctan(x) ** 2

    def dddarctan(x):
        y = darctan(x)
        return 2 * (4.0 * x**2 * y - 1.0) * y**2

    def ddddarctan(x):
        y = darctan(x)
        return (1.0 - 2 * x**2 * y) * 24 * x * y**3

    return (arctan, darctan, ddarctan, dddarctan, ddddarctan, None)[min(n, 5)]


def derivative_sin(n):
    def dcos(x):
        return -sin(x)

    def ddcos(x):
        return -cos(x)

    return (sin, cos, dcos, ddcos)[n % 4]


def derivative_cos(n):
    return derivative_sin(n + 1)


def derivative_tan(n):
    def dtan(x):
        return 1.0 / np.cos(x) ** 2

    def ddtan(x):
        return 2 * tan(x) / cos(x) ** 2

    def dddtan(x):
        y = tan(x)
        return 2 * (y**2 + 1) * (3 * y**2 + 1)

    def ddddtan(x):
        y = tan(x)
        return 8 * y * (y**2 + 1) * (3 * y**2 + 2)

    return (tan, dtan, ddtan, dddtan, ddddtan, None)[min(n, 5)]


def derivative_sinh(n):
    return (sinh, cosh)[n % 2]


def derivative_cosh(n):
    return derivative_sinh(n + 1)


def derivative_tanh(n):
    def dtanh(x):
        return 1.0 / cosh(x) ** 2

    def ddtanh(x):
        return -2 * sinh(x) / cosh(x) ** 3

    def dddtanh(x):
        y = cosh(x)
        return 4 * (tanh(x) / y) ** 2 - 2.0 / y**4

    def ddddtanh(x):
        y = tanh(x)
        return 8 * y * (y**2 - 1) * (3 * y**2 - 2)

    return (tanh, dtanh, ddtanh, dddtanh, ddddtanh, None)[min(n, 5)]


def _dddarc_h(x, y):
    return (3 * (x * y) ** 2 - 1) * y**3


def derivative_arccosh(n):
    def darccosh(x):
        return 1.0 / sqrt(x**2 - 1)

    def ddarccosh(x):
        return -x * darccosh(x) ** 3

    def dddarccosh(x):
        return _dddarc_h(x, darccosh(x))

    return (arccosh, darccosh, ddarccosh, dddarccosh, None)[min(n, 4)]


def derivative_arcsinh(n):
    def darcsinh(x):
        return 1.0 / sqrt(1 + x**2)

    def ddarcsinh(x):
        return -x * darcsinh(x) ** 3

    def dddarcsinh(x):
        return _dddarc_h(x, darcsinh(x))

    return (arcsinh, darcsinh, ddarcsinh, dddarcsinh, None)[min(n, 4)]


def derivative_arctanh(n):
    def darctanh(x):
        return 1.0 / (1 - x**2)

    def ddarctanh(x):
        return 2 * x * darctanh(x) ** 2

    def dddarctanh(x):
        y = darctanh(x)
        return 2 * y**2 * (1 + 4 * x**2 * y)

    return (arctanh, darctanh, ddarctanh, dddarctanh, None)[min(n, 4)]


def derivative_exp(n):
    return exp


def derivative_expm1(n):
    return (expm1, exp)[min(n, 1)]


def derivative_exp2(n):
    def dexp2(x):
        return exp2(x) * log(2) ** n

    return dexp2


def derivative_square(n):
    def dsquare(x):
        return 2 * x

    def ddsquare(x):
        return 2 * np.ones_like(x)

    def dddsquare(x):
        return np.zeros_like(x)

    return (square, dsquare, ddsquare, dddsquare)[min(n, 3)]


def derivative_log1p(n):
    def dlog1p(x):
        return (-1) ** (n + 1) * special.gamma(n) / (1 + x) ** n

    if n > 5:
        return None
    return (log1p, dlog1p)[min(n, 1)]


def _derivative_loga(n, a=10):
    if n > 4:
        return None
    dlog = derivative_log(n)

    def dlog_a(x):
        return dlog(x) / log(a)

    return dlog_a


def derivative_log2(n):
    return _derivative_loga(n, a=2)


def derivative_log10(n):
    return _derivative_loga(n, a=10)


def derivative_log(n):
    if n > 4:
        return None

    def dlog(x):
        return (-1) ** (n + 1) * special.gamma(n) / x**n

    return (log, dlog)[min(n, 1)]


def derivative_sqrt(n):
    fact = 0.5 * (-1) ** (n + 1)
    for k in np.arange(0.5, n - 1):
        fact *= k

    def dsqrt(x):
        sx = sqrt(x)
        return fact / sx ** (2 * n - 1)

    if n > 5:
        return None
    return (sqrt, dsqrt)[min(n, 1)]


def derivative_inv(n):
    def inv(x):
        return 1.0 / x

    def dinv(x):
        return (-1) ** n * special.gamma(n) / x ** (n + 1)

    return (inv, dinv)[min(n, 1)]


def get_function(fun_name, n=1):
    f_dic = {
        "cosh": derivative_cosh,
        "cos": derivative_cos,
        "sin": derivative_sin,
        "sinh": derivative_sinh,
        "tan": derivative_tan,
        "tanh": derivative_tanh,
        "arccosh": derivative_arccosh,
        "arcsinh": derivative_arcsinh,
        "arctanh": derivative_arctanh,
        "arccos": derivative_arccos,
        "arcsin": derivative_arcsin,
        "arctan": derivative_arctan,
        "exp": derivative_exp,
        "expm1": derivative_expm1,
        "exp2": derivative_exp2,
        "log1p": derivative_log1p,
        "log2": derivative_log2,
        "log10": derivative_log10,
        "log": derivative_log,
        "sqrt": derivative_sqrt,
        "square": derivative_square,
        "inv": derivative_inv,
    }
    if fun_name == "all":
        return f_dic.keys()

    funs = f_dic.get(fun_name)
    return funs(0), funs(n)
