#   Unit "tools" of the FourierAnalysis Package for julia language
#
#   MIT License
#   Copyright (c) 2019, Marco Congedo, CNRS, Grenobe, France:
#   https://sites.google.com/site/marcocongedo/home

# ? CONTENTS :
#   This unit implements useful functions for Fourier analysis.

#   ~¤~~¤~~¤~~¤~~¤~~¤~~¤~~¤~~¤~~¤~~¤~~¤~~¤~~¤~~¤~~¤~~¤~~¤~~¤~~¤~~¤~~¤~~¤~~¤~  #
#                                                                             #
#   ~¤~~¤~~¤~~¤~~¤~~¤~~¤~~¤~~¤~~¤~~¤~~¤~~¤~~¤~~¤~~¤~~¤~~¤~~¤~~¤~~¤~~¤~~¤~~¤~  #

"""
```
function sinusoidal(a :: IntOrReal,
                    f :: IntOrReal,
                   sr :: Int,
                    t :: Int,
                    θ :: IntOrReal = 0.;
                DClevel = 0.)
```

Generate a sinusoidal wave with peak amplitude `a`, frequency `f`,
sampling rate `sr`, duration (in samples) `t`, angle `θ`
(θ=0 makes a sine, θ=π/2 makes a cosine)
and optional keyword argument `DC` (float), the DC level defaulting to zero.
It is adopted the convention that a sine wave starts at zero.

**See**: [IntOrReal](@ref)

**Examples**:
```
using FourierAnalysis, Plots

# create and plot a sinusoidal wave of 128 samples with
# peak amplitude 1, frequency 12Hz, sr=64, phase=π/2
v=sinusoidal(1., 12, 64, 128, π/2)
plot(v)

# estimate amplitude of a sinusoidal wave using Goertzel algorithm
f, sr, t = 32, 128, 128
v=sinusoidal(3., f, sr, t, 0)
c=goertzel(v, f, sr, t) # c should be equal to 0+3.0im
```
"""
function sinusoidal(a  :: IntOrReal,
                    f  :: IntOrReal,
                    sr :: Int,
                    t  :: Int,
                    θ  :: IntOrReal =0.;
                DC = 0.)
    Δi = inv(sr)
    f2π = f * 2π
    ν=Vector([DC+(a*sin(f2π * i * Δi + θ)) for i=0:t-1])
end

"""
```
function fres(sr :: Int,
              wl :: Int)
```

FFT **f**requency **res**olution given sampling rate `sr` and window length `wl`.

**See also**: [`f2b`](@ref), [`b2f`](@ref), [`fdf`](@ref), [`brange`](@ref).

**Examples**:
```
using FourierAnalysis
fres(1024, 2048) # return 0.5
```
"""
fres(sr :: Int,
     wl :: Int) = sr/wl

"""
```
function f2b(f :: IntOrReal,
            sr :: Int,
            wl :: Int;
        DC :: Bool = false)
```

**f**requency **to b**in. Return the bin (position) in a real-FFT vector best matching
a frequency `f` (in Hz), given sampling rate `sr` and window length `wl`.
The frequency can be given either as an integer or as a real number.

If the requested `f` is exactly in between two Fourier discrete frequencies,
then the smallest of the two equidistant frequencies is returned.

The FFT vector is assumed to be 1-based (as always in Julia).
If `DC` is false, the first discrete frequency is assumed to be at bin 1,
otherwise the DC is assumed to be at bin 1 and the first discrete frequency
at bin 2.

If `DC` is false return 0 for frequencies inferior to half the frequency
resolution.

**See**: [IntOrReal](@ref).

**See also**: [`fres`](@ref), [`b2f`](@ref), [`fdf`](@ref), [`brange`](@ref).

**Examples**:
```
using FourierAnalysis
f2b(10, 512, 1024) # return 20
```
"""
f2b(f  :: IntOrReal,
    sr :: Int,
    wl :: Int;
  DC :: Bool = false) =
    !DC*fres(sr, wl)<=f<=sr/2 ? (return round(Int, (f*(wl/sr))-eps()) + DC) :
    @error 📌*", call to f2b function; invalid frequency argument. The frequency must be comprised between $(!DC*fres(sr, wl)) and $(sr/2) (half the sampling rate)" f
#¤ if DC is true the first frequency is the DC level, hence !DC*fres(sr, wl)
#¤ is equal to zero since !DC=0, otherwise the first frequency is fres(sr, wl)

"""
```
function b2f(bin :: Int,
              sr :: Int,
              wl :: Int;
        DC :: Bool = false)
```

**b**in **to f**requency. Return the closest discrete Fourier frequency (in Hz)
that corresponds to a bin (position) in a real-FFT vector,
given sampling rate `sr` and window length `wl`.
The FFT vector is assumed to be 1-based, as always in Julia.

If `DC` is false, the first discrete frequency is assumed to be at bin 1,
otherwise the DC level is assumed to be at bin 1 and the first discrete
frequency at bin 2.

**See also**: [`f2b`](@ref), [`fres`](@ref), [`fdf`](@ref), [`brange`](@ref).

**Examples**:
```
using FourierAnalysis
f2b(20, 512, 1024) # return 40
f2b(10, 128, 128) # return 10
```
"""
b2f(bin :: Int,
    sr  :: Int,
    wl  :: Int;
  DC :: Bool = false) =
    0<bin<=wl÷2+DC ? (return (bin-DC)*(sr/wl) ) :
    @error 📌*", call to b2f function; invalid bin argument. The bin must be comprised between 1 and half the window length (+ 1 if DC=true)" bin
#¤ if DC is true there are wl÷2+1 bins, otherwise there are wl÷2 bins

"""
```
function fdf(sr :: Int,
             wl :: Int;
          DC :: Bool = false)
```
Return a vector with all **F**ourier **d**iscrete **f**requencies for a real-FFT,
given sampling rate `sr` and window length `wl`.
If `DC` is false, the first discrete frequency starts at bin (position) 1 and
the length of the vector is ``wl÷2`` (integer division), otherwise the DC level is at position 1.
and the length of the vector is ``(wl÷2)+1``.

**See also**: [`f2b`](@ref), [`fres`](@ref), [`b2f`](@ref), [`brange`](@ref).

**Examples**:
```
using FourierAnalysis
fdf(8, 16)
# return the 8-element Array{Float64,1}:
# [0.5, 1.0, 1.5, 2.0, 2.5, 3, 3.5, 4.0]
```
"""
fdf(sr :: Int,
    wl :: Int;
  DC :: Bool = false) = [i*(sr/wl) for i=!DC:wl÷2]
#¤ if DC is true !DC=0, hence !DC:wl÷2 starts at zero

"""
```
function brange(wl :: Int;
             DC :: Bool = false)
```
Return a range of bins for a real-FFT vector covering all Fourier discrete
frequencies given window length `wl`.

If `DC` is false, the range is ``1:(wl÷2)`` (integer division),
otherwise it is ``1:(wl÷2)+1``.

**See also**: [`f2b`](@ref), [`fres`](@ref), [`b2f`](@ref), [`fdf`](@ref).

**Examples**:
```
using FourierAnalysis
brange(0.5, 8) # return 1:4
```
"""
brange(wl::Int;
    DC::Bool=false) = (1:wl÷2+DC)

"""
```
function bbands(sr :: Int,
                wl :: Int,
         bandwidth :: IntOrReal;
    DC :: Bool = false)
```

Return a vector of integers holding the limits of all `bandwidth`-spaced
band-pass regions of a real-FFT, in bins of discrete Fourier frequencies,
from one to ``wl÷2`` (integer division).

This is used by function [`bands`](@ref).

To know the frequencies in Hz to which these bins correspond, call
[`fbands`](@ref).

**See**: [`bands`](@ref).

**See also**: [`fbands`](@ref).

**Examples**:
```
using FourierAnalysis
bbands(128, 256, 16) # return [1, 32, 64, 96, 128]
fbands(128, 256, 16) # return [0.5, 16.0, 32.0, 48.0, 64.0]

bbands(128, 256, 16; DC=true) # return [2, 33, 65, 97, 129]
fbands(128, 256, 16; DC=true) # return [0.5, 16.0, 32.0, 48.0, 64.0]

bbands(128, 128, 16) # return [1, 16, 32, 48, 64]
fbands(128, 128, 16) # return [1.0, 16.0, 32.0, 48.0, 64.0]
```
"""
function bbands(sr        :: Int,
                wl        :: Int,
                bandwidth :: IntOrReal;
            DC        :: Bool=false)
    fr=fres(sr, wl)
    if bandwidth<fr
        @error 📌*", call to function `bbands` or `bandsaverage`: bandwidth cannot be smaller than the FFT frequency resolution" bandwidth fr
        return
    end
    b=collect(StepRange(0, f2b(bandwidth, sr, wl; DC=DC), wl÷2))
    b[1]=1
    if b[2]==1 popfirst!(b) end
    return DC ? b.+=1 : b
end

"""
```
function fbands(sr :: Int,
                wl :: Int,
         bandwidth :: IntOrReal;
      DC :: Bool = false)
```
Return a vector of Frequencies (in Hz) to which the bins created by a call
to function [`bbands`](@ref) with the same arguments correspond.

**See**: [`bbands`](@ref).

**See also**: [`bands`](@ref).
"""
fbands(sr        :: Int,
       wl        :: Int,
       bandwidth :: IntOrReal;
    DC :: Bool = false) = b2f.(bbands(sr, wl, bandwidth; DC=DC), sr, wl; DC=DC)

"""
```
(1)
function decibel(S :: Union{Real, AbstractArray{T}}) where T<:Real

(2)
function decibel(S1 :: Union{Real, AbstractArray{T}},
            S2 :: Union{Real, AbstractArray{T}}) where T<:Real
```

Convert (1) a measure `S`, or (2) a ratio between two measures `S1`./`S2`
into deciBels.

Input measures can be real numbers or real arrays of any dimensions.

For array input, the ratio and the conversion is computed element-wise.

**Examples**:
```
using FourierAnalysis
v=sinusoidal(3., 1, 128, 256, 0)
s=spectra(v, 128, 256; func=decibel) # compute the spectra in dB
s.y # show the spectra

decibel(s.y)

decibel(10.0)

N=abs.(randn(3, 3))
decibel(N)
```
"""
decibel(S::Union{Real, AbstractArray{T}}) where T<:Real = 10*log10.(S)
decibel(S1::Union{Real, AbstractArray{T}},
   S2::Union{Real, AbstractArray{T}}) where T<:Real = 10*log10.(S1./S2)

"""
```
(1)
function amplitude(c::Complex;
                        func::Function=identity) = func(abs(c))

(2)
function amplitude(A::AbstractArray{T};
                        func::Function=identity) where T<:Complex

(3)
function amplitude(A::TFAnalyticSignal;
                        func::Function=identity)

(4)
function amplitude(𝐀::TFAnalyticSignalVector;
                        func::Function=identity)
```
(1)

Return the amplitude (modulus) of a complex number.
This corresponds to Julia's
[abs](https://docs.julialang.org/en/v1/base/math/#Base.abs) function.
It is here provided for syntactic consistency with the following methods.

(2)

Return the amplitude of a complex array `Z`.
Typically, `Z` holds analytic signal, in which case the output is
the analytic (instantaneous) amplitude (also known as envelope).
The output is a real array of the same size as `Z`.

(3)

Return a real matrix with the analytic (instantaneous) amplitude of the
[TFAnalyticSignal](@ref) object `Z`. The output is of the same
size as the data field `Z.y`.

(4)

As (3), but return a vector of amplitude matrices for all
[TFAnalyticSignal](@ref) objects in 𝐀

~

In all methods if a function is provided by the optional keyword
argument `func`, it is applied element-wise to the output. For example,
- passing `func=x->x^2` will return the power,
- passing `func=x->log(x^2)` will return the log-power,
- passing `func=x->decibel(x^2)` will return the power in deciBels.

**See**: [TFAnalyticSignal](@ref).

**Examples**:
```
using FourierAnalysis, Plots
x=sinusoidal(10, 2, 128, t*4, 0).*sinusoidal(10, 1, 128, t*4, 0)

# amplitude and phase of a vector using analytic signal standard method
y=analyticsignal(x)
a=amplitude(y)
ϕ=phase(y, func=x->(x+π)/2π*50)
plot([x, a, ϕ]; labels=["signal", "amplitude", "phase"])

# see what happen if `x` contains energy in frequencies below sr/wl Hz
# (see documentation of `analyticSignal` function)
y=analyticsignal(x, 64)
a=amplitude(y)
ϕ=phase(y, func=x->(x+π)/2π*50)
plot([x, a, ϕ]; labels=["signal", "amplitude", "phase"])

# unwrapped phase
# the line below will do nothing as argument `unwrapdims` is 0 by default
ϕ2=unwrapPhase(phase(y))
# this will do the job
ϕ2=unwrapPhase(phase(y); unwrapdims=1)
plot([x, a, ϕ2./25]; labels=["signal", "amplitude", "unwr. phase"])

# amplitude from analytic signal of a data matrix holding multiple series
X=randn(t, 4)
Y=analyticsignal(X)
A=amplitude(Y)
plot(A[:, 1:2])

# phase
𝛷=phase(Y)
plot(𝛷[:, 1:1])

# unwrapped phase
𝛷2=unwrapPhase(𝛷; unwrapdims=1)
plot(𝛷2)

# phase represented in [-1, 1]
𝛷=phase(Y, func=x->(x+π)/2π)
plot(𝛷[:, 1:1])

# sine of the phase
𝛷=phase(Y, func=sin)
plot(𝛷[:, 1:1])

# get Amplitude and phase from analytic Signal
A, 𝛷=polar(Y)
A
𝛷
```
"""
amplitude(c::Complex;
                func::Function=identity) = func(abs(c))

amplitude(A::AbstractArray{T};
                    func::Function=identity) where T<:Complex = @.func(abs(A))

amplitude(A::TFAnalyticSignal;
                    func::Function=identity) = amplitude(A.y, func)

amplitude(𝐀::TFAnalyticSignalVector;
                    func::Function=identity) = [amplitude(A.y, func) for A ∈ 𝐀]

# Standard atan function in other languages
atan2(z::Complex) = atan(imag(z), real(z))

"""
```
(1)
function phase(z::Complex; func::Function=identity)

(2)
function phase(Z::AbstractArray{T};
                        unwrapdims::Int=0,
                        func::Function=identity) where T<:Complex

(3)
function phase(Z::TFAnalyticSignal;
                        unwrapped::Bool=false,
                        func::Function=identity)

(4)
function phase(𝐙::TFAnalyticSignalVector;
                        unwrapped::Bool=false,
                        func::Function=identity)

```
(1)

Return the phase (argument) of a complex number.
This corresponds to a standard
[atan2](https://en.wikipedia.org/wiki/Atan2) function.
It is here provided for syntactic consistency with the following methods.

(2)

Return the phase of a complex array `Z`.
Typically, `Z` holds analytic signal, in which case the output is
the analytic (instantaneous) phase.
The output is a real array of the same size as `Z`.

If optional keyword argument `unwrapdims` is > 0, return the unwrapped phase
along the `unwrapdims` dimension of the array. For example, if `Z` is a matrix,
passing `unwrapdims=1` unwrap the phase indipendently along its columns.

(3)

Return a real matrix with the analytic (instantaneous) phase of the
[TFAnalyticSignal](@ref) object `Z`. The output is of the same
size as the data field `Z.y`.

If optional keyword argument `unwrapped` is true, return the unwrapped phase
along the time dimension of the analytic signal (dims=2).

(4)

As (3), but return a vector of phase matrices for all
[TFAnalyticSignal](@ref) objects in 𝚯.

~

In all methods by default the phase is returned in [−π, π].
If a function is provided by the optional keyword argument `func`,
it is applied to the phase. For example
- passing `func=x->x+π` will return the phase in [0, 2π],
- passing `func=x->x/π` will return the phase in [-1, 1],
- passing `func=sin` will return the sine of the phase.

!!! note "Nota Bene"
    If in method (2) `unwrapdims` is >0 or in method (3) and (4)
    `unwrapped` is true, the function `func` is applied to the unwrapped phase.

**See**: [`unwrapPhase`](@ref), [TFAnalyticSignal](@ref).

**Examples**: see examples of [`amplitude`](@ref).
"""
phase(z::Complex; func::Function=identity) = func(atan2(z))

phase(Z::AbstractArray{T};
                unwrapdims::Int=0,
                func::Function=identity) where T<:Complex =
    unwrapdims>0 ? func.(unwrapPhase(Z, unwrapdims=unwrapdims)) : @.func(atan2(Z))  # see Base.Broadcast.@__dot__

phase(Z::TFAnalyticSignal; unwrapped::Bool=false, func::Function=identity) =
    phase(Z.y; unwrapdims=2*unwrapped, func=func) # unwrap==true -> unwrapdims=2, else unwrapdims=0

phase(𝐙::TFAnalyticSignalVector; unwrapped::Bool=false, func::Function=identity) =
    [phase(Z; unwrapped=unwrapped, func=func) for Z ∈ 𝐙]

"""
```
(1)
function polar(c::Complex)

(2)
function polar(Z::AbstractArray{T}) where T<:Complex

(3)
function polar(Z::TFAnalyticSignal)

```

(1)

Return the amplitude (modulus) and phase (argument)
of a complex number as a 2-tuple.

(2)

Return the amplitude and phase of a complex array `Z`.
Typically, `Z` holds analytic signal, in which case return
the analytic (instantaneous) amplitude and phase.
The output is a tuple of two real arrays of the same size as data field `Z.y`.

(3)

Return the analytic (instantaneous) amplitude
and phase of the [TFAnalyticSignal](@ref) object `Z`.
The output is a tuple of two real arrays of the same size as data field `Z.y`.

~

In all methods the phase is returned in [−π, π].

**See**: [`amplitude`](@ref), [`phase`](@ref), [TFAnalyticSignal](@ref).

**Examples**: see examples of [`amplitude`](@ref).
"""
polar(c::Complex) = amp(c), atan2(c)

polar(Z::AbstractArray{T}) where T<:Complex = amplitude(Z), phase(Z)

polar(Z::TFAnalyticSignal) = polar(Z.y)

# instantaneous frequency from wikipedia
  #function frequency(Y::TimeFrequency)
    #  W=zeros(real(eltype(Y.Z)), size(Y.Z))
    #  @inbounds for j=2:size(Y.Z, 2), i=1:size(Y.Z, 1)
    #                W[i, j]=atan2(Y.Z[i, j]*conj(Y.Z[i, j-1]))/2π end
    #  return
 # end

"""
```
(1)
function unwrapPhase(Z::AbstractArray{T};
                                unwrapdims::Int=0) where T<:Complex

(2)
function unwrapPhase(ϴ::AbstractArray{T};
                                unwrapdims::Int=0) where T<:Real

(3)
unwrapPhase(ϴ::TFPhase) [constructor of a TFPhase object]

(4)
unwrapPhase(𝚯::TFPhaseVector) [constructor of a TFPhaseVector object]
```
(1)

If optional keyword argument `unwrapdims` is > 0, compute the phase
(argument) from a *complex* array and unwrap it along the `unwrapdims`
dimension, otherwise (default) return `Z`.
Typically, `Z` holds analytic signal.

(2)

If optional keyword argument `unwrapdims` is > 0, unwrap along the
`unwrapdims` dimension a *real* array holding phase data in [−π, π],
otherwise return `ϴ`.

(3)

Construct a [TFPhase](@ref) object by unwrapping its phase along the time
dimension and copying all other fields from the `ϴ` object. If `ϴ.func`
is different from the `identity` (do nothing) function,
return instead an error message.

(4)

As (3), but conctruct a [TFPhaseVector](@ref) holding
[TFPhase](@ref) objects in 𝚯 with the phase unwrapped.
`ϴ.func` must be the identity function for all ϴ ∈ 𝚯.

The unwrapped phase is defined as the cumulative sum
of the phase (along the relevant dimension)
once this is represented in [0, 2π].

**Examples**: see examples of [`amplitude`](@ref).
"""
unwrapPhase(Z::AbstractArray{T}; unwrapdims::Int=0) where T<:Complex =
    unwrapdims>0 ? accumulate(+, (atan2.(Z)).+π; dims=unwrapdims) : Z

unwrapPhase(ϴ::AbstractArray{T}; unwrapdims::Int=0) where T<:Real =
    unwrapdims>0 ? accumulate(+, ϴ.+π; dims=unwrapdims) : ϴ

unwrapPhase(ϴ::TFPhase) =
    ϴ.func==identity ? TFPhase(unwrapPhase(ϴ.y; unwrapdims=2), ϴ.bandwidth,
                               ϴ.flabels, ϴ.nonlinear,
                               ϴ.fsmoothing, ϴ.tsmoothing, true, ϴ.func) :
   @error 📌*", call to unwrapPhase constructor; I shall not unwrap a phase on which a function has been applied. The unwrapped phase object has not been created.)" ϴ.func

unwrapPhase(𝚯::TFPhaseVector) =
   sum(ϴ.func==identity for ϴ ∈ 𝚯)==length(𝚯) ? TFPhaseVector([
                    TFPhase(unwrapPhase(ϴ.y; unwrapdims=2), ϴ.bandwidth,
                    ϴ.flabels, ϴ.nonlinear, ϴ.fsmoothing, ϴ.tsmoothing,
                    true, ϴ.func) for ϴ ∈ 𝚯]) :
  @error 📌*", call to unwrapPhase constructor; I shall not unwrap a phase on which a function has been applied. The unwrapped phase object has not been created.)" ϴ.func

"""
```
function sameParams(𝐒        :: FDobjectsVector,
                    funcname :: String)
```

Return true if all objects in 𝐒 have the same `sr`, `wl`, `DC`, `taper`,
`func`(only for [SpectraVector](@ref) objects), `nonlinear` (only for
[CrossSpectraVector](@ref) and [CoherenceVector](@ref)) and `smoothing` fields,
otherwise print an error message pointing to the first field that is not
identical in all objects and return `Nothing`. This method applies to
all [FDobjectsVector](@ref) types, that is,
to [SpectraVector](@ref), [CrossSpectraVector](@ref) and
[CoherenceVector](@ref).

`funcname` is an optional string that the user can
provide. It is inserted into the error message to locate the part
of the code that generated the error. By defalut, "unknown" is used.
"""
sameParams(𝐒        :: FDobjectsVector,
           funcname :: String = "unknown") =
   if      !_allsame([S.sr for S ∈ 𝐒])
               @error 📌*", "*funcname*" function. All objects in argument of type $typeof(𝐒) must have the same sampling rate (.sr field)"
   elseif  !_allsame([S.wl for S ∈ 𝐒])
               @error 📌*", "*funcname*" function. All objects in argument of type $typeof(𝐒) must have the same window length (.wl field)"
   elseif  !_allsame([S.DC for S ∈ 𝐒])
               @error 📌*", "*funcname*" function. All objects in argument of type $typeof(𝐒) must all hold or not hold DC level (.DC field)"
   elseif  !_allsame([S.taper for S ∈ 𝐒])
               @error 📌*", "*funcname*". All objects in argument of type $typeof(𝐒) must have the same taper (.taper field)"
   elseif  𝐒 isa SpectraVector && !_allsame([S.func for S ∈ 𝐒])
               @error 📌*", "*funcname*". All objects in argument of type $typeof(𝐒) must have been subjected to the same function (.func field)"
   elseif  (𝐒 isa CrossSpectraVector || 𝐒 isa CoherenceVector) && !_allsame([S.nonlinear for S ∈ 𝐒])
               @error 📌*", "*funcname*". The objects in argument of type $typeof(𝐒) must be either all linear or all non-linear (.nonlinear field)"
   elseif  !_allsame([S.smoothing for S ∈ 𝐒])
               @error 📌*", "*funcname*". All the objects in argument of type $typeof(𝐒) must have been subjected to the same smoother (.smoothing field)"
   elseif      return true
   end

"""
```
function sameParams(𝒀        :: TFobjectsVector,
                    funcname :: String) =
```
Return true if all objects in 𝒀 have the same `bandwidth`, `nonlinear`,
`fsmoothing` and `tsmoothing` field, otherwise print an error message
pointing to the first field that is not identical in all objects and
return `Nothing`. This method applies to all [TFobjectsVector](@ref) types,
that is, [TFAnalyticSignalVector](@ref), [TFAmplitudeVector](@ref) and
[TFPhaseVector](@ref).

`funcname` has the same meaning as in the previous method.
"""
sameParams(𝒀        :: TFobjectsVector,
           funcname :: String = "unknown") =
   if      !_allsame([Y.bandwidth for Y ∈ 𝒀])
               @error 📌*", "*funcname*" function. All the objects in argument of type $typeof(𝒀) must all be definied with the same bandwidth"
   elseif  !isa(𝒀, TFAmplitudeVector) && !_allsame([Y.nonlinear for Y ∈ 𝒀])
               @error 📌*", "*funcname*" function. All the objects in argument of type $typeof(𝒀) must be definied as either all nonlinear or all linear"
   elseif  !_allsame([Y.fsmoothing for Y ∈ 𝒀])
               @error 📌*", "*funcname*" function. All the objects in argument of type $typeof(𝒀) must be definied with the same frequency smoother"
   elseif  !_allsame([Y.tsmoothing for Y ∈ 𝒀])
               @error 📌*", "*funcname*" function. All the objects in argument of type $typeof(𝒀) must be definied with the same time smoother"
   elseif      return true
   end

"""
```
function isLinear(𝒀::Union{FDobjectsVector, TFobjectsVector})
```
Return true if all objects in `𝒀` are linear.
By definition, [Spectra](@ref) and [TFAmplitude](@ref) objects are linear.
[CrossSpectra](@ref), [Coherence](@ref), [TFAnalyticSignal](@ref)
and [TFPhase](@ref) objects may be linear or non-linear.

**See**:[FDobjectsVector](@ref), [TFobjectsVector](@ref).
"""
isLinear(𝒀::FDobjectsVector) =
   𝒀 isa SpectraVector ? true : sum(Y.nonlinear for Y ∈ 𝒀)==0

isLinear(𝒀::TFobjectsVector) =
   𝒀 isa TFAmplitudeVector ? true : sum(Y.nonlinear for Y ∈ 𝒀)==0

"""
```
function isNonLinear(𝒀::Union{FDobjectsVector, TFobjectsVector})
```
Return true if all objects in `𝒀` are non-linear.
By definition, [Spectra](@ref) and [TFAmplitude](@ref) objects are linear.
[CrossSpectra](@ref), [Coherence](@ref), [TFAnalyticSignal](@ref)
and [TFPhase](@ref) objects may be linear or non-linear.

**See**:[FDobjectsVector](@ref), [TFobjectsVector](@ref).

"""
isNonLinear(𝒀::FDobjectsVector) =
   𝒀 isa SpectraVector ? false : sum(Y.nonlinear for Y ∈ 𝒀)==length(𝒀)

isNonLinear(𝒀::TFobjectsVector) =
   𝒀 isa TFAmplitudeVector ? false : sum(Y.nonlinear for Y ∈ 𝒀)==length(𝒀)

"""
```
(1)
function isUnwrapped(ϴ::TFPhase)

(2)
function isUnwrapped(𝚯::TFPhaseVector)
```
(1)
Return true if the TFPhase objects ϴ have the phase unwrapped.

(2)
Return true if all TFPhase objects in 𝚯 have the phase unwrapped.

**See**: [`unwrapPhase`](@ref), [TFPhase](@ref), [TFPhaseVector](@ref).
"""
isUnwrapped(ϴ::TFPhase) =  ϴ.unwrapped

isUnwrapped(𝚯::TFPhaseVector) =  sum(ϴ.unwrapped for ϴ ∈ 𝚯)==0

### Frequency-domain smoothing ###
###############################################################################

# Internal function: computes coefficients for smoothing
function _getSmoothCoeff(smoother::Smoother)
    if      smoother == hannSmoother
            return [0.25, 0.5, 0.25], 0.75, 0. # 0.75=c1+c2
    elseif  smoother == hammingSmoother
            return [0.23, 0.54, 0.23], 0.77, 0. # 0.77=c1+c2
    elseif  smoother == blackmanSmoother
            return [0.04, 0.25, 0.42, 0.25, 0.04], 0.71, 0.96 # 0.71=c1+c2+c3, 0.96=c1+c2+c3+c4
    end
end

# Internal function: apply frequency-domain smoothing to the `X` vector
# of real or complex numbers or real or complex lower-triangular matrices
function __smooth(X, c, s, t)
    Y=similar(X)
    k=length(X)
    type=typeof(X)

    if      type isa Vector{LowerTriangular} # || type===Array{LowerTriangular{Complex{Float64},S} where S<:AbstractArray{Complex{Float64},2},1}
            T=LowerTriangular
    elseif  type isa Array{Hermitian} # || Array{Hermitian{Complex{Float64},S} where S<:AbstractArray{Complex{Float64},2},1}
            T=Hermitian
    else    T=eltype(X)
    end

    if length(c) == 3 # `smoother` = `hannSmoother` or `hammingSmoother`
        Y[1] = T(X[1]*(c[2]/s) + X[2]*(c[3]/s))
        @inbounds for i=2:k-1 Y[i] = T(X[i-1]*c[1] + X[i]*c[2] + X[i+1]*c[3]) end
        Y[k] = T(X[k-1]*(c[1]/s) + X[k]*(c[2]/s))
    else # `smoother` = `blackmanSmoother`, for which length(c) == 5
        Y[1] = T(X[1]*(c[3]/s) + X[2]*(c[4]/s) + X[3]*(c[5]/s))
        Y[2] = T(X[1]*(c[2]/t) + X[2]*(c[3]/t) + X[3]*(c[4]/t) + X[4]*(c[5]/t))
        @inbounds for i=3:k-2 Y[i] = T(X[i-2]*c[1] + X[i-1]*c[2] + X[i]*c[3] + X[i+1]*c[4] + X[i+2]*c[5]) end
        Y[k-1] = T(X[k-3]*(c[1]/t) + X[k-2]*(c[2]/t) + X[k-1]*(c[3]/t) + X[k]*(c[4]/t))
        Y[k] = T(X[k-2]*(c[1]/s) + X[k-1]*(c[2]/s) + X[k]*(c[3]/s))
    end
    return type(Y)
end

smooth(smoothing::Smoother, S::Union{Vector{<:Real}, Vector{<:Complex}}) =
    return smoothing == noSmoother ? S : __smooth(S, _getSmoothCoeff(smoothing)...)

# Internal function: smooth the output of `spectra` function; spectra of a Matrix (columns)
function _smooth(smoother::Smoother, S::Spectra, γ, c1, c2)
    Y=similar(S.y)
    @inbounds for i=1:size(S.y, 2) Y[:, i]=__smooth(S.y[:, i], γ, c1, c2) end
    return Spectra(Y, S.sr, S.wl, S.DC, S.taper, S.flabels, S.func, smoother)
end

# Internal function: smooth the output of `spectra` function; k-spectra of a Matrix Vector
_smooth(smoother::Smoother, 𝐒::SpectraVector, γ, c1, c2) =
    SpectraVector([_smooth(smoother, S, γ, c1, c2) for S ∈ 𝐒])

# Internal function: smooth the output of `cross-spectra` or `coherence` function
_smooth(smoother::Smoother, 𝙎::Union{CrossSpectra, Coherence}, γ, c1, c2) =
    typeof(𝙎)(__smooth(𝙎.y, γ, c1, c2), 𝙎.sr, 𝙎.wl, 𝙎.DC, 𝙎.taper, 𝙎.flabels, 𝙎.nonlinear, smoother, 𝙎.tril)

# Internal function: smooth the output of `cross-spectra` or `coherence` function; k-cross-spectra of a Matrix Vector
_smooth(smoother::Smoother, 𝓢::Union{CrossSpectraVector, CoherenceVector}, γ, c1, c2) =
    typeof(𝓢)([_smooth(smoother, 𝐒, γ, c1, c2) for 𝐒 ∈ 𝓢])

"""
```
(1)
function smooth(smoother :: Smoother,
                       S :: Union{Vector{<:Real}, Vector{<:Complex}})

(2)
function smooth(smoother :: Smoother,
                       S :: Union{FDobjects, FDobjectsVector})

(3)
function smooth(fsmoothing :: Smoother,
                tsmoothing :: Smoother,
                         Y :: Union{TFobjects, TFobjectsVector})
```

Apply a smoothing function of type [Smoother](@ref) to
- (1) a vector of real or complex numbers,
- (2) a [FDobjects](@ref) or all objects in a [FDobjectsVector](@ref),
- (3) a [TFobjects](@ref) or all objects in a [TFobjectsVector](@ref).

This function is a constructor; for all methods the output is always of
the same type as the input.

Method (1) is provided for low-level computations,
but typically it is not needed.

Method (2) smooths across the frequency dimension:
- for [Spectra](@ref) objects this amounts to smoothing the column vectors in their `.y` field,
- for [CrossSpectra](@ref) and [Coherence](@ref) objects this amounts to smoothing adjacent matrices in their .y field.

Method (3) smooths across the frequency dimension, time dimension or both.
This amount to smooth across the column vectors (frequency) and/or row vectors
(time) in the `.y` field of the object.
A smoother must be specified for the frequency dimension
(`fsmoothing`) and for the time dimension (`tsmoothing`).
Either one may be `noSmoother`, but if the two are different from `noSmoother`,
then they must be the same. If smoothing is requested in both the frequency and
time dimension, then the data is smoothed indipendently in those dimensions
and the result of the two smoothings is averaged.
For [TFPhase](@ref) objects, smoothing is allowed only if the phase is unwrapped.

This function allow smoothing frequency domain and time-frequency domain objects
after they have been created, however, smoothing can also be requested upon
creation. For example, see the documentation of [Spectra](@ref).

!!! note "Nota Bene"
    For methods (1) and (2), if `Smoother` is `noSmoother`, then the input
    is returned unchanged. For method (3) this is the case if both `fsmoother`
    and `tsmoother` are `noSmoother`.

    The data input must hold in the concerned dimension at least three elements
    for applying an Hann or Hamming smoother and at least five elements for
    applying the Blackman smoother.

## Maths

Smoothing of a series ``x`` composed of ``k`` elements is carried out at element
``i`` such as

``x_{i}=ax_{i-2}+bx_i{-1}+cx_{i}+bx_{i+1}+ax_{i+2}``.

The coefficients are

| smoothing window  |  a   |  b   |  c   |
|:-----------------:|:----:|:----:|:----:|
| Hann              | 0    | 0.25 | 0.50 |
| Hamming           | 0    | 0.23 | 0.54 |
| Blackman          | 0.04 | 0.25 | 0.42 |

For 3-point smoothers, the first point is smoothed as

``x_{1}=\\frac{c}{b+c}x_{1} + \\frac{b}{b+c}x_{2}``

and the last (the ``k^{th}``) as

``x_{k}=\\frac{c}{b+c}x_{k} + \\frac{b}{b+c}x_{k-1}``.

For 5-point smoothers, the first point is smoothed as

``x_{1}=\\frac{c}{a+b+c}x_{1} + \\frac{b}{a+b+c}x_{2} + \\frac{a}{a+b+c}x_{3}``,

the second as

``x_{2}=\\frac{b}{a+2b+c}x_{1} + \\frac{c}{a+2b+c}x_{2} + \\frac{b}{a+2b+c}x_{3} + \\frac{a}{a+2b+c}x_{4}``,

the second to last as

``x_{k-1}=\\frac{a}{a+2b+c}x_{k-3} + \\frac{b}{a+2b+c}x_{k-2} + \\frac{c}{a+2b+c}x_{k-1} + \\frac{b}{a+2b+c}x_{k}``

and the last as

``x_{k}=\\frac{a}{a+b+c}x_{k-2} + \\frac{b}{a+b+c}x_{k-1} + \\frac{c}{a+b+c}x_{k}``.

**See**: [Smoother](@ref)

**Examples**:
```
using FourierAnalysis, Plots
sr, t, f, a = 128, 128, 10, 0.5
# create a sinusoidal superimposed to white noise
v=sinusoidal(a, f, sr, t*16, 0) + randn(t*16)
# compute Amplitude Spectra
Σ=spectra(v, sr, t; func=√)
bar(Σ.y, labels="raw amplitude spectra")

#smooth spectra
Σ2=smooth(blackmanSmoother, Σ)
bar!(Σ2.y, labels="smoothed amplitude spectra")

# smooth cross-spectra (or coherence) matrices
X=broadcast(+, v, randn(t*16, 3))*randn(3, 3)
S=crossSpectra(X, sr, t) # or coherence (X, sr, t)
# smooth the cross-spectra # or coherence
S2=smooth(blackmanSmoother, S)

# smooth time-frequency object
Y = TFanalyticsignal(v, sr, sr*4)
# smooth frequency
Z=smooth(blackmanSmoother, noSmoother, Y)
# plot amplitude of smoothed analytic signal
heatmap(Z, amplitude)

# smooth AS: smooth both frequency and time
E=smooth(blackmanSmoother, blackmanSmoother, Y)
# plot real part of smoothed analytic signal
heatmap(Z, real)
```
"""
smooth(smoothing::Smoother, S::Union{FDobjects, FDobjectsVector}) =
    return smoothing == noSmoother ? S : _smooth(smoothing, S, _getSmoothCoeff(smoothing)...)

# internal function: Smooth the TimeFrequency types; dim1 = time, dim2=frequency
function _smooth(smoother  :: Smoother,
                 Z         :: TFobjects,
                 frequency :: Bool,
                 time      :: Bool,
                 γ, c1, c2)

    cT=eltype(Z.y)
    if     !frequency & !time
        Z_=Z.y
    elseif !frequency & time
        Z_=Matrix{cT}(undef, size(Z.y))
        @inbounds for i=1:size(Z_, 1) Z_[i, :]=__smooth(Z.y[i, :], γ, c1, c2) end
    elseif frequency & !time
        Z_=Matrix{cT}(undef, size(Z.y))
        @inbounds for i=1:size(Z_, 2) Z_[:, i]=__smooth(Z.y[:, i], γ, c1, c2) end
    elseif frequency & time
        Zf=Matrix{cT}(undef, size(Z.y))
        @inbounds for i=1:size(Zf, 2) Zf[:, i]=__smooth(Z.y[:, i], γ, c1, c2) end
        Zt=Matrix{cT}(undef, size(Z.y))
        @inbounds for i=1:size(Zt, 1) Zt[i, :]=__smooth(Z.y[i, :], γ, c1, c2) end
        Z_=(Zf+Zt)/2
    end

    type=typeof(Z)
    if     type===TFAnalyticSignal
        return type(Z_, Z.bandwidth, Z.flabels, Z.nonlinear, frequency ? smoother : noSmoother, time ? smoother : noSmoother)
    elseif type===TFAmplitude
        return type(Z_, Z.bandwidth, Z.flabels, frequency ? smoother : noSmoother, time ? smoother : noSmoother, Z.func)
    elseif type===TFPhase
        return type(Z_, Z.bandwidth, Z.flabels, Z.nonlinear, frequency ? smoother : noSmoother, time ? smoother : noSmoother, Z.unwrapped, Z.func)
    end
end

# Smooth the TFobjects types; dim1 = time, dim2=frequency
smooth(fsmoothing :: Smoother,
       tsmoothing :: Smoother,
       Z          :: TFobjects,
       funcname   :: String="") =
    if Z isa TFPhase && !Z.unwrapped
        @error 📌*", function "*funcname*": smoothing of TFPhase objects can be applied only if the phase is unwrapped"
        return
    elseif fsmoothing == tsmoothing == noSmoother
        return Z
    elseif fsmoothing==tsmoothing || (fsmoothing==noSmoother || tsmoothing==noSmoother)
        smoother=max(fsmoothing, tsmoothing) # return the smoother if they are equal or one of them is noSmoother
        return _smooth(smoother, Z, fsmoothing≠noSmoother, tsmoothing≠noSmoother, _getSmoothCoeff(smoother)...)
    else
        @error 📌*", function "*funcname*": if smoothing is requested, then the `Smoother` for frequency and time must be of the same kind"
    end

# Smooth the TFobjectsVector types; dim1 = time, dim2=frequency
smooth(fsmoothing :: Smoother,
       tsmoothing :: Smoother,
       𝐙          :: TFobjectsVector,
       funcname   :: String="") =
   if 𝐙 isa TFPhaseVector && isUnwrapped(𝐙)
       @error 📌*", function "*funcname*": smoothing of TFPhaseVector objects can be applied only if the phase is unwrapped for all TFPhase objects it holds"
       return
   elseif fsmoothing == tsmoothing == noSmoother
       return 𝐙
   elseif fsmoothing==tsmoothing || (fsmoothing==noSmoother || tsmoothing==noSmoother)
       smoother=max(fsmoothing, tsmoothing) # return the smoother if they are equal or one of them is noSmoother
       return typeof(𝐙)([_smooth(smoother, Z, fsmoothing≠noSmoother, tsmoothing≠noSmoother, _getSmoothCoeff(smoother)...) for Z ∈ 𝐙])
   else
       @error 📌*", function "*funcname*": if smoothing is requested, then the `Smoother` for frequency and time must be of the same kind"
   end
#    return smoother == noSmoother ? 𝐙 :
#           typeof(𝐙)([_smooth(smoother, Z, frequency, time, _getSmoothCoeff(smoother)...) for Z ∈ 𝐙])

### Frequency-domain data extraction and averaging ###
###############################################################################

# get an fInterval type and output a UnitRange whose start and stop are the
# corresonding Fourier discrete frequency bins
function _getfrange(S::FDobjects, frange::fInterval, funcname::String)
    if      isa(frange, IntOrReal)
            !S.DC*fres(S.sr, S.wl)<=frange<=S.sr/2 ? (return f2b(frange, S.sr, S.wl; DC=S.DC):f2b(frange, S.sr, S.wl; DC=S.DC)) :
            @error 📌*", "*funcname*" function passed invalid frange Int or Real argument. The frequency must be comprised between $(!S.DC*fres(S.sr, S.wl)) and $(S.sr/2) (half the sampling rate)" frange
            return 0
    elseif  isa(frange, Colon)
            return 1:size(S.y, 1)
    elseif  isa(frange, Tuple{IntOrReal, IntOrReal})
            # println(frange[1], ", ", frange[2])
            if      frange[1]>frange[2]
                        @error 📌*", "*funcname*" function passed invalid frange 2-tuple argument. The second element cannot be inferior to the first" frange[1] frange[2]
                        return 0
            elseif  frange[1]<!S.DC*fres(S.sr, S.wl) || frange[2]>S.sr/2
                        @error 📌*", "*funcname*" function passed invalid frange 2-tuple argument. The two frequencies must be comprised between $(!S.DC*fres(S.sr, S.wl)) and $(S.sr/2) (half the sampling rate)" frange[1] frange[2]
                        return 0
            else        return f2b(frange[1], S.sr, S.wl; DC=S.DC): f2b(frange[2], S.sr, S.wl; DC=S.DC)
            end
    end
end

# as _getfrange above, but for a FDobjectsVector object.
# By default, it is checked that the elements of the vector are homogeneous.
# If so return the UnitRange obtained on the first FDobject in the vector,
# otherwise return 0.
# if `check` is false, the check is skypped.
_getfrange(𝐒::FDobjectsVector, frange::fInterval, funcname::String; check::Bool=true) =
    check ? (sameParams(𝐒, funcname) ? _getfrange(𝐒[1], frange, funcname) : 0) :
    _getfrange(𝐒[1], frange, funcname)

_extract(S::Spectra, frange::UnitRange{Int64}) =
    _isnull(frange) && size(S.y, 2)==1 ? S.y[first(frange), 1] :
                                         copy(S.y[frange, :])

_extract(𝐒::SpectraVector, frange::UnitRange{Int64}) =
    _isnull(frange) && size(𝐒[1].y, 2)==1 ? [S[first(frange), 1] for S ∈ 𝐒] :
                                            [S[frange, :] for S ∈ 𝐒]

function _extract(S::Union{CrossSpectra, Coherence}, frange::UnitRange{Int64})
    mattype = S.tril ? LowerTriangular : Hermitian
    _isnull(frange) ? mattype(copy(S.y[first(frange)])) :
                      typeof(S.y)([mattype(S.y[i]) for i∈frange])
end

function _extract(𝐒::Union{CrossSpectraVector, CoherenceVector}, frange::UnitRange{Int64})
    mattype = 𝐒[1].tril ? LowerTriangular : Hermitian
    _isnull(frange) ? typeof(𝐒[1].y)([mattype(S.y[first(frange)]) for S ∈ 𝐒]) :
                      [typeof(𝐒[1].y)([mattype(S.y[i]) for i∈frange]) for S ∈ 𝐒]
end

_mean(S::Spectra, frange::UnitRange{Int64}) =
    size(S.y, 2)==1 ? mean(view(S.y, frange, 1)) :
                     [mean(view(S.y, frange, k)) for k=1:size(S.y, 2)]

_mean(S::Union{CrossSpectra, Coherence}, frange::UnitRange{Int64}) =
    eltype(S.y)(mean(view(S.y[i], :, :) for i∈frange))

_mean(𝐒::SpectraVector, frange::UnitRange{Int64}) =
    size(𝐒[1].y, 2)==1 ? [mean(view(S.y, frange, 1)) for S ∈ 𝐒] :
                         [[mean(view(S.y, frange, k)) for k=1:size(S.y, 2)] for S ∈ 𝐒]

_mean(𝐒::Union{CrossSpectraVector, CoherenceVector}, frange::UnitRange{Int64}) =
    typeof(𝐒[1].y)([_mean(S, frange) for S ∈ 𝐒])

"""
```
(1)
function extract(S :: FDobjects,
            frange :: fInterval)

(2)
function extract(𝐒 :: FDobjectsVector,
            frange :: fInterval;
        w :: Vector = [],
    check :: Bool   = true)

(3)
function extract(Y :: TFobjects,
            frange :: fInterval,
            trange :: tInterval)

(4)
function extract(𝒀 :: TFobjectsVector,
            frange :: fInterval,
            trange :: tInterval;
        w :: Vector = [],
    check :: Bool   = true)
```

**alias**: `extr`

Extract data in a frequency region from [FDobjects](@ref) and data in a
time-frequency region from [TFobjects](@ref). The frequency and time region
are indicated by `frange` and `trange`, which are of type [fInterval](@ref)
and [tInterval](@ref), respectively.

The input/output types of this function for a region with more then one
frequency and more than one sample is reported in the following table:

|method|        input object          |                   output                |
|:---:|:------------------------------|:---------------------------------------------|
|(1.1)| [Spectra](@ref)               | a real matrix with spectra in `frange` arranged in columns¹|
|(1.2)| [CrossSpectra](@ref)          | a vector of complex matrices holding the cross-spectra in `frange`²|
|(1.3)| [Coherence](@ref)             | a vector of real matrices holding the coherence in `frange`²|
|(2.1)| [SpectraVector](@ref)         | a vector of matrices of type (1.1)|
|(2.2)| [CrossSpectraVector](@ref)    | a vector of vectors of type (1.2)|
|(2.3)| [CoherenceVector](@ref)       | a vector of vectors of type (1.3)|
|(3.1)| [TFAnalyticSignal](@ref)      | a complex matrix holding the analytic signal in [`frange`, `trange`]|
|(3.2)| [TFAmplitude](@ref)           | a real matrix holding the amplitude in [`frange`, `trange`]|
|(3.3)| [TFPhase](@ref)               | a real matrices holding the phase in [`frange`, `trange`]|
|(4.1)| [TFAnalyticSignalVector](@ref)| a vector of matrices of type (3.1)|
|(4.2)| [TFAmplitudeVector](@ref)     | a vector of matrices of type (3.2)|
|(4.3)| [TFPhaseVector](@ref)         | a vector of matrices of type (3.3)|
Legend: ¹ *each column refers to a time-series on which the spectra have been computed.*
² *depending on how the objects has been created, the matrices may be either
Hermitian or LowerTriangular. See the documentation of [CrossSpectra](@ref) and [Coherence](@ref).

Note that depending on the arguments the type of the output may loose one or two dimensions.
For instance,
- if the [Spectra](@ref) object holds only one spectrum, (1.1) will output a column vector and (2.1) a vector of column vectors.
- if `frange` points to a single frequency, (1.1) will output a row vector and (2.1) a vector of row vectors.
- if both the above two conditions hold, (1.1) will output a real number and (2.1) a vector.
- if `frange` points to a single frequency, (1.2), (1.3) will output a matrix and (2.2), (2.3) a vector of matrices.
- If `frange` points to a single frequency band, (3.1), (3.2), (3.3) will output a row vector and (4.1), (4.2), (4.3) a vector of row vectors.
- If `trange` points to a single time sample, (3.1), (3.2), (3.3) will output a column vector and (4.1), (4.2), (4.3) a vector of column vectors.
- if both the above two conditions hold, (3.1), (3.2), (3.3) will output a number and (4.1), (4.2), (4.3) a vector.

Method (2) and (4) allows the following *optional keyword arguments*:

`w`, a ``k``-vector of non-negative integers or real numbers, where ``k`` is the
numbers of objects hold in the input [FDobjectsVector](@ref) or
[TFobjectsVector](@ref). `w` is a vector of weights for the regions extracted from
the input objects. By default, no weights are assigned.

`check`, a boolean. If it is true (default), it is checked that the non-data fields
of the input objects are all the same (for example, sampling rate, bandwidth, etc.).
Set it to false to improve speed.

**See also**: [`mean`](@ref).

**Examples**:
```
using FourierAnalysis

# example with univariate Spectra objects (one series -> one spectrum)
sr, t, f, a = 128, 256, 10, 1
# create a sinusoidal superimposed to white noise
v=sinusoidal(a, f, sr, t*16, 0) + randn(t*16)
# compute univariate spectra
Σ=spectra(v, sr, t)
# spectra in between 8Hz and 12Hz
s=extract(Σ, (8, 12))
# spectra in between 8Hz and 12.5Hz
s=extract(Σ, (8, 12.5))
# spectra at 10Hz
s=extract(Σ, 10) # or s=extract(S, (10, 10))
# these two expressions are equivalent: s=extract(Σ, :), s=Σ.y

# example with multivariate spectra (several series -> several spectra)
Σ=spectra(hcat(v, v+randn(t*16)), sr, t)
# spectra in between 8Hz and 12Hz
S=extract(Σ, (8, 12))
# spectra at 10Hz
S=extract(Σ, 10)

# example with CrossSpectra objects (the same goes for Coherence objects)
X=broadcast(+, v, randn(t*16, 3))*randn(3, 3)
Σ=crossSpectra(X, sr, t)
# cross-spectra in between 8Hz and 12Hz (Hermitian matrices)
S=extract(Σ, (8, 12))
Σ=crossSpectra(X, sr, t; tril=true)
# cross-spectra in between 8Hz and 12Hz (LowerTriangular matrices)
S=extract(Σ, (8, 12))

# example with multiple cross-spectra
X2=broadcast(+, v, randn(t*16, 3))*randn(3, 3)
Σ=crossSpectra([X, X2], sr, t) # this is a CrossSpectraVector
S=extract(Σ, (8, 12); w=[0.4, 0.6])
# now S[1] holds the cross-spectra in range 8-12Hz for X
# and S[2] holds the cross-spectra in range 8-12Hz for X2

# example with time-frequency objects
# (work in the same way for TFAnalyticSignal, TFAmplitude and TFPhase)
Y = TFanalyticsignal(v, sr, t)
# analytic signal within frequencies 8Hz and 12Hz and time samples 1 to 64.
AS=extract(Y, (8, 12), (1, 64))

# all analytic signal within frequencies 8Hz and 12Hz.
AS=extract(Y, (8.0, 12), :) # accept integers and reals for frequencies

# all analytic signal within time samples 1 to 64.
AS=extract(Y, :, (1, 64))

# example with multiple time-frequency objects
# (notice how the type of the output changes)
Y = TFanalyticsignal([v, v+randn(t*16)], sr, t)
AS=extract(Y, (8, 12), (1, 64))
AS=extract(Y, (8), :)
AS=extract(Y, 8, 2)
```
"""
extract(S::FDobjects, frange::fInterval) =
    if (_frange=_getfrange(S, frange, "extract")) ≠ 0
        return _extract(S, _frange)
    end

extract(𝐒::FDobjectsVector, frange::fInterval;
        w::Vector=[], check::Bool=true) =
    if (_frange=_getfrange(𝐒, frange, "extract"; check=check)) ≠ 0
        return isempty(w) ? [_extract(S, _frange) for S ∈ 𝐒] :
                            [ω*_extract(S, _frange) for (ω, S) ∈ zip(w, 𝐒)]
    end

"""
```
(1)
function mean(S :: FDobjects,
         frange :: fInterval)

(2)
function mean(𝐒 :: FDobjectsVector,
         frange :: fInterval;
        w :: Vector = [],
    check :: Bool   = true)

(3)
function mean(Y :: TFobjects,
         frange :: fInterval,
         trange :: tInterval)

(4)
function mean(𝒀 :: TFobjectsVector,
         frange :: fInterval,
         trange :: tInterval;
          w :: Vector = [],
      check :: Bool   = true)
```

Return the mean of data in a frequency region from [FDobjects](@ref) and data in a
time-frequency region from [TFobjects](@ref). The frequency and time region
are indicated by `frange` and `trange`, which are of type [fInterval](@ref)
and [fInterval](@ref), respectively.

The complete input/output types for this function is reported in the following table:

|method|        input object           |                   output                |
|:----:|:------------------------------|:---------------------------------------------|
|(1.1)| [Spectra](@ref)                | a vector holding the mean spectra in `frange`¹|
|(1.2)| [CrossSpectra](@ref)           | a complex matrix holding the mean cross-spectra in `frange`²|
|(1.3)| [Coherence](@ref)              | a real matrix holding the mean coherence in `frange`²|
|(2.1)| [SpectraVector](@ref)          | a vector of vectors of type (1.1)|
|(2.2)| [CrossSpectraVector](@ref)     | a vector of matrices of type (1.2)|
|(2.3)| [CoherenceVector](@ref)        | a vector of matrices of type (1.3)|
|(3.1)| [TFAnalyticSignal](@ref)       | a complex number holding the mean analytic signal in [`frange`, `trange`]|
|(3.2)| [TFAmplitude](@ref)            | a real number holding the mean amplitude in [`frange`, `trange`]|
|(3.3)| [TFPhase](@ref)                | a real number holding the mean phase in [`frange`, `trange`]|
|(4.1)| [TFAnalyticSignalVector](@ref) | a vector of numbers of type (3.1)|
|(4.2)| [TFAmplitudeVector](@ref)      | a vector of numbers of type (3.2)|
|(4.3)| [TFPhaseVector](@ref)          | a vector of numbers of type (3.3)|
legend: ¹*each element of the vector refers to a time-series on which the spectra have been computed.*
² *depending on how the objects has been created, the matrices may be either
Hermitian or LowerTriangular.*

Method (2) and (4) allows the following *optional keyword arguments*:

`w`, a ``k``-vector of non-negative integers or real numbers, where ``k`` is the
numbers of objects hold in the input [FDobjectsVector](@ref) or
[TFobjectsVector](@ref). `w` is a vector of weights for the means extracted from
the input objects. By default, no weights are assigned.

`check`, a boolean. If it is true (default), it is checked that the non-data fields
of the input objects are all the same (for example, sampling rate, bandwidth, etc.).

**See also**: [`extract`](@ref).

**Examples**:
```
using FourierAnalysis, Plots

# example with univariate Spectra objects (one series -> one spectrum)
sr, t, f, a = 128, 256, 10, 1
# create a sinusoidal superimposed to white noise
v=sinusoidal(a, f, sr, t*16, 0) + randn(t*16)
# compute the spectrum
Σ=spectra(v, sr, t)
# mean spectrum in between 8Hz and 12Hz
s=mean(Σ, (8, 12))
# mean spectrum in between 8Hz and 12.5Hz
s=mean(Σ, (8, 12.5))

# example with multivariate spectra (several series -> several spectra)
Σ=spectra(hcat(v, v+randn(t*16)), sr, t)
# mean spectra in between 8Hz and 12Hz
S=mean(Σ, (8, 12))
# mean spectra at 10Hz, i.e., the spectra at 10Hz
S=mean(Σ, 10)

# example with CrossSpectra objects (the same goes for Coherence objects)
X=broadcast(+, v, randn(t*16, 3))*randn(3, 3)
Σ=crossSpectra(X, sr, t)
# mean cross-spectra in between 8Hz and 12Hz (an Hermitian matrix)
S=mean(Σ, (8, 12))
Σ=crossSpectra(X, sr, t; tril=true)
# mean cross-spectra in between 8Hz and 12Hz (a LowerTriangular matrix)
S=mean(Σ, (8.0, 12.0)) # accept integers and reals for frequencies

# example with multiple CrossSpectra objects
X2=broadcast(+, v, randn(t*16, 3))*randn(3, 3)
Σ=crossSpectra([X, X2], sr, t) # this is a CrossSpectraVector
S=mean(Σ, (8, 12); w=[0.4, 0.6])
# now S[1] will hold the mean cross-spectrum in range 8-12Hz for X
# and S[2] will hold the mean cross-spectrum in range 8-12Hz for X2

# example with time-frequency objects
# (work in the same way for TFAnalyticSignal, TFAmplitude and TFPhase)
Y = TFanalyticsignal(v, sr, t)
# mean analytic signal within frequencies 8Hz and 12Hz and time samples 1 to 64.
as=mean(Σ, (8, 12), (1, 64))
# mean analytic signal within frequencies 8Hz and 12Hz.
as=mean(Σ, (8, 12), :)
# mean analytic signal within time samples 1 to 64.
as=mean(Σ, :, (1, 64))

# example with multiple time-frequency objects
Y = TFanalyticsignal([v, v+randn(t*16)], sr, t)
AS=mean(Y, (8, 12), (1, 64))
# get the mean across TFobjects of those means
m=mean(mean(Y, (8, 12), (1, 64)))
AS=mean(Y, (8), :)
AS=mean(Y, 8, 2)
```
"""
mean(S::FDobjects, frange::fInterval) =
    if (_frange=_getfrange(S, frange, "mean")) ≠ 0
        return _mean(S, _frange)
    end

mean(𝐒::FDobjectsVector, frange::fInterval;
     w::Vector=[], check::Bool=true) =
    if (_frange=_getfrange(𝐒, frange, "mean"; check=check)) ≠ 0
        return isempty(w) ? [_mean(S, _frange) for S ∈ 𝐒] :
                            [ω*_mean(S, _frange) for (ω, S) ∈ zip(w, 𝐒)]
    end

### Time-Frequency-domain data extraction and averaging ###
###############################################################################

function _getfrange(Y::TFobjects, frange::fInterval, funcname::String)
    # find frequencies in filterbanks
    hb=Y.bandwidth/2
    if      isa(frange, IntOrReal)
            (Y.flabels[1]-hb)<=frange<=(Y.flabels[end]+hb) ? (return max(1, (findmin([abs(f-frange) for f∈Y.flabels])[2])):min(length(Y.flabels), (findmin([abs(f-frange)  for f∈Y.flabels])[2]))) :
            @error 📌*", "*funcname*" function passed invalid frange Int or Real argument. The frequency must be comprised between $((Y.flabels[1]-hb)) and $((Y.flabels[end]+hb))" frange
            return 0
    elseif  isa(frange, Colon)
            return 1:size(Y.y, 1)
    elseif  isa(frange, Tuple{IntOrReal, IntOrReal})
        if      frange[1]>frange[2]
                    @error 📌*", "*funcname*" function passed invalid frange 2-tuple argument. The second element cannot be inferior to the first" frange[1] frange[2]
                    return 0
        elseif  frange[1]<(Y.flabels[1]-hb) || frange[2]>(Y.flabels[end]+hb)
                    @error 📌*", "*funcname*" function passed invalid frange 2-tuple argument. The two frequencies must be comprised between $(Y.flabels[1]-hb) and $(Y.flabels[end]+hb)" frange[1] frange[2]
                    return 0
        else        return max(1, (findmin([abs(f-frange[1]) for f∈Y.flabels])[2])):min(length(Y.flabels), (findmin([abs(f-frange[2])  for f∈Y.flabels])[2]))
        end
    end
end

function _gettrange(Y::TFobjects, trange::tInterval, funcname::String)
    if      isa(trange, Int)
            1<=trange<=size(Y.y, 2) ? (return trange:trange) :
            @error 📌*", "*funcname*" function passed invalid trange Int argument. The time sample must be comprised between 1 and $(size(Y.y, 2))" trange
            return 0
    elseif  isa(trange, Colon)
            return 1:size(Y.y, 2)
    elseif  isa(trange, Tuple{Int, Int})
        if      trange[1]>trange[2]
                    @error 📌*", "*funcname*" function passed invalid trange 2-tuple argument. The second element cannot be inferior to the first" trange[1] trange[2]
                    return 0
        elseif  trange[1]<1 || trange[2]>size(Y.y, 2)
                    @error 📌*", "*funcname*" function passed invalid trange 2-tuple argument. The time limits in samples must be comprised between 1 and $(size(Y.y, 2))" trange[1] trange[2]
                    return 0
        else        return trange[1]:trange[2]
        end
    end
end

_getftrange(Y::TFobjects, frange::fInterval, trange::tInterval, funcname::String) =
    _getfrange(Y, frange, funcname), _gettrange(Y, trange, funcname)

function extract(Y::TFobjects, frange::fInterval, trange::tInterval)
    _frange, _trange = _getftrange(Y, frange, trange, "extract")
    if _frange ≠ 0 && _trange ≠ 0
        _isnull(_frange, _trange) ? Y.y[first(_frange), first(_trange)] :
                                    copy(Y.y[_frange, _trange])
    end
end

function mean(Y::TFobjects, frange::fInterval, trange::tInterval)
    _frange, _trange = _getftrange(Y, frange, trange, "mean")
    if _frange ≠ 0 && _trange ≠ 0
        return mean(view(Y.y, _frange, _trange))
    end
end

_getftrange(𝒀::TFobjectsVector, frange::fInterval, trange::tInterval, funcname::String;
            check::Bool=true) =
    check ? (sameParams(𝒀, funcname) ? _getftrange(𝒀[1], frange, trange, funcname) : 0) :
    _getftrange(𝒀[1], frange, trange, funcname)

extract(𝒀::TFobjectsVector, frange::fInterval, trange::tInterval;
        w::Vector=[], check::Bool=true) =
    if      check && frange isa Colon && !_allsame([size(Y.y, 1) for Y ∈ 𝒀])
            @error 📌*", extract function. In order of using a column (:) as `frange` argument, the number of rows (frequencies) must be identical in all time-frequency objects"
    elseif  check && trange isa Colon && !_allsame([size(Y.y, 2) for Y ∈ 𝒀])
            @error 📌*", extract function. In order of using a column (:) as `trange` argument, the number of columns (time samples) must be identical in all time-frequency objects"
    elseif  ((_frange, _trange)=_getftrange(𝒀, frange, trange, "extract"; check=check)) ≠ (0, 0)
            if _isnull(_frange, _trange)
                isempty(w) ? [Y.y[first(_frange), first(_trange)] for Y ∈ 𝒀] :
                             [ω*Y.y[first(_frange), first(_trange)] for (ω, Y) ∈ zip(w, 𝒀)]
            else
                isempty(w) ? [Y.y[_frange, _trange] for Y ∈ 𝒀] :
                             [ω*Y.y[_frange, _trange] for (ω, Y) ∈ zip(w, 𝒀)]
            end
    end

mean(𝒀::TFobjectsVector, frange::fInterval, trange::tInterval;
     w::Vector=[], check::Bool=true) =
    if      check && frange isa Colon && !_allsame([size(Y.y, 1) for Y ∈ 𝒀])
            @error 📌*", extract function. In order of using a column (:) as `frange` argument, the number of rows (frequencies) must be identical in all time-frequency objects"
    elseif  check && trange isa Colon && !_allsame([size(Y.y, 2) for Y ∈ 𝒀])
            @error 📌*", extract function. In order of using a column (:) as `trange` argument, the number of columns (time samples) must be identical in all time-frequency objects"
    elseif  ((_frange, _trange)=_getftrange(𝒀, frange, trange, "mean"; check=check)) ≠ (0, 0)
            return isempty(w) ? [mean(view(Y.y, _frange, _trange)) for Y ∈ 𝒀] :
                                [ω*mean(view(Y.y, _frange, _trange)) for (ω, Y) ∈ zip(w, 𝒀)]
    end

extr=extract

### Band-pass Averages ###

"""
```
function bands(S :: Union{FDobjects, FDobjectsVector}
       bandwidth :: IntOrReal)
```

Return band-pass average of spectral, cross-spectral or coherence estimates
in equally spaced band-pass regions with the given `bandwidth`.
`bandwidth` can be given as an integer or as a real number. See [`bbands`](@ref)
for details on the definition of band-pass regions.

Band-pass average is not supported for time-frequency objects as for those
objects a similar averaging is natively avaiable using argument `bandwidth`
in their constructors.

The output of this function is as it follows:
- for univariate [Spectra](@ref) objects (i.e., those hodling one spectrum only), a real column vector,
- for multivariate [Spectra](@ref) objects, a real matrix,
- for [SpectraVector](@ref) objects, a vector of the above,
- for [CrossSpectra](@ref) and [Coherence](@ref) objects, a vector of Hermitian or LowerTriangular matrices, depending on how the object has been cosntructed,
- for [CrossSpectraVector](@ref) and [CoherenceVector](@ref) objects, a vector  of the above.

**See**: [`bbands`](@ref).

**Examples**:
```
using FourierAnalysis, Plots

# example with univariate Spectra objects (one series -> one spectrum)
sr, t, f, a = 128, 256, 10, 1
# create a sinusoidal superimposed to white noise
v=sinusoidal(a, f, sr, t*16, 0) + randn(t*16)
# compute the spectrum
Σ=spectra(v, sr, t)
# mean spectra in 2Hz-band-pass regions
b=bands(Σ, 2)
plot(b)

# example with multivariate spectra (several series -> several spectra)
Σ=spectra(hcat(v, v+randn(t*16), v+randn(t*16) ), sr, t)
# mean spectra in 2Hz-band-pass regions for all time-series
b=bands(Σ, 2)
plot(b)
# plot mean spectra in 2Hz-band-pass regions for time-series 2 and 3 only
plot(bands(Σ, 2)[:, 2:3])

# example with CrossSpectra objects (the same goes for Coherence objects)
X=broadcast(+, v, randn(t*16, 3))*randn(3, 3)
Σ=crossSpectra(X, sr, t)
# mean cross-spectra in 4Hz-band-pass regions
B=bands(Σ, 4)

# example with multiple CrossSpectra objects
X2=broadcast(+, v, randn(t*16, 3))*randn(3, 3)
Σ=crossSpectra([X, X2], sr, t) # this is a CrossSpectraVector
# mean cross-spectra in 4Hz-band-pass regions for all cross-spectra objects
B=bands(Σ, 4)
```
"""
function bands(S::Spectra, bandwidth::IntOrReal)
    if (bands=bbands(S.sr, S.wl, bandwidth; DC=S.DC)) isa Nothing return S end
    r, n=length(bands)-1, size(S.y, 2)
    if n==1 return [mean(S.y[bands[b]:bands[b+1]]) for b=1:r]
    else
        Y=Matrix{eltype(S.y)}(undef, r, n)
        @inbounds for i=1:n Y[:, i]=[mean(S.y[bands[b]:bands[b+1], i]) for b=1:r] end
        return Y
    end
end # input Spectra, Output a vector or a Matrix

bands(𝐒::SpectraVector, bandwidth::IntOrReal) =
    [bands(S, bandwidth) for S ∈ 𝐒] # output a vector of vectors or matrices

function bands(S::Union{CrossSpectra, Coherence}, bandwidth::IntOrReal)
    if (bands=bbands(S.sr, S.wl, bandwidth; DC=S.DC)) isa Nothing return S end
    r=length(bands)-1
    mattype = S.tril ? LowerTriangular : Hermitian
    return typeof(S.y)([mattype(mean(S.y[i] for i=bands[b]:bands[b+1])) for b=1:r])
end # output: a vector of LowerTriangular (if tril=true) or Hermitian (if tril=false) matrices

bands(𝐒::Union{CrossSpectraVector, CoherenceVector}, bandwidth::IntOrReal) =
    [bands(S, bandwidth) for S ∈ 𝐒] # output: a vector of vectors of LowerTriangular (if tril=true) or Hermitian (if tril=false) matrices

# internal function: used by functions that may execute in multi-threading
# to decide to so so or not.
# The multi-threading is to be done for a for looping over `n` elements.
# Return true if `n` is large enough to justify multi-threading (n>thr, where
# `thr` is the number of threads Julia is instructed to use) and thr>1,
# otherwise return false.
function _thread(⏩, n)
    thr = nthreads() # of threads Julia is instructed to use
    ⏩ && n>=thr*2 && thr > 1 ? (return true) : (return false)
end

# check that all elements of a vector are equal. General function
# taken from https://stackoverflow.com/questions/47564825/check-if-all-the-elements-of-a-julia-array-are-equal
_allsame(x) = all(y -> y == (first(x)), x)

_isnull(frange::UnitRange{Int64}) = first(frange)==last(frange)
_isnull(frange::UnitRange{Int64}, trange::UnitRange{Int64}) =
    (first(frange)==last(frange)) & (first(trange)==last(trange))


export CubicSplineInterpolator, BicubicSplineInterpolator

#-------------------------------------------------------------------------------
# piecewise cubics with continuous derivatives (splines!)

struct CubicSplineInterpolator{T,B} <: OneDimensionalInterpolator
    r::InterpolatorRange{T}
    coef::Vector{NTuple{4,T}}
    boundaries::B
    i::RefValue{Int64} #previous cell index
end

#-------------------------------------------------------------------------------
# constructors

"""
    CubicSplineInterpolator(x, y, boundaries=StrictBoundaries())

Construct a `CubicSplineInterpolator` for the points defined by coordinates `x` and values `y`. This constructor creates a natural spline, where the second derivative is set to zero at the boundaries.
"""
function CubicSplineInterpolator(x, y, boundaries::AbstractBoundaries=StrictBoundaries())
    #construct the underlying range, triggering some checks
    T = promote_type(eltype(x), eltype(y))
    r = InterpolatorRange(x, y)
    n = r.n
    #compute coefficients
    #Burden, Richard L., and J. Douglas Faires. Numerical Analysis. 2011.
    a = collect(T, r.y)
    b = zeros(T, n - 1)
    d = zeros(T, n - 1)
    h = diff(r.x)
    α = zeros(T, n-1)
    for i = 2:n-1
        α[i] = 3*(a[i+1] - a[i])/h[i] - 3*(a[i] - a[i-1])/h[i-1]
    end
    c = zeros(T, n)
    l = ones(T, n)
    μ = zeros(T, n)
    z = zeros(T, n)
    l[1] = 1
    for i = 2:n-1
        l[i] = 2*(x[i+1] - x[i-1]) - h[i-1]*μ[i-1]
        μ[i] = h[i]/l[i]
        z[i] = (α[i] - h[i-1]*z[i-1])/l[i]
    end
    for j = n-1:-1:1
        c[j] = z[j] - μ[j]*c[j+1]
        b[j] = (a[j+1] - a[j])/h[j] - h[j]*(c[j+1] + 2*c[j])/3
        d[j] = (c[j+1] - c[j])/(3*h[j])
    end
    a = a[1:end-1]
    c = c[1:end-1]
    #static arrays
    coef = Vector{NTuple{4,T}}(undef,n-1)
    for i = 1:n-1
        coef[i] = (a[i], b[i], c[i], d[i])
    end
    #construct the object
    CubicSplineInterpolator(r, coef, boundaries, Ref(1))
end

"""
    CubicSplineInterpolator(x, y, dy₁, dyₙ, boundaries=StrictBoundaries())

Construct a `CubicSplineInterpolator` for the points defined by coordinates `x` and values `y`. This constructor creates a clamped spline, where the first derivatives at the boundaries are set by `dy₁` and `dyₙ`.
"""
function CubicSplineInterpolator(x,
                                 y,
                                 dy₁::Real,
                                 dyₙ::Real,
                                 boundaries::AbstractBoundaries=StrictBoundaries())
    #construct the underlying range, triggering some checks
    T = promote_type(eltype(x), eltype(y))
    r = InterpolatorRange(x, y)
    n = r.n
    #compute coefficients
    #Burden, Richard L., and J. Douglas Faires. Numerical Analysis. 2011.
    a = collect(T, r.y)
    b = zeros(T, n - 1)
    d = zeros(T, n - 1)
    h = diff(r.x)
    α = zeros(T, n)
    α[1] = 3*(a[2] - a[1])/h[1] - 3*dy₁
    for i = 2:n-1
        α[i] = 3*(a[i+1] - a[i])/h[i] - 3*(a[i] - a[i-1])/h[i-1]
    end
    α[n] = 3*dyₙ - 3*(a[n] - a[n - 1])/h[n-1]
    c = zeros(T, n)
    l = zeros(T, n)
    μ = zeros(T, n)
    z = zeros(T, n)
    l[1] = 2*h[1]
    μ[1] = 0.5
    z[1] = α[1]/l[1]
    for i = 2:n-1
        l[i] = 2*(x[i+1] - x[i-1]) - h[i-1]*μ[i-1]
        μ[i] = h[i]/l[i]
        z[i] = (α[i] - h[i-1]*z[i-1])/l[i]
    end
    l[n] = h[n-1]*(2 - μ[n-1])
    z[n] = (α[n] - h[n-1]*z[n-1])/l[n]
    c[n] = z[n]
    for j = n-1:-1:1
        c[j] = z[j] - μ[j]*c[j+1]
        b[j] = (a[j+1] - a[j])/h[j] - h[j]*(c[j+1] + 2*c[j])/3
        d[j] = (c[j+1] - c[j])/(3*h[j])
    end
    a = a[1:end-1]
    c = c[1:end-1]
    #static arrays
    coef = Vector{NTuple{4,T}}(undef,n-1)
    for i = 1:n-1
        coef[i] = (a[i], b[i], c[i], d[i])
    end
    #construct the object
    CubicSplineInterpolator(r, coef, boundaries, Ref(1))
end

"""
    CubicSplineInterpolator(f, xa, xb, n, boundaries=StrictBoundaries())

Construct a `CubicSplineInterpolator` for the function `f` using `n` evenly spaced function evaluations in the range [`xa`,`xb`]. A natural spline is created.
"""
function CubicSplineInterpolator(f,
                                 xa::Real,
                                 xb::Real,
                                 n::Int,
                                 boundaries::AbstractBoundaries=StrictBoundaries())
    linstruct(CubicSplineInterpolator, f, xa, xb, n, boundaries)
end

function (ϕ::CubicSplineInterpolator)(x)
    #enforce boundaries if desired
    ϕ.boundaries(x, ϕ.r.xa, ϕ.r.xb)
    #find the interpolation point
    i = findcell(x, ϕ)
    #offset from the nearest lower point
    @inbounds ξ = x - ϕ.r.x[i]
    #evaluate polynomial
    @inbounds ϕ.coef[i][1] + ϕ.coef[i][2]*ξ + ϕ.coef[i][3]*ξ^2 + ϕ.coef[i][4]*ξ^3
end

Base.getindex(ϕ::CubicSplineInterpolator, i) = ϕ.r.y[i]
function Base.copy(ϕ::CubicSplineInterpolator)
    CubicSplineInterpolator(ϕ.r, ϕ.coef, ϕ.boundaries, Ref(1))
end


using JuMP, LinearAlgebra, SparseArrays
using SCS
using MosekTools
const MOI = JuMP.MathOptInterface

# formulating LMI constraint with sparse matrices
#   https://github.com/JuliaOpt/JuMP.jl/issues/1950
# We are trying to solve:
# min c' x
# s.t.  A0 + A1 * x[1] + A2 * x[2] ⪰ 0

m = 2
A0 = sparse([1. 0; 0 3])
A1 = sparse([0. 1; 1 -2])
A2 = sparse([5. -1; -1 0])
B = [A1, A2]
c = [1.; 1]

# A0 = [1. 0; 0 3]
# A1 = [0. 1; 1 -2]
# A2 = [5. -1; -1 0]
# B = [A1, A2]
# c = [1.; 1]

# model = JuMP.Model(with_optimizer(SCS.Optimizer, verbose = 0));
model = JuMP.Model(with_optimizer(Mosek.Optimizer, QUIET=false))

@variable(model, x[1:m]);
@objective(model, Min, c' * x);

# formulate LMI constraint A0 + A1 x[1] + A2 x[2] in PSDCone
# works
K = A0 + sum(B[k] .* x[k] for k in 1:m)
@SDconstraint(model, con1,  Symmetric(K) >= 0);
# @constraint(model, con1,  Symmetric(K) in JuMP.PSDCone());

# throws error
# @constraint(model, con2,  A0 + sum(B[k] .* x[k] for k in 1:m) in JuMP.PSDCone());
# @SDconstraint(model, con1,  K >= 0);

JuMP.optimize!(model)
@show JuMP.objective_value(model)

@show JuMP.termination_status(model) == MOI.OPTIMAL
@show JuMP.primal_status(model) == MOI.FEASIBLE_POINT

L = JuMP.value.(K)
# L = A0 + sum(B[k] .* obj_x[k] for k in 1:m)
@show isposdef(L)


struct Connection
    from
    to
    dist
end

function euclidean(v, u)
    v .- u .|> (x -> x * x) |> sum |> sqrt
end

function getnearest(datapoints, linked, unlinked)
    mindist = Inf
    from = nothing
    to = nothing
    for i in linked
        for j in unlinked
            dist = euclidean(datapoints[i,:], datapoints[j,:])
            if dist < mindist
                mindist = dist
                from = i
                to = j
            end
        end
    end
    return Connection(from, to, mindist)
end

function mst(datapoints)
    result = []
    n, p = size(datapoints)
    allindices = collect(1:n)
    linkedset = [1]

    while true
        unlinkedset = setdiff(allindices, linkedset)
        if length(unlinkedset) == 0
            break
        end
        conn = getnearest(datapoints, linkedset, unlinkedset)
        push!(result, conn)
        push!(linkedset, conn.to)
    end
    return result
end

function cutree(tree)
    n = length(tree)
    maxdist = maximum(x -> x.dist, tree)
    part1 = []
    part2 = []
    i = 1
    while i < n
        if tree[i].dist < maxdist
            push!(part1, tree[i])
        else
            break
        end
        i += 1
    end

    for j = (i+1):n
        push!(part2, tree[j])
    end
    return (part1, part2)
end


using RDatasets, Clustering, Random, LinearAlgebra, Plots; pyplot()
Random.seed!(0)

df = dataset("cluster", "xclara")
n,_ = size(df)
dataPoints = [convert(Array{Float64,1},df[i,:]) for i in 1:n]
shuffle!(dataPoints)
D = [norm(pt1 - pt2) for pt1 in dataPoints, pt2 in dataPoints]

result = hclust(D)
for K in 1:30
    clusters = cutree(result,k=K)
    println("K=$(K): ",[sum(clusters .== i) for i in 1:K])
end

cluster(ell,K) = (1:n)[cutree(result,k=K) .== ell]

C1, C2, C3 = cluster(1,30),cluster(2,30),cluster(3,30)

plt = scatter( first.(dataPoints[C1]),last.(dataPoints[C1]),c=:blue, msw=0)
      scatter!( first.(dataPoints[C2]),last.(dataPoints[C2]), c=:red, msw=0)
      scatter!( first.(dataPoints[C3]),last.(dataPoints[C3]), c=:green, msw=0)
for ell in 4:30
    clst = cluster(ell,30)
    scatter!(first.(dataPoints[clst]),last.(dataPoints[clst]),
        ms=10, c=:purple, shape=:xcross, ratio=:equal, legend=:none,
        xlabel="V1", ylabel="V2")
end
plot(plt)


T = 250
Σ = [0.2 0.0; 0.0 0.8]
t = range(0, 20, length = T)
s = sin.(t)
y = s * [1 1] + randn(T, 2) * cholesky(Σ).L

H = Matrix(1.0I, 2, 2)
A = Matrix(1.0I, 2, 2)
Q = Matrix(0.01*I, 2, 2)
x = [0.0, 0.0]
P = Matrix(1000.0I, 2, 2)

m = Model(y, H, A, Q, Σ, x, P)
kf = kalmanfilter(m)
ks = kalmansmoother(m, kf.priors, kf.posteriors)

using Plots
l = @layout [a b]
p1 = scatter(t, y[:, 1], label = "Measurement", legend = :bottom, ylim = [-5, 4],
        title = "Variable 1", markerstrokewidth = 0, markercolor = "black", foreground_color_legend = nothing)
p1 = plot!(t, s, label = "Noiseless signal", linewidth = 3, color = "grey")
p1 = plot!(t, ks.μ[:, 1], label = "Smoother", linewidth = 2, color = :red)

p2 = scatter(t, y[:, 2], ylim = [-5, 4], legend = false, title = "Variable 2", markerstrokewidth = 0, markercolor = "black", foreground_color_legend = nothing)
p2 = plot!(t, s, linewidth = 3, color = "grey")
p2 = plot!(t, ks.μ[:, 2], linewidth = 2, color = :red)

plot(p1, p2, layout = l)


#=

Author: Shunsuke Hori

=#

using QuantEcon

"""
g: input policy function
grid: grid points
β: discount factor
u_prime: derivative of utility function
f: production function
f_prime: derivative of production function
shocks::shock draws, used for Monte Carlo integration to compute expectation
Kg: output value is stored
"""
function coleman_operator!(g::AbstractVector,
                           grid::AbstractVector,
                           β::AbstractFloat,
                           u_prime::Function,
                           f::Function,
                           f_prime::Function,
                           shocks::AbstractVector,
                           Kg::AbstractVector=similar(g))

    # This function requires the container of the output value as argument Kg

    # Construct linear interpolation object #
    g_func=LinInterp(grid, g)

    # solve for updated consumption value #
    for (i,y) in enumerate(grid)
        function h(c)
            vals = u_prime.(g_func.(f(y - c)*shocks)).*f_prime(y - c).*shocks
            return u_prime(c) - β * mean(vals)
        end
        Kg[i] = brent(h, 1e-10, y-1e-10)
    end
    return Kg
end

# The following function does NOT require the container of the output value as argument
function coleman_operator(g::AbstractVector,
                          grid::AbstractVector,
                          β::AbstractFloat,
                          u_prime::Function,
                          f::Function,
                          f_prime::Function,
                          shocks::AbstractVector)

    return coleman_operator!(g, grid, β, u_prime,
                             f, f_prime, shocks, similar(g))
end


## Test discretize given edges
function discretize_test()
	@testset "Discretize" begin
        n = 55;
        inV = collect(range(1, 10, length = n));
        edgeV = [0.0, 6.0, 11.0];
        outV = discretize(inV, edgeV);
		@test all(outV[inV .< edgeV[1]] .== 0)
		@test all(outV[inV .> edgeV[end]] .== 0)

		for ix = 1 : n
			iCl = outV[ix];
			if iCl > 0
				@test inV[ix] >= edgeV[iCl]  &&  inV[ix] <= edgeV[iCl+1]
			else
				@test inV[ix] <= edgeV[1]  ||  inV[ix] >= edgeV[end]
			end
		end
	end
end

## Test bin edges (unweighted)
function bin_edges_test()
    @testset "Bin edges" begin
        n = 450;
        inV = collect(range(-4.0, 3.0, length = n));
        pctV = [0.25, 0.4, 0.85];
        edgeV = bin_edges_from_percentiles(inV, pctV);
        @test length(edgeV) == (length(pctV) + 1)

        # test that fractions match target
        cntV = count_bins(inV, edgeV);
        @test sum(cntV) == round(n * pctV[end])
        fracV = cntV ./ n;
        tgFracV = diff(vcat(0.0, pctV));
        @test isapprox(fracV, tgFracV, atol = 2/n)
    end
end

## Test bin edges (weighted)
function bin_edges_weighted_test()
    @testset "Bin edges weighted" begin
        n = 450;
        inV = collect(range(-4.0, 3.0, length = n));
        # FrequencyWeights must be integers
        wtV = collect(range(0.2, 0.8, length = n));
        pctV = [0.25, 0.4, 0.85];
        edgeV = bin_edges_from_percentiles(inV, wtV, pctV);
        @test length(edgeV) == (length(pctV) + 1)

        # test that fractions below edges match target
        massV = count_bins(inV, wtV, edgeV);
        @test sum(massV) ≈ sum(wtV[inV .<= edgeV[end]])
        fracV = massV ./ sum(wtV);
        tgFracV = diff(vcat(0.0, pctV));
        @test isapprox(fracV, tgFracV, atol = 2/n)
    end
end

## Discretize given percentiles
function discretize_given_percentiles_test()
	@testset "discretize_given_percentiles" begin
        n = 38;
        inV = collect(range(1.0, 5.0, length = n));
        pctV = [0.25, 0.4, 0.85, 1.0];
        classV = discretize_given_percentiles(inV, pctV);
		@test all(classV .>= 1);
		@test all(classV .<= length(pctV));
	end
end

## Discretize (weighted)
function discretize_given_pct_weighted_test()
    @testset "discretize pct weighted" begin
        n = 38;
        inV = collect(range(1.0, 5.0, length = n));
        wtV = collect(range(0.1, 0.5, length = n));
        pctV = [0.25, 0.4, 0.85, 1.0];
        classV = discretize_given_percentiles(inV, wtV, pctV);
	    @test all(classV .>= 1);
	    @test all(classV .<= length(pctV));
    end
end

@testset "Discretize" begin
   discretize_test()
   discretize_given_pct_weighted_test();
   bin_edges_test();
   bin_edges_weighted_test();
   discretize_given_percentiles_test()
end

# ----------------


function MapFeature(X1,X2,d)

    m=size(X1,1)
    n=((d+d^2)/2)+d

    p=zeros(m,round(Int,n))
    count=0
    for i =1:d
        for j=0:i
            count=count+1
            p[:,count]=(X1.^(i-j)).*(X2.^j)
        end
    end

    return p
end

function PolyFeature(X,d)

    m=size(X,1)
    n=size(X,2)
    p=zeros(m,d*n)
c=0
for j=1:n
    for i=1:d
        c=c+1
        p[:,c]=X[:,j].^i
    end
end
    return p

end


using TuringModels

Turing.setadbackend(:reverse_diff)
#nbTuring.turnprogress(false);

d = CSV.read(joinpath(@__DIR__, "..", "..", "data", "Kline.csv"), delim=';');
size(d) # Should be 10x5

# New col log_pop, set log() for population data
d[!, :log_pop] = map((x) -> log(x), d[:, :population]);
d[!, :society] = 1:10;

# Turing model

@model m12_6(total_tools, log_pop, society) = begin

    # Total num of y
    N = length(total_tools)

    # priors
    α ~ Normal(0, 10)
    βp ~ Normal(0, 1)

    # Separate σ priors for each society
    σ_society ~ Truncated(Cauchy(0, 1), 0, Inf)

    # Number of unique societies in the data set
    N_society = length(unique(society)) #10

    # Vector of societies (1,..,10) which we'll set priors on
    α_society = Vector{Real}(undef, N_society)

    # For each society [1,..,10] set a prior N(0, σ_society)
    α_society ~ [Normal(0, σ_society)]

    for i ∈ 1:N
        λ = exp(α + α_society[society[i]] + βp*log_pop[i])
        total_tools[i] ~ Poisson(λ)
    end
end

# Sample

chns = sample(m12_6(d[:, :total_tools], d[:, :log_pop],
    d[:, :society]), NUTS(0.95), 1000);

# Results rethinking
m12_6rethinking = "
              Mean StdDev lower 0.89 upper 0.89 n_eff Rhat
a              1.11   0.75      -0.05       2.24  1256    1
bp             0.26   0.08       0.13       0.38  1276    1
a_society[1]  -0.20   0.24      -0.57       0.16  2389    1
a_society[2]   0.04   0.21      -0.29       0.38  2220    1
a_society[3]  -0.05   0.19      -0.36       0.25  3018    1
a_society[4]   0.32   0.18       0.01       0.60  2153    1
a_society[5]   0.04   0.18      -0.22       0.33  3196    1
a_society[6]  -0.32   0.21      -0.62       0.02  2574    1
a_society[7]   0.14   0.17      -0.13       0.40  2751    1
a_society[8]  -0.18   0.19      -0.46       0.12  2952    1
a_society[9]   0.27   0.17      -0.02       0.52  2540    1
a_society[10] -0.10   0.30      -0.52       0.37  1433    1
sigma_society  0.31   0.13       0.11       0.47  1345    1
";

# Describe the posterior samples

describe(chns)

# End of `12/m12.6t.jl`


# A Julia implementation of the Affine ciphertext

# Determines if 2 values are coprime
function coprime(x, y)
    xlist = []
    ylist = []
    for i in 2 : x
        if x % i == 0
            push!(xlist, i)
        end
    end
    for j in 2 : y
        if y % j == 0
            push!(ylist, j)
        end
    end
    for a in xlist
        for b in ylist
            if a == b
                return false
            end
        end
    end
    return true
end

# Changes a char to an int value
function charToVal(c)
    alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
    val = findfirst(isequal(c), alphabet)
    return val
end

# Changes a value to a char1
function valToChar(i)
    alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
    char = alphabet[i]
    return char
end

# Main function
function affine()

    m = 26 # The number of letters in the alphabet used
    println("Please input the plaintext")
    plaintext = readline()
    plaintext = uppercase(plaintext)
    plaintext = replace(plaintext, " " => "")

    # Take keys and ensure they're valid
    println("Please enter the a value")
    a = parse(Int, readline())
    while !coprime(a, m)
        println("a value invalid, must be coprime with m value " * string(m))
        a = parse(Int, readline())
    end
    println("Please enter the b value")
    b = parse(Int, readline())

    # Produce the ciphertext
    ciphertext = ""
    for c in plaintext
        val = charToVal(c) - 1 # Must account for Julia being 1-indexed
        newVal = mod(((a * val) + b), m)
        newchar = valToChar(newVal + 1)
        ciphertext *= newchar
    end
    println(ciphertext)
end

affine()


function make_vandermonte_real(c::Vector{T}) where {T}
    n = length(c)
    V = zeros(T, n, n)
    for k = 1:n
        for kk = 1:n
            V[k, kk] = c[kk] ^ (k-1)
        end
    end
    return V
end

function find_real_eigenvalue(A::Matrix{BigFloat})
    n, _ = size(A)
    isodd(n) || error("A needs to be odd sized")
    ev = eigvals(A)
    min_val, min_ind = findmin(abs.(imag.(ev)))
    (min_val != 0.0) && error("something is wrong")
    return real.(ev[min_ind])
end

function calc_b_hat(A, c)
    gamma_0 = find_real_eigenvalue(A)
    n, _ = size(A)
    rhs = BigFloat.(collect(1 .// (1:n)))
    rhs[1] = 1 - gamma_0
    V = make_vandermonte_real(c)
    return vcat([gamma_0], V \ rhs)
end

function radau_butcher_table_plus(s::Int64)
    # ABC returns coefficients for S-stage Gauss-Legendre methods. (From RKGL, DiffMan package v.2 by K. Engo, A. Marthinsen & H. Munthe-Kaas.)
    P_Radau = output_p_Radau(s)
    c = roots(BigFloat.(coeffs(P_Radau)))
    c = sort(real.(c))
    V = make_vandermonte_real(c)'
    J = diagm(0 => 1 .// (1:s) )
    A = diagm(0 => c) * V * J / V
    b = (ones(BigFloat, 1, s) * J / V)[:]
    bi = get_dense_radau(J, V, c)
    if isodd(s)
        b̂ = calc_b_hat(A, c)
    else
        b̂ = zeros(BigFloat, s + 1) .+ NaN
    end
    return A, b, c, bi, b̂
end

function make_legendre_poly_by_order(n::Int64)
    P_x2_minus_1 = Poly([-1, 0, 1])
    return (1 // (2^n * factorial(n))) * polyder(P_x2_minus_1^n, n)
end

function output_p_Radau(n::Int64)
    Pn = make_legendre_poly_by_order(n)
    Pn_m = make_legendre_poly_by_order(n-1)
    P_2x_minus_1 = Poly([-1, 2])
    P_Radau = polyval(Pn, P_2x_minus_1) - polyval(Pn_m, P_2x_minus_1)
    return P_Radau
end

function get_dense_radau(J, V, c)
    s = length(c)
    interp_poly = (J / V)' .* ((1 ./ c) * collect(1:s)')
    return interp_poly
end


"""
    MahalanobisKernel(P::AbstractMatrix)

Mahalanobis distance-based kernel given by
```math
    κ(x,y) =  exp(-r^2), r^2 = maha(x,P,y) = (x-y)'*inv(P)*(x-y)
```
where the matrix P is the metric.

"""
struct MahalanobisKernel{T<:Real, A<:AbstractMatrix{T}} <: SimpleKernel
    P::A
    function MahalanobisKernel(P::AbstractMatrix{T}) where {T<:Real}
        LinearAlgebra.checksquare(P)
        new{T,typeof(P)}(P)
    end
end

kappa(κ::MahalanobisKernel, d::T) where {T<:Real} = exp(-d)

metric(κ::MahalanobisKernel) = SqMahalanobis(κ.P)

Base.show(io::IO, κ::MahalanobisKernel) = print(io, "Mahalanobis Kernel (size(P) = ", size(κ.P), ")")


using DifferentialEquations
using LinearAlgebra
using Plots
using ForwardDiff
using ProgressMeter

# 2D Ball Example
function physics(u, p, t)
    # x, dx = u[1, 3]
    # y, dy = u[2, 4]

    # g = gravity, b = drag
    g, b = p[1], p[2]

    dx = u[2]
    ddx = b * u[1]^2
    dy = u[4]
    ddy = -g - b * u[3]^2

    [dx, ddx, dy, ddy]
end

# Callback
condition_cont(u,t,integrator) = u[3]
# condition(u,t,integrator) = u[3]<=0
affect!(integrator) = terminate!(integrator)
# cb = DiscreteCallback(condition,affect!)
cb = ContinuousCallback(condition_cont,nothing,affect!)

#Lets wrap this into a "predict" method
function predict(vel)
    # Simply roll this forward in time and we get our solution
    tspan = (0.0, 10.0)
    tspan_dual = convert.(eltype(vel),tspan)
    p = [9.8, 0.0]

    start_state = [0.0, vel[1], 10.0, vel[2]]
    prob = ODEProblem(physics, start_state, tspan_dual, p)
    sol = solve(prob, callback=cb)

    sol
end

## Now let's try to optimize the system to hit a particular point
# want point (1.0, 0)
function loss(vel)
    simulation = predict(vel)
    final_pos = simulation[[1, 3], end]
    l = (final_pos[1] - 1.0)^2 + final_pos[2]^2 # x -> 1.0, y -> 0
    return l
end

# Plots loss surface over velocities in x and y
function plot_loss_surf()
    v_x = 0.0:0.01:3
    v_y = 0.0:0.01:2

    loss_mat = zeros(length(v_x), length(v_y))
    for x in 1:length(v_x)
        for y in 1:length(v_y)
            loss_mat[x, y] = loss([v_x[x], v_y[y]])
        end
    end

    # sol_f(x, y) = loss_mat[x, y]
    # plot(1:length(v_x), 1:length(v_y), sol_f, st=:surf, color=:viridis)
    plot(loss_mat, st=:heatmap, color=:viridis)

end

# Plots loss over just x velocities
function plot_loss_x()

    v_x = 0.05:0.01:3

    loss_mat = zeros(length(v_x))
    for x in 1:length(v_x)
        loss_mat[x] = loss([v_x[x], 0.0])
    end

    plot(v_x, loss_mat)

end

# Optimize the xy velocities needed
function optimize(vel_opt, η)
    @showprogress for idx in 1:1000
        grads = ForwardDiff.gradient(s -> loss(s), vel_opt) # Magic
        vel_opt .-= η*grads

        if loss(vel_opt) < 0.00001
            break
        end
    end

    vel_opt
end

function optimize_and_save(vel_opt, η)
    solns = []
    for idx in 1:1000
        if idx % 10 == 1
            append!(solns, [deepcopy(vel_opt)])
            # @show idx
            # @show vel_opt
            # @show solns
        end

        grads = ForwardDiff.gradient(s -> loss(s), vel_opt) # Magic
        vel_opt .-= η*grads

        if loss(vel_opt) < 0.00001
            break
        end

    end
    display(solns)
    solns
end

# Example run of optimize
function run_test()
    p = [1.0, 0.2]
    init_sol = predict(p);
    v = optimize(p, 0.2);
    final_sol = predict(v);

    display(plot(init_sol, vars=(1, 3), label="Initial")); display(plot!(final_sol, vars=(1,3), label="Optimized to land at x=1.0, y=0.0"))
end

function run_and_save()
    p = [2.0, 0.4]
    vs = optimize_and_save(p, 0.03);
    display(plot(title="Ballistic Optimizer: Goal final position (1.0, 0.0)",
                 xlabel="x distance [m]",
                 ylabel="y distance [m]"))
    i = 0
    for v in vs
        display(
            plot!(predict(v),
                vars=(1,3),
                label="Iteration " * string(i),
                color=:Blues,
                colorbar=:none,
                legend=:bottomleft,
                line_z=i+1)
            )
        i += 10
    end

    savefig("plots/ballistic_example.png")
end


#
# Evaluates definite integral
# ∫ erf(b-y)/(sqrt(2)*ℓ), y∈[c,d]
#
# Wolfram code: Integrate[erf(b-y)/(sqrt(2)*ℓ), {y, c, d}]
#

function sub_1(; b = b, c = c, d = d, ℓ = ℓ)

    @assert(c < d)

    sqrt(2/π) * ℓ * ( exp(-(b - c)^2/(2 * ℓ^2)) - exp(-(b - d)^2/(2 * ℓ^2))) +
        (b - c) * erf((b - c)/(sqrt(2) * ℓ)) +
        (d - b) * erf((b - d)/(sqrt(2) * ℓ))

end

function verify_sub_1(; b = b, c = c, d = d, ℓ = ℓ)

    # integrand
    f(y) = erf((b-y)/(sqrt(2)*ℓ))

    # own numerical verification on 1d regular grid
    dx = 1e-6
    X  = c:dx:d
    num = 0.0
    for x in X
        num += f(x)*dx
    end

    # numerical verification via Cuba.jl library
    h = Cuba.vegas((x, out) -> out[1] = (d-c) * f(c + (d-c)*x[1]), 1, 1, maxevals=1_000_000)

    display(h)

    # numerical verification via HCubature.jl library
    hc = hquadrature(f, c, d)

    # return exact and numerical results
    sub_1(; b = b, c = c, d = d, ℓ = ℓ),
    h.integral[1],
    hc[1],
    num

end

#
# Evaluates definite integral
# ∫∫ exp(- (x-y)² / (2ℓ²) ) dx dy, x∈[a,b], y∈[c,d]
#
# Wolfram code: Integrate[E^(-(x - y)^2/(2 (ℓ^2))), {x, a, b}]
#

function integralfunction_1(; lowerx = lowerx, upperx = upperx, lowery = lowery, uppery = uppery, ℓ = ℓ)

    @assert(lowerx < upperx)
    @assert(lowery < uppery)

    # easier names
    a = lowerx
    b = upperx
    c = lowery
    d = uppery

    # Result of ∫ exp(- (x-y)² / (2ℓ²) ) dx  x∈[a,b] is:
    # sqrt(π/2) r (erf((b - y)/(sqrt(2) ℓ)) - erf((a - y)/(sqrt(2) ℓ)))
    # We need to carry out the second integral over y.
    # The result of this second integral over y is:

    sqrt(π/2) * ℓ * (sub_1(b=b, c=c, d=d, ℓ=ℓ) - sub_1(b=a, c=c, d=d, ℓ=ℓ))

end

#
# Verification: numerical evaluation for definite integral
# ∫∫ exp(- (x-y)² / (2ℓ²) ) dx dy, x∈[a, b], y∈[c, d]
#

function verify_1(; lowerx = lowerx, upperx = upperx, lowery = lowery, uppery = uppery, ℓ = ℓ)

    @assert(lowerx < upperx)
    @assert(lowery < uppery)

    # intergrand
    f(x,y) =  exp(-(x-y)^2 / (2*(ℓ^2)))

    # numerical verification on regular grid
    Δx = 1e-3
    X  = lowerx:Δx:upperx
    Y  = lowery:Δx:uppery

    num = 0.0
    for i in 1:length(X)
        for j in 1:length(Y)
            @inbounds num += f(X[i], Y[j])
        end
    end
    num *= Δx^2

    # numerical verification via Cuba.jl libary
    h = divonne((x, out) -> out[1] = (uppery-lowery)*(upperx-lowerx) * f(lowerx + (upperx-lowerx)*x[1], lowery + (uppery-lowery)*x[2]), 2, 1)

    display(h)

    # return exact and numerical results to verify
    integralfunction_1(lowerx = lowerx, upperx = upperx, lowery = lowery, uppery = uppery, ℓ = ℓ), num, h.integral[1]

end

################################################################################
################################################################################
################################################################################

#
# Evaluates definite integral
# ∫ exp(- (a-y)² / (2ℓ²) ) dy,  y ∈ [c, d]
#
function sub_2a(; a = a, c = c, d = d, ℓ = ℓ)

    # Wolfram code: Integrate[E^(-(a - y)^2/(2 ℓ^2)), {y, c, d}]
    # Solution returned in plain text:
    # integral_c^d e^(-(a - y)^2/(2 ℓ^2)) dy = sqrt(π/2) r (erf((a - c)/(sqrt(2) ℓ)) - erf((a - d)/(sqrt(2) ℓ)))

    sqrt(π/2) * ℓ * (erf((a - c)/(sqrt(2) * ℓ)) - erf((a - d)/(sqrt(2) * ℓ)))

end

#
# Verifies definite integral
# ∫ exp(- (a-y)² / (2ℓ²) ) dy,  y ∈ [c, d]
#
function verify_sub_2a(; a = a, c = c, d = d, ℓ = ℓ)

    f(y) = exp(-(a-y)^2/(2*ℓ^2))

    # own numerical verification on 1d regular grid
    dx = 1e-6
    X  = c:dx:d
    num = 0.0
    for x in X
        num += f(x)*dx
    end

    # numerical verification via Cuba.jl libary
    h = Cuba.vegas((x, out) -> out[1] = (d-c) * f(c + (d-c)*x[1]), 1, 1, maxevals=3_000_000)

    display(h)

    # return exact and numerical results
    sub_2a(; a = a, c = c, d = d, ℓ = ℓ),
    h.integral[1],
    num

end

#
# Evaluates definite integral
# ∫ y erf((a-y)/(sqrt(2)ℓ) dy,  y ∈ [c, d]
#
function sub_2b(; a = a, c = c, d = d, ℓ = ℓ)

    # Wolfram code: Integrate[y Erf[(a - y)/(Sqrt[2] ℓ)], {y, c, d}]
    # Solution returned in plain text:
    # integral_c^d y erf((a - y)/(sqrt(2) ℓ)) dy = 1/2 (a^2 - c^2 + ℓ^2) erf((a - c)/(sqrt(2) ℓ)) - 1/2 (a^2 - d^2 + ℓ^2) erf((a - d)/(sqrt(2) ℓ)) + (ℓ (a + c) e^(-(a - c)^2/(2 ℓ^2)))/sqrt(2 π) - (ℓ (a + d) e^(-(a - d)^2/(2 ℓ^2)))/sqrt(2 π)

    1/2 * (a^2 - c^2 + ℓ^2) * erf((a - c)/(sqrt(2) * ℓ)) -
    1/2 * (a^2 - d^2 + ℓ^2) * erf((a - d)/(sqrt(2) * ℓ)) +
    (ℓ * (a + c) * exp(-(a - c)^2/(2 * ℓ^2)))/sqrt(2*π) - (ℓ*(a + d) * exp(-(a - d)^2/(2*ℓ^2)))/sqrt(2π)

end

#
# Verifies definite integral
# ∫ y erf((a-y)/(sqrt(2)ℓ) dy,  y ∈ [c, d]
#
function verify_sub_2b(; a = a, c = c, d = d, ℓ = ℓ)

    f(y) = y * erf((a-y)/(sqrt(2)*ℓ))

    # own numerical verification on 1d regular grid
    dx = 1e-6
    X  = c:dx:d
    num = 0.0
    for x in X
        num += f(x)*dx
    end

    # numerical verification via Cuba.jl libary
    h = Cuba.vegas((x, out) -> out[1] = (d-c) * f(c + (d-c)*x[1]), 1, 1, maxevals=1_000_000)

    display(h)

    # return exact and numerical results
    sub_2b(; a = a, c = c, d = d, ℓ = ℓ),
    h.integral[1],
    num

end

#
# Evaluates definite integral
# ∫∫ x exp(- (x-y)² / (2ℓ²) ) dx dy, x∈[a, b], y∈[c, d]
#
function integralfunction_2(; lowerx = lowerx, upperx = upperx, lowery = lowery, uppery = uppery, ℓ = ℓ)

    @assert(lowerx < upperx)
    @assert(lowery < uppery)

    # easier names
    a = lowerx
    b = upperx
    c = lowery
    d = uppery

    # Intermediate step:
    # Solution of integral ∫ᵇₐ x exp(-(x-y)^2 / (2ℓ²)) dx is
    # integral_a^b x e^(-(x - y)^2/(2 ℓ^2)) dx = 1/2 ℓ (2 ℓ (e^(-(a - y)^2/(2 ℓ^2)) - e^(-(b - y)^2/(2 ℓ^2))) - sqrt(2 π) y erf((a - y)/(sqrt(2) ℓ)) + sqrt(2 π) y erf((b - y)/(sqrt(2) ℓ)))
    # (Wolfram code: Integrate[x * E^(-((x - y)^2)/(2*ℓ^2)), {x, a, b}]
    #

    #1/2 * ℓ *(2*ℓ * (exp(-(a - y)^2/(2*ℓ^2)) - exp(-(b - y)^2/(2*ℓ^2))) -
    #          sqrt(2π) * y * erf((a - y)/(sqrt(2)*ℓ)) + sqrt(2π) * y * erf((b - y) /(sqrt(2)*ℓ)))

    1/2 * ℓ *(2*ℓ * (sub_2a(a=a, c=c, d=d, ℓ=ℓ) - sub_2a(a=b, c=c, d=d, ℓ=ℓ)) -
              sqrt(2π) * (sub_2b(a = a, c = c, d = d, ℓ = ℓ) -  sub_2b(a = b, c = c, d = d, ℓ = ℓ)))

end

function verify_2(; lowerx = lowerx, upperx = upperx, lowery = lowery, uppery = uppery, ℓ = ℓ)

    f(x,y) =  x * exp(- (x-y)^2 / (2*ℓ^2) )

    # numerical verification on regular grid
    Δx = 1e-3
    X  = lowerx:Δx:upperx
    Y  = lowery:Δx:uppery

    num = 0.0
    for i in 1:length(X)
        for j in 1:length(Y)
            @inbounds num += f(X[i], Y[j])
        end
    end
    num *= Δx^2

    # numerical verification via Cuba.jl libary
    h = divonne((x, out) -> out[1] = (uppery-lowery)*(upperx-lowerx) * f(lowerx + (upperx-lowerx)*x[1], lowery + (uppery-lowery)*x[2]), 2, 1)

    display(h)

    # return exact and numerical results to verify
    integralfunction_2(lowerx = lowerx, upperx = upperx, lowery = lowery, uppery = uppery, ℓ = ℓ), num, h.integral[1]

end

################################################################################
################################################################################
################################################################################

#
# Evaluate integral:
# ∫ y² erf( (a-y) / (sqrt(2) ℓ) ) dy
#

function sub_3a(; a = a, c = c, d = d, ℓ = ℓ)

    # Wolram code: Integrate[y^2 Erf[(a - y)/(Sqrt[2] ℓ)], {y, c, d}]
    #
    # solution:
    # integral_c^d y y erf((a - y)/(sqrt(2) ℓ)) dy = 1/3 (-(a^3 + 3 a ℓ^2) erf((c - a)/(sqrt(2) ℓ)) + (a^3 + 3 a ℓ^2) erf((d - a)/(sqrt(2) ℓ)) + sqrt(2/π) ℓ e^(-(a - c)^2/(2 ℓ^2)) (a^2 + a c + c^2 + 2 ℓ^2) - sqrt(2/π) ℓ e^(-(a - d)^2/(2 ℓ^2)) (a^2 + a d + d^2 + 2 ℓ^2) + c^3 (-erf((a - c)/(sqrt(2) ℓ))) + d^3 erf((a - d)/(sqrt(2) ℓ)))

    1/3 * (-(a^3 + 3*a*ℓ^2) * (erf((c - a)/(sqrt(2)*ℓ)) - erf((d - a)/(sqrt(2)*ℓ))) +
              sqrt(2/π) * ℓ * (exp(-(a - c)^2/(2 * ℓ^2)) * (a^2 + a * c + c^2 + 2 * ℓ^2) -
                               exp(-(a - d)^2/(2 * ℓ^2)) * (a^2 + a * d + d^2 + 2 * ℓ^2)) +
          c^3 * (-erf((a - c)/(sqrt(2) * ℓ))) + d^3 * erf((a - d)/(sqrt(2) * ℓ)))

end

#
# Verify integral:
# ∫ y² erf( (a-y) / (sqrt(2) ℓ) ) dy
#

function verify_sub_3a(; a = a, c = c, d = d, ℓ = ℓ)

    f(y) = y^2 * erf( (a-y) / (sqrt(2) * ℓ) )

    # own numerical verification on 1d regular grid
    dx = 1e-6
    X  = c:dx:d
    num = 0.0
    for x in X
        num += f(x)*dx
    end

    # numerical verification via Cuba.jl libary
    h = Cuba.vegas((x, out) -> out[1] = (d-c) * f(c + (d-c)*x[1]), 1, 1, maxevals=10_000_000)

    display(h)

    # return exact and numerical results
    sub_3a(; a = a, c = c, d = d, ℓ = ℓ),
    h.integral[1],
    num

end

#
# Evaluate integral:
# ∫ y exp( -(a-y)² / (2 ℓ²) ) dy
#

function sub_3b(; a = a, c = c, d = d, ℓ = ℓ)

    # Wolram code: Integrate[y/E^((a - y)^2/(2 ℓ^2)), {y, c, d}]
    #
    # solution:
    # integral_c^d y e^(-(a - y)^2/(2 ℓ^2)) dy = 1/2 ℓ (2 ℓ (e^(-(a - c)^2/(2 ℓ^2)) - e^(-(a - d)^2/(2 ℓ^2))) + sqrt(2 π) a erf((a - c)/(sqrt(2) ℓ)) - sqrt(2 π) a erf((a - d)/(sqrt(2) ℓ)))

    1/2 * ℓ * (2 * ℓ * (exp(-(a - c)^2/(2*ℓ^2)) - exp(-(a - d)^2/(2*ℓ^2))) +
            sqrt(2*π) * a * erf((a - c)/(sqrt(2)*ℓ)) - sqrt(2*π) * a * erf((a - d)/(sqrt(2)*ℓ)))

end

#
# Verify integral:
# ∫ y exp( -(a-y)² / (2 ℓ²) ) dy
#

function verify_sub_3b(; a = a, c = c, d = d, ℓ = ℓ)

    f(y) = y * exp( -(a-y)^2 / (2 * ℓ^2) )

    # own numerical verification on 1d regular grid
    dx = 1e-6
    X  = c:dx:d
    num = 0.0
    for x in X
        num += f(x)*dx
    end

    # numerical verification via Cuba.jl libary
    h = Cuba.vegas((x, out) -> out[1] = (d-c) * f(c + (d-c)*x[1]), 1, 1, maxevals=10_000_000)

    display(h)

    # return exact and numerical results
    sub_3b(; a = a, c = c, d = d, ℓ = ℓ),
    h.integral[1],
    num

end

#
# Evaluate integral:
# ∫∫ y x exp(- (x-y)² / (2ℓ²) ) dx dy, x∈[a, b], y∈[c, d]
#
function integralfunction_3(; lowerx = lowerx, upperx = upperx, lowery = lowery, uppery = uppery, ℓ = ℓ)

    @assert(lowerx < upperx)
    @assert(lowery < uppery)

    # easier names
    a = lowerx
    b = upperx
    c = lowery
    d = uppery

    # Intermediate step: ∫ y x exp(- (x-y)² / (2ℓ²) ) dx dy, x∈[a, b]
    # Wolfram code: Integrate[(x y)/E^((x - y)^2/(2 ℓ^2)), {x, a, b}]
    # solution:
    # integral_a^b y x e^(-(x - y)^2/(2 ℓ^2)) dx = 1/2 ℓ y (2 ℓ (e^(-(a - y)^2/(2 ℓ^2)) - e^(-(b - y)^2/(2 ℓ^2))) - sqrt(2 π) y erf((a - y)/(sqrt(2) ℓ)) + sqrt(2 π) y erf((b - y)/(sqrt(2) ℓ)))
    #
    # We need to integrate the above solution over y y∈[c, d]

    1/2 * ℓ * (2 * ℓ * (sub_3b(; a = a, c = c, d = d, ℓ = ℓ) - sub_3b(; a = b, c = c, d = d, ℓ = ℓ)) -
                    sqrt(2*π) * sub_3a(; a = a, c = c, d = d, ℓ = ℓ) +
                    sqrt(2*π) * sub_3a(; a = b, c = c, d = d, ℓ = ℓ))

end

function verify_3(; lowerx = lowerx, upperx = upperx, lowery = lowery, uppery = uppery, ℓ = ℓ)

    # integrand
    f(x,y) =  y * x * exp(- (x-y)^2 / (2ℓ^2) )

    # numerical verification on regular grid
    Δx = 1e-3
    X  = lowerx:Δx:upperx
    Y  = lowery:Δx:uppery

    num = 0.0
    for i in 1:length(X)
        for j in 1:length(Y)
            @inbounds num += f(X[i], Y[j])
        end
    end
    num *= Δx^2

    # numerical verification via Cuba.jl libary
    h = divonne((x, out) -> out[1] = (uppery-lowery)*(upperx-lowerx) * f(lowerx + (upperx-lowerx)*x[1], lowery + (uppery-lowery)*x[2]), 2, 1)

    display(h)

    # return exact and numerical results to verify
    integralfunction_3(lowerx = lowerx, upperx = upperx, lowery = lowery, uppery = uppery, ℓ = ℓ), num, h.integral[1]

end


using LinearAlgebra: norm, ⋅, ×
using StaticArrays: SVector

export
    sec2rad, rad2sec, normalize_angle, angle,
    dms2rad, rad2dms, sec2deg, deg2sec

"""
    sec2rad(sec)

Convert an angle in arcseconds to radians.

# Example

```jldoctest
julia> sec2rad(3600 * 30)
0.5235987755982988
```
"""
sec2rad(sec) = deg2rad(sec / 3600)

"""
    rad2sec(rad)
Convert an angle in radians to arcseconds.

# Example

```jldoctest
julia> rad2sec(0.5235987755982988)
107999.99999999999
```
"""
rad2sec(rad) = rad2deg(rad) * 3600

sec2deg(sec) = sec / 3600
deg2sec(deg) = deg * 3600

function normalize_angle(angle, center)
    angle - 2π * floor((angle + π - center) / 2π)
end

function dms2rad(deg, arcmin, arcsec)
    deg2rad(deg + arcmin/60 + arcsec/3600)
end

function rad2dms(rad)
    d = rad2deg(rad)
    deg = trunc(d)
    arcmin = trunc((d-deg)*60)
    arcsec = (d-deg-arcmin/60)*3600
    return deg, arcmin, arcsec
end

function angle(v1, v2)
    normprod = norm(v1) * norm(v2)
    if normprod == 0.0
        throw(DomainError())
    else
        v1v2 = v1 ⋅ v2
        threshold = normprod * 0.9999
        if v1v2 >= -threshold && v1v2 <= threshold
            return acos(v1v2 / normprod)
        else
            v3n = norm(v1 × v2)
            return v1v2 >= 0.0 ? (v3n / normprod) : π - asin(v3n / normprod)
        end
    end
end

function azimuth(v)
    atan(v[2], v[1])
end

function elevation(v)
    asin(v[3] / norm(v))
end

function vector_azel(az, el)
    saz, caz = sincos(az)
    sel, cel = sincos(el)
    SVector(caz * cel, saz * cel, sel)
end

function angular_velocity(ψ, δψ, θ, δθ, ϕ, δϕ)
    Ω₁ = δψ * sin(θ) * sin(ϕ) - δθ * cos(ϕ)
    Ω₂ = δψ * sin(θ) * cos(ϕ) + δθ * sin(ϕ)
    Ω₃ = δψ * cos(θ) + δϕ
    [Ω₁, Ω₂, Ω₃]
end