"""

Series of functions to manage wave information.

Author:
-------
Nearshore Modeling Group
Gabriel Garcia Medina
ggarcia@coas.oregonstate.edu

Log of edits:
-------------
April 2014 - Created module
  Gabriel Garcia Medina (ggarcia@coas.oregonstate.edu)

External dependencies:
  numpy
  scipy

Internal dependencies:
  gsignal

"""

from __future__ import division, print_function

# Import generic modules
import numpy as np
import scipy
import scipy.optimize
import scipy.signal

# Internal modules
import pynmd.data.signal as gsignal
import pynmd.data.angles as _gangles


# ===============================================================================
# Compute radian frequency from wave number
# ===============================================================================
def idispersion(k, h):
    '''
    Inverse linear dispersion relation

    USAGE:
    ------
    sigma = idispersion(k,h)

    INPUT:
    ------
    k      : Wave number [m**-1]
    h      : water depth [m]

    OUTPUT:
    -------
    sigma  : radian frequency [Hz]

    '''

    # Get radian wave frequency
    sigma = (9.81 * k * np.tanh(k * h)) ** 0.5

    # Return stokes velocities
    return sigma


# ===============================================================================
# Compute wave number
# ===============================================================================
def dispersion(period, h, u=None):
    '''

    Computes the linear dispersion relation.

    USAGE:
    ------
    k = dispersion(period,h,u)

    Input:
    ------
    Period   : Wave period [s]
    h        : Water depth [m]
    u        : (Optional) Ambient current velocity [m/s]

    RETURNS:
    --------
    k        : Wave number (2*pi/wave_length) [m**-1]

    '''

    if u is None:
        u = 0.0

    # Initialize wave number
    #     k = np.zeros(len(period))
    sigma = (2.0 * np.pi) / period
    #     kinit = (((sigma**2)*h/9.81)*
    #              ((1/np.tanh(((sigma**2)/9.81*h)**0.75))**(2.0/3.0)))
    kinit = 0.0001

    def f(k, sigma, h, u):
        y = (9.81 * k * np.tanh(k * h)) ** 0.5 - sigma + u * k
        return y

    if h <= 0:
        k = np.NAN
    else:
        try:
            k = scipy.optimize.newton(f, kinit, fprime=None,
                                      args=(sigma, h, u), maxiter=100)
        except RuntimeError:
            k = np.NAN

    return k


# ===============================================================================
# Compute wave number using the Kirby and Dalrymple (1986) composite equation
# ===============================================================================
def dispersion_kd86(period, h, wh, u=None):
    '''

    Return the wave number from the composite dispersion relation by
    Kirby and Dalrymple (1986).

    USAGE:
    ------
    k = dispersion_kd86(period,h,wh,u)

    Input:
    ------
    Period   : Wave period [s]
    h        : Water depth [m]
    wh       : Wave height [m]
    u        : (Optional) Ambient current velocity [m/s]

    RETURNS:
    --------
    k        : Wave number (2*pi/wave_length) [m**-1]

    CELERITY EQUATION:
    ------------------
    (c-u)^2 = g / k * (1 + f1 * eps^2 * D) tanh( k * h + f2 * eps)
    f1      = tanh(k*h)^5
    f2      = (k*h/sinh(k*h))^4
    eps     = k*wh/2
    D       = (8 + cosh(4*k*h) - 2 * tanh(k*h)^2)/(8*sinh(k*h)^4)

    REFERENCES:
    -----------
    Catalan, P., and M. C. Haller, 2008: Remote sensing of breaking wave phase
        speeds with application to non-linear depth inversions. Coastal
        Engineering, 55, 93 - 111.
    Kirby, J. T., and R. A. Dalrymple, 1986: An approximate model for nonlinear
        dispersion in monochromatic wave propagation models. Coastal
        Engineering, 9, 545-561.
    '''

    # Depth averaged ambient velocity
    if u is None:
        u = 0

    # Compute radian frequency
    sigma = (2.0 * np.pi) / period

    # Initialize wave number
    # kinit = 0.0001                # Manually initialize
    kinit = dispersion(period, h, u)  # Initialize with Airy dispersion relation

    # Kirby and Dalrymple (1986) dispersion function
    def f(k, sigma, h, wh, u):
        eps = k * wh / 2
        f1 = np.tanh(k * h) ** 5
        f2 = (k * h / np.sinh(k * h)) ** 4
        D = ((8.0 + np.cosh(4.0 * k * h) - 2.0 * (np.tanh(k * h) ** 2)) /
             (8.0 * (np.sinh(k * h) ** 4)))
        y = ((9.81 * k * np.tanh(k * h + f2 * eps) * (1.0 + f1 * (eps ** 2) * D)) ** 0.5 +
             u * k - sigma)
        return y

    if h <= 0:
        k = np.NAN
    else:
        try:
            k = scipy.optimize.newton(f, kinit, fprime=None,
                                      args=(sigma, h, wh, u), maxiter=100)
        except RuntimeError:
            k = np.NAN

    return k


# ===============================================================================
# Compute wave number using the Booij
# ===============================================================================
def dispersion_booij(period, h, wh, u=None):
    '''

    Return the wave number from the composite dispersion relation by
    Booij (1981).

    USAGE:
    ------
    k = dispersion_booij(period,h,wh,u)

    Input:
    ------
    Period   : Wave period [s]
    h        : Water depth [m]
    wh       : Wave height [m]
    u        : (Optional) Ambient current velocity [m/s]

    RETURNS:
    --------
    k        : Wave number (2*pi/wave_length) [m**-1]

    CELERITY EQUATION:
    ------------------
    (c-u)^2 = g / k * tanh(k*(h+wh/2))

    REFERENCES:
    -----------
    Booij, N. 1981: Gravity waves on water with non-uniform depth and current.
        Tech. Rep. No. 81-1. Dept. Civil Engineering, Delft University of
        Technology.
    Catalan, P., and M. C. Haller, 2008: Remote sensing of breaking wave phase
        speeds with application to non-linear depth inversions. Coastal
        Engineering, 55, 93 - 111.
    '''

    # Depth averaged ambient velocity
    if u is None:
        u = 0.0

    # Compute radian frequency
    sigma = (2.0 * np.pi) / period

    # Initialize wave number
    # kinit = 0.0001                     # Manually initialize
    kinit = dispersion(period, h + wh / 2, u)  # Initialize with Airy dispersion relation

    # Booij (1981) dispersion function
    def f(k, sigma, h, wh, u):
        y = (9.81 * k * np.tanh(k * (h + wh / 2.0))) ** 0.5 + u * k - sigma
        return y

    if h <= 0:
        k = np.NAN
    else:
        try:
            k = scipy.optimize.newton(f, kinit, fprime=None,
                                      args=(sigma, h, wh, u), maxiter=100)
        except RuntimeError:
            k = np.NAN

    return k


# ===============================================================================
# Compute dispersion relation from Nwogu's equations
# ===============================================================================
def dispersion_nwogu(period, h, alpha):
    '''
    Compute dispersion relation for the Boussinesq equations based on
    Nwogu 1993.

    PARAMETERS:
    -----------
    period    : Wave period [s]
    h         : Water depth [m]
    alpha     : Non-dimensional wave steepness parameter (see notes)

    RETURNS:
    --------
    k         : Wave number [m**-1]

    NOTES:
    ------
    alpha = 0       for celerity at the surface
    alpha = -1/3    solves the traditional depth averaged equations.
    alpha = -0.39   gives similar results to linear wave theory (Nwogu 1993)

    REFERENCES:
    -----------
    Nwogu, O., 1993: Alternative Form of Boussinesq Equations for Nearshore
      Wave Propagation. Journal of Waterway, Port, Coastal, and Ocean
      Engineering, 119, 618-638.

    '''

    # Compute randian frequency
    sigma = (2.0 * np.pi) / period

    # Initialize wave number
    # kinit = dispersion(period,h) # Linear wave theory
    kinit = sigma ** 2 / 9.81  # Deep water equivalent

    # kinit = 0.05                 # Hard code parameter

    # Define the dispersion relation function
    def bd(k, sigma, h, alpha):
        # y = 9.81 * (k**2) * h * (1.0 - 1.0/3.0 * (k*h)**2) - sigma**2
        y = (9.81 * (k ** 2) * h * (1.0 - (alpha + 1.0 / 3.0) * ((k * h) ** 2)) /
             (1.0 - alpha * ((k * h) ** 2)) - sigma ** 2)
        return y

    # Use Newton-Rhapson method to find the wave number
    if h <= 0:
        k = np.NAN
    else:
        try:
            k = scipy.optimize.newton(bd, kinit, fprime=None,
                                      args=(sigma, h, alpha), maxiter=1000)
        except RuntimeError:
            k = np.NAN

    return k


# ===============================================================================
# Linear wave approximations
# ===============================================================================
def shallow_water_depth(period):
    '''

    h_shallow = shallow_water_depth(period)

    Find where the waves with the given period enter shallow water according to
    linear wave theory

    PARAMETERS:
    -----------
    period     : wave period [s]

    RETURNS:
    --------
    h_shallow  : Water depth where shallow water approximation is valid [m]

    '''

    # Initialize shallow water depth
    hinit = 2.0

    # Define shallow water limit
    def swd(h, period):
        y = dispersion(period, h) * h - np.pi / 10.0
        return y

    # Find zeros of the swd function
    try:
        h = scipy.optimize.newton(swd, hinit, fprime=None,
                                  args=(period,), maxiter=100)
    except RuntimeError:
        h = np.NAN

    return h


def deep_water_period(depth):
    '''

    t_deep = deep_water_depth(depth)

    Find the period of waves entering intermediate water depth at a give depth
    according to linear wave theory (i.e. kh = pi)

    PARAMETERS:
    -----------
    depth     : water depth [m]

    RETURNS:
    --------
    t_deep  : waves of this period and shorter do not feel the bottom [s]

    '''

    return 2.0 * np.pi / idispersion(np.pi / depth, depth)


# ===============================================================================
# Wave length
# ===============================================================================
def wave_length(period, h, verbose=True):
    '''
    Compute wave length using linear wave theory

    Parameters
    ----------
    period   : wave period [s]
    h        : water depth [m]

    Results
    -------
    wl_int   : real wave length [m]

    Screen output
    -------------
    wl_deep  : deep water wave length [m]
    wl_sha   : shallow water wave length [m]

    '''

    wl_deep = 9.81 * period ** 2 / 2.0 / np.pi
    wl_sha = period * np.sqrt(9.81 * h)
    k = dispersion(period, h)
    wl_int = 9.81 / 2.0 / np.pi * period ** 2 * np.tanh(k * h)

    if verbose:
        print(' ')
        print('---------------------------------------------------------')
        print('Wave Length deep water approx      = ' + np.str(wl_deep) + ' m')
        print('Wave Length shallow water approx   = ' + np.str(wl_sha) + ' m')
        print('Wave Length linear wave theory     = ' + np.str(wl_int) + ' m')
        print('---------------------------------------------------------')
        print(' ')

    return wl_int


# ===============================================================================
# Compute wave number using the Kirby and Dalrymple (1986) composite equation
# ===============================================================================
def celerity(period, h, u=None):
    '''

    Find celerity and group velocity from linear wave theory

    USAGE:
    ------
    c,n,cg = celerity(period,h,u)

    Input:
    ------
    period   : Wave period [s]
    h        : Water depth [m]
    u        : (Optional) Ambient current velocity [m/s]

    RETURNS:
    --------
    c        : celerity [m/s]
    n        : cg = c*n
    cg       : group velocity [m/s]

    '''

    # Radian frequency
    sigma = 2.0 * np.pi / period

    # Find wave number
    k = dispersion(period, h, u)

    # Celerity
    c = sigma / k

    # Group velocity
    n = 0.5 + k * h / np.sinh(2 * k * h)
    cg = c * n

    return c, n, cg


# ===============================================================================
# Compute stokes velocities from linear wave theory
# ===============================================================================
def uvstokes(Hwave, Dwave, Lwave, WDepth, VertDisc):
    '''
    Compute stokes velocities

    Parameters
    ----------
    Hwave          : Wave height [m]
    Dwave          : Wave direction
    Lwave          : Wave length [m]
    WDepth         : Water depth [m]
    VertDisc       : Number of vertical layers

    Returns
    -------
    ustokes, vstokes

    '''

    k = 2 * np.pi / Lwave  # Compute wave number
    deno = np.sinh(2.0 * k * WDepth)  # sinh(2kh)
    sigma = idispersion(k, WDepth)  # Radian frequency

    # Vertical discretization
    z = np.linspace(0, WDepth, VertDisc)
    nume = np.cosh(2.0 * k * (WDepth - z))

    # Get zonal and meridional components
    kx = k * np.cos((270 - Dwave) * np.pi / 180)
    ky = k * np.sin((270 - Dwave) * np.pi / 180)

    # Stokes drift
    ustokes = (Hwave ** 2) / 4.0 * 1027.0 * 9.81 / sigma * kx * nume / deno
    vstokes = (Hwave ** 2) / 4.0 * 1027.0 * 9.81 / sigma * ky * nume / deno

    # Return stokes velocities
    return ustokes, vstokes


# ===============================================================================
# TMA Spectrum
# ===============================================================================
def tma(freq_peak, gamma, h, Hmo, freq_min=0.01, freq_max=1.0, freq_int=0.001,
        zeroth=True):
    '''
    Function to generate TMA spectrum

    Parameters
    ----------
    peak_freq    : Peak frequency [Hz]
    gamma        : Peak enhancement factor (3.3 is a good guess)
    h            : water depth [m]
    Hmo          : Significant wave height [m]

    Optional Parameters
    -------------------
    freq_min     : Minimum frequency to compute the spectrum [Hz]
    freq_max     : Maximum frequency to compute the spectrum [Hz]
    freq_int     : Frequency inteval [Hz]
    zeroth       : Prepend zeroth frequency (Default = True)

    Default values are 0.01, 1.0, and 0.001 Hz, respectively.

    Returns
    -------
    s_tma        : Tma spectrum as a function of frequency.
    freq         : Frequency axis [Hz]

    References
    ----------
    Bouws, E., H. Gunther, W. Rosenthal, and C. L. Vincent, 1985: Similarity
        of the wind wave spectrum in finite depth water 1. Spectral form.
        Journal of Geophysical Research, 90 (C1), 975-986.
    Hughes, S. 1985: The TMA shallow-water spectrum, description and
        applications. Technical Report CERC-84-7.

    Notes
    -----
    - The units of the spectrum still do not make sense to me, must be verified.
    - No scaling applied, alpha = 1
    - Zeroth frequency is added to the spectrum

    '''

    # For testing only
    # freq_peak = 0.1
    # gamma = 3.3
    # freq_min = 0.01
    # freq_max = 1.0
    # freq_int = 0.001
    # h = 10.0
    # Hmo = 1.0

    # Compute frequency vector
    freq = np.arange(freq_min, freq_max + freq_int, freq_int)

    # Constants for peak enhancement factor
    delta = np.ones_like(freq) * 0.07
    delta[freq > freq_peak] = 0.09

    # Compute alpha parameter (equation 26,27) TMA report
    # wlen = 2.0*np.pi/dispersion(freq_peak**-1,h)
    # alpha = (2.0 * np.pi* Hmo / wlen)**2
    alpha = 1.0

    # TMA scaling factor
    omh = 2.0 * np.pi * freq * (h / 9.81) ** 0.5
    phi = 1.0 - 0.5 * (2.0 - omh) ** 2
    phi[omh < 1.0] = (0.5 * omh ** 2)[omh < 1.0]
    phi[omh > 2.0] = 1.0

    # Generate spectrum
    s_tma = (alpha * 9.81 ** 2 * (2.0 * np.pi) ** -4 * (freq ** -5) * phi *
             np.exp(-1.25 * (freq_peak / freq) ** 4) *
             gamma ** np.exp(-1.0 * ((freq - freq_peak) ** 2)
                             / (2.0 * (delta ** 2) * freq_peak ** 2)))

    # Add zeroth frequency
    if zeroth:
        s_tma = np.r_[np.array([0.0]), spec_tma]
        freq = np.r_[np.array([0.0]), freq]

    # End of function
    return s_tma, freq


# ===============================================================================
# JONSWAP Spectrum
# ===============================================================================
def jonswap(freq_peak, Hmo, gamma=3.3, freq_min=0.01, freq_max=1.0,
            freq_int=0.001, goda=False, zeroth=True):
    '''
    Function to generate JONSWAP spectrum

    Parameters
    ----------
    peak_freq    : Peak frequency [Hz]
    Hmo          : Significant wave height [m]

    Optional Parameters
    -------------------
    gamma        : Peak enhancement factor (defaults to 3.3)
    freq_min     : Minimum frequency to compute the spectrum [Hz]
    freq_max     : Maximum frequency to compute the spectrum [Hz]
    freq_int     : Frequency inteval [Hz]
    goda         : Use Y. Goda's approximation to the Jonswap spectrum.
                   If false we force the scaling of the spectrum to give the
                   same wave height passed as argument.
    zeroth       : Prepend zeroth frequency (Default = True)

    Returns
    -------
    spec_jonswap : JONSWAP spectrum as a function of frequency.
    freq         : Frequency vector [Hz]

    References
    ----------
    Many, but a good description can be found in:
        Y. Goda, Random Seas and Design of Maritime Structures.

    Notes
    -----
    - Default frequency values are 0.01, 1.0, and 0.001 Hz.

    '''

    # For code development only -----------------------------------------------
    # freq_peak = 1.0/10.0
    # gamma = 3.3
    # freq_min = 0.01
    # freq_max = 0.5
    # freq_int = 0.001
    # Hmo = 1.0
    # -------------------------------------------------------------------------

    # Create frequency vector
    freq = np.arange(freq_min, freq_max + freq_int, freq_int)

    # Constants for peak enhancement factor
    sigma = np.ones_like(freq) * 0.07
    sigma[freq > freq_peak] = 0.09

    # Goda's formulation
    if goda:
        # Beta parameter
        beta = (0.0624 / (0.230 + 0.0336 * gamma - 0.185 * ((1.9 + gamma) ** -1)) *
                (1.094 - 0.01915 * np.log(gamma)))

        # Generate spectrum
        spec_jonswap = (beta * (Hmo ** 2) * (freq_peak ** 4) * (freq ** -5) *
                        np.exp(-1.25 * ((freq / freq_peak) ** -4)) *
                        gamma ** (np.exp(-1.0 * (freq / freq_peak - 1.0) ** 2 /
                                         (2.0 * sigma ** 2))))

    else:

        # Generate spectrum
        spec_jonswap = ((freq ** -5) *
                        np.exp(-1.25 * ((freq / freq_peak) ** -4)) *
                        gamma ** (np.exp(-1.0 * (freq / freq_peak - 1.0) ** 2 /
                                         (2.0 * sigma ** 2))))

        # Scale parameter to match wave height energy in deep water
        # I am not sure this is the right way to proceed but I'll still do it.
        alpha = 1.0 / 16.0 * Hmo ** 2 / np.trapz(spec_jonswap, freq)
        spec_jonswap = spec_jonswap * alpha

    # Add zeroth frequency and expand the spectrum
    if zeroth:
        spec_jonswap_old = np.r_[np.array([0.0]), spec_jonswap]
        freq_old = np.r_[np.array([0.0]), freq]

        freq = np.arange(0.0, freq_max + freq_int, freq_int)
        spec_jonswap = np.interp(freq, freq_old, spec_jonswap_old)

    # End of function
    return spec_jonswap, freq


# ===============================================================================
# Add directional distribution to spectrum
# ===============================================================================
def directional_spreading(spec, peak_dir, m, dirs=None):
    """
    This function computes a directionally spread spectrum from a frequency
    spectrum passed to the function.

    PARAMETERS:
    -----------
    spec         : frequency spectrum (as vector)
    peak_dir     : Peak wave direction in Nautical convention [degrees]
    m            : Directional width [cos(theta)**2m]
    dirs         : (Optional) vector of directions. If not given, the spectrum
                   will be computed every 5 degrees.

    RETURNS:
    --------
    dir_spec     : Directional spectrum in the same units given by the input
                   spectrum by degrees.
    dirs         : Vector with directions

    NOTES:
    ------
    Nautical convention refers to the direction the waves (wind) are coming
      (is blowing) from with respect to the true north measured clockwise.
    To recover the significant wave height
       4.004 * np.trapz(np.sum(dir_spec,axis=-1)*(dirs[2]-dirs[1]),freq)**0.5
    """

    # If direction vector is not passed as argument the directional distribution
    # will be computed every five degrees.
    try:
        if dirs == None:
            dirs = np.arange(0, 360, 5)
    except:
        pass

    # Change directions to radians for internal computations
    peak_dir_r = np.pi / 180.0 * peak_dir
    dirs_r = np.pi / 180.0 * dirs

    # Compute directional spread
    g = np.cos(0.5 * (dirs_r - peak_dir_r)) ** (2 * m)

    # Normalize the directional spread so that no energy is added
    # Scaling is done in direction space to account for the units
    # In other workds dir_spec will be of [m2/Hz-deg]
    dth = dirs[2] - dirs[1]
    g /= (np.sum(g) * dth)

    # Generate the directionally spread spectrum
    dir_spec = np.zeros((spec.shape[0], dirs.shape[0]))
    for aa in range(spec.shape[0]):
        dir_spec[aa, :] = spec[aa] * g

    # Return directional spectrum
    return dir_spec, dirs


# ===============================================================================
# Compute bulk wave parameters from water surface elevation time series
# ===============================================================================
def eta_bulk_params(eta, ot, band_ave=False, window=False, IGBand=[0.005, 0.05]):
    """
    Compute bulk wave parameters from water surface elevation time series.

    Parameters:
    -----------
    eta        : Water surface elevation time series at a point [m]
    ot         : Time vector [s]
    band_ave   : (Optional) Bin average stencil
    Window     : (Optional) Application of a hanning window (True or False)
    IGBand     : (Optional) Infragravity wave frequency band
                  defaults to 0.005-0.05 Hz
    Output:
    -------
    Dictionary containing
    freq       : Spectral frequencies [Hz]
    spec       : Wave variance spectrum [m**2/Hz]
    cl         : 95% confidence levels on the spectral estimates
    Hs         : Significant wave height [m]
    H1         : Mean wave height [m]
    Tp         : Peak wave period [s]
    Tp_fit     : Peak wave period computed from second order polynomial fit
                 near Tp[s]
    Tm01       : First moment wave period [s]
    Tm02       : Second moment wave period [s]
    Te         : Energy period [s]
    Sw         : Spectral width (m0*m2/m1/m1 - 1)**2
    H1_t       : Mean wave height computed in the time domain [m]
    T1_t       : Mean wave period computed in the time domain [s]
    Tm01IG     : First moment wave period over the infragravity frequency band
    TpIG       : Peak wave period over the infragravity frequency band [s]
    skew_t     : Wave skewness from time domain

    Notes:
    ------
    mn are the different spectral moments

    """

    # Remove mean from the data
    etaw = eta - eta.mean()

    # Compute variance of original time series
    var_ts = np.var(etaw)

    # Compute in time domain
    [wh, wp, zcind] = whwpts(ot, eta)

    # Wave skewness
    skew_t = np.mean(etaw ** 3) / (np.mean(etaw ** 2) ** 1.5)

    # Data windowing
    if window:
        tmpwindow = scipy.signal.hanning(etaw.shape[0])
        etaw *= tmpwindow

    # Compute variance spectrum
    freq, spec = gsignal.psdraw(etaw, np.mean(ot[1:] - ot[:-1]))

    # If data has been windowed we must boost the variance of the spectrum
    # to match the original time series
    if window:
        # Compute variance of from the psd
        var_psd = np.sum(spec) * (freq[2] - freq[1])

        # Adjust variance from the windowed time series
        spec *= var_ts / var_psd

    # Band averaging if requested
    if band_ave:
        [freq, spec] = gsignal.band_averaging(spec, freq, band_ave)
    else:
        band_ave = 1

    # Compute confidence levels on the spectral parameters, to estimate noise
    # threshold.
    conf_lev = 0.95
    alpha = 1.0 - conf_lev
    cl_upper = scipy.stats.chi2.ppf(alpha / 2, band_ave * 2)
    cl_lower = scipy.stats.chi2.ppf(1.0 - alpha / 2.0, band_ave * 2)
    cl = np.array([spec - spec * band_ave * 2.0 / cl_lower,
                   spec * band_ave * 2.0 / cl_upper - spec])

    # Compute bulk wave parameters
    bwp = fspec_bulk_params(freq, spec, IGBand)
    bwp['freq'] = freq
    bwp['spec'] = spec
    bwp['cl'] = cl

    bwp['H1_t'] = np.nanmean(wh)
    bwp['T1_t'] = np.nanmean(wp)
    bwp['skew_t'] = skew_t

    return bwp


# ===============================================================================
# Function to compute bulk wave parameters from frequency spectrum
# ===============================================================================
def fspec_bulk_params(freq, spec, IGBand=[0.005, 0.05], zeroth=True):
    """
    Function to compute bulk wave parameters from frequency spectrum

    Parameters:
    -----------
    freq    : Vector of spectral frequencies [Hz]
    spec    : Frequency spectrum [m2/Hz]
    IGBand  : Infragravity wave frequency cutoff
              defaults to 0.005-0.05 Hz
    zeroth  : If true will remove the first frequency from the analysis

    Returns:
    --------
    Dictionary containing bulk wave parameters
    Hs         : Significant wave height [m]
    H1         : Mean wave height [m]
    Tp         : Peak wave period [s]
    Tp_fit     : Peak wave period computed from second order polynomial fit
                 near Tp[s]
    Tm01       : First moment wave period [s]
    Tm02       : Second moment wave period [s]
    Te         : Energy period [s]
    Sw         : Spectral width (m0*m2/m1/m1 - 1)**2
    Tm01IG     : First moment wave period over the infragravity frequency band
    TpIG       : Peak wave period over the infragravity frequency band [s]
    HsIG       : Significant wave height in the infragravity frequency band [m]

    Notes:
    ------
    - mn are the different spectral moments

    """

    # Remove zeroth frequencies ------------------------------------------------
    if zeroth:
        spec = spec[1:]
        freq = freq[1:]

    # Compute spectral moments
    moment0 = np.trapz(spec, freq, axis=-1)
    moment1 = np.trapz(spec * freq, freq, axis=-1)
    moment2 = np.trapz(spec * (freq) ** 2, freq, axis=-1)
    momentn1 = np.trapz(spec * (freq) ** -1, freq, axis=-1)

    # Wave heights
    Hs = 4.004 * (moment0) ** 0.5
    H1 = Hs * 2.0 / 3.0

    # Spectral width
    Sw = (moment0 * moment2 / moment1 / moment1 - 1) ** 0.5

    # Spectral periods -----------------------
    # Energy period
    Te = momentn1 / moment0

    # Mean wave period
    Tm01 = moment0 / moment1

    # Second moment period
    Tm02 = (moment0 / moment2) ** 0.5

    # Peak wave period
    freq_max_ind = np.argmax(spec)
    Tp = freq[freq_max_ind] ** -1

    # Peak wave period using a quadratic fit over the largest frequencies
    if freq_max_ind == 0:
        Tp_fit = np.nan
    elif freq_max_ind == freq.shape[0] - 1:
        Tp_fit = np.nan
    else:
        minfreq = freq_max_ind - 1
        maxfreq = freq_max_ind + 2
        tmp_fit = np.polyfit(freq[minfreq:maxfreq], spec[minfreq:maxfreq], 2)
        Tp_fit = (-1.0 * tmp_fit[1] / (2.0 * tmp_fit[0])) ** -1

    # Infragravity wave periods ------------------------------------------------
    igInd = np.logical_and(freq >= IGBand[0], freq < IGBand[1])
    moment0 = np.trapz(spec[igInd], freq[igInd], axis=-1)
    moment1 = np.trapz(spec[igInd] * freq[igInd], freq[igInd], axis=-1)
    Tm01IG = moment0 / moment1

    HsIG = 4.004 * (moment0) ** 0.5

    freq_max_ind = np.argmax(spec[igInd])
    TpIG = freq[igInd][freq_max_ind] ** -1

    # Exit function
    return {'Hs': Hs, 'H1': H1, 'Tp': Tp, 'Tp_fit': Tp_fit, 'Tm01': Tm01, 'Tm02': Tm02,
            'Te': Te, 'Sw': Sw, 'Tm01IG': Tm01IG, 'TpIG': TpIG, 'HsIG': HsIG}


# ===============================================================================
# Bulk parameters from a directional spectrum
# ===============================================================================
def spec_bulk_params(freq, dirs, spec, IGBand=[0.005, 0.05], zeroth=True):
    """
    Function to compute bulk wave parameters from frequency-direction spectrum

    Parameters:
    -----------
    freq    : Vector of spectral frequencies [Hz]
    dirs    : Vector of directions (nautical convention) [Deg or Rad]
    spec    : Wave spectrum [m2/Hz/Deg or m2/Hz/Deg]
              The shape must be [freq,dirs]
    IGBand  : Infragravity wave frequency cutoff
              defaults to 0.005-0.05 Hz
    zeroth  : If true will remove the first frequency from the analysis

    Returns:
    --------
    Dictionary containing bulk wave parameters
    Hs         : Significant wave height [m]
    H1         : Mean wave height [m]
    Tp         : Peak wave period [s]
    Tp_fit     : Peak wave period computed from second order polynomial fit
                 near Tp[s]
    Tm01       : First moment wave period [s]
    Tm02       : Second moment wave period [s]
    Te         : Energy period [s]
    Sw         : Spectral width (m0*m2/m1/m1 - 1)**2
    Tm01IG     : First moment wave period over the infragravity frequency band
    TpIG       : Peak wave period over the infragravity frequency band [s]
    HsIG       : Significant wave height in the infragravity frequency band [m]
    Dm         : Mean wave direction, second moment (Kuik et al. 1988)
    Dp         : Peak wave direction (computed from directional spectrum)
    Dp_fit     : Peak wave direction (computed using second order
                 polynomial fit around the peak of the directional spectrum)

    Notes:
    ------
    - mn are the different spectral moments
    - First frequency will be discarded from the analysis. It is assumed to be
      the zeroth-frequency.

    REFERENCES:
    -----------
    Kuik, A.J., G.P. Van Vledder, and L.H. Holthuijsen, 1988: A method for the
        routine analysis of pitch-and-roll buoy wave data. Journal of Physical
        Oceanography, 18(7), pp.1020-1034.
    """

    # Get directional properties
    dirSpec = np.trapz(spec, freq, axis=0)
    dth = np.abs(dirs[2] - dirs[1])

    # Find peak wave directon
    Dp = dirs[np.argmax(dirSpec)]
    dirDict = dict(Dp=Dp)

    # Peak wave direction using a quadratic fit over the largest directions
    dir_max_ind = np.argmax(dirSpec)
    if dir_max_ind == 0:
        tmp_fit = np.polyfit([dirs[0] - dth, dirs[0], dirs[1]],
                             [dirSpec[-1], dirSpec[0], dirSpec[1]], 2)
    elif dir_max_ind == dirs.shape[0] - 1:
        tmp_fit = np.polyfit([dirs[-2], dirs[-1], dirs[-1] + dth],
                             [dirSpec[-2], dirSpec[-1], dirSpec[0]], 2)
    else:
        minDirInd = dir_max_ind - 1
        maxDirInd = dir_max_ind + 2
        tmp_fit = np.polyfit(dirs[minDirInd:maxDirInd], dirSpec[minDirInd:maxDirInd], 2)
    Dp_fit = -1.0 * tmp_fit[1] / (2.0 * tmp_fit[0])
    dirDict.update(Dp_fit=_gangles.wrapto360(Dp_fit))

    # Find mean wave direction (Kuik et al 1988)
    a1 = np.trapz(dirSpec * np.cos((270.0 - dirs) * np.pi / 180), dirs)
    b1 = np.trapz(dirSpec * np.sin((270.0 - dirs) * np.pi / 180), dirs)
    # Mean wave direction in nautical coordinates
    Dm = _gangles.wrapto360(270.0 - np.arctan2(b1, a1) * 180.0 / np.pi)
    dirDict.update(Dm=Dm)

    # Get the parameter from the frequency spectrum
    # freqSpec = np.trapz(spec,dirs,axis=-1)
    freqSpec = np.sum(spec, axis=-1) * dth
    bp = fspec_bulk_params(freq, freqSpec, IGBand, zeroth=zeroth)
    bp.update(dirDict)

    return bp


# ===============================================================================
# Wave Height and Period From time series
# ===============================================================================
def whwpts(t, x, d='up'):
    '''
    Function to compute the wave height and wave period from a given time
    series.

    USAGE:
    ------
    [wh,wp,zcind] = whwpts(t,x,d)

    PARAMETERS:
    -----------
    t     : Time vector
    x     : Water surface elevation time series
    d     : Accepts 'up' or 'down' for zero upcrossing or downcrossing,
            respectively.

    RETURNS:
    --------
    wh    : Wave height time series
    wp    : Wave period time series
    zcind : zero-crossing index

    METHODS:
    --------
    1. Find zero crossings
    2. Find time difference between zero crossing to get wp
    3. Find the amplitude of the time series between each zero crossing to find
       wh.

    NOTES:
    ------
    - x and t must have the same length

    '''

    # Find the zero crossings
    zcross = gsignal.zero_crossing(x, d=d)

    # Find the wave period
    wp = t[zcross[1:]] - t[zcross[:-1]]

    # Find wave height
    wh = np.zeros_like(wp)
    for aa in range(wh.shape[0]):
        wh[aa] = (np.max(x[zcross[aa]:zcross[aa + 1]]) -
                  np.min(x[zcross[aa]:zcross[aa + 1]]))

    return wh, wp, zcross


# ===============================================================================
# Iribarrren number
# ===============================================================================
def iribarren(m, H, wl, verbose=False):
    """
    Function to compute the Iribarren number (aka surf similarity parameter) for
    deep water conditions

    USAGE:
    ------
    ssp = iribarren(m,H,wl,verbose)

    PARAMETERS:
    -----------
    m       : Beach slope
    H       : Wave height [m]
    wl      : Wave length [m]
    verbose : Display result in the terminal (Optional defaults to False)

    RETURNS:
    --------
    ssp     : surf similarity parameter

    FORMULA:
    --------
    ssp = tan(m)/(H/wl)**0.5

    NOTES:
    ------
    - ssp > 3.3: Reflective beach (surging or collapsing breakers)
    - 0.5 < ssp < 3.3 : Intermediate beach (plunging breaker)
    - ssp < 0.5 : Dissipative beach (spilling breaker)

    """

    # Compute iribarren number
    ssp = np.tan(m) / (H / wl) ** 0.5

    # Display message
    if verbose:
        if ssp >= 3.3:
            print('ssp = ' + np.str(ssp) + ': Reflective conditions')
        elif ssp < 0.5:
            print('ssp = ' + np.str(ssp) + ': Dissipative conditions')
        else:
            print('ssp = ' + np.str(ssp) + ': Intermediate conditions')

    return ssp


# ===============================================================================
# Battjes Parameter
# ===============================================================================
def battjes04(m, h, igFreq, verbose=False):
    '''
    Function to compute the normalized bed slope proposed by Battjes et al 2004

    USAGE:
    ------
    nbd = battjes04(m,h,igFreq,verbose)

    PARAMETERS:
    -----------
    m       : Beach slope
    h       : Characteristic water depth in shoaling region [m]
    igFreq  : infragravity wave frequency [1/s]
    verbose : Display result in the terminal (Optional defaults to False)

    RETURNS:
    --------
    nbd     : Normalized bed slope

    FORMULA:
    --------
    nbd = m/(2 * pi * igFreq) * (g/h)**0.5

    NOTES:
    ------
    The next values are given if the breaking depth is substituted for h
    - nbd < 0.3: Mild slope regime, were breaking generation is not as important
    - nbd > 0.3: Steep slope regime

    References:
    -----------
    Battjes , J. A., H. J. Bakkenes, T. T. Janssen, and A. R. van Dongeren,
      2004: Shoaling of subharmonic gravity waves. Journal of Geophysical
      Research, 109, C02009, doi:10.1029/2003JC001863.

    '''

    # Compute iribarren number
    nbd = m / (2 * np.pi * igFreq) * (9.81 / h) ** 0.5

    # Display message
    if verbose:
        if nbd < 0.3:
            print('nbd = ' + np.str(nbd) + ': Mild slope')
        else:
            print('nbd = ' + np.str(nbd) + ': Steep slope')

    return nbd


# ===============================================================================
# Baldock Parameter
# ===============================================================================
def baldock12(m, h, igFreq, H, wl, verbose=False):
    '''
    Function to compute the surfbeat similarity parameter by Baldock 2012

    USAGE:
    ------
    sbs = baldock12(m,h,igFreq,H,wl,verbose)

    PARAMETERS:
    -----------
    m       : Beach slope
    h       : Characteristic water depth in shoaling region [m]
    igFreq  : infragravity wave frequency [1/s]
    H       : Shortwave offshore wave height [m]
    wl      : Offshore wave length [m]
    verbose : Display result in the terminal (Optional defaults to False)

    RETURNS:
    --------
    sbs     : Surfbeat similarity parameter

    FORMULA:
    --------
    sbs = m/(2 * pi * igFreq) * (g/h)**0.5 * (H/wl)**0.5

    NOTES:
    ------
    Really no clear guidance is given, however the smallest the number the least
    effective breakpoint generation is.

    References:
    -----------
    Baldock, T. E., 2012: Dissipation of incident forced long waves in the
      surf zone - Implications for the concept of "bound" wave realease at
      short wave breaking. Coastal Engineering, 276-285.


    '''

    # Compute iribarren number
    sbs = m / (2 * np.pi * igFreq) * (9.81 / h) ** 0.5 * (H / wl) ** 0.5

    # Display message
    if verbose:
        print('sbs = ' + np.str(sbs))

    return sbs


# ===============================================================================
# Reverse shoal waves
# ===============================================================================
def deep_water_equivalent(H, h, period):
    """
    Code to find the equivalent deep water linear wave parameters based on
    intermediate to shallow water conditions

    PARAMETERS:
    -----------
    H      : Wave height [m]
    h      : Water depth [m]
    period : Wave period [s]

    RETURNS:
    --------
    Deep water equivalents
    H0     : Wave height [m]
    k0     : Wave number [m-1]

    NOTES:
    ------
    1. Will not work if waves have broken already

    """

    # Find the wave celerity
    c1, n1, cg1 = celerity(period, h)  # Intermediate water
    cg0 = (9.81 * period) / (4 * np.pi)  # Deep water

    # Deep water wave length
    H0 = H * ((cg1 / cg0) ** 0.5)

    # Deep water wave number
    k0 = ((2.0 * np.pi / period) ** 2) / 9.81

    return H0, k0


def shoal(H1, h1, period, h0):
    """
    Code to shoal waves based on linear wave theory

    PARAMETERS:
    -----------
    H1     : Wave height [m]
    h1     : Water depth [m]
    period : Wave period [s]
    h0     : Water depth to find equivalent conditions [m]

    RETURNS:
    --------
    Equivalent parameters at h0
    H0     : Wave height [m]
    k0     : Wave number [m-1]

    NOTES:
    ------
    1. Will not work if waves have broken already

    """

    # Find the wave celerity
    c1, n1, cg1 = celerity(period, h1)  # Current conditions
    c0, n0, cg0 = celerity(period, h0)  # Desired conditions

    # Deep water wave length
    H0 = H1 * ((cg1 / cg0) ** 0.5)

    # Deep water wave number
    k0 = dispersion(period, h0)

    return H0, k0


# ===============================================================================
# Compute IEC Bulk Parameters from Spectrum
# ===============================================================================
def iec_params(freq, dirs, spec, dpt, rho=1025.0):
    """
    Function to compute IEC recommended wave energy parameters from
    frequency-direction spectrum

    Parameters:
    -----------
    freq    : Vector of spectral frequencies [Hz]
    dirs    : Vector of directions (nautical convention) [Deg]
    spec    : Wave spectrum [m2/Hz/Deg]
              The shape must be [time,npts,freq,dirs]
    dpt     : Water depth of shape [npts]
    rho     : (Optional) Density of water

    Returns:
    --------
    Dictionary containing bulk wave parameters
    Hs         : Significant wave height [m]
    OWP        : Omnidirectional wave power [W/m]
    Jth        : Maximum directionally resolved wave power [W/m]
    Th         : Direction of maximum directionally resolved wave power [deg]
    d          : Directionality coefficient (Jth/OWP)
    Te         : Energy period [s]
    Sw         : Spectral width (m0*m-2/m-1/m-1 - 1)**2

    Notes:
    ------
    - mn are the different spectral moments
    - Directional grid is assumed to be regular
      need to implement a trapz for when this is not true.
    - Pass everything as arrays:
      >>> dpt = np.array([100.0]) # If you only have one point

    REFERENCES:
    -----------
    Marine energy - Wave, tidal and other water current converters -
        Part 101: Wave energy resoure assessment and characterization.
        IEC TS 62600-101
    """

    # Figure out the size of the array and adjust if necessary
    _, npts, nfreq, ndirs = np.shape(spec)

    # Directions must be sorted (if they exist)
    if ndirs > 1:
        sortInd = np.argsort(dirs)
        spec = spec[..., sortInd]
        dirs = dirs[sortInd]

    # Compute the group velocity for each point
    cg = np.zeros((npts, nfreq)) * np.NAN
    for aa in range(cg.shape[0]):
        for bb in range(cg.shape[1]):
            _, _, cg[aa, bb] = celerity(1 / freq[bb], dpt[aa])

    # Non directional parameters -----------------------------------------------
    if ndirs > 1:
        # freqSpec = np.trapz(spec,dirs*np.pi/180,axis=-1)
        dth = dirs[1] - dirs[0]
        freqSpec = np.sum(spec, axis=-1) * dth
    else:
        freqSpec = spec[..., 0]

    # Compute omnidirectional wave power
    moment0 = np.trapz(freqSpec, freq, axis=-1)
    momentn1 = np.trapz(freqSpec * (freq) ** -1, freq, axis=-1)
    momentn2 = np.trapz(freqSpec * (freq) ** -2, freq, axis=-1)

    # Significant wave height
    bp = {}
    bp['Hs'] = 4.004 * (moment0 ** 0.5)

    # Energy Period
    bp['Te'] = momentn1 / moment0

    # Spectral width
    bp['Sw'] = (moment0 * momentn2 / momentn1 / momentn1 - 1.0) ** 0.5

    # Omnidirectional wave power (loop extravaganza here)
    owp = np.zeros_like(bp['Hs']) * np.NAN
    # Time loop
    for aa in range(spec.shape[0]):
        # Point loop
        for bb in range(spec.shape[1]):
            # Omnidirectional wave power
            owp[aa, bb] = np.trapz(freqSpec[aa, bb, :] * cg[bb, :], freq)

    # Get directional properties -----------------------------------------------

    if ndirs > 1:
        # Omidirectional wave power through a plane
        cg = np.repeat(np.expand_dims(cg, axis=-1), ndirs, axis=-1)
        jth = np.zeros_like(owp)
        th = np.zeros_like(owp) * np.NAN
        # Time loop
        for aa in range(spec.shape[0]):
            # Point loop
            for bb in range(spec.shape[1]):
                # Directional wave power
                tmpJth = 0.0
                dirSpec = np.trapz(spec[aa, bb, ...] * cg[bb, ...], freq, axis=-2)
                # Direction loop
                for cc in range(ndirs):
                    fac = np.cos(np.pi / 180.0 * (dirs[cc] - dirs))
                    fac[fac < 0] = 0.0
                    tmpJth = np.sum(dirSpec * fac, axis=-1) * dth
                    if tmpJth > jth[aa, bb]:
                        jth[aa, bb] = tmpJth
                        th[aa, bb] = dirs[cc]

    else:
        th = np.zeros_like(owp) * np.NAN
        jth = np.zeros_like(owp) * np.NAN

    # Take care of units here
    owp *= 9.81 * rho
    jth *= 9.81 * rho

    # Allocate in arrays
    bp['Th'] = th
    bp['OWP'] = owp
    bp['Jth'] = jth
    bp['d'] = jth / owp  # Directionality coefficient

    return bp
