'''
Created on 3. juni 2011

@author: pab
'''
from __future__ import absolute_import
import numpy as np
from numpy import exp, expm1, inf, nan, pi, hstack, where, atleast_1d, cos, sin
from .dispersion_relation import w2k, k2w  # @UnusedImport
from ..misc import JITImport
__all__ = ['w2k', 'k2w', 'sensor_typeid', 'sensor_type', 'TransferFunction']

_MODELS = JITImport('wafo.spectrum.models')


def hyperbolic_ratio(a, b, sa, sb):
    '''
    Return ratio of hyperbolic functions
          to allow extreme variations of arguments.

    Parameters
    ----------
    a, b : array-like
        arguments vectors of the same size
    sa, sb : scalar integers
        defining the hyperbolic function used, i.e.,
        f(x,1)=cosh(x), f(x,-1)=sinh(x)

    Returns
    -------
    r : ndarray
        f(a,sa)/f(b,sb), ratio of hyperbolic functions of same
                    size as a and b
     Examples
     --------
     >>> x = [-2,0,2]
     >>> hyperbolic_ratio(x,1,1,1)   # gives r=cosh(x)/cosh(1)
     array([ 2.438107  ,  0.64805427,  2.438107  ])
     >>> hyperbolic_ratio(x,1,1,-1)  # gives r=cosh(x)/sinh(1)
     array([ 3.20132052,  0.85091813,  3.20132052])
     >>> hyperbolic_ratio(x,1,-1,1)  # gives r=sinh(x)/cosh(1)
     array([-2.35040239,  0.        ,  2.35040239])
     >>> hyperbolic_ratio(x,1,-1,-1) # gives r=sinh(x)/sinh(1)
     array([-3.08616127,  0.        ,  3.08616127])
     >>> hyperbolic_ratio(1,x,1,1)   # gives r=cosh(1)/cosh(x)
     array([ 0.41015427,  1.54308063,  0.41015427])
     >>> hyperbolic_ratio(1,x,1,-1)  # gives r=cosh(1)/sinh(x)
     array([-0.42545906,         inf,  0.42545906])
     >>> hyperbolic_ratio(1,x,-1,1)  # gives r=sinh(1)/cosh(x)
     array([ 0.3123711 ,  1.17520119,  0.3123711 ])
     >>> hyperbolic_ratio(1,x,-1,-1) # gives r=sinh(1)/sinh(x)
     array([-0.32402714,         inf,  0.32402714])

     See also
     --------
     tran
    '''

    ak, bk, sak, sbk = np.atleast_1d(a, b, np.sign(sa), np.sign(sb))
    # old call
    # return exp(ak-bk)*(1+sak*exp(-2*ak))/(1+sbk*exp(-2*bk))
    # TODO: Does not always handle division by zero correctly

    signRatio = np.where(sak * ak < 0, sak, 1)
    signRatio = np.where(sbk * bk < 0, sbk * signRatio, signRatio)

    bk = np.abs(bk)
    ak = np.abs(ak)

    num = np.where(sak < 0, expm1(-2 * ak), 1 + exp(-2 * ak))
    den = np.where(sbk < 0, expm1(-2 * bk), 1 + exp(-2 * bk))
    iden = np.ones(den.shape) * inf
    ind = np.flatnonzero(den != 0)
    iden.flat[ind] = 1.0 / den[ind]
    val = np.where(num == den, 1, num * iden)
    # ((sak+exp(-2*ak))/(sbk+exp(-2*bk)))
    return signRatio * exp(ak - bk) * val


def sensor_typeid(*sensortypes):
    ''' Return ID for sensortype name

    Parameter
    ---------
    sensortypes : list of strings defining the sensortype

    Returns
    -------
    sensorids : list of integers defining the sensortype

    Valid senor-ids and -types for time series are as follows:
        0,  'n'    : Surface elevation              (n=Eta)
        1,  'n_t'  : Vertical surface velocity
        2,  'n_tt' : Vertical surface acceleration
        3,  'n_x'  : Surface slope in x-direction
        4,  'n_y'  : Surface slope in y-direction
        5,  'n_xx' : Surface curvature in x-direction
        6,  'n_yy' : Surface curvature in y-direction
        7,  'n_xy' : Surface curvature in xy-direction
        8,  'P'    : Pressure fluctuation about static MWL pressure
        9,  'U'    : Water particle velocity in x-direction
        10, 'V'    : Water particle velocity in y-direction
        11, 'W'    : Water particle velocity in z-direction
        12, 'U_t'  : Water particle acceleration in x-direction
        13, 'V_t'  : Water particle acceleration in y-direction
        14, 'W_t'  : Water particle acceleration in z-direction
        15, 'X_p'  : Water particle displacement in x-direction from mean pos.
        16, 'Y_p'  : Water particle displacement in y-direction from mean pos.
        17, 'Z_p'  : Water particle displacement in z-direction from mean pos.

    Example:
    >>> sensor_typeid('W','v')
    [11, 10]
    >>> sensor_typeid('rubbish')
    [nan]

    See also
    --------
    sensor_type
    '''

    sensorid_table = dict(n=0, n_t=1, n_tt=2, n_x=3, n_y=4, n_xx=5,
                          n_yy=6, n_xy=7, p=8, u=9, v=10, w=11, u_t=12,
                          v_t=13, w_t=14, x_p=15, y_p=16, z_p=17)
    try:
        return [sensorid_table.get(name.lower(), nan) for name in sensortypes]
    except:
        raise ValueError('Input must be a string!')


def sensor_type(*sensorids):
    '''
    Return sensortype name

    Parameter
    ---------
    sensorids : vector or list of integers defining the sensortype

    Returns
    -------
    sensornames : tuple of strings defining the sensortype
        Valid senor-ids and -types for time series are as follows:
        0,  'n'    : Surface elevation              (n=Eta)
        1,  'n_t'  : Vertical surface velocity
        2,  'n_tt' : Vertical surface acceleration
        3,  'n_x'  : Surface slope in x-direction
        4,  'n_y'  : Surface slope in y-direction
        5,  'n_xx' : Surface curvature in x-direction
        6,  'n_yy' : Surface curvature in y-direction
        7,  'n_xy' : Surface curvature in xy-direction
        8,  'P'    : Pressure fluctuation about static MWL pressure
        9,  'U'    : Water particle velocity in x-direction
        10, 'V'    : Water particle velocity in y-direction
        11, 'W'    : Water particle velocity in z-direction
        12, 'U_t'  : Water particle acceleration in x-direction
        13, 'V_t'  : Water particle acceleration in y-direction
        14, 'W_t'  : Water particle acceleration in z-direction
        15, 'X_p'  : Water particle displacement in x-direction from mean pos.
        16, 'Y_p'  : Water particle displacement in y-direction from mean pos.
        17, 'Z_p'  : Water particle displacement in z-direction from mean pos.

    Example:
    >>> sensor_type(range(3))
    ('n', 'n_t', 'n_tt')

    See also
    --------
    sensor_typeid, tran
    '''
    valid_names = ('n', 'n_t', 'n_tt', 'n_x', 'n_y', 'n_xx', 'n_yy', 'n_xy',
                   'p', 'u', 'v', 'w', 'u_t', 'v_t', 'w_t', 'x_p', 'y_p',
                   'z_p', nan)
    ids = atleast_1d(*sensorids)
    if isinstance(ids, list):
        ids = hstack(ids)
    n = len(valid_names) - 1
    ids = where(((ids < 0) | (n < ids)), n, ids)
    return tuple(valid_names[i] for i in ids)


class TransferFunction(object):

    '''
    Class for computing transfer functions based on linear wave theory
        of the system with input surface elevation,
        eta(x0,y0,t) = exp(i*(kx*x0+ky*y0-w*t)),
        and output Y determined by sensortype and position of sensor.

    Member methods
    --------------
    tran(w, theta, kw)

    Hw  = a function of frequency only (not direction)   size  1 x Nf
    Gwt = a function of frequency and direction          size Nt x Nf
    w = vector of angular frequencies in Rad/sec. Length Nf
    theta = vector of directions in radians           Length Nt   (default 0)
            ( theta = 0 -> positive x axis theta = pi/2 -> positive y axis)
    Member variables
    ----------------
    pos : [x,y,z],  (default [0,0,0])
        vector giving coordinate position relative to [x0 y0 z0]
    sensortype = string
        defining the sensortype or transfer function in output.
        0,  'n'    : Surface elevation              (n=Eta)     (default)
        1,  'n_t'  : Vertical surface velocity
        2,  'n_tt' : Vertical surface acceleration
        3,  'n_x'  : Surface slope in x-direction
        4,  'n_y'  : Surface slope in y-direction
        5,  'n_xx' : Surface curvature in x-direction
        6,  'n_yy' : Surface curvature in y-direction
        7,  'n_xy' : Surface curvature in xy-direction
        8,  'P'    : Pressure fluctuation about static MWL pressure
        9,  'U'    : Water particle velocity in x-direction
        10, 'V'    : Water particle velocity in y-direction
        11, 'W'    : Water particle velocity in z-direction
        12, 'U_t'  : Water particle acceleration in x-direction
        13, 'V_t'  : Water particle acceleration in y-direction
        14, 'W_t'  : Water particle acceleration in z-direction
        15, 'X_p'  : Water particle displacement in x-direction from mean pos.
        16, 'Y_p'  : Water particle displacement in y-direction from mean pos.
        17, 'Z_p'  : Water particle displacement in z-direction from mean pos.
    h : real scalar
        water depth      (default inf)
    g : real scalar
        acceleration of gravity (default 9.81 m/s**2)
    rho : real scalar
        water density    (default 1028 kg/m**3)
    bet : 1 or -1 (default 1)
        1, theta given in terms of directions toward which waves travel
        -1, theta given in terms of directions from which waves come
    igam : 1,2 or 3
        1, if z is measured positive upward from mean water level (default)
        2, if z is measured positive downward from mean water level
        3, if z is measured positive upward from sea floor
    thetax, thetay : real scalars
        angle in degrees clockwise from true north to positive x-axis and
        positive y-axis, respectively. (default theatx=90, thetay=0)

    Example
    -------
    >>> import pylab as plt
    >>> N=50; f0=0.1; th0=0; h=50; w0 = 2*pi*f0
    >>> t = np.linspace(0,15,N)
    >>> eta0 = np.exp(-1j*w0*t)
    >>> stypes = ['n', 'n_x', 'n_y'];
    >>> tf = TransferFunction(pos=(0, 0, 0), h=50)
    >>> vals = []
    >>> for i,stype in enumerate(stypes):
    ...    tf.sensortype = stype
    ...    Hw, Gwt = tf.tran(w0,th0)
    ...    vals.append((Hw*Gwt*eta0).real.ravel())

    fh = plt.plot(t, eta0.real, 'r.')
    plt.hold(True)
    for val in vals:
        fh = plt.plot(t, val)
    plt.show()


    See also
    --------
    dat2dspec, sensor_type, sensor_typeid

    Reference
    ---------
    Young I.R. (1994)
    "On the measurement of directional spectra",
    Applied Ocean Research, Vol 16, pp 283-294
    '''

    def __init__(self, pos=(0, 0, 0), sensortype='n', h=inf, g=9.81, rho=1028,
                 bet=1, igam=1, thetax=90, thetay=0):
        self.pos = pos
        self.sensortype = sensortype if isinstance(
            sensortype, str) else sensor_type(sensortype)
        self.h = h
        self.g = g
        self.rho = rho
        self.bet = bet
        self.igam = igam
        self.thetax = thetax
        self.thetay = thetay
        self._tran_dict = dict(n=self._n, n_t=self._n_t, n_tt=self._n_tt,
                               n_x=self._n_x, n_y=self._n_y, n_xx=self._n_xx,
                               n_yy=self._n_yy, n_xy=self._n_xy,
                               P=self._p, p=self._p,
                               U=self._u, u=self._u,
                               V=self._v, v=self._v,
                               W=self._w, w=self._w,
                               U_t=self._u_t, u_t=self._u_t,
                               V_t=self._v_t, v_t=self._v_t,
                               W_t=self._w_t, w_t=self._w_t,
                               X_p=self._x_p, x_p=self._x_p,
                               Y_p=self._y_p, y_p=self._y_p,
                               Z_p=self._z_p, z_p=self._z_p)

    def tran(self, w, theta=0, kw=None):
        '''
        Return transfer functions based on linear wave theory
        of the system with input surface elevation,
        eta(x0,y0,t) = exp(i*(kx*x0+ky*y0-w*t)),
        and output,
        Y  = Hw*Gwt*eta, determined by sensortype and position of sensor.

        Parameters
        ----------
        w : array-like
            vector of angular frequencies in Rad/sec. Length Nf
        theta : array-like
            vector of directions in radians           Length Nt   (default 0)
            ( theta = 0 -> positive x axis theta = pi/2 -> positive y axis)
        kw : array-like
            vector of wave numbers corresponding to angular frequencies, w.
            Length Nf (default calculated with w2k)

        Returns
        -------
        Hw  = transfer function of frequency only (not direction) size  1 x Nf
        Gwt = transfer function of frequency and direction        size Nt x Nf

        The complete transfer function Hwt = Hw*Gwt is a function of
        w (columns) and theta (rows)                   size Nt x Nf
        '''
        if kw is None:
            # wave number as function of angular frequency
            kw, unusedkw2 = w2k(w, 0, self.h)

        w, theta, kw = np.atleast_1d(w, theta, kw)
        # make sure they have the correct orientation
        theta.shape = (-1, 1)
        kw.shape = (-1,)
        w.shape = (-1,)

        tran_fun = self._tran_dict[self.sensortype]
        Hw, Gwt = tran_fun(w, theta, kw)

        # New call to avoid singularities. pab 07.11.2000
        # Set Hw to 0 for expressions w*hyperbolic_ratio(z*k,h*k,1,-1)= 0*inf
        ind = np.flatnonzero(1 - np.isfinite(Hw))
        Hw.flat[ind] = 0

        sgn = np.sign(Hw)
        k0 = np.flatnonzero(sgn < 0)
        if len(k0):  # make sure Hw>=0 ie. transfer negative signs to Gwt
            Gwt[:, k0] = -Gwt[:, k0]
            Hw[:, k0] = -Hw[:, k0]

        if self.igam == 2:
            # pab 09 Oct.2002: bug fix
            # Changing igam by 2 should affect the directional result in the
            # same way that changing eta by -eta!
            Gwt = -Gwt
        return Hw, Gwt
    __call__ = tran

# --- Private member methods ---

    def _get_ee_cthxy(self, theta, kw):
        # convert from angle in degrees to radians
        bet = self.bet
        thxr = self.thetax * pi / 180
        thyr = self.thetay * pi / 180

        cthx = bet * cos(theta - thxr + pi / 2)
        # cthy = cos(theta-thyr-pi/2)
        cthy = bet * sin(theta - thyr)

        # Compute location complex exponential
        x, y, unused_z = list(self.pos)
        # exp(i*k(w)*(x*cos(theta)+y*sin(theta)) size Nt X Nf
        ee = exp((1j * (x * cthx + y * cthy)) * kw)
        return ee, cthx, cthy

    def _get_zk(self, kw):
        h = self.h
        z = self.pos[2]
        if self.igam == 1:
            # z measured positive upward from mean water level (default)
            zk = kw * (h + z)
        elif self.igam == 2:
            # z measured positive downward from mean water level
            zk = kw * (h - z)
        else:
            zk = kw * z  # z measured positive upward from sea floor
        return zk

    # --- Surface elevation ---
    def _n(self, w, theta, kw):
        '''n = Eta = wave profile
        '''
        ee, unused_cthx, unused_cthy = self._get_ee_cthxy(theta, kw)
        return np.ones_like(w), ee

    # --- Vertical surface velocity and acceleration ---
    def _n_t(self, w, theta, kw):
        ''' n_t = Eta_t '''
        ee, unused_cthx, unused_cthy = self._get_ee_cthxy(theta, kw)
        return w, -1j * ee

    def _n_tt(self, w, theta, kw):
        '''n_tt = Eta_tt'''
        ee, unused_cthx, unused_cthy = self._get_ee_cthxy(theta, kw)
        return w ** 2, -ee

    # --- Surface slopes ---
    def _n_x(self, w, theta, kw):
        ''' n_x = Eta_x = x-slope'''
        ee, cthx, unused_cthy = self._get_ee_cthxy(theta, kw)
        return kw, 1j * cthx * ee

    def _n_y(self, w, theta, kw):
        ''' n_y = Eta_y = y-slope'''
        ee, unused_cthx, cthy = self._get_ee_cthxy(theta, kw)
        return kw, 1j * cthy * ee

    # --- Surface curvatures ---
    def _n_xx(self, w, theta, kw):
        ''' n_xx = Eta_xx = Surface curvature (x-dir)'''
        ee, cthx, unused_cthy = self._get_ee_cthxy(theta, kw)
        return kw ** 2, -(cthx ** 2) * ee

    def _n_yy(self, w, theta, kw):
        ''' n_yy = Eta_yy = Surface curvature (y-dir)'''
        ee, unused_cthx, cthy = self._get_ee_cthxy(theta, kw)
        return kw ** 2, -cthy ** 2 * ee

    def _n_xy(self, w, theta, kw):
        ''' n_xy = Eta_xy = Surface curvature (xy-dir)'''
        ee, cthx, cthy = self._get_ee_cthxy(theta, kw)
        return kw ** 2, -cthx * cthy * ee

    # --- Pressure---
    def _p(self, w, theta, kw):
        ''' pressure fluctuations'''
        ee, unused_cthx, unused_cthy = self._get_ee_cthxy(theta, kw)
        hk = kw * self.h
        zk = self._get_zk(kw)
        # hyperbolic_ratio =  cosh(zk)/cosh(hk)
        return self.rho * self.g * hyperbolic_ratio(zk, hk, 1, 1), ee

    # --- Water particle velocities ---
    def _u(self, w, theta, kw):
        ''' U = x-velocity'''
        ee, cthx, unused_cthy = self._get_ee_cthxy(theta, kw)
        hk = kw * self.h
        zk = self._get_zk(kw)
        # w*cosh(zk)/sinh(hk), cos(theta)*ee
        return w * hyperbolic_ratio(zk, hk, 1, -1), cthx * ee

    def _v(self, w, theta, kw):
        '''V = y-velocity'''
        ee, unused_cthx, cthy = self._get_ee_cthxy(theta, kw)
        hk = kw * self.h
        zk = self._get_zk(kw)
        # w*cosh(zk)/sinh(hk), sin(theta)*ee
        return w * hyperbolic_ratio(zk, hk, 1, -1), cthy * ee

    def _w(self, w, theta, kw):
        ''' W = z-velocity'''
        ee, unused_cthx, unused_cthy = self._get_ee_cthxy(theta, kw)
        hk = kw * self.h
        zk = self._get_zk(kw)
        # w*sinh(zk)/sinh(hk), -?
        return w * hyperbolic_ratio(zk, hk, -1, -1), -1j * ee

    # --- Water particle acceleration ---
    def _u_t(self, w, theta, kw):
        ''' U_t = x-acceleration'''
        ee, cthx, unused_cthy = self._get_ee_cthxy(theta, kw)
        hk = kw * self.h
        zk = self._get_zk(kw)
        # w^2*cosh(zk)/sinh(hk), ?
        return (w ** 2) * hyperbolic_ratio(zk, hk, 1, -1), -1j * cthx * ee

    def _v_t(self, w, theta, kw):
        ''' V_t = y-acceleration'''
        ee, unused_cthx, cthy = self._get_ee_cthxy(theta, kw)
        hk = kw * self.h
        zk = self._get_zk(kw)
        # w^2*cosh(zk)/sinh(hk), ?
        return (w ** 2) * hyperbolic_ratio(zk, hk, 1, -1), -1j * cthy * ee

    def _w_t(self, w, theta, kw):
        ''' W_t = z-acceleration'''
        ee, unused_cthx, unused_cthy = self._get_ee_cthxy(theta, kw)
        hk = kw * self.h
        zk = self._get_zk(kw)
        # w*sinh(zk)/sinh(hk), ?
        return (w ** 2) * hyperbolic_ratio(zk, hk, -1, -1), -ee

    # --- Water particle displacement ---
    def _x_p(self, w, theta, kw):
        ''' X_p = x-displacement'''
        ee, cthx, unused_cthy = self._get_ee_cthxy(theta, kw)
        hk = kw * self.h
        zk = self._get_zk(kw)
        # cosh(zk)./sinh(hk), ?
        return hyperbolic_ratio(zk, hk, 1, -1), 1j * cthx * ee

    def _y_p(self, w, theta, kw):
        ''' Y_p = y-displacement'''
        ee, unused_cthx, cthy = self._get_ee_cthxy(theta, kw)
        hk = kw * self.h
        zk = self._get_zk(kw)
        # cosh(zk)./sinh(hk), ?
        return hyperbolic_ratio(zk, hk, 1, -1), 1j * cthy * ee

    def _z_p(self, w, theta, kw):
        ''' Z_p = z-displacement'''
        ee, unused_cthx, unused_cthy = self._get_ee_cthxy(theta, kw)
        hk = kw * self.h
        zk = self._get_zk(kw)
        return hyperbolic_ratio(zk, hk, -1, -1), ee  # sinh(zk)./sinh(hk), ee


def wave_pressure(z, Hm0, h=10000, g=9.81, rho=1028):
    '''
    Calculate pressure amplitude due to water waves.

    Parameters
    ----------
    z : array-like
        depth where pressure is calculated [m]
    Hm0 : array-like
        significant wave height (same as the average of the 1/3'rd highest
        waves in a seastate. [m]
    h : real scalar
        waterdepth (default 10000 [m])
    g : real scalar
        acceleration of gravity (default 9.81 m/s**2)
    rho : real scalar
        water density    (default 1028 kg/m**3)


    Returns
    -------
    p : ndarray
        pressure amplitude due to water waves at water depth z. [Pa]

    PRESSURE calculate pressure amplitude due to water waves according to
    linear theory.

    Example
    -----
    >>> import pylab as plt
    >>> z = -np.linspace(10,20)
    >>> p = wave_pressure(z, Hm0=1, h=20)

    fh = plt.plot(z, p)
    plt.show()

    See also
    --------
    w2k

    '''

    # Assume seastate with jonswap spectrum:
    Tp = 4 * np.sqrt(Hm0)
    gam = _MODELS.jonswap_peakfact(Hm0, Tp)
    Tm02 = Tp / (1.30301 - 0.01698 * gam + 0.12102 / gam)
    w = 2 * np.pi / Tm02
    kw, unused_kw2 = w2k(w, 0, h)

    hk = kw * h
    zk1 = kw * z
    zk = hk + zk1  # z measured positive upward from mean water level (default)
    #  zk = hk-zk1  # z measured positive downward from mean water level
    #  zk1 = -zk1
    #  zk = zk1  # z measured positive upward from sea floor

    #  cosh(zk)/cosh(hk) approx exp(zk) for large h
    #  hyperbolic_ratio(zk,hk,1,1) = cosh(zk)/cosh(hk)
    # pr = np.where(np.pi < hk, np.exp(zk1), hyperbolic_ratio(zk, hk, 1, 1))
    pr = hyperbolic_ratio(zk, hk, 1, 1)
    pressure = (rho * g * Hm0 / 2) * pr

    #    pos = [np.zeros_like(z),np.zeros_like(z),z]
    #    tf = TransferFunction(pos=pos, sensortype='p', h=h, rho=rho, g=g)
    #    Hw, Gwt = tf.tran(w,0)
    #    pressure2 = np.abs(Hw) * Hm0 / 2

    return pressure


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


def main():
    sensor_type(range(21))
if __name__ == '__main__':
    test_docstrings()
