"""
Dispersion relation module
--------------------------
k2w - Translates from wave number to frequency
w2k - Translates from frequency to wave number
"""
import warnings
import numpy as np
from wafo.misc import lazywhere
from numpy import (atleast_1d, sqrt, ones_like, zeros_like, arctan2, where,
                   tanh, sin, cos, sign, inf,
                   flatnonzero, finfo, cosh, abs)

__all__ = ['k2w', 'w2k']


def k2w(k1, k2=0e0, h=inf, g=9.81, u1=0e0, u2=0e0):
    ''' Translates from wave number to frequency
        using the dispersion relation

    Parameters
    ----------
    k1 : array-like
        wave numbers [rad/m].
    k2 : array-like, optional
        second dimension wave number
    h : real scalar, optional
        water depth [m].
    g : real scalar, optional
        acceleration of gravity, see gravity
    u1, u2 : real scalars, optional
        current velocity [m/s] along dimension 1 and 2.
        note: when u1!=0 | u2!=0 then theta is not calculated correctly

    Returns
    -------
    w : ndarray
        angular frequency [rad/s].
    theta : ndarray
        direction [rad].

    Dispersion relation
    -------------------
        w     = sqrt(g*K*tanh(K*h))   (  0 <   w   < inf)
        theta = arctan2(k2,k1)        (-pi < theta <  pi)
    where
        K = sqrt(k1**2+k2**2)

    The shape of w and theta is the common shape of k1 and k2 according to the
    numpy broadcasting rules.

    See also
    --------
    w2k

    Example
    -------
    >>> from numpy import arange
    >>> import wafo.wave_theory.dispersion_relation as wsd
    >>> wsd.k2w(arange(0.01,.5,0.2))[0]
    array([ 0.3132092 ,  1.43530485,  2.00551739])
    >>> wsd.k2w(arange(0.01,.5,0.2),h=20)[0]
    array([ 0.13914927,  1.43498213,  2.00551724])
    '''

    k1i, k2i, hi, gi, u1i, u2i = atleast_1d(k1, k2, h, g, u1, u2)

    if k1i.size == 0:
        return zeros_like(k1i)
    ku1 = k1i * u1i
    ku2 = k2i * u2i

    theta = arctan2(k2, k1)

    k = sqrt(k1i ** 2 + k2i ** 2)
    w = where(k > 0, ku1 + ku2 + sqrt(gi * k * tanh(k * hi)), 0.0)

    cond = (w < 0)
    if np.any(cond):
        txt0 = '''
               Waves and current are in opposite directions
               making some of the frequencies negative.
               Here we are forcing the negative frequencies to zero.
               '''
        warnings.warn(txt0)
        w = where(cond, 0.0, w)  # force w to zero

    return w, theta


def w2k(w, theta=0.0, h=inf, g=9.81, count_limit=100):
    '''
    Translates from frequency to wave number
      using the dispersion relation

    Parameters
    ----------
    w : array-like
        angular frequency [rad/s].
    theta : array-like, optional
        direction [rad].
    h : real scalar, optional
        water depth [m].
    g : real scalar or array-like of size 2.
        constant of gravity [m/s**2] or 3D normalizing constant

    Returns
    -------
    k1, k2 : ndarray
        wave numbers [rad/m] along dimension 1 and 2.

    Description
    -----------
    Uses Newton Raphson method to find the wave number k in the dispersion
    relation
        w**2= g*k*tanh(k*h).
    The solution k(w) => k1 = k(w)*cos(theta)
                         k2 = k(w)*sin(theta)
    The size of k1,k2 is the common shape of w and theta according to numpy
    broadcasting rules. If w or theta is scalar it functions as a constant
    matrix of the same shape as the other.

    Example
    -------
    >>> import pylab as plb
    >>> import wafo.wave_theory.dispersion_relation as wsd
    >>> w = plb.linspace(0,3);
    >>> wsd.w2k(range(4))[0]
    array([ 0.        ,  0.1019368 ,  0.4077472 ,  0.91743119])
    >>> wsd.w2k(range(4),h=20)[0]
    array([ 0.        ,  0.10503601,  0.40774726,  0.91743119])

    h = plb.plot(w,w2k(w)[0])
    plb.close('all')

    See also
    --------
    k2w
    '''
    wi, th, hi, gi = atleast_1d(w, theta, h, g)

    if wi.size == 0:
        return zeros_like(wi)

    k = 1.0 * sign(wi) * wi ** 2.0 / gi[0]  # deep water
    if (hi > 10. ** 25).all():
        k2 = k * sin(th) * gi[0] / gi[-1]  # size np x nf
        k1 = k * cos(th)
        return k1, k2

    if gi.size > 1:
        raise ValueError('Finite depth in combination with 3D normalization' +
                         ' (len(g)=2) is not implemented yet.')

    find = flatnonzero
    eps = finfo(float).eps

    oshape = k.shape
    wi, k, hi = wi.ravel(), k.ravel(), hi.ravel()

    # Newton's Method
    # Permit no more than count_limit iterations.
    hi = hi * ones_like(k)
    hn = zeros_like(k)
    ix = find((wi < 0) | (0 < wi))

    # Break out of the iteration loop for three reasons:
    #  1) the last update is very small (compared to x)
    #  2) the last update is very small (compared to sqrt(eps))
    #  3) There are more than 100 iterations. This should NEVER happen.
    count = 0
    while (ix.size > 0 and count < count_limit):
        ki = k[ix]
        kh = ki * hi[ix]
        coshkh2 = lazywhere(np.abs(kh) < 350, (kh, ),
                            lambda kh: cosh(kh) ** 2.0, fillvalue=np.inf)
        hn[ix] = (ki * tanh(kh) - wi[ix] ** 2.0 / gi) / \
                 (tanh(kh) + kh / coshkh2)
        knew = ki - hn[ix]
        # Make sure that the current guess is not zero.
        # When Newton's Method suggests steps that lead to zero guesses
        # take a step 9/10ths of the way to zero:
        ksmall = find(np.abs(knew) == 0)
        if ksmall.size > 0:
            knew[ksmall] = ki[ksmall] / 10.0
            hn[ix[ksmall]] = ki[ksmall] - knew[ksmall]

        k[ix] = knew
        # disp(['Iteration ',num2str(count),'  Number of points left:  '
        # num2str(length(ix)) ]),

        ix = find((np.abs(hn) > sqrt(eps) * np.abs(k)) *
                  np.abs(hn) > sqrt(eps))
        count += 1

    if count == count_limit:
        warnings.warn('W2K did not converge. The maximum error in the ' +
                      'last step was: %13.8f' % max(hn[ix]))

    k.shape = oshape

    k2 = k * sin(th)
    k1 = k * cos(th)
    return k1, k2


def test_docstrings():
    import doctest
    print('Testing docstrings in %s' % __file__)
    doctest.testmod(optionflags=doctest.NORMALIZE_WHITESPACE)


if __name__ == '__main__':
    test_docstrings()
