##################################################
## Classical derivative-based, iterative, root-finding algorithms: Newton and Halley
## Historic, we have derivative free versions of similar order

## Newton
"""

    Roots.Newton()

Implements Newton's [method](http://tinyurl.com/b4d7vls): `x_n1 = xn -
f(xn)/f'(xn)`.  This is a quadratically converging method requiring
one derivative. Two function calls per step.
"""
struct Newton <: AbstractUnivariateZeroMethod
end

function update_state(method::Newton, fs, o, options)
    xn = o.xn1
    fxn = o.fxn1
    fpxn = fs(xn,1)

    if isissue(fpxn)
        o.stopped=true
        return
    end

    xn1 = xn - fxn / fpxn
    fxn1 = fs(xn1)
    incfn(o)

    o.xn0, o.xn1 = xn, xn1
    o.fxn0, o.fxn1 = fxn, fxn1

end

"""
    newton(f, fp, x0; kwargs...)

Implementation of Newton's method: `x_n1 = x_n - f(x_n)/ f'(x_n)`

Arguments:

* `f::Function` -- function to find zero of

* `fp::Function` -- the derivative of `f`.

* `x0::Number` -- initial guess. For Newton's method this may be complex.

With the `FowardDiff` package derivatives may be computed automatically. For example,  defining
`D(f) = x -> ForwardDiff.derivative(f, float(x))` allows `D(f)` to be used for the first derivative.

Keyword arguments are passed to `find_zero` using the `Roots.Newton()` method.

"""
newton(f, fp, x0; kwargs...) = find_zero((f, fp), x0, Newton(); kwargs...)
@deprecate newton(f, x0; kwargs...)  newton(f, fp, x0; kwargs...)

## Halley

"""
    Roots.Halley()

Implements Halley's [method](http://tinyurl.com/yd83eytb),
`x_n1 = xn - (2 f(xn)*f'(xn)) / (2 f'(xn)^2 - f(xn) * f''(xn))`.
This method is cubically converging, but requires more function calls per step (3) than
other methods.
"""
struct Halley <: AbstractUnivariateZeroMethod
end

function update_state(method::Halley, fs, o::UnivariateZeroState{T,S}, options::UnivariateZeroOptions) where {T,S}
    xn = o.xn1
    fxn = o.fxn1
    fpxn = fs(xn,1); incfn(o)
    fppxn = fs(xn,2); incfn(o)

    xn1 = xn - 2fxn*fpxn / (2*fpxn*fpxn - fxn * fppxn)
    fxn1 = fs(xn1); incfn(o)

    o.xn0, o.xn1 = xn, xn1
    o.fxn0, o.fxn1 = fxn, fxn1
end

"""
    halley(f, fp, fpp, x0; kwargs...)

Implementation of Halley's method. `xn1 = xn - 2f(xn)*f'(xn) / (2*f'(xn)^2 - f(xn) * f''(xn))`

Arguments:

* `f::Function` -- function to find zero of

* `fp::Function` -- derivative of `f`.

* `fpp:Function` -- second derivative of `f`.

* `x0::Number` -- initial guess

With the `FowardDiff` package derivatives may be computed automatically. For example,  defining
`D(f) = x -> ForwardDiff.derivative(f, float(x))` allows `D(f)` and `D(D(f))` to be used for the first and second
derivatives, respectively.

Keyword arguments are passed to `find_zero` using the `Roots.Halley()` method.

"""
halley(f, fp, fpp, x0; kwargs...) = find_zero((f, fp, fpp), x0, Halley(); kwargs...)
#halley(f,  x0; kwargs...) = find_zero(f, x0, Halley(); kwargs...) # deprecated
#halley(f, fp, x0; kwargs...) = find_zero((f, fp), x0, Halley(); kwargs...) # deprecated
@deprecate halley(f,  x0; kwargs...)    halley(f, fp, fpp, x0; kwargs...)
@deprecate halley(f, fp, x0; kwargs...) halley(f, fp, fpp, x0; kwargs...)


function plot_country_posterior(chains, grouped_dat, country; tspan = [0.0, 365.0], subsample_size=100)

    country_pop = grouped_dat[country].pop[1]
    country_name = grouped_dat[country].Country_Region[1]

    post_dat = DataFrame(chains);

    init = [(country_pop - 100.0) / country_pop, 100.0 / country_pop, 0.0]
    country_param_cols = [Symbol("β[" * string(country) * "]"), Symbol("γ[" * string(country) * "]")]

    posterior_mean_params = mean.(eachcol(post_dat[:, country_param_cols]))
    posterior_mean_prob = ODEProblem(sir_ode, init, tspan, posterior_mean_params)
    posterior_mean_sol = solve(posterior_mean_prob);

    # plt = plot(posterior_mean_sol;
    #     title = country_name * "\n",
    #     title_location = :left,
    #     top_margin = 5mm,
    #     vars = [1],
    #     xguidefontsize = 10, yguidefontsize = 10,
    #     xlabel = "Days since 100 confirmed cases",
    #     ylabel = "Proportion of population",
    #     label = "S",
    #     color = "blue",
    #     linewidth = 2,
    #     legend = :outerright)
    plt = plot(posterior_mean_sol;
        title = country_name * "\n",
        title_location = :left,
        top_margin = 5mm,
        vars = [2],
        xguidefontsize = 10, yguidefontsize = 10,
        xlabel = "Days since 100 confirmed cases",
        ylabel = "Proportion of population",
        label = "I",
        color = "red",
        linewidth = 2,
        legend = :outerright)
    # plot!(posterior_mean_sol; label="I", vars = [2], color = "red", linewidth = 2)
    plot!(posterior_mean_sol; label="R", vars = [3], color = "green", linewidth = 2)

    # scatter!(grouped_dat[country].susc_prop, color = "blue", label = "Obs. S", markerstrokewidth = 0)
    scatter!(grouped_dat[country].case_prop, color = "red", label = "Obs. I", markerstrokewidth = 0)
    scatter!(grouped_dat[country].recov_prop, color = "green", label = "Obs. R", markerstrokewidth = 0)

    # subsample to avoid a crowded plot
    sub_idx = sample(axes(post_dat, 1), subsample_size; replace = false);
    for i in sub_idx
        prob = ODEProblem(sir_ode, init, tspan, post_dat[i, country_param_cols])
        sol = solve(prob);
        # plot!(sol; vars = [1], label="", color = "blue", alpha = 0.1, xlabel = "Days since 100 confirmed cases")
        plot!(sol; vars = [2], label="", color = "red", alpha = 0.1, xlabel = "Days since 100 confirmed cases")
        plot!(sol; vars = [3], label="", color = "green", alpha = 0.1, xlabel = "Days since 100 confirmed cases")
    end

    return plt
end


# The normal fit api in Distributions.jl is:
#   fit(Distr, values)
# so we want to do a more general
#   fitall(values)
# that will try all distributions that can be fitted and return back the "best" one
# for relevant criteria of "best", typically BIC or AIC.

using Distributions

AllContinuousDistributions = [
    Arcsine, Beta, BetaPrime, Cauchy, Chi, Chisq, Erlang, Exponential,
    FDist, Frechet, Gamma, GeneralizedExtremeValue, GeneralizedPareto,
    Gumbel, InverseGamma, InverseGaussian, Laplace, Levy, LogNormal,
    Logistic, Normal, NormalInverseGaussian, Pareto, Rayleigh,
    SymTriangularDist, TDist, TriangularDist, Uniform, VonMises, Weibull
]

samples1 = rand(Normal(3.4, 1.2), 100)

ContinuousDistributionsThatCanFit = filter(AllContinuousDistributions) do D
    try
        fit_mle(D, samples1)
        true
    catch _err
        false
    end
end

# Start for continuous distributions
function fitalldistr{T<:Real}(x::AbstractArray{T}, sortby = :BIC)
    n = length(x)
    allfits = map(ContinuousDistributionsThatCanFit) do D
        d = fit_mle(D, x)
        loglik = loglikelihood(d, x)
        k = length(params(d))
        if sortby == :BIC
            criteria = -2 * loglik + k * log(n)
        elseif sortby == :AIC
            criteria = -2 * loglik + 2 * k
        elseif sortby == :AICc
            criteria = -2 * loglik + 2 * k + ((2*k*(k+1))/(n-k-1))
        elseif sortby == :NLogL
            criteria = -loglik
        end
        (d, criteria)
    end
    sort(allfits, by = t -> t[2])
end

function fitall{T<:Real}(x::AbstractArray{T}, sortby = :BIC)
    fitalldistr(x, sortby)[1][1]
end

samples1 = rand(Normal(3.4, 1.2), 100)
d1 = fitall(samples1)

samples2 = rand(Exponential(0.3), 100)
d2 = fitall(samples2)

samples3 = rand(LogNormal(-5.0, 2.0), 100)
d3s = fitalldistr(samples3)
d3 = fitall(samples3)

samples4 = rand(Gamma(6.20, 1.3), 200)
d4s = fitalldistr(samples4)
d4 = fitall(samples4)

# Should we maybe use a non-parametric test to select between them?


function mirror(A,d::Int=3)
    w = div(d-1,2) #width of window, d is an ODD integer
    mA = [A[(w+1):-1:2];A;A[lastindex(A)-1:-1:(length(A)-w)]]
end

function find_extrema(A::Array,d::Int=3)
    mA = mirror(A)
    count = 0
    maxs = Int[]
    mins = Int[]
    exts = Int[]
    for i in 1:lastindex(mA)-d+1
        win = view(mA,i:i+d-1)
        if (win[2] > win[1] && win[2] > win[3])
                count+=1
                push!(maxs,i)
                push!(exts,i)
        elseif (win[2] < win[1] && win[2] < win[3])
                count+=1
                push!(mins,i)
                push!(exts,i)
        end
    end
    maxs, mins,exts
end

function find_extrema_count(A::Array,d::Int=3)
    dd = diff(sign.(diff(A)))
    return length(dd[dd .!= 0]) + 2
end

function find_extrema_minmax(A::Array,d::Int=3)
    dd = diff(sign.(diff(A)))
    return findall(dd .< 0), findall(dd .> 0)
end

function stream_minmax(env1, env2, A, d::Int)
    a = mirror(A, d)
    N = length(a)
    upper = UInt32[] #buffer for indices
    lower = UInt32[]
    push!(upper,1)
    push!(lower,1)
    w = d
    for i in 2:lastindex(a)
        if i >= w
            env1[i-w+1] = a[upper[1]]
            env2[i-w+1] = a[lower[1]]
        end

        if a[i] > a[i-1]
            pop!(upper)
            # remove maxima from buffer that are less than our new one
            while isempty(upper) != true
                if a[i] <= a[upper[end]]
                    break
                end
                pop!(upper)
            end
        else
            pop!(lower)
            # remove minima from buffer that are greater than our new one
            while isempty(lower) != true
                if a[i] >= a[lower[end]]
                    break
                end
                pop!(lower)
            end
        end
        push!(upper,i)
        push!(lower,i)
        if i == w + upper[1]
            popfirst!(upper)
        elseif i == w + lower[1]
            popfirst!(lower)
        end
        env1[end-w] = a[upper[1]]
        env2[end-w] = a[lower[1]]
    end
    return nothing
end

function moving_average(A, d::Int=3)
    A = mirror(A,d)
    T = typeof(one(eltype(A))/1)
    ret = Vector{T}(undef, length(A) - d + 1)
    id = 1 / d
    s = sum(view(A, 1:d))
    ret[1] = s * id
    @inbounds for n = 1:length(ret)-1
        s += A[n+d] - A[n]
        ret[n+1] = s * id
    end
    return ret
end


function ^(a::STaylor1{N,T}, n::Integer) where {N,T<:Real}
    n == 0 && return one(a)
    n == 1 && return a
    n == 2 && return square(a)
    n < 0 && return a^float(n)
    return power_by_squaring(a, n)
end

^(a::STaylor1{N,T}, b::STaylor1{N,T}) where {N,T<:Number} = exp(b*log(a))

function power_by_squaring(x::STaylor1{N,T}, p::Integer) where {N,T<:Number}
    p == 1 && return x
    p == 0 && return one(x)
    p == 2 && return square(x)
    t = trailing_zeros(p) + 1
    p >>= t

    while (t -= 1) > 0
        x = square(x)
    end

    y = x
    while p > 0
        t = trailing_zeros(p) + 1
        p >>= t
        while (t -= 1) ≥ 0
            x = square(x)
        end
        y *= x
    end

    return y
end

@generated function ^(a::STaylor1{N,T}, r::S) where {N, T<:Number, S<:Real}

    ex_calc = quote end
    append!(ex_calc.args, Any[nothing for i in 1:N])
    syms = Symbol[Symbol("c$i") for i in 1:N]
    ctuple = Expr(:tuple)
    for i = 1:N
        push!(ctuple.args, syms[i])
    end

    for i = 1:N
        push!(ex_calc.args, :($(syms[i]) = zero(T)))
    end

    expr_quote = quote
        iszero(r) && return one(a)
        r == 1 && return a
        r == 2 && return square(a)
        r == 1/2 && return sqrt(a)
        $ex_calc
    end

    c = STaylor1(zero(T), Val{N}())
    for k = 0:(N - 1)
        symk = syms[k + 1]
        temp_quote = quote
            # First non-zero coefficient
            l0 = findfirst(a)
            if l0 < 0
                $symk = zero(T)
            else
                # The first non-zero coefficient of the result; must be integer
                !isinteger(r*l0) && throw(ArgumentError(
                    """The 0th order Taylor1 coefficient must be non-zero
                    to raise the Taylor1 polynomial to a non-integer exponent."""))
                lnull = trunc(Int, r*l0)
                kprime = $k - lnull
                if (kprime < 0) || (lnull > N-1)
                    $symk = zero(T)
                else
                    # Relevant for positive integer r, to avoid round-off errors
                    if isinteger(r) && ($k > r*findlast(a))
                        $symk = zero(T)
                    else
                        if $k == lnull
                            $symk = a[l0]^r
                        else
                            # The recursion formula
                            if l0 + kprime ≤ (N - 1)
                                tup_in = $ctuple
                                $symk = r*kprime*tup_sel(lnull, tup_in)*a[l0 + kprime]
                            else
                                $symk = zero(T)
                            end
                            for i = 1:($k - lnull - 1)
                                if !((i + lnull) > (N - 1) || (l0 + kprime - i > (N - 1)))
                                    aux = r*(kprime - i) - i
                                    tup_in = $ctuple
                                    $symk += aux*tup_sel(i + lnull, tup_in)*a[l0 + kprime - i]
                                end
                            end
                            $symk /= kprime*a[l0]
                        end
                    end
                end
            end
        end
        one_tup = ntuple(i -> i == 1 ? one(T) : zero(T), Val{N}())
        expr_quote = quote
            $expr_quote
            if r == 0
                $ctuple = $one_tup
            elseif r == 1 # DO NOTHING
            elseif r == 2
                temp_st1 = square(STaylor1{N,T}($ctuple))
            elseif r == 0.5
                temp_st2 = sqrt(STaylor1{N,T}($ctuple))
            else
                $temp_quote
            end
        end
    end

    exout = :(($(syms[1]),))
    for i = 2:N
        push!(exout.args, syms[i])
    end

    return quote
               Base.@_inline_meta
               $expr_quote
               return STaylor1{N,T}($exout)
            end
end

@generated function square(a::STaylor1{N,T}) where {N, T<:Number}
    ex_calc = quote end
    append!(ex_calc.args, Any[nothing for i in 1:N])
    syms = Symbol[Symbol("c$i") for i in 1:N]

    sym = syms[1]
    ex_line = :($(syms[1]) = a[0]^2)
    ex_calc.args[1] = ex_line

    for k in 1:(N-1)
        kodd = k%2
        kend = div(k - 2 + kodd, 2)
        ex_line = :(a[0] * a[$k])
        @inbounds for i = 1:kend
            ex_line = :($ex_line + a[$i] * a[$(k-i)])
        end
        ex_line = :(2.0*($ex_line)) # float(2)* TODO: ADD BACK IN
        if kodd !== 1
            ex_line = :($ex_line +a[$(div(k,2))]^2)
        end
        ex_line = :($(syms[k+1]) = $ex_line)
        ex_calc.args[k+1] = ex_line
    end

    exout = :(($(syms[1]),))
    for i = 2:N
        push!(exout.args, syms[i])
    end
    return quote
               Base.@_inline_meta
               $ex_calc
               return STaylor1{N,T}($exout)
            end
end

@generated function inverse(a::STaylor1{N,T}) where {N,T<:Real}
    ex_calc = quote end
    append!(ex_calc.args, Any[nothing for i in 1:(2*N)])
    syms = Symbol[Symbol("c$i") for i in 1:N]

    exout = :(($(syms[1]),))
    for i = 2:N
        push!(exout.args, syms[i])
    end

    count = 1
    for n = 1:(N - 1)
        ex_calc.args[2*count - 1] = :(syms[n + 1] = zdivfpown[n - 1]/n)
        ex_calc.args[2*count] = :(zdivfpown *= zdivf)
        count += 1
    end

    return quote
               Base.@_inline_meta
               if a[0] != zero(T)
                   throw(ArgumentError(
                   """
                   Evaluation of Taylor1 series at 0 is non-zero. For high accuracy, revert
                   a Taylor1 series with first coefficient 0 and re-expand about f(0).
                   """))
               end
               z = copy(a)
               zdivf = z/a
               zdivfpown = zdivf
               S = eltype(zdivf)
               $ex_calc
               return STaylor1{N,T}($exout)
            end
end

function tup_sel(i, vargs)
    return vargs[i+1]
end

@generated function sqrt(a::STaylor1{N,T}) where {N,T<:Number}

    ex_calc = quote end
    append!(ex_calc.args, Any[nothing for i in 1:N])
    syms = Symbol[Symbol("c$i") for i in 1:N]
    ctuple = Expr(:tuple)
    for i = 1:N
        push!(ctuple.args, syms[i])
    end

    # First non-zero coefficient
    expr_quote = quote
        l0nz = findfirst(a)
        aux = zero(T)
        if l0nz < 0
            return zero(STaylor1{N,T})
        elseif l0nz%2 == 1 # l0nz must be pair
            throw(ArgumentError(
            """First non-vanishing Taylor1 coefficient must correspond
            to an **even power** in order to expand `sqrt` around 0."""))
        end

        # The last l0nz coefficients are set to zero.
        lnull = div(l0nz, 2)
    end

    for i = 1:N
        push!(ex_calc.args, :($(syms[i]) = zero(T)))
    end

    for i = 1:N
        switch_expr = :((lnull == $(i-1)) && ($(syms[i]) = sqrt(a[l0nz])))
        expr_quote = quote
            $expr_quote
            $switch_expr
        end
    end

    for k = 0:(N - 1)
        symk = syms[k + 1]
        temp_expr = quote
            if $k >= lnull + 1
                if $k == lnull
                    $symk = sqrt(a[2*lnull])
                else
                    kodd = ($k - lnull)%2
                    kend = div($k - lnull - 2 + kodd, 2)
                    imax = min(lnull + kend, N - 1)
                    imin = max(lnull + 1, $k + lnull - (N - 1))
                    if imin ≤ imax
                        tup_in = $ctuple
                        $symk = tup_sel(imin, tup_in)*tup_sel($k + lnull - imin, tup_in)
                    end
                    for i = (imin + 1):imax
                        tup_in = $ctuple
                        $symk += tup_sel(i, tup_in)*tup_sel($k + lnull - i, tup_in)
                    end
                    if $k + lnull ≤ (N - 1)
                        aux = a[$k + lnull] - 2*$symk
                    else
                        aux = -2*$symk
                    end
                    tup_in = $ctuple
                    if kodd == 0
                        aux -= tup_sel(kend + lnull + 1, tup_in)^2
                    end
                    $symk = aux/(2*tup_sel(lnull, tup_in))
                end
            end
        end
        expr_quote = quote
            $expr_quote
            $temp_expr
        end
    end

    exout = :(($(syms[1]),))
    for i = 2:N
        push!(exout.args, syms[i])
    end
    return quote
               Base.@_inline_meta
               $ex_calc
               $expr_quote
               return STaylor1{N,T}($exout)
            end
end


# This file includes IFS tools.
using LinearAlgebra
using StatsBase

export IFS, attractor, DetAlg, RandAlg, dimension, contfactor, Sierpinski, Fern, Square, Tree

export w, Transformation, randalg_sequential_for_generator, randalg_sequential_generator

"""
    $(TYPEDEF)
Affine transformation

# Fields
    $(TYPEDFIELDS)
"""
struct Transformation{T1<:AbstractMatrix{<:Real}, T2<:AbstractVector{<:Real}}
    "Transformation matrix"
    A::T1
    "Transformation vector"
    b::T2
end

(w::Transformation)(x) = w.A * x + w.b

"""
    $SIGNATURES

Returns dimension of `w`.
"""
dimension(w::Transformation) = size(w.A,1)

"""
    $SIGNATURES

Returns contraction factor of `w`. Contraction factor is computed as the norm of `w.A`.
"""
contfactor(w::Transformation) = norm(w.A)

"""
    $(TYPEDEF)

Iterated fucntion sytem (IFS)

# Fields

    $(TYPEDFIELDS)
"""
struct IFS{T1<:AbstractVector{<:Transformation}, T2<:AbstractVector{<:Real},T3}
    "Vector of transformations of IFS"
    ws::T1
    "Vector of probabilities of IFS"
    probs::T2
    "generator of the ifs"
    generator::T3
end

function IFS(ws::T1, probs::T2, args...) where {T1, T2}
    # Note: For the floating point numbers, aproximation(≈), instead of exact equal (==), should be considered
    sum(probs) ≈ 1 || throw(ArgumentError("Sum of probabilities must be 1."))
    generator = randalg_sequential_generator(ws, probs, args...)
    IFS{T1, T2, typeof(generator)}(ws, probs, generator)
end
IFS(ws,args...) = (n = length(ws); IFS(ws, 1  / n * ones(n), args...))

"""
    $SIGNATURES

Conctructs an IFS for Sierpinski triangle.
"""
Sierpinski() = IFS([
    Transformation([0.5 0.0; 0. 0.5], [0; 0]),
    Transformation([0.5 0.0; 0. 0.5], [0; 1/2.]),
    Transformation([0.5 0.0; 0. 0.5], [1/2.; 1/2.])
    ], [1/3., 1/3., 1/3.])

"""
    $SIGNATURES

Constructs and IFS for a sqaure.
"""
Square() = IFS([
    Transformation([0.5 0.0; 0. 0.5], [1.; 1.]),
    Transformation([0.5 0.0; 0. 0.5], [50.; 1.]),
    Transformation([0.5 0.0; 0. 0.5], [1.; 50.]),
    Transformation([0.5 0.0; 0. 0.5], [50.; 50.])
    ], [0.25, 0.25, 0.25, 0.25])

"""
    $SIGNATURES

Constructs and IFS for a fern.
"""
Fern() = IFS([
    Transformation([0 0; 0 0.16], [0.; 0.]),
    Transformation([0.85 0.04; -0.04 0.85],[0.; 1.6]),
    Transformation([0.2 -0.26; 0.23 0.22], [0.; 1.6]),
    Transformation([-0.15 0.28; 0.26 0.24], [0.; 0.44])
    ], [0.01, 0.85, 0.07, 0.07])

"""
    $SIGNATURES

Constructs and IFS for a fractal tree.
"""
Tree() = IFS([
    Transformation([0 0; 0 0.5], [0.; 0.]),
    Transformation([0.42 -0.42; 0.42 0.42], [0.; 0.2]),
    Transformation([0.42 0.42; -0.42 0.42], [0.; 0.2]),
    Transformation([0.1 0; 0 0.1], [0.; 0.2])
    ], [0.05, 0.40, 0.40, 0.15])

"""
    $SIGNATURES

Returns dimension of `ifs`.
"""
dimension(ifs::IFS) = dimension(ifs.ws[1])

"""
    $SIGNATURES

Returns the contraction factor of `IFS`.
"""
contfactor(ifs::IFS) = maximum(contfactor.(ifs.ws))

"""
    $TYPEDEF

A type signifying that deterministic algorithm is used when calculating the attractor of and IFS.
"""
struct DetAlg end

"""
    $TYPEDEF

A type signifying that random algorithm is used when calculating the attractor of and IFS.
"""
struct RandAlg end

"""
    $TYPEDEF

Attractor of `IFS` type

# Fields

    $TYPEDFIELDS
"""
struct Attractor{T, S, R1, R2}
    "IFS of Attractor"
    ifs::T
    "Type of algorithm to be used to compute attractor(Options are DetAlg and RandAlg"
    alg::S
    "Initial set of attractor"
    initset::R1
    "Set of the attractor"
    set::R2
    "Number of iterations"
    numiter::Int
    "Sequential or parallel"
    parallel::Bool
end

"""
    $SIGNATURES

Computes the attractor of `ifs`. If `alg` is of type `DetAlg`, the deterministic algorithm is used. If `alg` is of type
`RandAlg`, random algorithm is used. `kwargs` may include

* `numiter::Int` : Number of iterations to used to calcuate the attractor (defaults to 10)

* `numtransient::Int` : Number of transient iterations to used to calcuate a transient set. When the transient set is
  constructed, the computation of attractor is continued with distributed computation if `alg` is `RandAlg` and `parallel` is
  `true`. (defaults to 10)

* `parallel::Bool`: If  `true`, the attractor is computed using distrbuted computation. (defaults to false)

* `placedependent::Bool` : If `true`, place dependent attractor is computed if α and β is given accordingly. (default to
  false)

* `α::AbstractVector` : Place-dependent probility coefficient(defaults to nothing)

* `β::AbstractVector` : Place-dependent probility coefficient. (default to nothing)
"""
attractor(ifs, initset; alg=DetAlg(), kwargs...) = typeof(alg) == DetAlg ?
                                                   detalg(ifs,initset; kwargs...) :
                                                   randalg(ifs,initset; kwargs...)

"""
    $SIGNATURES

Computes the attractor of `ifs` with deterministic algorithm.`numiter` is number of iterations. (Defaults to 10). If
`parallel` is true, attractor is computed via parallel computation.
"""
function detalg(ifs, initset; numiter=10, parallel=false)
    copiedset = copy(initset)
    set = parallel ?
          detalg_parallel(ifs.ws, copiedset, numiter) :
          detalg_sequential(ifs.ws, copiedset, numiter)
    Attractor(ifs, DetAlg(), initset, set, numiter, parallel)
end

# Computes the attractor of an ifs via deterministic algorithm sequentially.
function detalg_sequential(ws, set, numiter)
    for i in 1 : numiter
        set = vcat(map(w -> w.(set), ws)...)
    end
    set
end

# Computes the attractor of an ifs via deterministic algorithm in parallel.
function detalg_parallel(ws, set, numiter)
    loadprocs()
    for i in 1 : numiter
        set = vcat(map(w -> pmap(w, set), ws)...)
    end
    set
end

"""
    $SIGNATURES

Computes the attractor of `ifs` with random algorithm.`numiter` is number of iterations. (Defaults to 100). `numtransient` is
the number of transient iterations. If `parallel` is true, attractor is computed via parallel computation. If
`placedependent` is true, the probabilties of the ifs are dependent on the coordinates `x`. This dependency `p(x)` is given
via the parameters `α` and `β` where p(x) = α x + β.
"""
function randalg(ifs, initset; numiter=100, numtransient=10, parallel=false, placedependent=false, α=nothing, β=nothing, allocated::Bool=false)
    ws = ifs.ws
    probs = ifs.probs
    if parallel
        if placedependent
            transient = randalg_sequential_pd(ws, copy(initset), numtransient, probs, α, β, allocated)
            set = randalg_parallel_pd(ws, transient, numiter, probs, α, β, allocated)
        else
            transient = randalg_sequential(ws, copy(initset), numtransient, probs, allocated)
            set = randalg_parallel(ws, transient, numiter, probs, allocated)
        end
    else
        if placedependent
            set = randalg_sequential_pd(ws, copy(initset), numiter, probs, α, β, allocated)
        else
            set = randalg_sequential(ws, copy(initset), numiter, probs, allocated)
        end
    end
    Attractor(ifs, RandAlg(), initset, set, numiter, parallel)
end

function _randalg_sequential(set::AbstractVector, ws, numiter, probs)
    weights = Weights(probs)
    xi = set[end]
    for i = 1 : numiter
        trfmi = sample(ws, weights)
        xi = trfmi(xi)
        push!(set, xi)
    end
    return set
end

# Computes the attractor of an ifs via random algorithm sequentially.
function _randalg_sequential(ch::AbstractChannel, xinit, ws, numiter, probs)
    weights = Weights(probs)
    #TODO: Cancel the numiter, determine the post-error, K(contraction)
    for i = 1 : numiter
        trfmi = sample(ws, weights)
        xnew = trfmi(xinit)
        put!(ch, xnew)
        xinit = xnew
    end
    ch
end

function randalg_sequential(ws, set, numiter, probs, allocated::Bool=false)
    if allocated
        _randalg_sequential(set, ws, numiter, probs)
    else
        # NOTE: The set is assumed to have just a single initial point. If the set consists of more element, then we need a fix.
        ch = Channel(0)
        task = @async _randalg_sequential(ch, only(set), ws, numiter, probs)
        # task = @async _randalg_sequential(ch, ws, probs)
        bind(ch, task)
        ch
    end
end

function _randalg_sequential_generator(ch::AbstractChannel, ws, probs, xinit=nothing; num_iter=nothing, chunk_size=10, ϵ = 1e-8)
    # Compute initial set with a single point.
    if xinit === nothing
        n = size(ws[1].b)[1]
        xinit = rand(n)
    else
        n = size(xinit)[1]
    end

    # Compute number of iterations
    σ, index = findmax(contfactor.(ws))
    if num_iter === nothing
        # Compute num_iter with respect to ϵ
        x1 = ws[index](xinit)
        _k = (log(ϵ) - log(norm(x1 - xinit))) / log(σ) + 1
        k = Int(ceil(_k))
    else
        # Assign num_iter directly
        k = num_iter
    end

    # Compute transients
    weights = Weights(probs)
    xnew = xinit
    for i = 1 : k
        trfmi = sample(ws, weights)
        xnew = trfmi(xnew)
    end

    # Compute attractor
    chunk = zeros(n, chunk_size)
    while true
        for i = 1 : chunk_size
            trfmi = sample(ws, weights)
            xnew = trfmi(xnew)
            chunk[:,i] = xnew
        end
        put!(ch, chunk)
    end
end

function randalg_sequential_generator(ws, probs, args...)
    ch = Channel(0)
    task = @async _randalg_sequential_generator(ch, ws, probs, args...)
    bind(ch, task)
    ch
end

#TODO: Cancel the numiter, determine the post-error, K(contraction), choose num_iter from consraction factor

# function estimate_contraction_factor(ws)
#     σ = zeros(lenght(ws))
#     for i, transformation in ws
#         σ[i] = norm(transformation.A, inf)
#     end
#     # TODO : Returns nothing
#     σ
# end

# Computes the attractor of an ifs via random algorithm sequentially with placedependent probabilties.
function randalg_sequential_pd(ws, set, numiter, probs, α, β, allocated::Bool=false)
    # TODO: Impletement no allocation method
    xi = set[end]
    for i = 1 : numiter
        trfmi = sample(ws, Weights(probs))
        xi = trfmi(xi)
        probs = α * xi + β
    end
    set
end

# Computes the attractor of an ifs via random algorithm in parallel.
function randalg_parallel(ws, set, numiter, probs, allocated::Bool=false)
    # TODO: Impletement no allocation method
    weights = Weights(probs)
    loadprocs()
    vcat(pmap(process_chunk, [(ws, set, floor(Int, numiter / nworkers()), weights) for i =  1 : nworkers()])...)
end

# Computes the attractor of an ifs via random algorithm in parallel with placedependent probabilties.
function randalg_parallel_pd(ws, set, numiter, probs, α, β)
    loadprocs()
    vcat(pmap(process_chunk_pd, [(ws, set, floor(Int, numiter / nworkers()), probs, α, β) for i =  1 : nworkers()])...)
end

# `process_chunk` is the worker function that is used in all processes(both in master and worker process). when calculating
# the attractor if alg is `RandAlg` and `parallel` is true.
function process_chunk(ws_set_niter_weights)
    ws, set, niter, weights = ws_set_niter_weights
    xi = set[end]
    for i = 1 : niter
        wi = sample(ws, weights)
        xi = wi(xi)
        push!(set, xi)
    end
    set
end

# `process_chunk` is the worker function that is used in all processes(both in master and worker process). when calculating
# the attractor if alg is `RandAlg` and `parallel` is true with placedependent probabilties.
function process_chunk_pd(ws_set_niter_probs_alpha_beta)
    ws, set, niter, probs, α, β = ws_set_niter_probs_alpha_beta
    xi = set[end]
    for i = 1 : niter
        wi = sample(ws, Weights(probs))
        xi = wi(xi)
        probs = α * xi + β
        push!(set, xi)
    end
    set
end

# Load worker processes and load FractalTools to those worker processes.
function loadprocs(numprocs=Base.Sys.CPU_THREADS - 1 - nprocs())
    addprocs(numprocs)
    @everywhere @eval using FractalTools
end


#========================================================================================#
#	Laboratory 11
#
# Deterministic CHAOS and the course project.
#
# Author: Niall Palfreyman, 23/03/2022
#========================================================================================#
[
	Activity(
		"""
		This is the last laboratory in this course, and it presents the assessed project for
		the course. In this laboratory, you will implement a program to demonstrate chaos in a
		gravitational system containing three bodies of equal mass in two dimensions. You will use
		Julia to analyse the execution of a complex simulation program, and adapt this program to
		use Runge-Kutta integration to (a) simulate 3-body motion and (b) evaluate its chaoticity.
		You will present your methods and results in ONE deliverable: A complete Julia module with
		associated demonstration code that presents a pedagogically pleasing description of the
		n-body problem, its solution using Runge-Kutta-2 integration and an analysis of the
		dynamics of your chosen system. This deliverable will also contain a title, information on
		authors and references, and diagrams.
		""",
		"",
		x -> true
	),
	Activity(
		"""
		In your project, you will investigate DETERMINISTIC CHAOS. Chaos is important for biology
		because it is the source of the spontaneity that we observe in living organisms. To see
		how chaos works, first define a Julia version of the Mathematica function nestlist():

			function nestlist( f::Function, x0, n::Integer)
				(n ≤ 0) ? [x0] : begin
					list = Vector{typeof(x0)}(undef, n+1)
					list[1] = x0
					for i in 1:n
						list[i+1] = f(list[i])
					end
					list
				end
			end

		Experiment with nestlist(). For example, what is the result of the following invocation?

			nestlist(x->2x,3,5)
		""",
		"",
		x -> x == [3,6,12,24,48,96]
	),
	Activity(
		"""
		nestlist() applies the function f repeatedly to the initial value x0, creating a list of
		the values that it generates in this way. You can see how this might be useful if we want
		to create a simulation of timesteps that repeat themselves over time. We'll do that now ...

		Imagine a population of wasps living on an island with renewable but limited resources.
		They can fill the island's carrying capacity (population x = 1), they can die out (x = 0),
		or else their population can have any value in the range 0 ≤ x ≤ 1. Also, they all die each
		winter, so their population takes on a new value each year: [x0,x1,x2,...,xn].

		For specific birthrate r, the wasp population's growth is governed by this logistic equation:

			x[i+1] = Λ(r)(x[i]), where
			Λ(r) is the function: (x -> r x (1-x))

		Implement this model now and experiment with it, then tell me the population two years
		after an initial population of x0 = 0.3, if r = 0.5
		""",
		"Λ(r) = (x -> r .* x .* (1 .- x))",
		x -> (0.04698 < x < 0.04699)
	),
	Activity(
		"""
		Use nestlist() to create a list of 5 simulation steps of the wasp population, starting from
		an initial value of 0.3 and using specific growth rate r = 0.5. You will see that the
		population x is tending towards a particular limiting value - what is that limit value?
		""",
		"nestlist(Λ(1),0.3,5)",
		x -> x==0
	),
	Activity(
		"""
		You should now be able to do the following: Use nestlist() to create a list of 20
		simulation steps of the wasp population with x0=0.3, r=0.9, then plot them in a graph.
		What is the limit point of this sequence?
		""",
		"lines(nestlist(Λ(0.9),0.3,20))",
		x -> x==0
	),
	Activity(
		"""
		OK, now we have the equipment, we can investigate the onset of chaos. We will slowly
		increase the value of the specific birthrate r to discover how this affects the
		developmental motion of the wasp population. Use the initial value x0=0.3 for all of the
		folloiwng excercises until I tell you otherwise.

		Just to make our language clear: a LIMIT POINT of the wasp population's motion is any
		value of the population that stays the same from one generation to the next: Λ(r)(x) == x.

		What was the limit point of the motion Λ(0.9)?
		""",
		"",
		x -> x==0
	),
	Activity(
		"""
		What is the approximate value of the limit point of the motion Λ(1.1)?
		""",
		"",
		x -> 0.07<x<0.1
	),
	Activity(
		"""
		What is the EXACT value of the limit point of the motion Λ(1.5)?
		""",
		"You can work it out for yourself by solving the equation Λ(r)(x) == x, or: r x (1-x) = x",
		x -> x == 1//3
	),
	Activity(
		"""
		Use the technique from the hint in the previous activity to calculate the limit point of
		the motion Λ(2), then check this value using your simulator:
		""",
		"",
		x -> x==0.5
	),
	Activity(
		"""
		Now investigate the motion Λ(2.1). In which direction does the population change from x[6]
		to x[7]: positive (+) or negative (-)?
		""",
		"",
		x -> (x == -)
	),
	Activity(
		"""
		This is interesting 00! Up to now, the development of the wasps has been monotone: the
		population has only either shrunk or grown. But now it grows above the value, then drops
		back down again. Let's investigate this further: find the limit point of the motion
		Λ(2.5) both by calculating and by simulating:
		""",
		"",
		x -> x==0.6
	),
	Activity(
		"""
		Notice that now we have an oscillating motion that dies away as x comes to rest at the
		limit point. Try out various values of r between 2.5 and 2.95. Does the oscillation
		still die away?
		""",
		"",
		x -> occursin("y",lowercase(x))
	),
	Activity(
		"""
		Now find the limiting motion of the population for Λ(3). Does the oscillation die away?
		""",
		"",
		x -> occursin("n",lowercase(x))
	),
	Activity(
		"""
		It now no longer makes sense to speak of a limit VALUE, since the motion oscillates
		forever around the value 2/3. Instead, we speak of a limit CYCLE: the motion Λ(3) displays
		a limit cycle around the value 2/3. "cycle" means the value oscillates forever, and
		"limit" means that it will converge to this cycle, no matter where we start the motion.
		Check this for yourself by changing the value of x0. Does the motion always converge on
		a cycle around 2/3?
		""",
		"",
		x -> occursin("y",lowercase(x))
	),
	Activity(
		"""
		How many oscillatory periods are contained within one complete limit cycle of the motion
		Λ(3)? That is, how many times does the population wobble up and down within ONE cycle? If
		you have difficulty understanding exactly what I am asking, try looking at the hint for
		this activity.
		""",
		"We call this limit cycle a \"period-1 limit-cycle\"!",
		x -> x==1
	),
	Activity(
		"""
		Let's investigate further. Look at the limit-cycle of the motion Λ(3.2) (You may want
		to extend the simulation's duration to 50). How many periods are in this limit cycle?
		""",
		"The number of periods should not (yet) have changed",
		x -> x==1
	),
	Activity(
		"""
		How many periods are in the limit-cycle of the motion Λ(3.45)? That is, how many complete
		oscillations does the population make before the motion repeats itself?
		""",
		"Count very carefully: This should be a period-2 limit-cycle!",
		x -> x==2
	),
	Activity(
		"""
		How many periods are in the limit-cycle of the motion Λ(3.55)? By now, things will move
		quite quickly and the wasp population will need a while to stabilise towards the limit-
		cycle. You may like to save time by using a line like this:

			lines(nestlist(Λ(3.55),0.5,5000)[end-40:end])
		""",
		"Look very carefully!",
		x -> x==4
	),
	Activity(
		"""
		How many periods are in the limit-cycle of the motion Λ(3.567)?
		""",
		"Count ve-ery carefully - remembering to inspect the smaller oscillations as well!",
		x -> x==8
	),
	Activity(
		"""
		How many periods are in the limit-cycle of the motion Λ(3.5695)? You will find this
		one difficult to count, but it is actually a period-16 limit-cycle. If you can't
		distinguish the oscillations, don't worry - just go on to the next activity.
		""",
		"",
		x -> x==16
	),
	Activity(
		"""
		We have seen that as r increases, period-doubling occurs, so the motion takes longer
		and longer before it repeats. Now check out the motion Λ(3.7). How long does it take
		before this motion repeats itself? Or in other words: What is the period-length of
		this limit-cycle?
		""",
		"Find out how to write \"Infinity\" in Julia :)",
		x -> x==Inf
	),
	Activity(
		"""
		We have come a long way, and what we have learned is that some naturally occurring systems
		can enter a type of motion in which they require an infinite amount of time to repeat, and
		so we call them CHAOTIC. Notice that this phenomenon has nothing to do with randomness. The
		population of Vespula Island is perfectly deterministic - we can always predict what will
		happen in the next step, but ONLY by actually simulating it! We cannot calculate in advance
		what will happen after 1000 iterations without actually performing ALL of the 1000 steps
		that lead up to it.

		Actually, the situation is even worse than this. Use the following command to inspect the
		behaviour of Λ(3.7) for the different starting conditions x0 ∈ [0.5,0.50001,4.99999]

			lines(nestlist(Λ(3.7),0.5,5000)[end-40:end])

		Are these three graphs at all similar to each other?
		""",
		"",
		x -> occursin("n",lowercase(x))
	),
	Activity(
		"""
		So you see, chaotic motion meanns that even tinsey-tiny changes in the initial conditions
		of a chaotic system lead to completely different behaviour. So even if we measured the
		current wasp population, we could never be sure that we had done it sufficiently accurately
		to be ABSOLUTELY sure that we are accurately predicting the development of the system!

		So. What has all this to do with your course project? Henri Poincaré was the first to
		notice deterministic chaos. In 1908, he showed that the gravitational motion of three
		orbiting bodies cannot be solved exactly. Weather, dripping taps and driven pendula have a
		similar problem. We cannot predict the story of their future motion without simulation,
		which as you know is never precise! As an introduction to chaos in the three-body problem,
		please view the following video-clip now, and then proceed to the next activity:

		https://www.youtube.com/watch?v=LwkvO3t1b30&t=113s
		""",
		"",
		x -> true
	),
	Activity(
		"""
		The demo() function of the module NBodies creates an instance of the type NBody to simulate
		the motion of TWO bodies in ONE spatial dimension. Run it now: do the blue and red bodies
		orbit each other? Investigate this further by increasing the resolution of the simulation
		and of the figure. Try focussing your graphic on the time-snapshots around where the two
		bodies meet, and magnify the image. Do they orbit now? What exactly is happening here?
		This is the origin of chaotic motion!

		Check that the vector x returned by the method NBodies.simulate() has three levels: the
		elements of x span the individual time snapshots of the simulation; each snapshot contains
		a list of the bodies participating in the motion; and each body contains a list of that
		body's coordinates. What do the contents of p represent?
		""",
		"",
		x -> occursin("time",lowercase(x))
	),
	Activity(
		"""
		Software development is always driven by the needs of its CLIENT program - in our case,
		this is the function NBodies.demo(). As you work with demo(), think about what kinds of
		behaviour you as a user demand of the type NBody, and include that functionality in your
		NBodies module design. Your use-cases and newly developed design will form an important
		part of your deliverables for this project.

		Notice that the two bodies (blue and red) do not orbit around each other. Why not? It may
		help your thinking to consider the following question: Is energy conserved in this system?
		""",
		"Think about the bodies' positions and momenta during the simulation",
		x -> occursin("no",lowercase(x))
	),
	Activity(
		"""
		Be aware that I have not given much attention to the naming of variables - you will need to
		comment, reformat and document the module much better than I have! Doing this will help you
		to understand the matrix programming techniques I have used, but they are not easy! You
		will need to thoroughly analyse the code and use Help and internet search to look up
		EVERYTHING you do not yet understand. Here is how to do this ...

		First remember: Matrices work very naturally in Julia, so if a=[1 2;3 5] and b=[2 3;4 5],
		then a*b and a.*b will deliver very different results. First test this idea at the Julia
		prompt, and then tell me the value of a*b-a.*b :
		""",
		"",
		x -> x == [8 7;14 9]
	),
	Activity(
		"""
		The secret of matrix programming is this: Whenever you think you need a for-loop to
		manipulate some values, build them together instead into a matrix that does the job for you.
		For example define and test an anonymous version of the factorial function that requires no
		recursion or iteration, but uses the prod() function.
		""",
		"",
		x -> occursin("->prod(1:",replace(x," "=>""))
	),
	Activity(
		"""
		Analyse the internal helper function relpos(), which calculates the relative positions
		x[i]-x[j] between all pairs of particles. Instead of looping over the particles, I compute
		the relative positions more efficiently in a matrix operation. Find where relpos() is
		called, and notice that its argument is a Vector - for example [1,2].

		At the Julia prompt, define relpos() and then call relpos([1,2]). What does the result look
		like? What does relpos() do? At the Julia prompt, analyse the calls

			locations = [1,2]
			locPerBody = repeat(locations,1,length(locations))
			permutedims(locPerBody)

		Discuss your findings with others: How, exactly, does relPos() work?

		Now tell me the result of repeat([1,2],2,3)?
		""",
		"",
		x -> x==[1 1 1;2 2 2;1 1 1;2 2 2]
	),
	Activity(
		"""
		Set a breakpoint and use the Debugger to investigate how simulate() uses relpos() to
		calculate the gravitational forces acting between the different bodies in the system.
		Which variable is used to calculate Newton's inverse-square law that falls with
		increased distance between the sources?
		""",
		"",
		x -> x=="invSq"
	),
	Activity(
		"""
		OK, now we can start on your actual project. First, extend my simulator to 2 bodies in
		2 dimensions and get the two bodies to orbit each other. This is your first major
		success - well done!

		Remember to keep your solution very neat, so that others can understand what you have
		done by reading your code.
		""",
		"Just move on when you have completed this activity",
		x -> true
	),
	Activity(
		"""
		Notice that your two bodies spiral outwards on their orbits because of my inaccurate
		simulator. Improve my integration method to achieve (practically) closed orbits.

		What is the name of the integration technique you are using?
		""",
		"",
		x -> occursin("runge-kutta",lowercase(x))
	),
	Activity(
		"""
		Now introduce a third body into your system (use the same mass for all three bodies). Try
		to create a situation in which the three bodies circle around each other without any of
		them getting thrown out of the system to infinity. Ensure that your system is conserving
		energy; that is, the three bodies taken together should neither gain nor lose energy!
		""",
		"Just move on when you have completed this activity",
		x -> true
	),
	Activity(
		"""
		You will need to find a way of testing your simulation for chaotic motion. There are
		several ways to do this - some are easy, some more difficult; some are approximate, some
		are very accurate. Here is a paper containing an accurate method of detecting chaos:

		https://www.nature.com/articles/s42003-019-0715-9.pdf

		Alternatively, in chaotic dynamics, small differences in initial conditions grow
		exponentially over time in the short term. The easiest way to test for exponential growth
		is by plotting a logarithm graph over time to see if it is linear. Decide how you want to
		measure chaoticity in your system.
		""",
		"",
		x -> true
	),
	Activity(
		"""
		Your project deliverable is a Julia module that must fulfil the following constraints:
			- the demonstration program MUST execute correctly within its own folder;
			- simulates the motion of three or more gravitational bodies;
			- executes an aesthetically pleasing chaotic trajectory;
			- demonstrates that the motion conserves energy;
			- demonstrates that the motion is indeed chaotic;
			- displays explanatory text so that anyone can understand it WITHOUT studying the code!
			- the code must be neat, well documented and easily readable and comprehensible.
		""",
		"Good luck!!!",
		x -> true
	),
]


"""
function logmean(aL,aR)
"""

function logmean(aL,aR)
    return logmean(aL,aR,log(aL),log(aR))
end

"""
function logmean(aL,aR,logL,logR)
    Compute logarithmic mean using pre-computed log values
"""

function logmean(aL,aR,logL,logR)

    "from: Entropy stable num. approx. for the isothermal and polytropic Euler"
    # assumes the specific value γ = 1.4
    da = aR-aL;
    aavg = .5*(aR+aL);
    f = da/aavg;
    v = f^2;
    if abs(f)<1e-4
        return aavg*(1 + v*(-.2-v*(.0512 - v*0.026038857142857)))
    else
        return -da/(logL-logR)
    end
end


using Plots
using ForwardDiff
using LinearAlgebra

a = 1.0
b = 100.0

f(x) = (a - x[1])^2.0 + b * (x[2] - x[1]^2.0)^2.0
f(x, y) = f([x; y])
∇f(x) = ForwardDiff.gradient(f, x)
∇²f(x) = ForwardDiff.hessian(f, x)

n = 1000
x_limits = range(-1.0, stop = 1.0, length = n)
y_limits = range(-1.0, stop = 1.0, length = n)

x = zeros(2)

function solve(x;
        max_iter = 100,
        visualize = true)
    visualize && (plt = Plots.contour(x_limits, y_limits, f))
    visualize && (plt = plot!((x[1], x[2]), markershape = :circle, color = :cyan, outline = :cyan))

    for i = 1:max_iter
        grad = ∇f(x)
        println("x:  $x")
        println("grad norm: $(norm(grad))")
        norm(grad) < 1.0e-3 && break

        hess = ∇²f(x)
        # Δ = hess \ grad
        Δ = grad

        α = 1.0
        iter = 1
        while norm(∇f(x - α * Δ)) > norm(grad) && iter < 100
            α *= 0.5
            iter += 1
        end

        x .-= α * Δ

        if i % 10 == 0 && visualize
            plt = plot!((x[1], x[2]),
                markershape = :circle, color = :cyan, outline = :cyan,
                legend = false)
            display(plt)
        end
    end
    print("iters: $i")
    return x
end

solve([0.0, -1.0], max_iter = 1000)


#normalization factor
abstract type YLMNorm{T} end

struct Schmidt{T} <: YLMNorm{T}; end
struct Laplace{T} <: YLMNorm{T}; end
struct Nonorm{T} <: YLMNorm{T}; end
struct Full{T} <: YLMNorm{T}; end

function ylmcoeff(N::Schmidt{T},l::Int64, m::Int64) where T

	k = one(T)
	for i in (l-m+1):(l+m)
	  k *= i
	end
	return sqrt(1/k)
end

function ylmcoeff(N::Laplace{T}, l::Int64, m::Int64) where T

  k = one(T)
  for i in (l-m+1):(l+m)
    k *= i
  end

  return sqrt((2*l+1) / (4*T(pi)*k))
end

function ylmcoeff(N::Full{T}, l::Int64, m::Int64) where T
	fac = m==0 ? one(T) : sqrt(2*one(T))
	return ylmcoeff(Laplace{T}(),l,m)*sqrt(4T(pi))*fac
end

function ylmcoeff(N::Nonorm{T}, l::Int64, m::Int64) where T
 return one(T)
end

function cossinpoly(m::Int64, x::Variable, y::Variable)

  sum = zero(x*y)
  for j in 0:div(m,2)
	  k = 2j
    sum += ((-1)^j)*binomial(m, k)*y^k*x^(m-k)
  end
  return sum
end

function sinsinpoly(m::Int64, x::Variable, y::Variable)

  sum = zero(x*y)
  for j in 0:div((m-1),2)
	  k = 2j+1
    sum += ((-1)^j)*binomial(m, k)*y^k*x^(m-k)
  end
  return sum
end

# #legendre polynomial:
# P(l::BigInt,x) = differentiate((x^2 - 1)^l,x,l)
#
# #associated legendre polynomial (without sin(θ)^m)
# P(l::BigInt,m::BigInt,x) = 1//(big(2)^l*factorial(l))*differentiate(P(l,x),x,m)

"""
    ylm(l::Int64, m::Int64, x::Variable, y::Variable, z::Variable)
*Description:*  Calculation of the spherical harmonic for a given order (l,m) in Cartesian coordinates\\

*Input:*  `l`       - Degree of the spherical harmonic\\
          `m`       - Order of the spherical harmonic\\
          `x, y, z` - Cartesian coordinates\\

*Output:*  Spherical harmonic polynomial
"""
function ylm(l::Int64, m::Int64, x::Variable, y::Variable, z::Variable;
			 norm::YLMNorm{T}=Laplace{Float64}(),real=true) where T

	if abs(m) > l
	throw(DomainError(m,"-l <= m <= l expected, but m = $m and l = $l."))
	end

	p = (z^2 - 1)^l

	for i = 1:l+abs(m)
	  c = i <= l ? 1/(2one(T)*i) : one(T)
	  p = c*differentiate(p, z)
	end

	p *= (-1)^m

	if real
		if m > 0
			out = ylmcoeff(norm, l, m)*cossinpoly(m,x,y)*p
			if norm != Nonorm{T}()
				out *= sqrt(2one(T))
			end
			return out
		elseif m < 0
			out = ylmcoeff(norm, l, abs(m))*sinsinpoly(abs(m),x,y)*p
			if norm != Nonorm{T}()
				out *= sqrt(2one(T))
			end
			return out
		else
			return ylmcoeff(norm, l, 0)*p
		end
	else
		return ylmcoeff(norm, l, m)*(cossinpoly(m,x,y)+sign(m)*im*sinsinpoly(m,x,y))*p
	end
end

# multiplying r^l*ylm(x,y,z)
function rlylm(l::Int64, m::Int, x::Variable, y::Variable, z::Variable;
			   norm::YLMNorm{T}=Laplace{Float64}(),real=true) where T
	p = ylm(l,m,x,y,z;norm=norm,real=real)
	tout = []
	# Zerlegung des Polynoms in Terme:
	for t in terms(p)
		deg = degree(monomial(t)) # Gibt den gesamten Grad des Monoms an
		degR = l-deg # durch das Kürzen ergibt sich ein Grad von l-deg fuer r
		push!(tout,(x^2+y^2+z^2)^div(degR,2)*t) # r² wird durch x²+y²+z² ersetzt
	end

	return polynomial(tout)
end

# solid harmonics
function rlm(l::Int64, m::Int64, x::Variable, y::Variable, z::Variable;
			 norm::YLMNorm{T}=Laplace{Float64}(),real=true) where T
	rlm = rlylm(l,m,x,y,z; norm=norm,real=real)
	if norm != Nonorm{T}()
		rlm = sqrt(4one(T)*T(pi)/(2*l+1))*rlm
	end
	return rlm
end


#  Copyright 2017, Oscar Dowson

export binexpand, bincontract, bitsrequired

# Cache these for speed
const log2inv = 1 / log(2)
# dereferencing an array is faster than a 2^x call
const _2i_ = Int[2^(i-1) for i in 1:floor(Int, log(typemax(Int)) * log2inv)+1]
const _2i_L = length(_2i_)

function binexpand!(y::Vector{Int}, x::Int)
    if x < 0
        error("Values to be expanded must be nonnegative. Currently x = $x.")
    end
    @inbounds for i in length(y):-1:1
        k = _2i_[i]
        if x >= k
            y[i] = 1
            x -= k
        end
    end
    if x > 0
        error("Unable to expand binary. Overflow of $x.")
    end
end

function bitsrequired(x::Int)
    floor(Int, log(x) * log2inv) + 1
end
function bitsrequired(x::Float64, eps::Float64=0.1)
    xx = round(Int, x / eps)
    floor(Int, log(xx) * log2inv) + 1
end

"""
    binexpand(x::Int; length::Int=-1, maximum::Real=-1)

Returns an array of 0/1 coefficients for the binary expansion of `x`.
If trailing zeroes are needed, use `length` to specify the length of the output.
"""
function binexpand(x::Int; length::Int=-1, maximum::Real=-1)
    x < 0 && error("Cannot perform binary expansion on a negative number.")
    if maximum != -1
        if length != -1
            warn("Length is being ignored.")
        end
        length = bitsrequired(floor(Int, maximum))
    end
    if length == -1
        y = zeros(Int, bitsrequired(x))
    else
        y = zeros(Int, length)
    end
    binexpand!(y, x)
    y
end

"""
    binexpand(x::Float64, eps::Float64=0.1; length::Int=-1, maximum::Real=-1)

Returns an array of 0/1 coefficients for the binary expansion of `x`.
If trailing zeroes are needed, use `length` to specify the length of the output.
"""
function binexpand(x::Float64, eps::Float64=0.1; length::Int=-1, maximum::Real=-1)
    x < 0 && error("Cannot perform binary expansion on a negative number.")
    if eps <= 0.0
        error("Epsilon tolerance for Float binary expansion must be strictly greater than 0.")
    end
    xx = round(Int, x / eps)
    if maximum != -1
        if length != -1
            warn("Length is being ignored.")
        end
        length = bitsrequired(floor(Int, maximum / eps))
    end
    binexpand(xx, length=length)
end

function bincontract_2i_(y::Vector{T}) where T
    x = zero(T)
    @inbounds for i in 1:length(y)
        x += _2i_[i] * y[i]
    end
    x
end
function bincontract_pow(y::Vector{T}) where T
    x = zero(T)
    @inbounds for i in 1:length(y)
        x += 2^(i-1) * y[i]
    end
    x
end

"""
    bincontract{T}(y::Vector{T})

For vector `y`, evaluates ∑ᵢ 2ⁱ⁻¹yᵢ.
"""
function bincontract(y::Vector{T}) where T
    if length(y) < _2i_L
        bincontract_2i_(y)
    else
        bincontract_pow(y)
    end
end

function bincontract(::Type{Float64}, y::Vector{T}, eps::Float64=0.1) where T
    if eps <= 0
        error("Epsilon tolerance for Float binary contraction must be strictly greater than 0.")
    end
    xx = bincontract(y)
    xx * eps
end


# -*- coding: utf-8 -*-
# ---
# jupyter:
#   jupytext:
#     formats: ipynb,jl:hydrogen
#     text_representation:
#       extension: .jl
#       format_name: hydrogen
#       format_version: '1.3'
#       jupytext_version: 1.10.3
#   kernelspec:
#     display_name: Julia 1.7.0
#     language: julia
#     name: julia-1.7
# ---

# %%
using Distributions

tstat(μ, X) = (mean(X) - μ)/√(var(X) / length(X))
pvalue(μ, X) = 2ccdf(TDist(length(X) - 1), abs(tstat(μ, X)))

function ci(X, α)
    X̄ = mean(X)
    c = quantile(TDist(length(X) - 1), 1 - α/2)
    d = √(var(X) / length(X))
    X̄ - c*d, X̄ + c*d
end

isininterval(x, int) = first(int) ≤ x ≤ last(int)
isinci(μ, X, α) = isininterval(μ, ci(X, α))

prob_trueisinci(dist, n, α; L=10^6) = mean(isinci(mean(dist), rand(dist, n), α) for _ in 1:L)

# %%
prob_trueisinci(Normal(), 20, 0.05)

# %%
prob_trueisinci(Uniform(), 20, 0.05)

# %%
prob_trueisinci(Exponential(), 20, 0.05)

# %%
prob_trueisinci(MixtureModel([Normal(), Normal(10, 1)], [0.95, 0.05]), 20, 0.05)

# %%
using Plots

# %%
normal = Normal(mean(Uniform()), std(Uniform()))
P = plot(x -> pdf(Uniform(), x), -1, 2; label="Uniform()")
plot!(x -> pdf(normal, x), -1, 2; label="normal approx.", ls=:dash)
title!("pdfs")
Q = plot(x -> cdf(Uniform(), x), -1, 2; label="Uniform()")
plot!(x -> cdf(normal, x), -1, 2; label="normal approx.", ls=:dash)
title!("cdfs"; legend=:bottomright)
plot(P, Q; size=(720, 300))

# %%
normal = Normal(mean(Exponential()), std(Exponential()))
P = plot(x -> pdf(Exponential(), x), -3, 6; label="Exponential()")
plot!(x -> pdf(normal, x), -3, 6; label="normal approx.", ls=:dash)
title!("pdfs")
Q = plot(x -> cdf(Exponential(), x), -3, 6; label="Exponential()")
plot!(x -> cdf(normal, x), -3, 6; label="normal approx.", ls=:dash)
title!("cdfs"; legend=:bottomright)
plot(P, Q; size=(720, 300))

# %%
mixnormal = MixtureModel([Normal(), Normal(10, 1)], [0.95, 0.05])
P = plot(x -> pdf(mixnormal, x), -5, 15; label="mixnormal")
plot!(x -> pdf(Normal(), x), -5, 15; label="Normal()", ls=:dash)
title!("pdfs")
Q = plot(x -> cdf(mixnormal, x), -5, 15; label="mixnormal")
plot!(x -> cdf(Normal(), x), -5, 15; label="Normal()", ls=:dash)
title!("cdfs"; legend=:bottomright)
plot(P, Q; size=(720, 300))

# %%
mixnormal = MixtureModel([Normal(), Normal(10, 1)], [0.95, 0.05])
normal = Normal(mean(mixnormal), std(mixnormal))
P = plot(x -> pdf(mixnormal, x), -5, 15; label="mixnormal")
plot!(x -> pdf(normal, x), -5, 15; label="normal approx.", ls=:dash)
title!("pdfs")
Q = plot(x -> cdf(mixnormal, x), -5, 15; label="mixnormal")
plot!(x -> cdf(normal, x), -5, 15; label="normal approx.", ls=:dash)
title!("cdfs"; legend=:bottomright)
plot(P, Q; size=(720, 300))

# %%
model = Normal()
@show model
μ, σ² = mean(model), var(model)
PP = []
for n in (5, 10, 20, 100)
    Z = [(mean(rand(model, n)) - μ)/√(σ²/n) for _ in 1:10^5]
    P = histogram(Z; norm=true, alpha=0.3, bin = -3.5:0.1:6, label="")
    plot!(x -> pdf(Normal(), x); label="", ls=:dash, lw=1.5)
    title!("sample size n = $n")
    push!(PP, P)
end
plot(PP...; size=(800, 600), layout=(2, 2), titlefontsize=11)

# %%
model = Uniform()
@show model
μ, σ² = mean(model), var(model)
PP = []
for n in (5, 10, 20, 100)
    Z = [(mean(rand(model, n)) - μ)/√(σ²/n) for _ in 1:10^5]
    P = histogram(Z; norm=true, alpha=0.3, bin = -3.5:0.1:6, label="")
    plot!(x -> pdf(Normal(), x); label="", ls=:dash, lw=1.5)
    title!("sample size n = $n")
    push!(PP, P)
end
plot(PP...; size=(800, 600), layout=(2, 2), titlefontsize=11)

# %%
model = Exponential()
@show model
μ, σ² = mean(model), var(model)
PP = []
for n in (5, 10, 20, 100)
    Z = [(mean(rand(model, n)) - μ)/√(σ²/n) for _ in 1:10^5]
    P = histogram(Z; norm=true, alpha=0.3, bin = -3.5:0.1:6, label="")
    plot!(x -> pdf(Normal(), x); label="", ls=:dash, lw=1.5)
    title!("sample size n = $n")
    push!(PP, P)
end
plot(PP...; size=(800, 600), layout=(2, 2), titlefontsize=11)

# %%
model = MixtureModel([Normal(), Normal(10, 1)], [0.95, 0.05])
@show model
μ, σ² = mean(model), var(model)
PP = []
for n in (5, 10, 20, 100)
    Z = [(mean(rand(model, n)) - μ)/√(σ²/n) for _ in 1:10^5]
    P = histogram(Z; norm=true, alpha=0.3, bin = -3.5:0.1:6, label="")
    plot!(x -> pdf(Normal(), x); label="", ls=:dash, lw=1.5)
    title!("sample size n = $n")
    push!(PP, P)
end
plot(PP...; size=(800, 600), layout=(2, 2), titlefontsize=11)

# %%

0012/probability of true value being in CI.jl


###############################################################
#                Combined Mean and Variance
###############################################################
"""
    combined_mean_and_var(xs...) -> meanc, varc
Calculates the combined mean and variance of the concatenated sample `vcat(xs...)`.
"""
function combined_mean_and_var(xstuple::AbstractVector{T}...) where T
    xs = collect(xstuple)
    ns = length.(xs)
    μs = mean.(xs)
    vs = var.(xs)
    return combined_mean_and_var(ns, μs, vs)
end

export combined_mean_and_var

"""
    combined_mean_and_var(ns, μs, vs) -> meanc, varc
Given N samples characterized by their lengths `ns`,
their means `μs`, and their variances `vs`,
calculates the combined (or pooled) mean and variance of the
overall (concatenated) sample.
"""
function combined_mean_and_var(ns::AbstractVector{<:Integer},
                               μs::AbstractVector{<:Number},
                               vs::AbstractVector{<:Number})
    nsum = sum(ns)
    meanc = dot(ns, μs) / nsum
    varc = sum((ns .- 1) .* vs + ns .* abs2.(μs .- meanc)) / (nsum - 1)
    return meanc, varc
end

function combined_mean_and_var(ns::AbstractVector{<:Integer},
                               μs::AbstractVector{<:AbstractArray{<:Number}},
                               vs::AbstractVector{<:AbstractArray{<:Number}})
    meanc = zero(μs[1])
    varc = zero(vs[1])

    nsum = sum(ns)
    N = length(μs) # number of samples
    for i in eachindex(meanc)

        for k in 1:N
            meanc[i] += ns[k] * μs[k][i]
        end
        meanc[i] = meanc[i] / nsum

        for k in 1:N
            varc[i] += (ns[k] - 1) * vs[k][i] + ns[k] * abs2(μs[k][i] - meanc[i])
        end
        varc[i] = varc[i] / (nsum - 1)
    end

    return meanc, varc
end

###############################################################
#       Combined Mean and Variance ("Hand-written")
###############################################################
"""
    combined_mean_and_var(x1, x2) -> meanc, varc
Given two samples `x1`,`x2` calculates the mean and variance of the
concatenated sample.
"""
function combined_mean_and_var(x1::AbstractVector{<:Number}, x2::AbstractVector{<:Number})
    n1, n2 = length(x1), length(x2)
    μ1, μ2 = mean(x1), mean(x2)
    v1, v2 = var(x1), var(x2)
    return combined_mean_and_var(n1, μ1, v1, n2, μ2, v2)
end

"""
    combined_mean_and_var(n1, μ1, v1, n2, μ2, v2) -> meanc, varc
Given two samples characterized by their lengths `n1`, `n2`,
their means `μ1`, `μ2`, and their variances `v1`, `v2`,
calculates the combined (or pooled) mean and variance of the
concatenated sample.
"""
function combined_mean_and_var(n1::Integer, μ1::Number, v1::Number,
                               n2::Integer, μ2::Number, v2::Number)
    meanc = (n1 * μ1 + n2 * μ2) / (n1 + n2)

    # Based on https://www.emathzone.com/tutorials/basic-statistics/combined-variance.html,
    # including Robert Matheson's comment and adding abs for complex number support.
    varc = ((n1-1)*v1 + (n2-1)*v2 + n1*abs2(μ1 - meanc) + n2*abs2(μ2 - meanc)) /
                                (n1 + n2 - 1)
    return meanc, varc
end

# Explicit version for three samples (only for testing)
function combined_mean_and_var_three(x1, x2, x3)
    n1, n2, n3 = length(x1), length(x2), length(x3)
    μ1, μ2, μ3 = mean(x1), mean(x2), mean(x3)
    v1, v2, v3 = var(x1), var(x2), var(x3)

    meanc12, varc12 = combined_mean_and_var(n1, μ1, v1, n2, μ2, v2)
    n12 = n1 + n2
    return combined_mean_and_var(n12, meanc12, varc12, n3, μ3, v3)
end

"""
    combined_mean_and_var(ors::Vector{<:ObservableResult}) -> meanc, stderrc
Calculates the combined mean and standard error of the concatenated observable results.
"""
function combined_mean_and_error(ors::Vector{<:ObservableResult})
    @assert length(ors) > 0 "Provided vector of ObservableResults must not be empty."

    ns = [r.count for r in ors]
    μs = [r.mean for r in ors]
    vs = [r.error^2 for r in ors]

    meanc, varc = combined_mean_and_var(ns, μs, vs)
    return meanc, sqrt.(varc)
end


# Struct to hold an x, y position. A tuple would also be fine.
struct Pos
    x::Float64
    y::Float64
end

# This returns a measure of how far "left" the vector is with a cross product
function cross(point1::Pos, point2::Pos, point3::Pos)
    vec1 = Pos(point2.x - point1.x, point2.y - point1.y)
    vec2 = Pos(point3.x - point2.x, point3.y - point2.y)
    ret_angle = vec1.x*vec2.y - vec1.y*vec2.x
    return ret_angle*ret_angle
end

function jarvis_march(points::Vector{Pos})
    hull = Vector{Pos}()

    # sorting array based on leftmost point
    sort!(points, by = item -> item.x)
    push!(hull, points[1])

    i = 1
    curr_point = points[2]

    # Find angle between points
    curr_product = cross(Pos(0,0), hull[1], curr_point)

    # We will hold a temp variable with the highest cross product as we iterate
    # through all the points and move our hull vector forward.
    while (curr_point != hull[1])
        for point in points
            product = 0.0

            # Special case for the first element when there is no hull[i-1]
            if (i == 1)
                if (hull[i] != point)
                    product = cross(Pos(0,0), hull[i], point)
                end
            else
                if (hull[i] != point && hull[i-1] != point)
                    product = cross(hull[i-1], hull[i], point)
                end
            end
            if (product > curr_product)
                curr_point = point
                curr_product = product
            end
        end

        # Pushing to hull, moving simulation forward and resetting the product
        push!(hull, curr_point)
        curr_product = 0
        i += 1
    end

    return hull
end

function main()

    # These points are chosen such that there is a clearly defined hull with
    # several interior points. As a note, these will be generated either
    # randomly or via some mesh in practice.
    points = [Pos(2,1.5), Pos(1, 1), Pos(2, 4), Pos(3, 1), Pos(2,2), Pos(2,0.5)]
    hull = jarvis_march(points)
    println(hull)
end

main()


# See https://jp.mathworks.com/help/matlab/ref/peaks.html
function peaks(N=49::Int)
    function formula(x::T, y::T) where T<:Float64
        z = 3(1-x)^2*exp(-(x^2)-(y+1)^2)-10(x/5 - x^3 - y^5)*exp(-x^2-y^2)-(1/3)exp(-(x+1)^2-y^2)
    end
    vec = collect(Float64, LinRange(-3.,3.,N))
    x = repeat(vec', outer=(N,1))
    y = repeat(vec, outer=(1,N))
    z = formula.(x,y)
    return (x,y,z)
end


# =============================================================================
# CALCULATRICE SCIENTIFIQUE
# =============================================================================

# opérateurs  classiques : +-*/\

1 + 3
3 / 7
3 \ 7 # ≈ 7 / 3
1e2
im # nombre complexe i
im^2
# Les fonctions usuelles : exp, log, log10, abs, sqrt, cbrt, sign

log10(100_000)
log(exp(2))
1 / (1 + exp(1))
# Les fonctions trigonométriques usuelles : sin, cos, tan, cot, sec, csc, sinh, cosh, tanh, coth, sech, csch, asin, acos, atan...

sin(π+√2) # Code Is Math !!  tapez "\pi<tab>" et "\sqrt<tab>2"

# =============================================================================
# VALEURS (tout ce qui est manipulable en Julia) & TYPES (Int32, Int64, Int128, Float32, Float64, Complex32, Complex64, Complex128...)
# =============================================================================

# types et collections

3 + 5 # nombre entier stockée en 32/64 bits en fonction de l'architecture machine
typeof(3 + 5)
3. * 4. # nombre en virgule flottante stockée en 32/64 bits en fonction de l'architecture machine
typeof(3. * 4.)
typeof(3. * 4)
typeof(Float64)
π # tapez "\pi<tab>"
typeof(π)
Float64(π)
Float32(π)
typeof(1 + 2im)
[1,2,3]
typeof([1,2,3])
1:5 # itérateur
typeof(1:5)
collect(1:5)
0:0.1:1
typeof(0:0.1:1)
collect(0:0.1:1)
[4. 5. 6.;7. 8. 9.]
(1, 2.0, "test") # tuple
typeof((1, 2.0, "test"))

# Caractères et chaînes de caractères

'A'
typeof('A')
"hello"

typeof("hello")

"racine carrée de 2 : $(sqrt(2))"

" √3 : $(√3)"

"Hello" * " John Doe"

# Boléens et opérateurs de comparaison | "==" | "!=" | > | ">=" | "<" | "<=" | et de conjonction | "&" | "|" | "!"|

1 > 2
typeof(1 > 2)

# supertype et sous-types

supertype(Int64)
subtypes(Integer)

# type utilisateur

struct MyComplex
    _real::Float64
    _imag::Float64
end
typeof(MyComplex)
MyComplex(1,2)
typeof(MyComplex(1, 2))

# =============================================================================
# REGLE DE NOMMAGE(variables, fonctions/méthodes, modules, types, macros)
# =============================================================================
#=
- noms des variables  en minuscule, la séparation des mots dans une variable se fait à l'aide d'un underscore ('_') ("snake case")
- noms de type et de modules commencent par une lettre majuscule, les majuscules séparant les différents mots du nom (exemple "MonModule") ("camel case")
- noms des fonctions et macros en minuscule sans underscores.
- une fonction qui modifie ses arguments s'écrit avec ! à la fin. =#

# =============================================================================
# VARIABLES (étiquettes associées aux valeurs)
# =============================================================================

# Les variables contenant des nombres scalaires

α = 6 # variable latex-unicode  names — tapez "\alpha<tab>"
typeof(α)
β = 2α + 1.5
typeof(β)
γ = α + β * im
typeof(γ)
δ = Float64(10)
δ = MyComplex(1, 2)
δ._real

# Les vecteurs (tableau de nombres scalaires ou non de dim=1)

v = [1, 17, 32, 15]
@show v
size(v)
n = length(v)
eltype(v)
v[2] # accès à l'élément d'index n°2 (indice de départ = 1)
for i in 1:n
	println("v[$i] = $(v[i])")
end
for elt in v
	println(elt)
end
v[2:3] # slicing
v' # transposée de x
v2 = Float64[1, 2, 3, 4]
v + v2
v' * v2
sin.(v) # opérateur . de broadcast ( sin de chaque terme)
v .* v2 # opérateur . de broadcast (ici multiplication terme à terme)
v3 = ones(Int64, 3)
v4 = []
typeof(v4)
push!(v4,1)     # => [1]
push!(v4,2.)     # => [1,2.]
push!(v4,"test")     # => [1,2, "test"]
append!(v4,v4)

# Les matrices (tableau de nombres scalaires ou non dim=2 )

A = [1. 2 3
     4 5 6
     7 8 10]
ni, nj = size(A)
length(A)
eltype(A)
A[2,3]
A[end,end]
A[2,:]
A[:,2]
A[2:3,2:3]
A[1:end]
for j in 1:nj
    for i in 1:ni
        println("A[$i,$j] = $(A[i,j])")
    end
end
for elt in A
	println(elt)
end
As = similar(A)
B = fill(2.0, (3, 3))
C = A + B
C = A * B
sin.(A)
AA = [A  A] # concaténation horizontale par bloc (hcat)
AAAA = [AA;AA] # concaténation verticale par bloc (vcat)
C = [ 1 2 3; 4 5 6; 7 8 9]
C = [ u * v for u = 0:0.1:1, v = 0:0.1:1]

# =============================================================================
# Programmation
# =============================================================================

for i = 1:5
    println("i = $i")
end
for i = 1:1000
    println(i)
    if i >= 5
       break
    end
end
i = 1
if i == 1
    println("i = 1")
end
i = 2
if i != 1
    println("i ≠ 1")
else
    println("i = 1")
end
i = 0
if i == 1
    println("i = 1")
elseif i > 1
    println("i > 1")
else
    println("i < 1")
end
i=0
while i<5
    global i+=1  # i =i+1
    println("i = $i")
end
i=0
while i<10
    global i+=1
    if i <= 5
    println("i = $i")
    else
        break
    end
end

# =============================================================================
# Algèbre linéaire
# Déterminant, valeurs et vecteurs propres...
# =============================================================================

using LinearAlgebra

A = [1. 2. 3.; 4. 5. 6.; 7. 8. 10.]
diag(A)
tr(A)
sum(A)
det(A)
F = eigen(A)
F.values
F.vectors
b = [3, 2, 1] # a vector
x = A \ b # résolution du système linéaire Ax=b
xx = inv(A) * b # résolution du système linéaire Ax=b
x - xx

# =============================================================================
# Fonctions, Méthodes, Compilation, Spécialisation, Multiple Dispatch
# =============================================================================

# Version longue

function fun_name(arg1, arg2)
    tmp = arg1 + arg2
    return tmp * arg1          # <- le mot-clé return est optionnel, la dernière valeur calculée est retournée
end

# Appel

fun_name(2, 3)

# Version courte

fun_name(arg1, arg2) = arg1 * (arg1 + arg2)

# Appel

fun_name(2, 3)

# Multiple Dispatch
# différentes implémentations (méthodes) de la fonction f

function f end
f(x)          = println("Generic method with 1 argument : $x")
f(x::Integer) = println("Specific method with 1 argument (Integer) : $x")
f(x, y)       = println("Generic method with 2 arguments: $x, $y")
f(x::Real, y) = println("Specific method with 2 arguments (Real, Any): $x, $y")

methods(f)

f(π)
f(1)
f(im, 2)
f(π, [1, 2, 3])

@which f(im, im)

using BenchmarkTools

function my_sum(X)
    acc = zero(eltype(X)) # zero du type de l'élément de l'argument
    for x in X
        acc += x
    end
    acc
end

V = rand(1_000_000)
@time my_sum(V) # Compilation + Exécution de la fonction  my_sum avec argument de type vecteur (spécialisation)
@time my_sum(V) # Exécution de la fonction  my_sum
@time my_sum(V)

@code_native debuginfo = :none my_sum(V)

UR = (1:1_000_000)
@time my_sum(UR) # # Compilation + Exécution de la fonction  my_sum avec argument de type UnitRange (spécialisation)
@time my_sum(UR)
@time my_sum(UR)

@code_native debuginfo = :none my_sum(UR) # code natif différent du précédent  avec même implémnetation => spécialisation

@btime my_sum(UR)
@btime Base.sum(UR) # pas de magie dans Julia !!

function my_sum(X::Array{Float64,2})
    acc = zero(eltype(X))
    ni, nj = size(X)
    for j in 1:nj
        for i in 1:ni
            acc += X[i,j]
        end
    end
    acc
end
A = rand(Float64, 1_000, 1_000)
methods(my_sum)
@time my_sum(A) # type est connu, donc pas de perte de temps pour le branchement
@time my_sum(A)
@time my_sum(A)
@btime my_sum(A)
@which my_sum(A)

@code_native debuginfo = :none my_sum(A)

# =============================================================================
# Graphique avec Plots
#  =============================================================================

using Plots
x=0:2π/100:2π
y=sin.(x)
theme(:dark)
plot(x,y,title="Mon premier graphique avec Plots")
y=cos.(x)
plot!(x,y)

# =============================================================================
# Sciences des données
# https://en.wikibooks.org/wiki/Introducing_Julia/DataFrames
# =============================================================================

# using Pkg; Pkg.add("CSV"); Pkg.add("HTTP");Pkg.add("DataFrames")
using CSV
using HTTP
using DataFrames

df = CSV.File(HTTP.get("https://raw.githubusercontent.com/nassarhuda/easy_data/master/programming_languages.csv").body) |> DataFrame
@show df

# =============================================================================
# Calcul parallèle sur GPU NVidia
# =============================================================================

using BenchmarkTools

mcpu = rand(2^12, 2^12)
@btime mcpu*mcpu

# using Pkg; Pkg.add("CUDA")
using CUDA

mgpu = cu(mcpu)
@btime  mgpu*mgpu

# =============================================================================
# Calcul symbolique
# =============================================================================

# using Pkg; Pkg.add("SymPy")
using SymPy

E, S, L, θ = symbols("E S L θ",real=true)
ke = E * S / L * Sym[1 0 -1 0;0 0 0 0;-1 0 1 0;0 0 0 0 ]
Re=Sym[cos(θ) sin(θ) 0 0; -sin(θ) cos(θ) 0 0; 0 0 cos(θ) sin(θ); 0 0 -sin(θ) cos(θ)]
Ke=Re'*ke*Re

# using Pkg; Pkg.add("Symbolics");Pkg.add("SymbolicUtils")
using Symbolics

@variables Eₑ Sₑ Lₑ θₑ
kₑ = Eₑ * Sₑ / Lₑ * [1 0 -1 0;0 0 0 0;-1 0 1 0;0 0 0 0 ]
Rₑ = [cos(θₑ) sin(θₑ) 0 0; -sin(θₑ) cos(θₑ) 0 0; 0 0 cos(θₑ) sin(θₑ); 0 0 -sin(θₑ) cos(θₑ)]
Kₑ = Rₑ' * kₑ * Rₑ


"""
alpha,beta,vk,vkm1 = LanczosStep!(A::Function,vk,vkm1,beta;sigma=0.0,tol=1e-10)

Performs a Lanczos step

Implementation is based on Table 2.1 in

Choi, S.-C. T. (2006).
Iterative Methods for Singular Linear Equations and Least-squares Problems.
Phd thesis, Stanford University.
"""
function LanczosStep!(A::Function,vk,vkm1,beta;sigma=0.0,tol=1e-10)
    n     = length(vk)

    pk    = A(vk)
    if sigma!=0.0
        BLAS.axpy!(n,-sigma,vk,1,pk,1) #pk   -= sigma*vk
    end

    alpha = BLAS.dot(n,vk,1,pk,1)  # alpha = dot(vk,pk)
    BLAS.axpy!(n,-alpha,vk,1,pk,1) # pk   -= alpha*vk
    BLAS.axpy!(n,-beta,vkm1,1,pk,1)# pk   -= beta*vkm1
    vkm1  = BLAS.blascopy!(n,vk,1,vkm1,1) # vkm1 = copy(vk)
    vk    = pk
    beta  = BLAS.nrm2(n,vk,1)
    if beta>tol
         vk /= beta
    end
    return alpha, beta,vk,vkm1
end

"""
c,s,r = SymOrtho(a,b)

Computes a Givens rotation

Implementation is based on Table 2.9 in

Choi, S.-C. T. (2006).
Iterative Methods for Singular Linear Equations and Least-squares Problems.
Phd thesis, Stanford University.
"""
function symOrtho(a,b)
    c = 0.0; s = 0.0; r = 0.0
	if b==0
		s = 0.0
		r = abs(a)
		c = (a==0) ? c=1.0 : c = sign(a)
	elseif a == 0
		c = 0.0
		s = sign(b)
		r = abs(b)
	elseif abs(b) > abs(a)
		tau = a/b
		s   = sign(b)/sqrt(1+tau^2)
		c   = s*tau
		r   = b/s
	elseif abs(a) > abs(b)
		tau = b/a
		c   = sign(a)/sqrt(1+tau^2)
		s   = c*tau
		r   = a/c
	end
	return c,s,r
end


struct PoissonSolver{DT <: Real}
    nx::Int
    Δx::DT
    xgrid::Vector{DT}
    ρ::Vector{DT}
    ϕ::Vector{DT}

    function PoissonSolver{DT}(nx::Int) where {DT}
        Δx = 1/nx
        xgrid = collect(0:Δx:1)
        new(nx, Δx, xgrid, zeros(DT, nx), zeros(DT, nx))
    end
end

function solve!(p::PoissonSolver{DT}, x::AbstractVector{DT}) where {DT}
    h = fit(Histogram, mod.(x, 1), p.xgrid)
    p.ρ .= h.weights ./ length(x)
    ρ̂ = rfft(p.ρ)
    k² = [(i-1)^2 for i in eachindex(ρ̂)]
    ϕ̂ = - ρ̂ ./ k²
    ϕ̂[1] = 0
    p.ϕ .= irfft(ϕ̂, length(p.ρ))
    return p
end

function eval_field(p::PoissonSolver{DT}, x::DT) where {DT}
    y = mod(x, one(x))
    i1 = floor(Int, y / p.Δx) + 1
    i2 = mod( ceil(Int, y / p.Δx), p.nx) + 1
    i1 == i2 && (i1 = i1-1)
    i1 == 0 && (i1 = lastindex(p.ϕ))
    return - (p.ϕ[i2] - p.ϕ[i1]) / p.Δx
end


##---begin-Plot-functions---------------------------------------------------------------------------
"""
    chromatogram(t, tR, τR)

Calculate the chromatogram as a sum of gaussian peaks over the time `t` for peaks centered at retention times `tR` and with peak width `τR`.
"""
function chromatogram(t::Array{Float64,1}, tR::Array{Float64,1}, τR::Array{Float64,1})
	g(t,tR,τR) = 1/sqrt(2*π*τR^2)*exp(-(t-tR)^2/(2*τR^2))
	chromatograms = Array{Array{Float64,1}}(undef, length(tR))
	for j=1:length(tR)
		chromatograms[j] = g.(t, tR[j], τR[j])
	end
	return sum(chromatograms)
end

"""
    plot_chromatogram(peaklist, tlims; annotation=true, number=true, mirror=false, offset=0.0)

Plot the chromatogram of the peaks listed in `peaklist` over the time tupel `tlims = (t_start, t_end`).

# Arguments
* `peaklist`: DataFrame with the names, retention times and peak widths of the simulated substances.
* `tlims`: Tuple defining the start and end time of the plotted chromatogram.
* `annotation`: Boolean, switching the annotation of the peaks on/off; default = true.
* `number`: Boolean, switching the type of the annotation between _number_ of the substance in the peaklist (`number = true`) or _name_ of the substance (`number = false`); default = true.
* `mirror`: Boolean, if `mirror = true` the chromatogram is multiplied by `-1`; default = false.
* `offset`: Float64, this value is added to the chromatogram; default = 0.0.

# Output
Tupel `(p_chrom, t, chrom)`
* `p_chrom`: the plot of the chromatogram `chrom` over time `t`
* `t`: Array of time of the chromatogram
* `chrom`: Array of the abundance values of the chromatogram
"""
function plot_chromatogram(peaklist, tlims; annotation=true, number=true, mirror=false, offset=0.0)
	t₀ = tlims[1]
	tMax = tlims[2]
	t = t₀:tMax/10000:tMax
	if mirror==true
		sgn = -1.0
	else
		sgn = 1.0
	end
	chrom = sgn.*chromatogram(collect(t), peaklist.tR, peaklist.τR) .+ offset
	chrom_tR = sgn.*chromatogram(peaklist.tR, peaklist.tR, peaklist.τR) .+ offset
	p_chrom = plot(t, chrom, xlabel="time in s", ylabel="abundance", legend=false)
	if annotation==true && number==true
		plot!(p_chrom, annotations = [(peaklist.tR[i], chrom_tR[i], text(i, 10, rotation=0, :center)) for i in 1:length(peaklist.tR)])
	elseif annotation==true && number==false
		plot!(p_chrom, annotations = [(peaklist.tR[i], chrom_tR[i], text(peaklist.Name[i], 10, rotation=90, :center)) for i in 1:length(peaklist.tR)])
	end
	return p_chrom, t, chrom
end

"""
    plot_chromatogram!(p_chrom, peaklist, tlims; annotation=true, number=true, mirror=false, offset=0.0)

Add the chromatogram of the peaks listed in `peaklist` over the time tupel `tlims = (t_start, t_end`) to the plot `p_chrom`.

# Arguments
* `p_chrom`: Plot of an existing chromatogram.
* `peaklist`: DataFrame with the names, retention times and peak widths of the simulated substances.
* `tlims`: Tuple defining the start and end time of the plotted chromatogram.
* `annotation`: Boolean, switching the annotation of the peaks on/off; default = true.
* `number`: Boolean, switching the type of the annotation between _number_ of the substance in the peaklist (`number = true`) or _name_ of the substance (`number = false`); default = true.
* `mirror`: Boolean, if `mirror = true` the chromatogram is multiplied by `-1`; default = false.
* `offset`: Float64, this value is added to the chromatogram; default = 0.0.

# Output
Tupel `(t, chrom)`
* `t`: Array of time of the chromatogram
* `chrom`: Array of the abundance values of the chromatogram
"""
function plot_chromatogram!(p_chrom, peaklist, tlims; t₀=0.0, annotation=true, number=true, mirror=false, offset=0.0)
	t₀ = tlims[1]
	tMax = tlims[2]
	t = t₀:tMax/10000:tMax
	if mirror==true
		sgn = -1.0
	else
		sgn = 1.0
	end
	chrom = sgn.*chromatogram(collect(t), peaklist.tR, peaklist.τR) .+ offset
	chrom_tR = sgn.*chromatogram(peaklist.tR, peaklist.tR, peaklist.τR) .+ offset
	plot!(p_chrom, t, chrom)
	if annotation==true && number==true
		plot!(p_chrom, annotations = [(peaklist.tR[i], chrom_tR[i], text(i, 10, rotation=0, :center)) for i in 1:length(peaklist.tR)])
	elseif annotation==true && number==false
		plot!(p_chrom, annotations = [(peaklist.tR[i], chrom_tR[i], text(peaklist.Name[i], 10, rotation=90, :center)) for i in 1:length(peaklist.tR)])
	end
	return t, chrom
end

"""
	plot_flow(par)

Calculate and plot the flow (in mL/min, normalized) of the carrier gas in a GC Column with a program defined in the parameters `par::GasChromatography.Parameters`.
"""
function plot_flow(par)
	t = 0.0:sum(par.prog.time_steps)/1000.0:sum(par.prog.time_steps)
	F = Array{Float64}(undef, length(t))
	for i=1:length(t)
		F[i] = flow(t[i], par.prog.T_itp, par.prog.Fpin_itp, par.prog.pout_itp, par.col.L, par.col.d, par.col.gas; ng=par.opt.ng, vis=par.opt.vis, control=par.opt.control)
	end
	p_flow = plot(t, F.*60e6, xlabel="time in s", ylabel="column flow in mL/min", legend=false)
	return p_flow
end

"""
	plot_pressure(prog)

Plot the inlet and outlet pressure over time of the program `prog::GasChromatographySimulator.Program`.
"""
function plot_pressure(par)
	t = 0.0:sum(par.prog.time_steps)/1000.0:sum(par.prog.time_steps)
	pin = Array{Float64}(undef, length(t))
	pout = Array{Float64}(undef, length(t))
	for i=1:length(t)
		pin[i] = inlet_pressure(t[i], par.prog.T_itp, par.prog.Fpin_itp, par.prog.pout_itp, par.col.L, par.col.d, par.col.gas; ng=par.opt.ng, vis=par.opt.vis, control=par.opt.control)
		pout[i] = par.prog.pout_itp(t[i])
	end
	p_press = plot(t, pin, xlabel="time in s", ylabel="pressure in Pa", label="inlet", legend=:right)
	plot!(p_press, t, pout, label="outlet")
	return p_press
end

"""
	plot_temperature(par; selector="T(t)")

Plot the temperature program of the GC Column.

# Arguments
* `par::GasChromatographySimulator.Parameters`: parameters of the GC system
* `selector::String`: selection of the plot as:
	* `selector = "T(t)"`: 2D-plot of temperature `T` over time `t` at inlet (`x=0`) and outlet (`x=L`)
	* `selector = "T(x)"`: 2D-plot of temperature `T` over column position `x` at the `time_steps` of the program
	* `selector = "T(x,t)"`: 3D-plot of temperature `T` over column position `x` and `t`
"""
function plot_temperature(par::GasChromatographySimulator.Parameters; selector="T(t)")
	if selector=="T(x)"
		nx = 0.0:par.col.L/1000:par.col.L
		p_temp = plot(xlabel="x in m", ylabel="T in °C", legend=:top)
		for i=1:length(par.prog.time_steps)
			T = par.prog.T_itp.(nx, cumsum(par.prog.time_steps)[i]).-273.15
			plot!(p_temp, nx, T, label="t=$(cumsum(par.prog.time_steps)[i])s")
		end
	elseif selector=="T(t)"
		nt = 0.0:sum(par.prog.time_steps)/1000:sum(par.prog.time_steps)
		T0 = par.prog.T_itp.(0.0, nt).-273.15
		p_temp = plot(nt, T0, xlabel="t in s", ylabel="T in °C", legend=:top, label="inlet", c=:red)
		TL = par.prog.T_itp.(par.col.L, nt).-273.15
		plot!(p_temp, nt, TL, label="outlet", c=:blue)
	elseif selector=="T(x,t)"
		nx = 0.0:par.col.L/1000:par.col.L
		nt = 0.0:sum(par.prog.time_steps)/1000:sum(par.prog.time_steps)
		T = Array{Float64}(undef, length(nx), length(nt))
		for j=1:length(nt)
			for i=1:length(nx)
				T[i,j] = par.prog.T_itp(nx[i], nt[j])-273.15
			end
		end
		p_temp = plot(nx, nt, T', st=:surface, xlabel="x in m", ylabel="t in s", zlabel="T in °C")
	end
	return p_temp
end

# functions from GasChromatographyTools.jl:
"""
	local_plots(xx, yy, sol, par)

Show additional 'local' plots of selected `yy` quantities over selected `xx`
quantities.

# Arguments
* `xx`: Selected quantity shown on the x-axis. Possible values: "z", "t", "T",
  "τ", "σ" and "u".
* `yy`: Selected quantity shown on the y-axis. Possible values: "z", "t", "T",
  "τ", "σ" and "u".
* `sol`: The solution of the simulation.
* `par`: The parameters of the simulated GC-system.
"""
function local_plots(xx, yy, sol, par)
	n = size(sol)[1]

	df_sol = GasChromatographySimulator.sol_extraction(sol, par)
	xvalues = Array{Array{Float64,1}}(undef, n)
	yvalues = Array{Array{Float64,1}}(undef, n)

	p_add = plot(legend=false)
	for i=1:n
		if xx=="z"
			xvalues[i] = df_sol.z[i]
			xlabel = "position z in m"
		elseif xx=="t"
			xvalues[i] = df_sol.t[i]
			xlabel = "time t in s"
		elseif xx=="T"
			xvalues[i] = par.prog.T_itp.(df_sol.z[i], df_sol.t[i]).-273.15
			xlabel = "temperature T in °C"
		elseif xx=="τ"
			xvalues[i] = sqrt.(df_sol.τ²[i])
			xlabel = "peak width τ in s"
		elseif xx=="σ"
			xvalues[i] = velocity(df_sol, i, par).*sqrt.(df_sol.τ²[i])
			xlabel = "band width in m"
		elseif xx=="u"
			xvalues[i] = velocity(df_sol, i, par)
			xlabel = "solute velocity in m/s"
		end
		if yy=="z"
			yvalues[i] = df_sol.z[i]
			ylabel = "position z in m"
		elseif yy=="t"
			yvalues[i] = df_sol.t[i]
			ylabel = "time t in s"
		elseif yy=="T"
			yvalues[i] = par.prog.T_itp.(df_sol.z[i], df_sol.t[i]).-273.15
			ylabel = "temperature T in °C"
		elseif yy=="τ"
			yvalues[i] = sqrt.(df_sol.τ²[i])
			ylabel = "peak width in s"
		elseif yy=="σ"
			yvalues[i] = velocity(df_sol, i, par).*sqrt.(df_sol.τ²[i])
			ylabel = "band width in m"
		elseif yy=="u"
			yvalues[i] = velocity(df_sol, i, par)
			ylabel = "solute velocity in m/s"
		end
		plot!(p_add, xvalues[i], yvalues[i], xlabel=xlabel, ylabel=ylabel, label=par.sub[i].name, m=:o)
	end
	return p_add
end

"""
	velocity(df_sol, i, par)

Calculate the velocity (in m/s) coressponding to solution of the `i-th` sunstance of a
GC-system defined by `par`.
"""
function velocity(df_sol, i, par)
	x = df_sol.z[i]
	t = df_sol.t[i]
	T_itp = par.prog.T_itp
	pin_itp = par.prog.Fpin_itp
	pout_itp = par.prog.pout_itp
	L = par.col.L
	d = par.col.d
	df = par.col.df
	gas = par.col.gas
	ΔCp = par.sub[i].ΔCp
	Tchar = par.sub[i].Tchar
	θchar = par.sub[i].θchar
	φ₀ = par.sub[i].φ₀
	u = Array{Float64}(undef, length(x))
	for j=1:length(x)
		u[j] = 1/GasChromatographySimulator.residency(x[j], t[j], T_itp, pin_itp, pout_itp, L, d, df, gas, ΔCp, Tchar, θchar, φ₀; ng=par.opt.ng, vis=par.opt.vis, control=par.opt.control)
	end
	return u
end


## these are differently named than SymPy or missing or ...

Base.abs(x::SymbolicObject) = sympy.Abs(x)
Base.abs2(x::SymbolicObject) = x * conj(x)
Base.max(x::Sym, a) = sympy.Max(x, a)
Base.min(x::Sym, a) = sympy.Min(x, a)

Base.cbrt(x::Sym) = x^(1//3)
Base.ceil(x::Sym) = sympy.ceiling(x)

## Trig
Base.asech(z::Sym) = log(sqrt(1/z-1)*sqrt(1/z+1) + 1/z)
Base.acsch(z::Sym) = log(sqrt(1+1/z^2) + 1/z) ## http://mathworld.wolfram.com/InverseHyperbolicCosecant.html
Base.atan(y::Sym, x) = sympy.atan2(y,x)

Base.sinc(x::Sym) = iszero(x) ? one(x) : sin(PI*x)/(PI*x)
cosc(x::Sym) = diff(sinc(x))

Base.sincos(x::Sym) = (sin(x), cos(x))
Base.sinpi(x::Sym) = sympy.sin(x*PI)
Base.cospi(x::Sym) = sympy.cos(x*PI)
degree_variants = (:sind, :cosd, :tand, :cotd, :secd, :cscd,
                   :asind, :acosd, :atand, :acotd, :asecd, :acscd)

for  methvar in degree_variants
    meth = Symbol(String(methvar)[1:end-1])
    @eval begin
        (Base.$methvar)(ex::SymbolicObject) = ($meth)((PI/180)*ex)
    end
end

Base.rad2deg(x::Sym) = (x * 180) / PI
Base.deg2rad(x::Sym) = (x * PI) / 180

Base.hypot(x::Sym, y::Number) = hypot(promote(x,y)...)
Base.hypot(xs::Sym...) = sqrt(sum(abs(xᵢ)^2 for xᵢ ∈ xs))

## exponential
Base.log1p(x::Sym) = sympy.log(1 + x)
Base.log(x::Sym) = sympy.log(x)
Base.log(b::Number, x::Sym) = sympy.log(x, b)
Base.log2(x::SymbolicObject) = log(2,x)
Base.log10(x::SymbolicObject) = log(10,x)

## calculus.
## use a pair for limit x=>0
limit(x::SymbolicObject, xc::Pair, args...;kwargs...) = limit(x, xc[1], xc[2], args...;kwargs...)
## allow a function
limit(f::Function, x::Sym, c;kwargs...) = limit(Sym(f(x)), x, c; kwargs...)
function  limit(f::Function, c;kwargs...)
    @vars  x
    limit(f, x, c; kwargs...)
end

## This is  type piracy  and a bad   idea
function Base.diff(f::Function, n::Integer=1)
    @vars x
    sympy.diff(f(x), x, n)
end

## integrate(ex,a,b)
function integrate(ex::SymbolicObject, a::Number, b::Number)
    fs = free_symbols(ex)
    if length(fs) !== 1
        @warn "Need exactly on free symbol. Use `integrate(ex, (x, a, b))` instead"
        return
    end
    integrate(ex, (fs[1], a, b))
end
function integrate(f::Function, a::Number, b::Number)
    @vars x
    sympy.integrate(f(x), (x, a, b))
end
function integrate(f::Function)
    @syms  x
    sympy.integrate(f(x), x)
end

## Add interfaces for solve, nonlinsolve when vector of equations passed in

## An alternative to Eq(lhs, rhs) following Symbolics.jl
"""
    lhs ~ rhs

Specify an equation.

Alternative syntax to `Eq(lhs, rhs)` or `lhs ⩵ rhs` (`\\Equal[tab]`) following `Symbolics.jl`.
"""
Base.:~(lhs::Number, rhs::SymbolicObject) = Eq(lhs, rhs)
Base.:~(lhs::SymbolicObject, rhs::Number) = Eq(lhs, rhs)
Base.:~(lhs::SymbolicObject, rhs::SymbolicObject) = Eq(lhs, rhs)

"""
    solve

Use `solve` to solve algebraic equations.

Examples:

```julia
julia> using SymPy

julia> @syms x y a b c d
(x, y, a, b, c, d)

julia> solve(x^2 + 2x + 1, x) # [-1]
1-element Vector{Sym}:
 -1

julia> solve(x^2 + 2a*x + a^2, x) # [-a]
1-element Vector{Sym}:
 -a

julia> solve([a*x + b*y-3, c*x + b*y - 1], [x,y]) # Dict(y => (a - 3*c)/(b*(a - c)),x => 2/(a - c))
Dict{Any, Any} with 2 entries:
  y => (a - 3*c)/(a*b - b*c)
  x => 2/(a - c)

```

!!! note
    A very nice example using `solve` is a [blog](https://newptcai.github.io/euclidean-plane-geometry-with-julia.html) entry on [Napolean's theorem](https://en.wikipedia.org/wiki/Napoleon%27s_theorem) by Xing Shi Cai.
"""
solve() = ()
solve(V::Vector{T}, args...; kwargs...) where {T <: SymbolicObject} =
    sympy.solve(V, args...; kwargs...)

"""
    nonlinsolve

Note: if passing variables in use a tuple (e.g., `(x,y)`) and *not* a vector (e.g., `[x,y]`).
"""
nonlinsolve(V::AbstractArray{T,N}, args...; kwargs...) where {T <: SymbolicObject, N} =
    sympy.nonlinsolve(V, args...; kwargs...)

linsolve(V::AbstractArray{T,N}, args...; kwargs...) where {T <: SymbolicObject, N} =
    sympy.linsolve(V, args...; kwargs...)
linsolve(Ts::Tuple, args...; kwargs...) where {T <: SymbolicObject} =
    sympy.linsolve(Ts, args...; kwargs...)

## dsolve allowing initial condiation to be specified

"""
   dsolve(eqn, var, args..,; ics=nothing, kwargs...)

Call `sympy.dsolve`.

The initial conditions are specified with a dictionary.

Example:

```jldoctest dsolve
julia> using SymPy

julia> @syms α, x, f(), g()
(α, x, f, g)

julia> ∂ = Differential(x)
Differential(x)

julia> eqn = ∂(f(x)) ~ α * x
d
──(f(x)) = x⋅α
dx
```

```julia
julia> dsolve(eqn)
             2
            x ⋅α
f(x) = C₁ + ────
             2
```

```jldoctest dsolve
julia> dsolve(eqn(α=>2); ics=Dict(f(0)=>1)) |> print # fill in parameter, initial condition
Eq(f(x), x^2 + 1)

julia> eqn = ∂(∂(f(x))) ~ -f(x); print(eqn)
Eq(Derivative(f(x), (x, 2)), -f(x))

julia> dsolve(eqn)
f(x) = C₁⋅sin(x) + C₂⋅cos(x)

julia> dsolve(eqn; ics = Dict(f(0)=>1, ∂(f)(0) => -1))
f(x) = -sin(x) + cos(x)

julia> eqn = ∂(∂(f(x))) - f(x) - exp(x);

julia> dsolve(eqn, ics=Dict(f(0) => 1, f(1) => Sym(1//2))) |> print # not just 1//2
Eq(f(x), (x/2 + (-exp(2) - 2 + E)/(-2 + 2*exp(2)))*exp(x) + (-E + 3*exp(2))*exp(-x)/(-2 + 2*exp(2)))
```

Systems

```jldoctest dsolve
julia> @syms x() y() t g
(x, y, t, g)

julia> ∂ = Differential(t)
Differential(t)

julia> eqns = [∂(x(t)) ~ y(t), ∂(y(t)) ~ x(t)]
2-element Vector{Sym}:
 Eq(Derivative(x(t), t), y(t))
 Eq(Derivative(y(t), t), x(t))

julia> dsolve(eqns)
2-element Vector{Sym}:
 Eq(x(t), -C1*exp(-t) + C2*exp(t))
  Eq(y(t), C1*exp(-t) + C2*exp(t))

julia> dsolve(eqns, ics = Dict(x(0) => 1, y(0) => 2))
2-element Vector{Sym}:
 Eq(x(t), 3*exp(t)/2 - exp(-t)/2)
 Eq(y(t), 3*exp(t)/2 + exp(-t)/2)

julia> eqns = [∂(∂(x(t))) ~ 0, ∂(∂(y(t))) ~ -g]
2-element Vector{Sym}:
  Eq(Derivative(x(t), (t, 2)), 0)
 Eq(Derivative(y(t), (t, 2)), -g)

julia> dsolve(eqns)  # can't solve for initial conditions though! (NotAlgebraic)
2-element Vector{Sym}:
           x(t) = C₁ + C₂⋅t
 Eq(y(t), C3 + C4*t - g*t^2/2)

julia> @syms t x() y()
(t, x, y)

julia> eq = (∂(x)(t) ~ x(t)*y(t)*sin(t), ∂(y)(t) ~ y(t)^2 * sin(t))
(Eq(Derivative(x(t), t), x(t)*y(t)*sin(t)), Eq(Derivative(y(t), t), y(t)^2*sin(t)))
```

```julia
julia> dsolve(eq)  # returns a set to be `collect`ed:
PyObject {Eq(x(t), -exp(C1)/(C2*exp(C1) - cos(t))), Eq(y(t), -1/(C1 - cos(t)))}
```

```julia
julia> dsolve(eq) |> collect
2-element Vector{Any}:
 Eq(x(t), -exp(C1)/(C2*exp(C1) - cos(t)))
               Eq(y(t), -1/(C1 - cos(t)))
```

"""
function dsolve(eqn, args...;
                ics::Union{Nothing, AbstractDict, Tuple}=nothing,
                kwargs...)
    if isa(ics, Tuple) # legacy
        _dsolve(eqn, args...; ics=ics, kwargs...)
    else
        sympy.dsolve(eqn, args...; ics=ics, kwargs...)
    end
end

rhs(x::SymbolicObject) = pycall_hasproperty(x, :rhs) ? x.rhs : x
lhs(x::SymbolicObject) = pycall_hasproperty(x, :lhs) ? x.lhs : x

export dsolve, rhs, lhs

## ---- deprecate ----
## used with ics=(u,0,1) style
function _dsolve(eqn::Sym, args...; ics=nothing, kwargs...)

    Base.depwarn("Use of tuple(s), `(u, x₀, u₀)`, to specify initial conditions is deprecated. Use a dictionary: `ics=Dict(u(x₀) => u₀)`.", :_dsolve)

    if isempty(args)
        var = first(free_symbols(eqn))
    else
        var = first(args)
    end
    # var might be f(x) or x, we want `x`
    if Introspection.classname(var) != "Symbol"
        var = first(var.args)
    end
    ## if we have one initial condition, can be passed in a (u,x0,y0) *or* ((u,x0,y0),)
    ## if more than oneq a tuple of tuples
    if eltype(ics) <: Tuple
        __dsolve(eqn, var, ics; kwargs...)
    else
        __dsolve(eqn, var, (ics,); kwargs...)
        end
end

function __dsolve(eqn::Sym, var::Sym, ics; kwargs...)
    if length(ics) == 0
        throw(ArgumentError("""Some initial value specification is needed.
Specifying the function, as in `dsolve(ex, f(x))`, is deprecated.
Use `sympy.dsolve(ex, f(x); kwargs...)` directly for that underlying interface.
"""))
    end

    out = sympy.dsolve(eqn; kwargs...)
    ord = sympy.ode_order(eqn, var)

    ## `out` may be an array of solutions. If so we do each one.
    ## we want to use an array for output only if needed
    if !isa(out, Array)
        return _solve_ivp(out, var, ics,ord)
    else
        output = Sym[]
        for o in out
            a = _solve_ivp(o, var, ics,ord)
            a != nothing && push!(output, a)
        end
        return length(output) == 1 ? output[1] : output
    end
end

## Helper.
## out is an equation in var with constants. Args are intial conditions
## Return `nothing` if initial condition is not satisfied (found by `solve`)
function _solve_ivp(out, var, args, o)

    eqns = Sym[(diff(out.rhs(), var, f.n))(var=>x0) - y0 for (f, x0, y0) in args]
    sols = solve(eqns, Sym["C$i" for i in 1:o], dict=true)
    if length(sols) == 0
       return nothing
    end

    ## massage output
    ## Might have more than one solution, though unlikely. But if we substitute a variable
    ## for y0 we will get an array back from solve which may have length 1.
    if isa(sols, Array)
        if length(sols) == 1
            sols = sols[1]
        else
            return [out([Pair(k,v) for (k,v) in sol]...) for sol in sols]
        end
    end

    out([Pair(k,v) for (k,v) in sols]...)
end

## For System Of Ordinary Differential Equations
## may need to collect return values
# dsolve(eqs::Union{Array, Tuple}, args...; kwargs...) = sympy.dsolve(eqs, args...; kwargs...)