using Random

abstract type NNModule end

"""
    forward(m, x)

Forward `x` through NNModule `m`.
"""
forward(m::NNModule, x) = throw("unimplemented")

"""
    params(m)

Return all parameters of `m`.
"""
params(m::NNModule) = throw("unimplemented")

function to!(m::NNModule, device::Device)
    for p ∈ params(m)
        to!(p, device)
    end
end

struct LinearLayer <: NNModule
    W::Tensor
    b::Union{Tensor,Nothing}
    bias::Bool

    function LinearLayer(input_dim::Integer, output_dim::Integer; bias::Bool=true)
        a = sqrt(1 / input_dim)
        new(
            Tensor(rand(input_dim, output_dim) * 2a .- a),
            bias ? Tensor(rand(1, output_dim) * 2a .- a) : nothing,
            bias,
        )
    end
end

forward(m::LinearLayer, x) = m.bias ? x * m.W .+ m.b : x * m.W
params(m::LinearLayer) = m.bias ? [m.W, m.b] : [m.W]

struct Activation <: NNModule
    activation::Function
end

forward(m::Activation, x) = m.activation(x)
params(m::Activation) = []

SigmoidActivation() = Activation(logistic)

struct ModuleList <: NNModule
    modules::Array{NNModule}
end

function forward(m::ModuleList, x)
    for mod ∈ m.modules
        x = forward(mod, x)
    end
    x
end

params(m::ModuleList) = vcat([params(mod) for mod ∈ m.modules]...)


# -*- coding: utf-8 -*-
using Catlab
using ModelingToolkit
import ModelingToolkit: Constant
using Test
using Petri
using SemanticModels.PetriModels

using Catlab.WiringDiagrams
using Catlab.Doctrines
import Catlab.Doctrines.⊗
import Catlab.Graphics: to_graphviz, to_tikz
import Catlab.Graphics.Graphviz: run_graphviz
⊗(a::WiringDiagram, b::WiringDiagram) = otimes(a,b)
import Base: ∘
∘(a::WiringDiagram, b::WiringDiagram) = compose(b, a)
⊚(a,b) = b ∘ a

# Generators
S, E, I, R, D = Ob(FreeSymmetricMonoidalCategory, :S, :E, :I, :R, :D)

# +
infecting = Hom(:infection, S ⊗ I, I⊗I)

# -
# +
inf  = to_wiring_diagram(infecting)
expo = to_wiring_diagram(Hom(:exposure, S⊗I, E⊗I))
rec  = to_wiring_diagram(Hom(:recovery, I,   R))
wan  = to_wiring_diagram(Hom(:waning,   R,   S))

# -
# +
si    = to_wiring_diagram(Hom(:infection,   S⊗I, I⊗I))
se    = to_wiring_diagram(Hom(:exposure,    S⊗I, E⊗I))
prog  = to_wiring_diagram(Hom(:progression, E,   I))
fatal = to_wiring_diagram(Hom(:die,  I, D))
rip   = to_wiring_diagram(Hom(:rest, D, D))

sir    = si    ⊚ (rec   ⊗ rec)
seir   = se    ⊚ (prog  ⊗ rec)
seirs  = seir  ⊚ (wan   ⊗ wan)
seird  = seir  ⊚ (fatal ⊗ to_wiring_diagram(Hom(:id, R, R)))
seirds = seird ⊚ (rip   ⊗ wan)

model(PetriModel, sir)

models = [sir, seir, seirs, seird, seirds]
nets   = model.(PetriModel, models)

modelnames = ["sir",
         "seir",
         "seirs",
         "seird",
         "seirds",
         ]
map(zip(nets, modelnames)) do (net, name)
    Δ = reverse(net.model.Δ)
    println("Model: $name\n  Connections:")
    println("    $Δ\n")
    Δ
end

function writesvg(f::Union{IO,AbstractString}, d::WiringDiagram)
    write(f, to_graphviz(d, labels=true)
          |>g->run_graphviz(g, format="svg")
          )
end

writesvg(x) = writesvg(x...)

try
    mkpath("img")
    writesvg.(zip(["img/$f.svg"
                   for f in modelnames],
                  models))
catch e
    error("Could not write images, does ./img exist?\n $e")
end
println("Model Drawings are in ./img")


"""
TullioStatistics implements many basic statistics functions, but faster.
"""
module TullioStatistics

using Tullio
using LoopVectorization

###### BASIC FUNCTIONS

sum(A::AbstractVector{T}) where {T} = @tullio (+) R := A[i]
sum(f::Function,A::AbstractVector{T},c=0.0) where {T} = @tullio (+) R := f(A[i]-c)

prod(A::AbstractVector{T}) where {T} = @tullio (*) R := A[i]
prod(f::Function,A::AbstractVector{T},c=0.0) where {T} = @tullio (*) R := f(A[i]-c)

maximum(A::AbstractVector{T}) where {T} = @tullio (max) R := A[i]
maximum(f::Function,A::AbstractVector{T},c=0.0) where {T} = @tullio (max) R := f(A[i]-c)

minimum(A::AbstractVector{T}) where {T} = @tullio (min) R := A[i]
minimum(f::Function,A::AbstractVector{T},c=0.0) where {T} = @tullio (min) R := f(A[i]-c)

function sum(A::AbstractMatrix{T};dims=:) where {T}
    (isequal(dims,:) || isequal(dims,(1,2))) && (return @tullio (+) R := A[i,j])
    isequal(dims,1) && (return @tullio (+) R[1,j] := A[i,j])
    isequal(dims,2) && (return @tullio (+) R[i] := A[i,j])
    dims > 2 && return A
end

function sum(f::Function,A::AbstractMatrix{T},c::Number=0.0;dims=:) where {T}
    (isequal(dims,:) || isequal(dims,(1,2))) && (return @tullio (+) R := f(A[i,j]-c))
    isequal(dims,1) && (return @tullio (+) R[1,j] := f(A[i,j]-c))
    isequal(dims,2) && (return @tullio (+) R[i] := f(A[i,j]-c))
    dims > 2 && return f.(A.-c)
end
function sum(f::Function,A::AbstractMatrix{T},c::AbstractVecOrMat{S};dims=1) where {T,S}
    N,K = size(c)
    @assert (isequal(dims,:) || isequal(dims,(1,2))) "`dims` "
    isequal(dims,1) && (return @tullio (+) R[1,j] := f(A[i,j]-c[i]))
    isequal(dims,2) && (return @tullio (+) R[i] := f(A[i,j]-c[j]))
    dims > 2 && return f.(A.-c)
end
prod(A::AbstractMatrix{T}) where {T} = @tullio (*) R := A[i]
maximum(A::AbstractMatrix{T}) where {T} = @tullio (max) R := A[i]
minimum(A::AbstractMatrix{T}) where {T} = @tullio (min) R := A[i]

###### UNIVARIATE STATS

# MEAN
function mean(A::AbstractVector{T}) where {T}
    N = length(A)
    return sum(A)/N
end

function mean(f::Function,A::AbstractVector{T},c=0.0) where {T}
    N = length(A)
    return sum(f,A,c)/N
end

function mean(A::AbstractMatrix{T};dims=:) where {T}
    N,K = size(A)
    Y = N*K
    isequal(dims,:) && return sum(A)/Y
    isequal(dims,1) && return sum(A,dims=1)/N
    isequal(dims,2) && return sum(A,dims=2)/K
    dims > 2 && return A
end

function mean(f::Function,A::AbstractMatrix{T},c=0.0;dims=:) where {T}
    N,K = size(A)
    Y = N*K
    isequal(dims,:) && return sum(f,A,c)/Y
    isequal(dims,1) && return sum(f,A,c;dims=1)/N
    isequal(dims,2) && return sum(f,A,c;dims=2)/K
    dims > 2 && return f.(A.-c)
end

# VARM
function varm(A::AbstractVector{T}, m; corrected::Bool=true) where {T}
    N = length(A)
    res = mean(abs2,A,m)
    corrected && return res*N/(N-1)
    return res
end

function varm(f::Function,A::AbstractVector{T}, m; corrected::Bool=true) where {T}
    N = length(A)
    res = mean(abs2 ∘ f,A) - m^2
    corrected && return res*N/(N-1)
    return res
end

function varm(A::AbstractMatrix{T}, m; dims=:, corrected::Bool=true) where {T}
    N,K = size(A)

    if isequal(dims,:)
        @assert length(m) == 1 "Dimension mismatch between `m` and `dims`."
        return varm(vec(A),m; corrected=corrected)
    elseif isequal(dims,1) & length(m)==1
        top = sum(abs2,A,m; dims=1)
        res = corrected ? top./(N-1) : top./N
        return res
    elseif isequal(dims,2)
        return sum(A,dims=2)/K
    else
        dims > 2 && return A
    end
end

### MOVING WINDOW STATS

# SUM
# tulinnersum!(B,A,n) = @tullio B[i] = A[i+j] (j in 0:n)

# function tulouter(A::AbstractVector{T},w; padded::Bool=true,padvalue=missing) where {T}
#     L = length(A)
#     Lw = length(w)
#     if !padded
#         R = tulinnersum!(zeros(L-Lw+1),A,Lw-1)
#     else
#         minw = minimum(w)
#         maxw = maximum(w)
#         R = Vector{Union{T,typeof(P)}}(undef,L)

#     end
#     return R
# end

# DIFFERENCE

# AVERAGE

# MAX / MIN

# PRODUCT

# STD

##### MULTIVARIATE STATS

end # module


push!(LOAD_PATH, ".")

module GACrossover

using Debug
using Individual
using RealIndividual
using Parallel

abstract type PointCrossover end
abstract type AritmeticCrossover end

function crossover(p1::IndType, p2::IndType, ::Type{PointCrossover}, cr::Float64) where {IndType <: Individual.AbstractIndividual}
  gene_num = Individual.getNumGenes(p1)
  c1, c2 = deepcopy(p1), deepcopy(p2)

  Debug.ga_debug && println("----- Point Crossover -----\n")

  pr = Parallel.threadRand()

  Debug.ga_debug && println("Test: ", pr, " < ", cr, "\n")

  if pr < cr

    if Debug.ga_debug
      println("Parent 1: ", c1[:])
      println("Parent 2: ", c2[:], "\n")
    end

    point = Parallel.threadRand(1:(gene_num-1))

    Debug.ga_debug && println("Cut point: ", point, "\n")

    cuts = [1:point, (point + 1):gene_num]
    chosen_cut = Parallel.threadRand(cuts)
    c1[chosen_cut], c2[chosen_cut] = c2[chosen_cut], c1[chosen_cut]

    if Debug.ga_debug
      println("Child 1: ", c1[:])
      println("Child 2: ", c2[:], "\n")
    end

  end

  Debug.ga_debug && println("----- Point Crossover End -----\n")

  return (c1, c2)
end

function crossover(p1::IndType, p2::IndType, ::Type{AritmeticCrossover}, cr::Float64) where {IndType <: RealIndividual.AbstractRealIndividual}
  c1, c2 = deepcopy(p1), deepcopy(p2)

  pr = Parallel.threadRand()
  if pr < cr
    α = Parallel.threadRand()
    c1[:] = α * c1 + (1 - α) * c2
    c2[:] = α * c2 + (1 - α) * c1
  end
  return (c1, c2)
end

end


# Fig_GS_Roessler_Lorenz_v2.jl
# =============================

using DrWatson
@quickactivate "NonlinearDynamicsTextbook"
include(srcdir("style.jl"))
using DynamicalSystems, PyPlot, OrdinaryDiffEq
using StaticArrays
using Random
using Statistics
using Neighborhood
using LinearAlgebra

function ccm(x, y, d, τ)
    ψ = copy(y); Mx = embed(x, d, τ); tw = Theiler(τ);
    # bulk search of d+1 neighbors for all i points:
    idxs=bulkisearch(KDTree(Mx), Mx, NeighborNumber(d+1), tw)
    for i in 1:length(Mx)
      nni = idxs[i]; xi = Mx[i]; n1 = norm(xi - Mx[nni[1]])
      # implement equation (7.5):
      u = [exp(-norm(xi - Mx[j])/n1) for j ∈ nni]
      w = u ./ sum(u)
      ψ[i] = sum(w[k]*y[j] for (k, j) in enumerate(nni))
    end
    return cor(y, ψ) # Pearson correlation coef.
  end

function Roessler_Lorenz_aux_ODE(du,u,p,t)
    a, b, c, alpha,  cc = p

    du[1] = -u[2] - u[3]
    du[2] = u[1] + a*u[2]
    du[3] = b + u[3]*(u[1]-c)

    du[1:3] = alpha .* du[1:3]   # adjust time scales

    du[4] = 10. * (-u[4] + u[5])
    du[5] = 28*u[4] - u[5] - u[4]*u[6]  + cc*u[2]
    du[6] = u[4]*u[5] - 2.666666 * u[6]

    du[7] = 10. * (-u[7] + u[8])
    du[8] = 28*u[7] - u[8] - u[7]*u[9]  + cc*u[2]
    du[9] = u[7]*u[8] - 2.666666 * u[9]

end

function Roessler_Lorenz_CLE_ODE(du,u,p,t)
    a, b, c, alpha,  cc = p

    du[1] = -u[2] - u[3]
    du[2] = u[1] + a*u[2]
    du[3] = b + u[3]*(u[1]-c)

    du[1:3] = alpha .* du[1:3]   # adjust time scales

    du[4] = 10. * (-u[4] + u[5])
    du[5] = 28*u[4] - u[5] - u[4]*u[6] + cc*u[2]
    du[6] = u[4]*u[5] - 2.666666 * u[6]

    # linearized equation
    du[7] =       -10 * u[7]  +   10 * u[8]
    du[8] = (28-u[6]) * u[7]         - u[8]     -  u[4] * u[9]
    du[9] =      u[5] * u[7]  + u[4] * u[8]  - 2.666666 * u[9]
    return nothing
end

#  ------------------ main program --------------------------

@time begin

# parameters and initial values
# -----------------------------
a = 0.2
b = 0.2
c = 5.7
alpha = 6

ccmin = 0.
ccmax = 12.
ncc = 100

ccvec = LinRange(ccmin,ccmax,ncc)
GSvec = zeros(ncc)
cxyvec = zeros(ncc)
cyxvec = zeros(ncc)

dxyvec = zeros(ncc)
dyxvec = zeros(ncc)

CLEvec = zeros(ncc)

ttrans = 2000  # transient time
tattr = 50000   # time for averaging on attractor

d = 6
τ = 1  #  3
tsamp = 0.1 # 0.1

# coupling parameter loop
# -----------------------
for icc = 1:ncc
    @show icc
    cc = ccvec[icc]
    p = a, b, c, alpha,  cc
    uinit = [-1.589656  3.0478997  0.823033  0.11  0.19  0.1 -0.1 -0.2 0.1]

   # transient to attractor
   # ----------------------
     tspan = (0., ttrans)
     prob = ODEProblem(Roessler_Lorenz_aux_ODE, uinit, tspan, p)
     sol = solve(prob, Tsit5(), reltol=1e-8, abstol=1e-8, maxiters = 1e7)

   # attractor
   # ---------

    tspan = (0., tattr)
    y_init = sol.u[end]
    prob = ODEProblem(Roessler_Lorenz_aux_ODE, y_init, tspan, p)
    sol = solve(prob, Tsit5(), reltol=1e-8, abstol=1e-8, maxiters = 1e7, saveat = tsamp)

    tvec = sol.t
    ures  = sol.u
    nts = length(tvec)

    xpts = zeros(3,nts)
    ypts = zeros(3,nts)
    zpts = zeros(3,nts)

    for its = 1:nts     # .... this can be done more effciently
        uu = ures[its]
        xpts[1:3,its] = uu[1:3]
        ypts[1:3,its] = uu[4:6]
        zpts[1:3,its] = uu[7:9]
    end

   # Synchronization error (auxiliary system method)
   # -----------------------------------------------
    GSerror = 0.
    for its = 1:nts # .... can this be done without a loop?
        GSerror = GSerror +  norm(ypts[:,its] - zpts[:,its] )
    end
    GSerror = GSerror / nts
    GSvec[icc] = GSerror

    # convergent cross mapping
    # ------------------------
    xx = xpts[1,:]
    yy = ypts[1,:]

    cxyvec[icc] = ccm(xx, yy, d, τ)
    cyxvec[icc] = ccm(yy, xx, d, τ)

    if icc == 1
    fig = plt.figure("Fig Roessler Lorenz d=3 τ=1",figsize=(figx,figy))

    subplot(1,2,1)
    plt.scatter(xx[1:end-1],xx[2:end],s=1)

    subplot(1,2,2)
    plt.scatter(yy[1:end-1],yy[2:end],s=1)
    end

   # CLEs
   # ----
   tsamp = 1. # 0.1
   tspan = (0., tattr)
   y_init = sol.u[end]
   y_init[7:9] = [1 0 0]
   ncle = 50000 # Lyapunov steps
   csum = 0.

   for icle = 1:ncle
       tspan = (0., tsamp)
       prob = ODEProblem(Roessler_Lorenz_CLE_ODE, y_init, tspan, p)
       sol = solve(prob, Tsit5(), reltol=1e-8, abstol=1e-8, maxiters = 1e7, saveat = tsamp)

       tvec = sol.t
       y_init = sol.u[end]

       ynorm = sqrt( norm( y_init[7:9] ) )
       y_init[7:9] = y_init[7:9] ./ ynorm   # normalization of tangent vector

       if icle > 300
          csum = csum + log(ynorm)
       end

   nts = length(tvec)

   end

   CLE = csum / (ncle-300)
   CLEvec[icc] = CLE

# println("c=",cc,"  GS=",GSerror, "  ccm_xy=", cxyvec[icc],"  ccm_yx=",cyxvec[icc],"  dxy=",dxyvec[icc],"  dyx=",dyxvec[icc],"  CLE=", CLE)
println("c=",cc,"  GS=",GSerror, "  ccm_xy=", cxyvec[icc],"  ccm_yx=",cyxvec[icc],"  CLE=", CLE)

end

end  # of time

# %% plot

fig = plt.figure(figsize=(0.5*figx,figy))

plt.plot(ccvec,GSvec ./maximum(GSvec),color=COLORS[1])  # ,linewidth=1)
plt.plot(ccvec,CLEvec ./ maximum(CLEvec),color=COLORS[2], ls = "--")

plt.plot(ccvec,cxyvec,color=COLORS[3])
plt.plot(ccvec,cyxvec,color=COLORS[4], ls = "-.")

plt.xlabel(L"k"; labelpad = -20)

ax = gca()
ax.set_xlim([ccmin,ccmax])
ax.set_xticks(1:2:11)
# ax.set_ylim([-2.1,2.6])
ax.legend([L"E"; L"\lambda_1^{\rm{CLE}}";
           "CCM: \$x_1 \\to y_1\$"; "CCM: \$y_1 \\to x_1\$"],
         bbox_to_anchor=(0., 1.02, 1., .102), loc="lower left",
        ncol=2, mode="expand", borderaxespad=0, handlelength=2,
)

fig.tight_layout(pad=0.3)
wsave(plotsdir("9", "generalized_sync"), fig)


include(joinpath(pwd(), "src/models/car.jl"))
include(joinpath(pwd(), "src/constraints/obstacles.jl"))

optimize = true

# Horizon
T = 51

# Time step
tf = 1.0
h = tf / (T - 1)

# Bounds

# ul <= u <= uu
ul, uu = control_bounds(model, T, -3.0, 3.0)

# Initial and final states
x1 = [0.0; 0.0; 0.0]
xT = [1.0; 1.0; 0.0]
xl, xu = state_bounds(model, T, x1 = x1, xT = xT)

# Objective
obj = quadratic_tracking_objective(
        [t < T ? Diagonal(ones(model.n)) : Diagonal(10.0 * ones(model.n)) for t = 1:T],
        [Diagonal(1.0e-1 * ones(model.m)) for t = 1:T-1],
        [xT for t = 1:T], [zeros(model.m) for t = 1:T])

# Constraints
circles = [(0.85, 0.3, 0.1),
           (0.375, 0.75, 0.1),
           (0.25, 0.2, 0.1),
           (0.75, 0.8, 0.1)]

# Constraints
function obstacles!(c, x)
    c[1] = circle_obs(x[1], x[2], circles[1][1], circles[1][2], circles[1][3])
    c[2] = circle_obs(x[1], x[2], circles[2][1], circles[2][2], circles[2][3])
    c[3] = circle_obs(x[1], x[2], circles[3][1], circles[3][2], circles[3][3])
    c[4] = circle_obs(x[1], x[2], circles[4][1], circles[4][2], circles[4][3])
    nothing
end

n_stage = 4
n_con = n_stage * T
con_obstacles = ObstacleConstraints(n_con, (1:n_con), n_stage)

# Problem
prob = trajectory_optimization_problem(model,
           obj,
           T,
           h = h,
           xl = xl,
           xu = xu,
           ul = ul,
           uu = uu,
           con = con_obstacles
           )

# Trajectory initialization
X0 = linear_interp(x1, xT, T) # linear interpolation on state
U0 = random_controls(model, T, 0.001) # random controls

# Pack trajectories into vector
Z0 = pack(X0, U0, prob)

#NOTE: may need to run examples multiple times to get good trajectories
# Solve nominal problem
if optimize
    @time Z̄ = solve(prob, copy(Z0))
    @save joinpath(@__DIR__, "sol.jld2") Z̄
else
    println("Loading solution...")
    @load joinpath(@__DIR__, "sol.jld2") Z̄
end


@testset "Modulo Test" begin
    @test ALifeBenchmark.mod_1(1, 10) == 1
    @test ALifeBenchmark.mod_1(0, 10) == 10
    @test ALifeBenchmark.mod_1(10, 10) == 10
    @test ALifeBenchmark.mod_1(15, 10) == 5
    @test ALifeBenchmark.mod_1(-2, 10) == 8
end


# [[file:~/GitHub/J4Org.jl/docs/main.org::*A%20documented%20Julia%20=Foo=%20module][A documented Julia =Foo= module:1]]
module Foo

export Point, norm, foo

import Base: norm

#+Point L:Point_struct
# This is my Point structure
#
# *Example:*
#
# Creates a point $p$ of coordinates $(x=1,y=2)$.
#
# #+BEGIN_SRC julia :eval never :exports code
# p=Point(1,2)
# #+END_SRC
#
# You can add any valid Org mode directive. If you want to use
# in-documentation link, use [[norm_link_example][]]
#
struct Point
    x::Float64
    y::Float64
end

#+Point
# Creates Point at origin $(0,0)$
Point() = Point(0,0)

#+Enum
# An enum
@enum Alpha A=1 B C # just for example

#+Point,Method L:norm_link_example
# A simple function that computes $\sqrt{x^2+y^2}$
#
# *Example:*
#!p=Point(1.0,2.0);
#!norm(p)
#
# See: [[Point_struct][]]
#
norm(p::Point)::Float64 = sqrt(p.x*p.x+p.y*p.y)

# +Method,Internal
# An internal function
#
# For symbol that are not exported, do not forget the "Foo." prefix:
# !p=Point(1.0,2.0)
# !Foo.foo(2.0,p)
foo(r::Float64,p::Point) = Point(r*p.x,r*p.y)

end
# A documented Julia =Foo= module:1 ends here


# ---
# jupyter:
#   jupytext:
#     formats: ipynb,jl:hydrogen
#     text_representation:
#       extension: .jl
#       format_name: hydrogen
#       format_version: '1.3'
#       jupytext_version: 1.11.2
#   kernelspec:
#     display_name: Julia 1.7.0-beta2
#     language: julia
#     name: julia-1.7
# ---

# %%
VERSION

# %%
f((a, b, c)) = @show a b c
f(1:10);

# %%
g((; b, d, a)) = @show a b d
g((a = 1, b = 2, c = 3, d = 4, e = 5));

# %%
h((a, ((b, c), d))) = @show a b c d
h((1, ((2, 3, 4), 5, 6), 7, 8));

# %%
k(((; b, a), ((c, d), (; f, e)))) = @show a b c d e f
k(((a=1, b=2, g=7), ((3, 4, 8), (e=5, f=6, h=9), 10)));

# %%
(((x, y),) -> y - x^2)((5, 12))

# %%
:((x, y) -> y - x^2) |> Base.remove_linenums!

# %%
:(((x, y)) -> y - x^2) |> Base.remove_linenums!

# %%
:(((x, y),) -> y^2 - x) |> Base.remove_linenums!

# %%
using OffsetArrays

# %%
v = OffsetArray(-3:3, -3:3)

# %%
a, b, c = v
a, b, c

# %%
(((x, y),) -> y - x^2)(v)

# %%


# helper functions to check that the given system can be applied to the states/inputs
@inline _is_conformable_state(system, x) = statedim(system) == length(x)
@inline _is_conformable_input(system, u) = inputdim(system) == length(u)
@inline _is_conformable_noise(system, w) = noisedim(system) == length(w)
@inline _in_stateset(system, x) = x ∈ stateset(system)
@inline _in_inputset(system, u) = u ∈ inputset(system)
@inline _in_noiseset(system, w) = w ∈ noiseset(system)

@inline function _argument_error(sym, set=:none)
    if sym == :x
        vector = "state"
    elseif sym == :u
        vector = "input"
    elseif sym == :w
        vector = "noise"
    end
    if set == :none
        throw(ArgumentError("the $vector vector has the wrong dimensions"))
    else
        throw(ArgumentError("the $vector vector is not contained in the $(vector) set"))
    end
end

"""
    successor(system::DiscreteIdentitySystem, x::AbstractVector)

Return the successor state of a `DiscreteIdentitySystem`.

### Input

- `system` -- `DiscreteIdentitySystem`
- `x`      -- state (it should be any vector type)

### Output

The same state `x`.
"""
function successor(system::DiscreteIdentitySystem, x::AbstractVector)
    !_is_conformable_state(system, x) && _argument_error(:x)
    return x
end

"""
    successor(system::ConstrainedDiscreteIdentitySystem, x::AbstractVector;
              [check_constraints]=true)

Return the successor state of a `ConstrainedDiscreteIdentitySystem`.

### Input

- `system`            -- `ConstrainedDiscreteIdentitySystem`
- `x`                 -- state (it should be any vector type)
- `check_constraints` -- (optional, default: `true`) check if the state belongs to
                         the state set

### Output

The same state `x`.
"""
function successor(system::ConstrainedDiscreteIdentitySystem, x::AbstractVector;
                   check_constraints::Bool=true)
    !_is_conformable_state(system, x) && _argument_error(:x)
    if check_constraints
        !_in_stateset(system, x) && _argument_error(:x,:X)
    end
    return x
end

"""
    successor(system::LinearDiscreteSystem, x::AbstractVector)

Return the successor state of a `LinearDiscreteSystem`.

### Input

- `system` -- `LinearDiscreteSystem`
- `x`      -- state (it should be any vector type)

### Output

The result of applying the system to `x`.
"""
function successor(system::LinearDiscreteSystem, x::AbstractVector)
    !_is_conformable_state(system, x) && _argument_error(:x)
    return system.A * x
end

"""
    successor(system::AffineDiscreteSystem, x::AbstractVector)

Return the successor state of a `AffineDiscreteSystem`.

### Input

- `system` -- `AffineDiscreteSystem`
- `x`      -- state (it should be any vector type)

### Output

The result of applying the system to `x`.
"""
function successor(system::AffineDiscreteSystem, x::AbstractVector)
    !_is_conformable_state(system, x) && _argument_error(:x)
    return system.A * x + system.c
end

"""
    successor(system::LinearControlDiscreteSystem, x::AbstractVector, u::AbstractVector)

Return the successor state of a `LinearControlDiscreteSystem`.

### Input

- `system` -- `LinearControlDiscreteSystem`
- `x`      -- state (it should be any vector type)
- `u`      -- input (it should be any vector type)

### Output

The result of applying the system to `x`, with input `u`.
"""
function successor(system::LinearControlDiscreteSystem, x::AbstractVector, u::AbstractVector)
    !_is_conformable_state(system, x) && _argument_error(:x)
    !_is_conformable_input(system, u) && _argument_error(:u)
    return system.A * x + system.B * u
end

"""
    successor(system::ConstrainedLinearDiscreteSystem, x::AbstractVector;
              [check_constraints]=true)

Return the successor state of a `ConstrainedLinearDiscreteSystem`.

### Input

- `system`            -- `ConstrainedLinearDiscreteSystem`
- `x`                 -- state (it should be any vector type)
- `check_constraints` -- (optional, default: `true`) check if the state belongs to
                         the state set

### Output

The result of applying the system to `x`.
"""
function successor(system::ConstrainedLinearDiscreteSystem, x::AbstractVector;
                   check_constraints::Bool=true)
   !_is_conformable_state(system, x) && _argument_error(:x)
    if check_constraints
        !_in_stateset(system, x) && _argument_error(:x,:X)
    end
    return system.A * x
end

"""
    successor(system::ConstrainedAffineDiscreteSystem, x::AbstractVector;
              [check_constraints])

Return the successor state of a `ConstrainedAffineDiscreteSystem`.

### Input

- `system`            -- `ConstrainedAffineDiscreteSystem`
- `x`                 -- state (it should be any vector type)
- `check_constraints` -- (optional, default: `true`) check if the state belongs to
                         the state set

### Output

The result of applying the system to `x`.
"""
function successor(system::ConstrainedAffineDiscreteSystem, x::AbstractVector;
                   check_constraints::Bool=true)
    !_is_conformable_state(system, x) && _argument_error(:x)
    if check_constraints
        !_in_stateset(system, x) && _argument_error(:x,:X)
    end
    return system.A * x + system.c
end

"""
    successor(system::ConstrainedLinearControlDiscreteSystem, x::AbstractVector,
              u::AbstractVector; [check_constraints]=true)

Return the successor state of a `ConstrainedLinearControlDiscreteSystem`.

### Input

- `system`            -- `ConstrainedLinearControlDiscreteSystem`
- `x`                 -- state (it should be any vector type)
- `u`                 -- input (it should be any vector type)
- `check_constraints` -- (optional, default: `true`) check if the state (resp. input)
                         belongs to the state set (resp. input set)

### Output

The result of applying the system to `x`, with input `u`.
"""
function successor(system::ConstrainedLinearControlDiscreteSystem, x::AbstractVector,
                   u::AbstractVector; check_constraints::Bool=true)
    !_is_conformable_state(system, x) && _argument_error(:x)
    !_is_conformable_input(system, u) && _argument_error(:u)
    if check_constraints
        !_in_stateset(system, x) &&_argument_error(:x,:X)
        !_in_inputset(system, u) && _argument_error(:u,:U)
    end
    return system.A * x + system.B * u
end

"""
    successor(system::ConstrainedAffineControlDiscreteSystem, x::AbstractVector,
              u::AbstractVector; [check_constraints]=true)

Return the successor state of a `ConstrainedAffineControlDiscreteSystem`.

### Input

- `system`            -- `ConstrainedAffineControlDiscreteSystem`
- `x`                 -- state (it should be any vector type)
- `u`                 -- input (it should be any vector type)
- `check_constraints` -- (optional, default: `true`) check if the state (resp. input)
                         belongs to the state set (resp. input set)

### Output

The result of applying the system to `x`, with input `u`.
"""
function successor(system::ConstrainedAffineControlDiscreteSystem,
                   x::AbstractVector, u::AbstractVector; check_constraints::Bool=true)
    !_is_conformable_state(system, x) && _argument_error(:x)
    !_is_conformable_input(system, u) && _argument_error(:u)
    if check_constraints
        !_in_stateset(system, x) && _argument_error(:x,:X)
        !_in_inputset(system, u) && _argument_error(:u,:U)
    end
    return system.A * x + system.B * u + system.c
end

"""
    successor(system::BlackBoxDiscreteSystem, x::AbstractVector)

Return the successor state of a `BlackBoxDiscreteSystem`.

### Input

- `system` -- `BlackBoxDiscreteSystem`
- `x`      -- state (it should be any vector type)

### Output

The result of applying the system to `x`.
"""
function successor(system::BlackBoxDiscreteSystem, x::AbstractVector)
    !_is_conformable_state(system, x) && _argument_error(:x)
    return system.f(x)
end

"""
    successor(system::ConstrainedBlackBoxDiscreteSystem, x::AbstractVector;
              [check_constraints]=true)

Return the successor state of a `ConstrainedBlackBoxDiscreteSystem`.

### Input

- `system`            -- `ConstrainedBlackBoxDiscreteSystem`
- `x`                 -- state (it should be any vector type)
- `check_constraints` -- (optional, default: `true`) check if the state belongs to
                         the state set

### Output

The result of applying the system to `x`.
"""
function successor(system::ConstrainedBlackBoxDiscreteSystem, x::AbstractVector;
                   check_constraints::Bool=true)
    !_is_conformable_state(system, x) && _argument_error(:x)
    if check_constraints
        !_in_stateset(system, x) && _argument_error(:x,:X)
    end
    return system.f(x)
end

"""
    successor(system::ConstrainedBlackBoxControlDiscreteSystem, x::AbstractVector,
              u::AbstractVector; [check_constraints]=true)

Return the successor state of a `ConstrainedBlackBoxControlDiscreteSystem`.

### Input

- `system`            -- `ConstrainedBlackBoxControlDiscreteSystem`
- `x`                 -- state (it should be any vector type)
- `u`                 -- input (it should be any vector type)
- `check_constraints` -- (optional, default: `true`) check if the state belongs to
                         the state set

### Output

The result of applying the system to `x`, with input `u`.
"""
function successor(system::ConstrainedBlackBoxControlDiscreteSystem, x::AbstractVector,
                   u::AbstractVector; check_constraints::Bool=true)
    !_is_conformable_state(system, x) && _argument_error(:x)
    !_is_conformable_input(system, u) && _argument_error(:u)
    if check_constraints
        !_in_stateset(system, x) && _argument_error(:x,:X)
        !_in_inputset(system, u) && _argument_error(:u,:U)
    end
    return system.f(x, u)
end

"""
    successor(system::NoisyConstrainedLinearDiscreteSystem, x::AbstractVector,
              w::AbstractVector; [check_constraints]=true)

Return the successor state of a `NoisyConstrainedLinearDiscreteSystem`.

### Input

- `system`            -- `NoisyConstrainedLinearDiscreteSystem`
- `x`                 -- state (it should be any vector type)
- `w`                 -- noise (it should be any vector type)
- `check_constraints` -- (optional, default: `true`) check if the state belongs to
                         the state set

### Output

The result of applying the system to `x`, with noise `w`.
"""
function successor(system::NoisyConstrainedLinearDiscreteSystem, x::AbstractVector,
                   w::AbstractVector; check_constraints::Bool=true)
   !_is_conformable_state(system, x) && _argument_error(:x)
   !_is_conformable_noise(system, w) && _argument_error(:w)
    if check_constraints
        !_in_stateset(system, x) && _argument_error(:x,:X)
        !_in_noiseset(system, w) && _argument_error(:w,:W)
    end
    return system.A * x + system.D * w
end

"""
    successor(system::NoisyConstrainedLinearControlDiscreteSystem,
              x::AbstractVector, u::AbstractVector, w::AbstractVector; [check_constraints]=true)

Return the successor state of a `NoisyConstrainedLinearControlDiscreteSystem`.

### Input

- `system`            -- `NoisyConstrainedLinearControlDiscreteSystem`
- `x`                 -- state (it should be any vector type)
- `u`                 -- input (it should be any vector type)
- `w`                 -- noise (it should be any vector type)
- `check_constraints` -- (optional, default: `true`) check if the state (resp. input)
                         belongs to the state set (resp. input set)

### Output

The result of applying the system to `x`, with input `u` and noise `w`.
"""
function successor(system::NoisyConstrainedLinearControlDiscreteSystem,
                   x::AbstractVector, u::AbstractVector, w::AbstractVector; check_constraints::Bool=true)
    !_is_conformable_state(system, x) && _argument_error(:x)
    !_is_conformable_input(system, u) && _argument_error(:u)
    !_is_conformable_noise(system, w) && _argument_error(:w)
    if check_constraints
        !_in_stateset(system, x) && _argument_error(:x,:X)
        !_in_inputset(system, u) && _argument_error(:u,:U)
        !_in_noiseset(system, w) && _argument_error(:w,:W)
    end
    return system.A * x + system.B * u + system.D * w
end

"""
    successor(system::NoisyConstrainedAffineControlDiscreteSystem, x::AbstractVector,
              u::AbstractVector; [check_constraints]=true)

Return the successor state of a `NoisyConstrainedAffineControlDiscreteSystem`.

### Input

- `system`            -- `NoisyConstrainedAffineControlDiscreteSystem`
- `x`                 -- state (it should be any vector type)
- `u`                 -- input (it should be any vector type)
- `w`                 -- noise (it should be any vector type)
- `check_constraints` -- (optional, default: `true`) check if the state (resp. input)
                         belongs to the state set (resp. input set)

### Output

The result of applying the system to `x`, with input `u` and noise `w`.
"""
function successor(system::NoisyConstrainedAffineControlDiscreteSystem,
                   x::AbstractVector, u::AbstractVector, w::AbstractVector;
                   check_constraints::Bool=true)
    !_is_conformable_state(system, x) && _argument_error(:x)
    !_is_conformable_input(system, u) && _argument_error(:u)
    !_is_conformable_noise(system, w) && _argument_error(:w)
    if check_constraints
        !_in_stateset(system, x) && _argument_error(:x,:X)
        !_in_inputset(system, u) && _argument_error(:u,:U)
        !_in_noiseset(system, w) && _argument_error(:w,:W)
    end
    return system.A * x + system.B * u + system.c + system.D * w
end

"""
    successor(system::NoisyConstrainedBlackBoxControlDiscreteSystem, x::AbstractVector,
              u::AbstractVector, w::AbstractVector; [check_constraints]=true)

Return the successor state of a `NoisyConstrainedBlackBoxControlDiscreteSystem`.

### Input

- `system`            -- `NoisyConstrainedBlackBoxControlDiscreteSystem`
- `x`                 -- state (it should be any vector type)
- `u`                 -- input (it should be any vector type)
- `w`                 -- noise (it should be any vector type)
- `check_constraints` -- (optional, default: `true`) check if the state belongs to
                         the state set

### Output

The result of applying the system to `x`, with input `u` and noise `w`.
"""
function successor(system::NoisyConstrainedBlackBoxControlDiscreteSystem,
                   x::AbstractVector, u::AbstractVector, w::AbstractVector; check_constraints::Bool=true)
    !_is_conformable_state(system, x) && _argument_error(:x)
    !_is_conformable_input(system, u) && _argument_error(:u)
    !_is_conformable_noise(system, w) && _argument_error(:w)
    if check_constraints
        !_in_stateset(system, x) && _argument_error(:x,:X)
        !_in_inputset(system, u) && _argument_error(:u,:U)
        !_in_noiseset(system, w) && _argument_error(:w,:W)
    end
    return system.f(x, u, w)
end


"""
    getParameters(
        N::Int64,
        C::Float64,
        σ::Float64
        )

Get matrix of interactions `A` and instrisic growth rates `r`.

# Arguments:
- `N::Int64` : number of species
- `C::Float64` : connectivity
- `σ::Float64` : interaction strength
"""
function getParameters(
            N::Int64,
            C::Float64,
            σ::Float64
            )
    A = [ i==j ? -1. : rand()<C ? rand(Normal(0.,σ^2)) : 0. for i in 1:N, j in 1:N ]
    r = rand(N,1)
    return A, r
end

"""
    getGLV(
        x,
        pars::Array,
        t
    )

Get GLV dynamics.

# Arguments:
- `pars::Array` : concatenation of matrix `A` and vector `r`
- `x` and `t` : placeholders for ODE solver
"""
function getGLV(
            x,
            pars::Array,
            t
            )
    A = pars[:, 1:end-1]
    r = pars[:, end]
    inx = x .< 0
    x[inx] .= 0.0
    dx = diagm(vec(x)) * ( A * x + r )
    dx[inx] .= 0.0
    return dx
end

"""
    getIC(
        N::Int64,
        M::Int64
        )

Get collection of species `z` and numerical initial condition `x₀`.

# Arguments:
- `N::Int64` : number of species
- `M::Int64` : number of communities
"""
function getIC(
            N::Int64,
            M::Int64
            )
    z = zeros(Int64,N,M)
    inx = Any[]
    while size(inx,1) < M
        push!(inx, sort(unique( rand(1:N,rand(1:N)) )))
        unique!(inx)
    end
    for i in 1:M
        z[inx[i],i] .= 1
    end
    x₀ = z .* rand(N,M)
    return z, x₀
end

"""
    getSteadyState(
        x₀::Vector,
        τ::Float64,
        A::Array{Float64},
        r::Array{Float64}
        )

Get steady-state of dynamics by integrating ODE.

# Arguments:
- `x₀::Vector` : numerical initial condition
- `τ::Float64` : integration time
- `A::Array{Float64}` : interaction matrix
- `r::Array{Float64}` : intrinsic growth rates
"""
function getSteadyState(
            x₀::Vector,
            τ::Float64,
            A::Array{Float64},
            r::Array{Float64}
            )
    prob = ODEProblem(getGLV, x₀, (0.0,τ), hcat(A, r))
    sol = solve(prob,force_dtmin=true)
    xf = sol.u[end]
    return xf
end

"""
    getRewiredNetwork(
        A::Array{Float64},
        p::Float64
        )

Get rewired interaction matrix.

# Arguments:
- `A::Array{Float64}` : interaction matrix
- `p::Float64` : rewiring probability
"""
function getRewiredNetwork(
            A::Array{Float64},
            p::Float64
            )
    N = size(A,1)
    sA = sparse(A + diagm(ones(N)))
    row, col, s = findnz(sA)
    if p > 0.0
        for i in findall(rand(nnz(sA)) .< p)
            alreadyconnected = row[col .== col[i]]
            possibletargets = setdiff(1:N, alreadyconnected)
            length(possibletargets) >= 1 && (row[i] = rand(possibletargets))
        end
    end
    return sparse(row,col,s,N,N) - diagm(ones(N))
end

"""
        generate_data(
            N::Int64,
            M::Int64,
            repetitions::Int64,
            values::Int64,
            params::Array
        )

Generate synthetic data for parameters in `params`.
The function generates a number of `repetitions` for every value of `values`.

# Arguments:
- `N::Int64` : number of species
- `M::Int64` : number of communitites
- `repetitions::Int64` : number of ecological networks
- `values::Int64` : number of values per parameter
- `params::Array` : sweeping parameters
"""
function generate_data(N,M,repetitions,values,params)
    reps = 0:repetitions-1
    num = 1:values
    for par in params
        "Generating data: $par" |> println
        inx = [ (r,p) for r in reps, p in num ]
        @sync @distributed for i in inx
            # Changing parameters
            rep,p = i
            print(p-1,"_",rep)
            if par == "Strength"
                σ,c,η,ϵ,ρ = S[p],C[1],0,0,0.
            elseif par == "Connectivity"
                σ,c,η,ϵ,ρ = S[1],C[p],0,0,0.
            elseif par == "Universality"
                σ,c,η,ϵ,ρ = S[1],C[1],U[p],0,0.
            elseif par == "Noise"
                σ,c,η,ϵ,ρ = S[1],C[1],0,E[p],0.
            elseif par == "Rewiring"
                σ,c,η,ϵ,ρ = S[1],C[1],0,0,R[p]
            end
            print("  $σ,  $c, $η,  $ϵ,  $ρ\t")
            # Allocate
            X = SharedArray{Float64,2}(N,M)
            A = SharedArray{Float64,2}(N,N)
            r = SharedArray{Float64,2}(N,1)
            # Get initial values
            A₀,r = getParameters(N,c,σ)
            Z,x₀ = getIC(N,M)
            conv = 0
            j = 1
            while j <= M
                # Rewiring
                A = getRewiredNetwork(A₀,ρ)
                # Non-Universality: noise in non-zero entries
                if η > 0
                    A += η*( rand(Normal(0,σ^2),N,N) .* (x-> x != 0 ? 1 : 0 ).(A) )
                end
                # Solve
                x = getSteadyState(x₀[:,j], 1000., A, r)
                # Convergence
                conv += sum(diagm(vec(x)) * ( A * x + r))
                # Noise in non-zero entries
                x += ϵ*( rand(Uniform(0,mean(x[x.>0])),N) .* rand([-1,1],N) .* Z[:,j])
                x[x.<0].=0
                X[:,j] = x
                j += 1
                j%100==0&&print("··")
            end
            print("\t== $(conv/M) \n")

            path = "./data/synthetic/$N/$par/"
            !ispath(path) && mkpath(path)

            writedlm(path*"A$(p-1)_$rep.csv", hcat(A₀,r), ',')
            writedlm(path*"Z$(p-1)_$rep.csv", Z, ',')
            writedlm(path*"P$(p-1)_$rep.csv", X, ',')
        end
    end
end


#= Grid types for OceanTurb.jl.

See fields.jl for the geometry of an OceanTurb.Field.
=#

import Base: eltype, length, size

# Staggered grid lengths and sizes for fields
cell_length(N) = N+2
face_length(N) = N+3
  cell_size(N) = (cell_length(N),)
  face_size(N) = (face_length(N),)

     height(g::Grid) = g.H
     length(g::Grid) = g.N
       size(g::Grid) = (g.N,)
cell_length(g::Grid) = cell_length(g.N)
face_length(g::Grid) = face_length(g.N)
  cell_size(g::Grid) = cell_size(g.N)
  face_size(g::Grid) = face_size(g.N)

height(m::AbstractModel) = height(m.grid)
length(m::AbstractModel) = length(m.grid)
  size(m::AbstractModel) = size(m.grid)

eltype(::Grid{T}) where T = T

"""
    arraytype(grid::Grid)

Return the array type corresponding to data
that lives on `grid`. Defaults to `Array`.
New data types (for example, grids that exist on GPUs) must
implement new array types.
"""
arraytype(grid::Grid{T}) where T = Array{T, 1} # default array type

"""
    UniformGrid([T], H, N)

Construct a 1D finite-volume grid with type `T` and array type `A`,
with `N` cells on the domain `z = [0, -H]`.
A `Grid` has two type parameters: an element type `T`,
and and a range type `R`. `T` defaults to Float64.
"""
struct UniformGrid{T, R} <: Grid{T, R}
    N  :: Int
    H  :: T
    Δc :: T
    Δf :: T
    zc :: R
    zf :: R
end

function UniformGrid(T, N::Int, H::Number)
    Δ = convert(T, H/N)
    H = convert(T, H)
    half_Δ = convert(T, 0.5Δ)

    zc = range(-H+half_Δ; length=N, stop=-half_Δ)
    zf = range(-H; length=N+1, stop=zero(T))

    UniformGrid(N, H, Δ, Δ, zc, zf)
end

# Defaults
UniformGrid(N, H) = UniformGrid(Float64, N, H)
UniformGrid(T=Float64; N=3, H=1) = UniformGrid(T, N, H)

"Return the cell spacing at index i."
@inline Δc(grid::UniformGrid, i) = grid.Δc

"Return the face spacing at index i."
@inline Δf(grid::UniformGrid, i) = grid.Δf


using MakieLayout
using Makie

function axhline!(la::LAxis, y; kwargs...)
    points = lift(la.limits) do lims
        x1 = lims.origin[1]
        x2 = lims.origin[1] + lims.widths[1]
        Point2f0.([(x1, y), (x2, y)])
    end

    lines!(la, points; xautolimit=false, kwargs...)
end

function axvline!(la::LAxis, x; kwargs...)
    points = lift(la.limits) do lims
        y1 = lims.origin[2]
        y2 = lims.origin[2] + lims.widths[2]
        Point2f0.([(x, y1), (x, y2)])
    end

    lines!(la, points; yautolimit=false, kwargs...)
end

function abline!(la::LAxis, a, b; kwargs...)
    points = lift(la.limits) do lims
        x1 = lims.origin[1]
        x2 = lims.origin[1] + lims.widths[1]
        Point2f0.([(x1, a * x1 + b), (x2, a * x2 + b)])
    end

    lines!(la, points; xautolimit=false, yautolimit=false, kwargs...)
end

function dynamicfunc!(la::LAxis, func; n=nothing, kwargs...)

    points = lift(la.limits, la.layoutnodes.computedbbox) do lims, bbox

        if isnothing(n)
            xwidth = width(bbox)
            n = Int(round(xwidth))
        end

        x1 = lims.origin[1]
        x2 = lims.origin[1] + lims.widths[1]
        xs = LinRange(x1, x2, n)
        ys = func.(xs)
        [Point2f0(x, y) for (x, y) in zip(xs, ys)]
    end

    lines!(la, points; xautolimit=false, yautolimit=false, kwargs...)
end

function dynamicimage!(la::LAxis, func; n=300, kwargs...)
    pixels = lift(la.limits) do lims
        x1, y1 = lims.origin
        x2, y2 = lims.origin .+ lims.widths
        xs = LinRange(x1, x2, n)
        ys = LinRange(y1, y2, n)

        pixels = Float32[func(x, y) for x in xs, y in ys]
    end

    xrange = lift(lims -> lims.origin[1] .+ [0, lims.widths[1]], la.limits)
    yrange = lift(lims -> lims.origin[2] .+ [0, lims.widths[2]], la.limits)

    image!(la, xrange, yrange, pixels; xautolimit=false, yautolimit=false, kwargs...)
end

begin
    scene = Scene(resolution = (1000, 1000));
    screen = display(scene)
    campixel!(scene);

    nrows = 1
    ncols = 1

    maingl = GridLayout(scene, 1, 1, alignmode=Outside(30))

    gridgl = maingl[1, 1] = GridLayout(
        nrows, ncols)

    la = gridgl[1, 1] = LAxis(scene)

    # scatter!(la, rand(100, 2) .* 50, markersize=20)
    axhline!(la, 0)
    axvline!(la, 0)
    abline!(la, 1, 0)

    dynamicfunc!(la, sin, color=:red)
    dynamicfunc!(la, cos, color=:blue)

    nothing
end

# save("layout.png", scene)


# Code for Video 2.1:

Array{Float64, 2}
Dict{Symbol, Float64}

# Array{T, 2}
# Dict{K, V}

function double(f::Float64)
    f * 2
end

local n::Int32 = 7
# (31 + 42)::Float64
# ERROR: TypeError: typeassert: expected Float64, got Int64

super(Int64) # Signed

subtypes(Signed)
# 5-element Array{Any,1}:
#  Int128
#  Int16
#  Int32
#  Int64
#  Int8

Int32 <: Signed # true
Float64 <: Number # true

type Iris
    sepal_length::Float64
    sepal_width::Float64
    petal_length::Float64
    petal_width::Float64
    species::ASCIIString
end

fieldnames(Iris)
# 5-element Array{Symbol,1}:
#  :sepal_length
#  :sepal_width
#  :petal_length
#  :petal_width
#  :species

iris1 = Iris(5.1, 3.5, 1.4, 0.2, "I. setosa")
# Iris(5.1,3.5,1.4,0.2,"I. setosa")
iris1.petal_width # 0.2
iris1.petal_width = 0.3 # 0.3
iris1 # Iris(5.1,3.5,1.4,0.3,"I. setosa")

immutable Iris2
    sepal_length::Float64
    sepal_width::Float64
    petal_length::Float64
    petal_width::Float64
    species::ASCIIString
end
iris2 = Iris2(5.1, 3.5, 1.4, 0.2, "I. setosa")
# iris1.petal_width = 0.2 # ERROR: LoadError: type Iris2 is immutable

arr_iris = Iris[] # 0-element Array{Iris,1}
push!(arr_iris, iris1)
# 1-element Array{Iris,1}:
#  Iris(5.1,3.5,1.4,0.2,"I. setosa")

type IrisG{T <: Real}
   sepal_length::T
   sepal_width::T
   petal_length::T
   petal_width::T
   species::ASCIIString
end

add{T}(x::T, y::T) = x + y
# add (generic function with 1 method)
add{T <: Number}(x::T, y::T) = x + y

# add("I. setosa", "I. versicolor")
# ERROR: MethodError: `+` has no method matching +(::ASCIIString, ::ASCIIString)
# Closest candidates are:
#   +(::Any, ::Any, ::Any, ::Any...)
#  in add at none:1

function add(x::Float64, y::Float64)
    println("Adding 2 floating point numbers:")
    println(x + y)
    x + y
end

methods(add)
# 3 methods for generic function "add":
# add(x::Float64, y::Float64) at none:2
# add{T<:Number}(x::T<:Number, y::T<:Number) at none:1
# add{T}(x::T, y::T) at none:1

add(2, 5) # 7
add(3.1, 5.4)
# Adding 2 floating point numbers:
# 8.5

methods(+)
# 171 methods for generic function "+":
# +(x::Bool) at bool.jl:33
# +(x::Bool, y::Bool) at bool.jl:36
# +(y::AbstractFloat, x::Bool) at bool.jl:46
# +(x::Int64, y::Int64) at int.jl:8
# +(x::Int8, y::Int8) at int.jl:16
# +(x::UInt8, y::UInt8) at int.jl:16
# ....

type Setosa
    # some fields
end

type Versicolor
    # some fields
end

type Virginica
    # some fields
end

IrisU = Union{Setosa, Versicolor, Virginica}
Union{Setosa,Versicolor,Virginica}

set1 = Setosa() # Setosa() value
isa(set1, IrisU) # true


"""
Max-tree morphological representation of an image.

# Details
Let's consider a *thresholding* operation,
```julia
    mask = [val ≥ threshold for val in image]
```
One can identify the connected components (the sets of neighboring true values)
in `mask`. When *image thresholding* is sequentially applied for all possible
thresholds, it generates a collection of connected components that could be
organized into a hierarchical structure called *component tree*. Consider 1D
"image" with values 1, 2 and 3:
```
       2233233312223322
```
The connected components would be
```
    1: AAAAAAAAAAAAAAAA
    2: BBBBBBBB.CCCCCCC
    3: ..DD.EEE....FF..
```
Here, the letters are the labels of the resulting connected components,
and `.` specifies that the pixel value is below the threshold.
In this example, the corresponding *component tree* is:
```
      A
     ⭩ ⭨
    B   C
   ⭩ ⭨   ⭨
  D   E   F
```

A *max-tree* is an efficient representation of the *component tree*.
A connected component ``C`` at threshold level ``t`` is represented by the
single *reference pixel* ``r`` from this level (`image[r] == t`), which is the
parent to all other pixels of ``C`` and also to the *reference pixels* of the
connected components at higher thresholds, which are the children of ``C``.
In our example, the reference pixels (denoted by the letter of the corresponding
component) would be:
```
    1: ........A.......
    2: B........C......
    3: ..D..E......F...
```
I.e.

| Comp | Ref.Pixel |
| ---- | ---------:|
| *A* |  9 |
| *B* |  1 |
| *C* | 10 |
| *D* |  3 |
| *E* |  6 |
| *F* | 13 |

So the whole max-tree could be encoded as a vector of indices of parent pixels:
```
9  1  1  3  1  1  6  6  9  9 10 10 10 13 10 10
```

The *max-tree* is the basis for many morphological operators,
namely connected operators. Unlike morphological openings and closings, these
operators do not require a fixed structuring element, but rather act with a
flexible structuring element that meets a certain criterion.

# See also
[`area_opening`](@ref), [`area_closing`](@ref),
[`diameter_opening`](@ref), [`diameter_closing`](@ref).

# References
1. Salembier, P., Oliveras, A., & Garrido, L. (1998). *Antiextensive Connected Operators for Image and Sequence Processing*. IEEE Transactions on Image Processing, 7(4), 555-570.
   > https://doi.org/10.1109/83.663500
2. Berger, C., Geraud, T., Levillain, R., Widynski, N., Baillard, A., Bertin, E. (2007). *Effective Component Tree Computation with Application to Pattern Recognition in Astronomical Imaging*. In International Conference on Image Processing (ICIP), 41-44.
   > https://doi.org/10.1109/ICIP.2007.4379949
3. Najman, L., & Couprie, M. (2006). *Building the component tree in quasi-linear time*. IEEE Transactions on Image Processing, 15(11), 3531-3539.
   > https://doi.org/10.1109/TIP.2006.877518
4. Carlinet, E., & Geraud, T. (2014). *A Comparative Review of Component Tree Computation Algorithms*. IEEE Transactions on Image Processing, 23(9), 3885-3895.
   > https://doi.org/10.1109/TIP.2014.2336551
"""
struct MaxTree{N}
    """
    If `false`, the tree is constructed from the smallest to the largest values,
    the tree root being the smallest (darkest) pixel.
    Otherwise, if `true`, the direction is from large to small values.
    """
    rev::Bool

    """
    Array of the same shape as the source image.
    Each value is the linear index of the parent pixel in the max-tree.
    """
    parentindices::Array{Int,N}

    """
    The order of the elements in the tree, from root to leaves.
    Each element corresponds to the linear index of the pixel in the image.
    A parent of the pixel always comes before the pixel itself, i.e.
    `parentindices[traverse[1]] == traverse[1]` (the root pixel) and
    `parentindices[traverse[k]] ∈ traverse[1:k-1]` for all `k >= 2`.
    """
    traverse::Vector{Int}

    # create uninitialized MaxTree for image
    MaxTree{N}(image::GenericGrayImage{<:Any, N}, rev::Bool) where N =
        new{N}(rev, Array{Int,N}(undef, size(image)),
               Vector{Int}(undef, length(image)))
end

Base.ndims(::Type{<:MaxTree{N}}) where N = N
Base.ndims(maxtree::MaxTree) = ndims(typeof(maxtree))
Base.length(maxtree::MaxTree) = length(maxtree.parentindices)
Base.size(maxtree::MaxTree) = size(maxtree.parentindices)
Base.:(==)(a::MaxTree, b::MaxTree) =
    (ndims(a) == ndims(b)) && (a.rev == b.rev) &&
    (a.parentindices == b.parentindices) &&
    (a.traverse == b.traverse)

"""
    root_index(maxtree::MaxTree) -> Int

Linear index of the root pixel.
"""
root_index(maxtree::MaxTree) = maxtree.traverse[1]

# Gets the path to the root of the max-tree (defined by `ancestors`) starting from
# `node`. The indices of the nodes along the path are stored into `path`.
# Returns the index of the root
@inline function rootpath!(path::Vector{Int},
                           ancestors::AbstractArray{<:Integer},
                           node::Integer)
    empty!(path)
    p = node
    @label next
    push!(path, p)
    @inbounds q = ancestors[p]
    if p != q
        p = q
        @goto next
    end
    return p
end

# Corrects `maxtree.parentindices`, so that the parent of every pixel
# is a canonical node. I.e. for each connected component at a level l,
# all pixels point to the same representative, which in turn points to the
# representative pixel at the next level.
#
# A canonical node ``p`` is either a root of the max-tree, or
# ``image(parent(p)) != image(p)``
function canonize!(maxtree::MaxTree, image::GenericGrayImage)
    @assert size(maxtree) == size(image)
    parentindices = maxtree.parentindices
    @inbounds for p in maxtree.traverse
        q = parentindices[p]
        if image[q] == image[parentindices[q]]
            parentindices[p] = parentindices[q]
        end
    end
    return maxtree
end

# Checks whether the `pixel` + `offset` is still a valid pixel
# contained inside the image area defined by `axes`.
isvalid_offset(offset::CartesianIndex{N}, pixel::CartesianIndex{N},
               axes::NTuple{N}) where N =
    Base.checkbounds_indices(Bool, axes, (pixel + offset,))

# generates a vector of offsets to the CartesianIndex that defines
# the neighborhood of the pixel in an N-dimensional image.
# `connectivity` is the maximal number of orthogonal steps
# (+1/-1 to a single dimension) that are required to reach a neighbor.
# Note that the corresponding offsets of linear indices would be sorted in ascending order
# ensuring efficient memory access pattern.
function neighbor_cartesian_offsets(::Type{CartesianIndex{N}}, connectivity::Integer) where N
    (connectivity >= 1) || throw(ArgumentError("connectivity should be positive integer"))
    return [ci for ci in CartesianIndices(ntuple(_ -> -1:1, N))
            if 1 <= sum(abs, Tuple(ci)) <= connectivity]
end

# convert offsets of cartesian indices into linear offsets of the given array
linear_offsets(offsets::AbstractVector{<:CartesianIndex},
               arr::AbstractArray) =
    dot.(Tuple.(offsets), Ref(strides(arr)))

"""
    rebuild!(maxtree::MaxTree, image::GenericGrayImage,
             neighbors::AbstractVector{CartesianIndex}) -> maxtree

Rebuilds the `maxtree` for the `image` using `neighbors` as the pixel
connectivity specification.

# Details
The pixels in the connected components generated by the method should be
connected to each other by a path through neighboring pixels.
The pixels ``p_1`` and ``p_2`` are neighbors, if `neighbors` array contains
``d``, such that ``p_2 = p_1 + d``.

# See also
[`MaxTree`](@ref)
"""
function rebuild!(maxtree::MaxTree{N},
                  image::GenericGrayImage{<:Any, N},
                  #=mask::AbstractArray{Bool, N},=#
                  neighbors::AbstractVector{CartesianIndex{N}}) where N
    check_maxtree(maxtree, image, rev=maxtree.rev)
    # pixels need to be sorted according to their gray level.
    sortperm!(maxtree.traverse, vec(image), rev=maxtree.rev)

    neighbor_offsets = linear_offsets(neighbors, maxtree.parentindices)
    p_indices = fill!(maxtree.parentindices, 0) |> vec # 0=uninitialized parent
    p_cis = CartesianIndices(maxtree.parentindices)
    p_axes = axes(maxtree.parentindices)
    r_indices = fill!(similar(p_indices), 0) # temporary array containing current roots
    rootpath = Vector{Int}() # temp array to hold the path to the current root within ancestors

    # traverse the array in reversed order (from highest value to lowest value)
    @inbounds for p in Iterators.Reverse(maxtree.traverse)
        p_ci = p_cis[p]
        p_indices[p] = r_indices[p] = p # it's a new root

        for i in eachindex(neighbors)
            # check that p is in the mask and that it's neighbor is a valid image pixel
            (#=mask[p] && =#isvalid_offset(neighbors[i], p_ci, p_axes)) || continue
            index = p + neighbor_offsets[i] # linear index of the neighbor
            (p_indices[index] == 0) && continue # ignore neighbor without parent (this means image[index] < image[p])
            root = rootpath!(rootpath, r_indices, index) # neighbor's root
            if (root != p) || (length(rootpath) > 1) # attach neighbor's root to the p
                p_indices[root] = p
                r_indices[rootpath] .= p # also attach the ancestral nodes of neighbor to p
            end
        end
    end
    return canonize!(maxtree, image)
end

"""
    MaxTree(image::GenericGrayImage; connectivity=1, rev=false) -> MaxTree

Constructs the *max-tree* of the `image`.

# Arguments
- `connectivity::Integer=1`: defines the pixel neighborhood used to construct
  the connected components. The value is the maximum number of orthogonal steps
  to reach a neighbor. In 2D, it is 1 for a 4-neighborhood and 2 for a
  8-neighborhood. See [`rebuild!](@ref).
- `rev::Bool=false`: if `false`, the max-tree is traversed from the darkest
  (the root node) to the brightest, otherwise it's traversed from the brightest
  (the root) to the darkest.

# Examples
We create a small sample image (Figure 1 from [4]) and build the max-tree.

```jldoctest; setup = :(using ImageMorphology), filter = r"Array{Int64,2}|Matrix{Int64}"
julia> image = [15 13 16; 12 12 10; 16 12 14]
3×3 Array{Int64,2}:
 15  13  16
 12  12  10
 16  12  14

julia> mtree = MaxTree(image, connectivity=2)
MaxTree{2}(false, [4 2 4; 8 2 8; 2 2 2], [8, 2, 5, 6, 4, 9, 1, 3, 7])
```
"""
function MaxTree(image::GenericGrayImage;
                 connectivity::Integer=1, rev::Bool=false)
    N = ndims(image)
    # User defined masks are not allowed, as there might be more than one
    # connected component in the mask (and therefore not a single tree that
    # represents the image). Mask here is an image that is 0 on the border
    # and 1 everywhere else
    #=
    mask = np.ones(image.shape)
    for k in range(len(image.shape)):
        np.moveaxis(mask, k, 0)[0] = 0
        np.moveaxis(mask, k, 0)[-1] = 0
    =#
    neighbors = neighbor_cartesian_offsets(CartesianIndex{N}, connectivity)
    return rebuild!(MaxTree{N}(image, rev), #=mask,=# image, neighbors)
end

"""
    areas(maxtree::MaxTree) -> Array{Int}

Computes the areas of all `maxtree` components.

# Returns
The array of the same shape as the original image. The `i`-th element is
the area (in pixels) of the component that is represented by the reference
pixel with index `i`.

# See also
[`diameters`](@ref), [`area_opening`](@ref), [`area_closing`](@ref).
"""
function areas(maxtree::MaxTree)
    areas = fill(1, size(maxtree)) # start with 1-pixel areas
    parentindices = maxtree.parentindices
    @inbounds for p in Iterators.Reverse(maxtree.traverse)
        q = parentindices[p]
        (q != p) && (areas[q] += areas[p])
    end
    return areas
end

"""
    boundingboxes(maxtree::MaxTree) -> Array{NTuple{2, CartesianIndex}}

Computes the minimal bounding boxes of all `maxtree` components.

# Returns
The array of the same shape as the original image. The `i`-th element is
the tuple of the minimal and maximal cartesian indices for the bounding box
of the component that is represented by the reference pixel with index `i`.

# See also
[`diameters`](@ref).
"""
function boundingboxes(maxtree::MaxTree{N}) where N
    # initialize bboxes
    bboxes = [(ci, ci) for ci in CartesianIndices(maxtree.parentindices)]
    @inbounds for p in Iterators.Reverse(maxtree.traverse)
        q = maxtree.parentindices[p]
        bbp, bbq = bboxes[p], bboxes[q]
        bboxes[q] = (min(bbq[1], bbp[1]), max(bbq[2], bbp[2]))
    end
    return bboxes
end

"""
    diameters(maxtree::MaxTree) -> Array{Int}

Computes the "diameters" of all `maxtree` components.

"Diameter" of the *max-tree* connected component is the length of
the widest side of the component's bounding box.

# Returns
The array of the same shape as the original image. The `i`-th element is the
"diameter" of the component that is represented by the reference pixel with
index `i`.

# See also
[`boundingboxes`](@ref), [`areas`](@ref),
[`diameter_opening`](@ref), [`diameter_closing`](@ref).
"""
diameters(maxtree::MaxTree) =
    [maximum(Tuple(bbox[2] - bbox[1])) + 1 for bbox in boundingboxes(maxtree)]

"""
    filter_components!(output::GenericGrayImage, image::GenericGrayImage,
                       maxtree::MaxTree, attrs::AbstractVector,
                       min_attr, all_below_min) -> output

Filters the connected components of the `image` and stores the result in `output`.

The ``output`` is the copy of the ``image`` exluding the connected components,
whose attribute value is below `min_attr`. That is, the pixels of the exluded
component are reset to the value of the reference pixel of its first valid
ancestor (the connected component with the attribute value greater or equal
to `min_attr`).

# Arguments
- `maxtree::MaxTree`: pre-built max-tree representation of the `image`
- `attrs::AbstractVector`: `attrs[i]` is the attribute value for the ``i``-th
   component of the tree (``i`` being the linear index of its *reference pixel*)
- `all_below_min`: the value to fill the `output` if all attributes of all
  components (including the root one) are below `min_attr`

# Details
This function is the basis for [`area_opening`](@ref), [`diameter_opening`](@ref)
and similar transformations.
E.g. for [`area_opening`](@ref) the attribute is the area of the components.
In this case, the max-tree components of the `output` have area no smaller
than `min_attr` pixels.

The method assumes that the attribute values are monotone with respect to the
components hieararchy, i.e. ``attrs[i] <= attrs[maxtree.parentindices[i]]`` for
each `i`.
"""
function filter_components!(output::GenericGrayImage, image::GenericGrayImage,
                            maxtree::MaxTree,
                            attrs::AbstractArray, min_attr,
                            all_below_min = zero(eltype(output)))
    # should have been already checked by higher-level functions
    @assert size(output) == size(image)
    @assert size(image) == size(maxtree)
    @assert length(attrs) == length(maxtree)

    p_root = root_index(maxtree)
    # if p_root is below min_attr, then all others are as well
    (attrs[p_root] < min_attr) && return fill!(output, all_below_min)
    output[p_root] = image[p_root]
    parentindices = maxtree.parentindices

    for p in maxtree.traverse
        q = parentindices[p]
        # this means p is not canonical
        # in other words, it has a parent that has the
        # same image value.
        if image[p] == image[q]
            output[p] = output[q]
            continue
        end
        # if attribute is below the threshold, stop at the lower parent,
        # otherwise continue to reconstruct the image
        output[p] = attrs[p] < min_attr ? output[q] : image[p]
    end

    return output
end

function check_output_image(output::GenericGrayImage, image::GenericGrayImage)
    (size(output) == size(image)) ||
        throw(DimensionMismatch("The sizes of the output and the input image do not match"))
end

# checks if the provided maxtree is compatible with the given options
# or builds the new maxtree if none was given
function check_maxtree(maxtree::MaxTree,
                       image::GenericGrayImage;
                       rev::Bool=false)
    (size(maxtree) == size(image)) ||
        throw(DimensionMismatch("The sizes of the max-tree and the input image do not match"))
    (maxtree.rev == rev) ||
        throw(ArgumentError("The traversal order of the given max-tree is different from the requested one"))
end

"""
    area_opening!(output, image, [maxtree];
                  min_area=64, connectivity=1) -> output

Performs in-place *area opening* of the `image` and stores the result in `output`.
See [`area_opening`](@ref) for the detailed description of the method.
"""
function area_opening!(output::GenericGrayImage, image::GenericGrayImage,
                       maxtree::MaxTree; min_area::Number=64)
    check_output_image(output, image)
    check_maxtree(maxtree, image, rev=false)
    return filter_components!(output, image, maxtree, areas(maxtree), min_area)
end

"""
    area_opening(image, [maxtree]; min_area=64, connectivity=1) -> Array

Performs an *area opening* of the `image`.

*Area opening* replaces all bright components of an image that
have a surface smaller than `min_area` with the darker value taken from
their first ancestral component (in *max-tree* representation of `image`)
that has the area no smaller than `min_area`.

# Details
Area opening is similar to morphological opening (see [`opening`](@ref)),
but instead of using a fixed structuring element (e.g. disk) it employs
small (less than `min_area`) components of the *max-tree*. Consequently,
the `area_opening` with `min_area = 1` is the identity transformation.

In the binary case, area opening is equivalent to `remove_small_objects`;
this operator is thus extended to gray-level images.

# Arguments
- `image::GenericGrayImage`: the ``N``-dimensional input image
- `min_area::Number=64`: the smallest size (in pixels) of the image component
  to keep intact
- `connectivity::Integer=1`: the neighborhood connectivity. The maximum number
  of orthogonal steps to reach a neighbor of the pixel.
  In 2D, it is 1 for a 4-neighborhood and 2 for a 8-neighborhood.
- `maxtree::MaxTree`: optional pre-built *max-tree*. Note that
  `maxtree` and `connectivity` optional parameters are mutually exclusive.

# Returns
An array of the same type and shape as the `image`.

# See also
[`area_opening!`](@ref), [`area_closing`](@ref),
[`diameter_opening`](@ref),
[`MaxTree`](@ref), [`opening`](@ref)

# References
1. Vincent, L. (1993). *Grayscale area openings and closings, their efficient implementation and applications*, Proc. of EURASIP Workshop on Mathematical Morphology and its Applications to Signal Processing, Barcelona, Spain, 22-27
2. Soille, P. (2003). Chapter 6 *Geodesic Metrics* of *Morphological Image Analysis: Principles and Applications*, 2nd edition, Springer.
   > https://doi.org/10.1007/978-3-662-05088-0
3. Salembier, P., Oliveras, A., & Garrido, L. (1998). *Antiextensive Connected Operators for Image and Sequence Processing*. IEEE Transactions on Image Processing, 7(4), 555-570.
   > https://doi.org/10.1109/83.663500
4. Najman, L., & Couprie, M. (2006). *Building the component tree in quasi-linear time*. IEEE Transactions on Image Processing, 15(11), 3531-3539.
   > https://doi.org/10.1109/TIP.2006.877518
5. Carlinet, E., & Geraud, T. (2014). *A Comparative Review of Component Tree Computation Algorithms*. IEEE Transactions on Image Processing, 23(9), 3885-3895.
   > https://doi.org/10.1109/TIP.2014.2336551

# Examples
Creating a test image `f` (quadratic function with a maximum in the center and
4 additional local maxima):
```jldoctest; setup = :(using ImageMorphology)
julia> w = 12;

julia> f = [20 - 0.2*((x - w/2)^2 + (y-w/2)^2) for x in 0:w, y in 0:w];

julia> f[3:4, 2:6] .= 40; f[3:5, 10:12] .= 60; f[10:12, 3:5] .= 80;

julia> f[10:11, 10:12] .= 100; f[11, 11] = 100;

julia> f_aopen = area_opening(f, min_area=8, connectivity=1);
```
The peaks with a surface smaller than 8 are removed.
"""
area_opening

"""
    diameter_opening!(output, image, [maxtree];
                      min_diameter=8, connectivity=1) -> output

Performs in-place *diameter opening* of the `image` and stores the result in `output`.
See [`diameter_opening`](@ref) for the detailed description of the method.
"""
function diameter_opening!(output::GenericGrayImage, image::GenericGrayImage,
                           maxtree::MaxTree; min_diameter=8)
    check_output_image(output, image)
    check_maxtree(maxtree, image, rev=false)
    return filter_components!(output, image, maxtree, diameters(maxtree), min_diameter)
end

"""
    diameter_opening(image, [maxtree]; min_diameter=8, connectivity=1) -> Array

Performs a *diameter opening* of the `image`.

*Diameter opening* replaces all bright structures of an image that have
the diameter (the widest dimension of their bounding box) smaller than
`min_diameter` with the darker value taken from their first ancestral component
(in *max-tree* representation of `image`) that has the diameter no smaller than
`min_diameter`.

The operator is also called *Bounding Box Opening*. In practice,
the result is similar to a *morphological opening*, but long and thin
structures are not removed.

# Arguments
- `image::GenericGrayImage`: the ``N``-dimensional input image
- `min_diameter::Number=8`: the minimal length (in pixels) of the widest
  dimension of the bounding box of the image component to keep intact
- `connectivity::Integer=1`: the neighborhood connectivity. The maximum number
  of orthogonal steps to reach a neighbor of the pixel. In 2D, it is 1 for
  a 4-neighborhood and 2 for a 8-neighborhood.
- `maxtree::MaxTree`: optional pre-built *max-tree*. Note that
  `maxtree` and `connectivity` optional parameters are mutually exclusive.

# Returns
An array of the same type and shape as the `image`.

# See also
[`diameter_opening!`](@ref), [`diameter_closing`](@ref),
[`area_opening`](@ref),
[`MaxTree`](@ref), [`opening`](@ref)

# References
1. Walter, T., & Klein, J.-C. (2002). *Automatic Detection of Microaneurysms in Color Fundus Images of the Human Retina by Means of the Bounding Box Closing*. In A. Colosimo, P. Sirabella, A. Giuliani (Eds.), *Medical Data Analysis. Lecture Notes in Computer Science*, vol 2526, 210-220. Springer Berlin Heidelberg.
   > https://doi.org/10.1007/3-540-36104-9_23
2. Carlinet, E., & Geraud, T. (2014). *A Comparative Review of Component Tree Computation Algorithms*. IEEE Transactions on Image Processing, 23(9), 3885-3895.
   > https://doi.org/10.1109/TIP.2014.2336551

# Examples
Creating a test image `f` (quadratic function with a maximum in the center and
4 additional local maxima):
```jldoctest; setup = :(using ImageMorphology)
julia> w = 12;

julia> f = [20 - 0.2*((x - w/2)^2 + (y-w/2)^2) for x in 0:w, y in 0:w];

julia> f[3:4, 2:6] .= 40; f[3:5, 10:12] .= 60; f[10:12, 3:5] .= 80;

julia> f[10:11, 10:12] .= 100; f[11, 11] = 100;

julia> f_dopen = diameter_opening(f, min_diameter=3, connectivity=1);
```
The peaks with a maximal diameter of 2 or less are removed.
For the remaining peaks the widest side of the bounding box is at least 3.
"""
diameter_opening

"""
    area_closing!(output, image, [maxtree];
                  min_area=64, connectivity=1) -> output

Performs in-place *area closing* of the `image` and stores the result in `output`.
See [`area_closing`](@ref) for the detailed description of the method.
"""
function area_closing!(output::GenericGrayImage, image::GenericGrayImage,
                       maxtree::MaxTree; min_area::Number=64)
    check_output_image(output, image)
    check_maxtree(maxtree, image, rev=true)
    return filter_components!(output, image, maxtree, areas(maxtree), min_area)
end

"""
    area_closing(image, [maxtree]; min_area=64, connectivity=1) -> Array

Performs an *area closing* of the `image`.

*Area closing* replaces all dark components of an image that
have a surface smaller than `min_area` with the brighter value taken from
their first ancestral component (in *max-tree* representation of `image`)
that has the area no smaller than `min_area`.

# Details
*Area closing* is the dual operation to *area opening* (see [`area_opening`](@ref)).
It is similar to morphological closings (see [`closing`](@ref)),
but instead of using a fixed structuring element (e.g. disk) it employs
small (less than `min_area`) components of the *max-tree*. Consequently,
the `area_closing` with `min_area = 1` is the identity transformation.

In the binary case, area closing is equivalent to
`remove_small_holes`; this operator is thus extended to gray-level images.

# Arguments
- `image::GenericGrayImage`: the ``N``-dimensional input image
- `min_area::Number=64`: the smallest size (in pixels) of the image component
  to keep intact
- `connectivity::Integer=1`: the neighborhood connectivity. The maximum number
  of orthogonal steps to reach a neighbor of the pixel. In 2D, it is 1 for
  a 4-neighborhood and 2 for a 8-neighborhood.
- `maxtree::MaxTree`: optional pre-built *max-tree*. Note that
  `maxtree` and `connectivity` optional parameters are mutually exclusive.

# Returns
An array of the same type and shape as the `image`.

# See also
[`area_closing!`](@ref), [`area_opening`](@ref),
[`diameter_closing`](@ref),
[`MaxTree`](@ref), [`closing`](@ref)

# References
1. Vincent, L. (1993). *Grayscale area openings and closings, their efficient implementation and applications*, Proc. of EURASIP Workshop on Mathematical Morphology and its Applications to Signal Processing, Barcelona, Spain, 22-27
2. Soille, P. (2003). Chapter 6 *Geodesic Metrics* of *Morphological Image Analysis: Principles and Applications*, 2nd edition, Springer.
   > https://doi.org/10.1007/978-3-662-05088-0
3. Salembier, P., Oliveras, A., & Garrido, L. (1998). *Antiextensive Connected Operators for Image and Sequence Processing*. IEEE Transactions on Image Processing, 7(4), 555-570.
   > https://doi.org/10.1109/83.663500
4. Najman, L., & Couprie, M. (2006). *Building the component tree in quasi-linear time*. IEEE Transactions on Image Processing, 15(11), 3531-3539.
   > https://doi.org/10.1109/TIP.2006.877518
5. Carlinet, E., & Geraud, T. (2014). *A Comparative Review of Component Tree Computation Algorithms*. IEEE Transactions on Image Processing, 23(9), 3885-3895.
   > https://doi.org/10.1109/TIP.2014.2336551

# Examples
Creating a test image `f` (quadratic function with a minimum in the center and
4 additional local minima):
```jldoctest; setup = :(using ImageMorphology)
julia> w = 12;

julia> f = [180 + 0.2*((x - w/2)^2 + (y-w/2)^2) for x in 0:w, y in 0:w];

julia> f[3:4, 2:6] .= 40; f[3:5, 10:12] .= 60; f[10:12, 3:5] .= 80;

julia> f[10:11, 10:12] .= 100; f[11, 11] = 100;

julia> f_aclose = area_closing(f, min_area=8, connectivity=1);
```
All small minima are removed, and the remaining minima have at least
a size of 8.
"""
area_closing

"""
    diameter_closing!(output, image, [maxtree];
                      min_diameter=8, connectivity=1) -> output

Performs in-place *diameter closing* of the `image` and stores the result in `output`.
See [`diameter_closing`](@ref) for the detailed description of the method.
"""
function diameter_closing!(output::GenericGrayImage, image::GenericGrayImage,
                           maxtree::MaxTree; min_diameter::Number=8)
    check_output_image(output, image)
    check_maxtree(maxtree, image, rev=true)
    return filter_components!(output, image, maxtree, diameters(maxtree), min_diameter)
end

"""
    diameter_closing(image, [maxtree]; min_diameter=8, connectivity=1) -> Array

Performs a *diameter closing* of the `image`.

*Diameter closing* replaces all dark structures of an image that have
the diameter (the widest dimension of their bounding box) smaller than
`min_diameter` with the brighter value taken from their first ancestral component
(in *max-tree* representation of `image`) that has the diameter no smaller than
`min_diameter`.

# Arguments
- `image::GenericGrayImage`: the ``N``-dimensional input image
- `min_diameter::Number=8`: the minimal length (in pixels) of the widest
  dimension of the bounding box of the image component to keep intact
- `connectivity::Integer=1`: the neighborhood connectivity. The maximum number
  of orthogonal steps to reach a neighbor of the pixel. In 2D, it is 1 for
  a 4-neighborhood and 2 for a 8-neighborhood.
- `maxtree::MaxTree`: optional pre-built *max-tree*. Note that
  `maxtree` and `connectivity` optional parameters are mutually exclusive.

# Returns
An array of the same type and shape as the `image`.

# See also
[`diameter_closing!`](@ref), [`diameter_opening`](@ref),
[`area_closing`](@ref),
[`MaxTree`](@ref), [`closing`](@ref)

# References
1. Walter, T., & Klein, J.-C. (2002). *Automatic Detection of Microaneurysms in Color Fundus Images of the Human Retina by Means of the Bounding Box Closing*. In A. Colosimo, P. Sirabella, A. Giuliani (Eds.), *Medical Data Analysis. Lecture Notes in Computer Science*, vol 2526, 210-220. Springer Berlin Heidelberg.
   > https://doi.org/10.1007/3-540-36104-9_23
2. Carlinet, E., & Geraud, T. (2014). *A Comparative Review of Component Tree Computation Algorithms*. IEEE Transactions on Image Processing, 23(9), 3885-3895.
   > https://doi.org/10.1109/TIP.2014.2336551

# Examples
Creating a test image `f` (quadratic function with a minimum in the center and
4 additional local minima):
```jldoctest; setup = :(using ImageMorphology)
julia> w = 12;

julia> f = [180 + 0.2*((x - w/2)^2 + (y-w/2)^2) for x in 0:w, y in 0:w];

julia> f[3:4, 2:6] .= 40; f[3:5, 10:12] .= 60; f[10:12, 3:5] .= 80;

julia> f[10:11, 10:12] .= 100; f[11, 11] = 100;

julia> f_dclose = diameter_closing(f, min_diameter=3, connectivity=1);
```
All small minima with a diameter of 2 or less are removed.
For the remaining minima the widest bounding box side is at least 3.
"""
diameter_closing

# Calculates the local maxima or minima of the image using the max-tree
# representation (maxima if maxtree.rev=false, minima otherwise).
# It's not the most effecient method for extrema calculation, but could be
# useful if the max-tree is already calculated.
# Each minima/maxima region is labeled with the unique id (1 is the global
# maximum/minimum).
function local_extrema!(output::GenericGrayImage,
                        image::GenericGrayImage,
                        maxtree::MaxTree)
    (size(image) == size(maxtree)) || throw(DimensionMismatch())
    (size(output) == size(image)) || throw(DimensionMismatch())

    next_label = oneunit(eltype(output))
    fill!(output, next_label) # initialize output (just has to be non-zero)
    @inbounds for p in Iterators.reverse(maxtree.traverse)
        q = maxtree.parentindices[p]
        # if p is canonical (parent has a different value)
        if image[p] != image[q]
            output[q] = 0
            # if output[p] was the parent of some other canonical
            # pixel, it has been set to zero. Only the leaves
            # (local maxima) are thus > 0.
            if output[p] > 0
                output[p] = next_label
                next_label += 1
            end
        end
    end

    @inbounds for p in Iterators.reverse(maxtree.traverse)
        q = maxtree.parentindices[p]
        # if p is not canonical (parent has the same value)
        if image[p] == image[q]
            # in this case we propagate the value
            output[p] = output[q]
        end
    end

    return output
end

"""
    local_maxima!(output, image, [maxtree]; connectivity=1) -> output

Detects the local maxima of `image` and stores the result in `output`.
See [`local_maxima`](@ref) for the detailed description of the method.
"""
function local_maxima!(output::GenericGrayImage, image::GenericGrayImage,
                       maxtree::MaxTree)
    check_output_image(output, image)
    check_maxtree(maxtree, image, rev=false)
    return local_extrema!(output, image, maxtree)
end

"""
    local_maxima(image, [maxtree]; connectivity=1) -> Array

Determines and labels all *local maxima* of the `image`.

# Details
The *local maximum* is defined as the connected set of pixels that have the
same value, which is greater than the values of all pixels in direct
neighborhood of the set.

Technically, the implementation is based on the *max-tree* representation
of an image. It's beneficial if the max-tree is already computed,
otherwise [`Images.findlocalmaxima`](@ref) would be more efficient.

# Arguments
- `image::GenericGrayImage`: the ``N``-dimensional input image
- `connectivity::Integer=1`: the neighborhood connectivity.
  The maximum number of orthogonal steps to reach a neighbor of the pixel.
  In 2D, it is 1 for a 4-neighborhood and 2 for a 8-neighborhood.
- `maxtree::MaxTree`: optional pre-built *max-tree*. Note that
  `maxtree` and `connectivity` optional parameters are mutually exclusive.

# Returns
An integer array of the same shape as the `image`. Pixels that are not local
maxima have 0 value. Pixels of the same local maximum share the same positive
value (the local maximum id).

# See also
[`MaxTree`](@ref), [`local_maxima!`](@ref), [`local_minima`](@ref),
[`Images.findlocalmaxima`](@ref)

# Examples
Create `f` (quadratic function with a maximum in the center and
4 additional constant maxima):
```jldoctest; setup = :(using ImageMorphology)
julia> w = 10;

julia> f = [20 - 0.2*((x - w/2)^2 + (y-w/2)^2) for x in 0:w, y in 0:w];

julia> f[3:5, 3:5] .= 40; f[3:5, 8:10] .= 60; f[8:10, 3:5] .= 80; f[8:10, 8:10] .= 100;

julia> f_maxima = local_maxima(f); # Get all local maxima of `f`
```
The resulting image contains the 4 labeled local maxima.
"""
local_maxima

"""
    local_minima!(output, image, [maxtree]; connectivity=1) -> output

Detects the local minima of `image` and stores the result in `output`.
See [`local_minima`](@ref) for the detailed description of the method.
"""
function local_minima!(output::GenericGrayImage, image::GenericGrayImage,
                       maxtree::MaxTree; connectivity::Integer=1)
    check_output_image(output, image)
    check_maxtree(maxtree, image, rev=true)
    return local_extrema!(output, image, maxtree)
end

"""
    local_minima(image, [maxtree]; connectivity=1) -> Array

Determines and labels all *local minima* of the `image`.

# Details
The *local minimum* is defined as the connected set of pixels that have the
same value, which is less than the values of all pixels in direct
neighborhood of the set.

Technically, the implementation is based on the *max-tree* representation
of an image. It's beneficial if the max-tree is already computed,
otherwise [`Images.findlocalminima`](@ref) would be more efficient.

# Arguments
- `image::GenericGrayImage`: the ``N``-dimensional input image
- `connectivity::Integer=1`: the neighborhood connectivity. The maximum number
  of orthogonal steps to reach a neighbor of the pixel. In 2D, it is 1 for
  a 4-neighborhood and 2 for a 8-neighborhood.
- `maxtree::MaxTree`: optional pre-built *max-tree*. Note that
  `maxtree` and `connectivity` optional parameters are mutually exclusive.

# Returns
An integer array of the same shape as the `image`. Pixels that are not local
minima have 0 value. Pixels of the same local minimum share the same positive
value (the local minimum id).

# See also
[`MaxTree`](@ref), [`local_minima!`](@ref), [`local_maxima`](@ref),
[`Images.findlocalminima`](@ref)

# Examples
Create `f` (quadratic function with a minimum in the center and
4 additional constant minimum):
```jldoctest; setup = :(using ImageMorphology)
julia> w = 10;

julia> f = [180 + 0.2*((x - w/2)^2 + (y-w/2)^2) for x in 0:w, y in 0:w];

julia> f[3:5, 3:5] .= 40; f[3:5, 8:10] .= 60; f[8:10, 3:5] .= 80; f[8:10, 8:10] .= 100;

julia> f_minima = local_minima(f); # Calculate all local minima of `f`
```
The resulting image contains the labeled local minima.
"""
local_minima

# autogenerate in-place maxtree operations with "connectivity" keyarg
for (fun, rev) in [(:area_opening!, false), (:area_closing!, true),
                   (:diameter_opening!, false), (:diameter_closing!, true),
                   (:local_maxima!, false), (:local_minima!, true)]
    @eval begin
        $fun(output::GenericGrayImage, image::GenericGrayImage;
             connectivity::Integer=1, kwargs...) =
            $fun(output, image,
                 MaxTree(image, connectivity=connectivity, rev=$rev); kwargs...)
    end
end

# autogenerate maxtree operations (simple wrappers of in-place methods)
for fun in [:area_opening, :area_closing,
            :diameter_opening, :diameter_closing,
            :local_maxima, :local_minima]
    fun! = Symbol(fun, "!")
    @eval begin
        $fun(image::GenericGrayImage; kwargs...) =
             $fun!(similar(image), image; kwargs...)
        $fun(image::GenericGrayImage, maxtree::MaxTree; kwargs...) =
             $fun!(similar(image), image, maxtree; kwargs...)
    end
end


# Inspired by the Mods.jl package

module Modulos

import Base: ==, +, -, *, inv, /, ^, hash, show, unsigned
import Base.Checked: mul_with_overflow

export Modulo, is_invertible

"""
`Modulo{p,T}(n)` creates a modular number in mod `p` with value `n%p` represented
by the integer type `T`.
Signed `T` will yield better performance.
"""
struct Modulo{p,T<:Integer} <: Real
    value::T
    function Modulo{p,T}(n::Integer) where {p,T}
        @assert p > 0 # This check is elided at runtime since p is a compile-time constant
        return new{p,T}(mod(n, p))
    end
end

(::Type{T})(x::Modulo) where {T<:Integer} = T(x.value)

function hash(x::Modulo{p}, h::UInt64=UInt64(0)) where p
    hash(Integer(x), hash(p, h))
end

==(x::Modulo{p}, y::Modulo{p}) where {p} = Integer(x) == Integer(y)

function +(x::Modulo{p,T1}, y::Modulo{p,T2}) where {p,T1,T2}
    T = promote_type(T1,T2)
    if T1 != T2
        tmin = typemin(T)
        tmax = typemax(T)
        if !(tmin <= typemin(T1) && tmin <= typemin(T2) &&
             tmax >= typemax(T1) && tmax >= typemax(T2))
            T = widen(T)
        end
    end
    # Next are some fast-paths if p is small enough.
    # Note that the checks are elided at runtime since p and T are compile-time constants
    if p < typemax(T) ÷ 2
        Modulo{p,T}((Integer(x) % T) + (Integer(y) % T))
    elseif p < typemax(unsigned(T)) ÷ 2
        U = unsigned(T)
        Modulo{p,T}((Integer(x) % U) + (Integer(y) % U))
    else
        V = widen(T)
        Modulo{p,T}((Integer(x) % V) + (Integer(y) % V))
    end
end

function -(x::Modulo{p,T}) where {p,T<:Signed}
    Modulo{p,T}(-Integer(x))
end
function -(x::Modulo{p,T}) where {p,T<:Unsigned}
    U = signed(T)
    y = Integer(x)
    y > (typemax(U) % T) ? Modulo{p,T}(-(y % widen(U))) : Modulo{p,T}(-signed(y))
end

-(x::Modulo, y::Modulo) = x + (-y)

function *(x::Modulo{p,T1}, y::Modulo{p,T2}) where {p,T1,T2}
    T = promote_type(T1,T2)
    if T1 != T2
        tmin = typemin(T)
        tmax = typemax(T)
        if !(tmin <= typemin(T1) && tmin <= typemin(T2) &&
             tmax >= typemax(T1) && tmax >= typemax(T2))
            T = widen(T)
        end
    end
    r, flag = mul_with_overflow((Integer(x) % T), (Integer(y) % T))
    flag ? Modulo{p,T}(widemul(Integer(x), Integer(y))) : Modulo{p,T}(r)
end

# """
# `is_invertible(x::Modulo)` determines if `x` is invertible.
# """
# is_invertible(x::Modulo{p}) where {p} = return p>1 && isone(gcd(Integer(x), p))

@noinline __throw_notinvertible(x) = error(x, " is not invertible")
inv(x::Modulo{1}) = __throw_notinvertible(x)
function inv(x::Modulo{p,T}) where {p,T}
    g, v, _ = gcdx(Integer(x), p)
    g == 1 || __throw_notinvertible(x)
    return Modulo{p,T}(v)
end

function /(x::Modulo{p}, y::Modulo{p}) where {p}
    return x * inv(y)
end

function ^(x::Modulo{p,T}, k::Integer) where {p,T}
    if k>0
        return Modulo{p,T}(powermod(Integer(x), k, p))
    end
    if k==0
        return one(Modulo{p})
    end
    Modulo{p,T}(powermod(Integer(inv(x)), -k, p))
end

+(x::Modulo{p,T}, k::Integer) where {p,T} = x + Modulo{p,T}(k)
+(k::Integer, x::Modulo) = x + k

-(x::Modulo, k::Integer) = x + (-k)
-(k::Integer, x::Modulo) = (-x) + k

*(x::Modulo{p,T}, k::Integer) where {p,T} = x * Modulo{p,T}(k)
*(k::Integer, x::Modulo) = x * k

/(x::Modulo{p,T}, k::Integer) where {p,T} = x / Modulo{p,T}(k)
/(k::Integer, x::Modulo{p,T}) where {p,T} = Modulo{p,T}(k) / x

function ==(x::Modulo{p,T1}, k::T2) where {p,T1,T2<:Integer}
    T = promote_type(T1,T2)
    T(x) == T(mod(k, p))
end
==(k::Integer, x::Modulo) = x == k

function show(io::IO, x::Modulo{p,T}) where {p,T}
    verbose = get(io, :typeinfo, Any) != Modulo{p,T}
    if verbose
        print(io, Modulo{p,T}, '(')
    end
    print(io, x.value)
    if verbose
        print(io, ')')
    end
end

end # end of module Modulos


using Random, Test
using TupleLabXD

using Base: tail, front

n = 10

p = randperm(n)
ip = invperm(p)

t = (p...,)

@test @inferred(TupleLabXD.argtail2(1, 2, 3, 4)) == (3, 4)
@test @inferred(TupleLabXD.tail2(t)) == t[3:n]
@test @inferred(TupleLabXD.unsafe_tail(t)) == t[2:n]
@test @inferred(TupleLabXD.unsafe_front(t)) == t[1:n-1]
@test @inferred(TupleLabXD.unsafe_tail(())) == ()
@test @inferred(TupleLabXD.unsafe_front(())) == ()
@test @inferred(TupleLabXD.vcat()) == ()
@test @inferred(TupleLabXD.vcat((1, 2))) == (1, 2)
@test @inferred(TupleLabXD.getindices(t, ())) == ()
@test @inferred(TupleLabXD.getindices(t, (1,2,3))) == t[1:3]

@test @inferred(TupleLabXD.deleteat((1, 2), (1, ))) == (2, )
for i = 1:n
    @test @inferred(TupleLabXD.deleteat(t, i)) == (deleteat!(copy(p), i)...,)
    @test @inferred(TupleLabXD.insertat(t, i, (1,2,3))) == (vcat(p[1:i-1], [1,2,3], p[i+1:n])...,)
end
@test @inferred(TupleLabXD.deleteat((1,2,3,4,5,6), (3,1,5))) == (2,4,6)
@test_throws BoundsError TupleLabXD.deleteat(t, 0)
@test_throws BoundsError TupleLabXD.deleteat(t, n+1)
@test_throws BoundsError TupleLabXD.insertat(t, 0, (1,2,3))
@test_throws BoundsError TupleLabXD.insertat(t, n+1, (1,2,3))
for i = 0:n
    @test @inferred(TupleLabXD.insertafter(t, i, (1,2,3))) == (vcat(p[1:i], [1,2,3], p[i+1:n])...,)
end
@test_throws BoundsError TupleLabXD.insertafter(t, -1, (1,2,3))
@test_throws BoundsError TupleLabXD.insertafter(t, n+1, (1,2,3))
@test @inferred(TupleLabXD.vcat((1,2,3),4,(5,),(),(6,7,8))) == (1,2,3,4,5,6,7,8)
@test @inferred(TupleLabXD.flatten((1,2,3),4,(5,),(),(6,7,8))) == (1,2,3,4,5,6,7,8)
@test @inferred(TupleLabXD.flatten((1,(2,3)),4,(5,),(),((6,),(7,(8,))))) == (1,2,3,4,5,6,7,8)

@test @inferred(TupleLabXD.sum(())) == 0
@test @inferred(TupleLabXD.sum((1, ))) == 1
@test @inferred(TupleLabXD.sum(t)) == sum(t)
@test @inferred(TupleLabXD.cumsum(())) == ()
@test @inferred(TupleLabXD.cumsum((1, ))) == (1, )
@test @inferred(TupleLabXD.cumsum(t)) == (cumsum(p)...,)
@test @inferred(TupleLabXD.prod(())) == 1
@test @inferred(TupleLabXD.prod((2, ))) == 2
@test @inferred(TupleLabXD.prod(t)) == prod(t)
@test @inferred(TupleLabXD.cumprod(())) == ()
@test @inferred(TupleLabXD.cumprod((1, ))) == (1, )
@test @inferred(TupleLabXD.cumprod(t)) == (cumprod(p)...,)

for a in (t, (1, ))
    @test @inferred(TupleLabXD.findmin(a)) == findmin(a)
    @test @inferred(TupleLabXD.findmax(a)) == findmax(a)
    @test @inferred(TupleLabXD.minimum(a)) == minimum(a)
    @test @inferred(TupleLabXD.maximum(a)) == maximum(a)
    @test @inferred(TupleLabXD.argmin(a)) == argmin(a)
    @test @inferred(TupleLabXD.argmax(a)) == argmax(a)
end

@test @inferred(TupleLabXD.sort((1, ))) == (1, )
@test @inferred(TupleLabXD.sort(())) == ()
@test @inferred(TupleLabXD.sort(t; rev = true)) == (sort(p; rev = true)...,)
@test @inferred(TupleLabXD.sort(t; rev = false)) == (sort(p; rev = false)..., )
@test @inferred(TupleLabXD.sortperm(t)) == (sortperm(p)...,)
@test @inferred(TupleLabXD.sortperm(t; rev=true)) == (sortperm(p; rev=true)..., )
@test @inferred(TupleLabXD.invperm(t)) == (ip...,)
@test @inferred(TupleLabXD.isperm(t)) == true
@test @inferred(TupleLabXD.isperm((1,2,1))) == false
@test @inferred(TupleLabXD.permute(t, t)) == (p[p]...,)

@test @inferred(TupleLabXD.vcat()) == ()
@test @inferred(TupleLabXD.diff(())) == ()
@test @inferred(TupleLabXD.diff((1, ))) == ()
@test @inferred(TupleLabXD.diff((1, 2, 3))) == (1, 1)


@testset "TaylorExpansion" begin
    s0 = [0.2, 0.4, 1.1]
    x0 = [1.2, 0.9]

    N = 1000
    points = rand(N, 3)

    X_s = rand(2, 3)
    X_ss = rand(2, 3, 3)
    X_sss = rand(2, 3, 3, 3)
    dr1 = TaylorExpansion(s0, x0, X_s)
    dr2 = TaylorExpansion(s0, x0, X_s, X_ss)
    dr3 = TaylorExpansion(s0, x0, X_s, X_ss, X_sss)

    out1 = dr1(points)
    out2 = dr2(points)
    out3 = dr3(points)

    out1_1d = dr1(vec(points[1, :]))
    out2_1d = dr2(vec(points[1, :]))
    out3_1d = dr3(vec(points[1, :]))

    @test maximum(abs, out1_1d - vec(out1[1, :])) == 0.0
    @test maximum(abs, out2_1d - vec(out2[1, :])) == 0.0
    @test maximum(abs, out3_1d - vec(out3[1, :])) == 0.0

    ds = points .- s0'
    verif1 = x0 .+ X_s*ds'
    @test maximum(abs, out1 - verif1') < 1e-12

    # TODO: write verification tests for 2d and 3d
end


macro isbn_str(s)
    ISBN(s)
end

struct ISBN <: AbstractString
    s::AbstractString

    ISBN(s) = verify(s) ? new(replace(s, "-", "")) : throw(ArgumentError("invalid ISBN string: $s"))
end

string(s::ISBN) = s.s

function verify(s::AbstractString)
    s = replace(s, "-", "")
    chars = split(s, "")

    length(chars) == 10 || return false

    if last(chars) == "X"
        chars[end] = "10"
    end

    all(c -> all(isdigit, c), chars) || return false

    sum(parse(Int, c) * i for (c, i) in zip(chars, 10:-1:1)) % 11 == 0
end

Base.isvalid(::Type{ISBN}, s::AbstractString) = verify(s)