using CRF

include("util.jl")
include("features.jl")

const output_succ = colorize(:green, ".")
const output_fail = colorize(:red, "!")

# These parameters were estimated using sequences 1:5 of the weather data
const weights = [
    -0.28026, -0.54288,  0.04901, -0.25603,  1.09337,  0.49870,
     0.13389, -0.23739, -0.24375, -1.08821, -0.06286,  0.89308,
    -0.37372,  0.38654,  0.38167, -3.53123,  1.76168,  3.07147,
    -1.50942,  0.51421, -4.12856, -0.64580,  4.74698, -0.31943,
     2.64340, -0.58929, -2.62894,  0.10829,  0.18280, -0.54266,
    -0.47681,  2.13713, -1.66658, -2.48922,  1.82307, -0.30672,
     1.82972,  0.71780, -2.07108, -0.51137,  0.60169, -0.13208,
    -1.85513,  0.43814, -1.48759,  0.66490,  2.30342,  0.42272,
     1.62713,  0.70396, -1.13422,  3.62780,  4.14362,  1.62987,
    -0.85902, -3.39346, -3.32714, -2.12237, -1.70612, -0.31701,
     0.28682, -0.53939, -3.27933, -4.02713, -2.32070,  1.53830,
     2.50143,  1.38448,  1.65289, -0.06920,  0.63677,  0.05326,
     1.14765, -0.65981, -0.98722, -0.32634, -0.84214,  0.73448,
     1.06528,  0.95661,  0.84304, -0.59334, -0.36241, -0.66310,
     1.45682,  6.45608,  1.91501,  4.23405,  3.87607, -0.46321,
    -1.01568, -2.34954, -1.60510, -0.80973, -2.09774, -0.23512,
    -0.96984,  0.29281, -1.69349, -1.77548, -0.18955, -1.13785,
     0.13768, -2.34054,  0.25428, -1.02766, -0.30154, -0.35736,
    -0.48482,  0.05892, -0.01486, -0.04475,  1.87549, -4.50467,
    -1.51220, -0.85258, -1.91109,  3.69303,  1.07203,  4.04240,
     1.01121,  2.16412, -0.29784,  0.23954, -0.66587,  0.89715,
    -0.31191, -0.08533, -0.15711, -1.60186, -0.02909, -0.69339,
    -0.40224, -0.50869,  0.00055, -0.99869,  0.05103, -0.60559,
     0.44725,  0.13268, -2.81193, -1.61552, -1.24948, -2.67640,
    -1.28247, -2.80358, -0.34114, -0.74070,  1.51590, -1.77853,
     1.70655, -0.54594,  1.30247, -0.46603,  2.28510,  1.73859,
    -0.34424,  2.82764, -0.23264,  3.28881, -0.02248,  2.36526,
     0.10379,  1.30489,  0.37751,  0.86419,  0.42223,  0.25484,
]

# Load weather data
X, Y = load("weather.csv")

# Remove sequences we used for parameter estimation
splice!(X, 1:5)
splice!(Y, 1:5)

# Create sequences
crfs = Sequence[ Sequence(x, features, Θ=weights, labels=labels) for x in X ]

# Calling label on the array of sequences does the trick
Y_true = Y
Y_pred = label(crfs)

# Let's compare the predicted labels with the hidden labels
error = 0
total = 0
for (y_true, y_pred) in zip(Y_true, Y_pred)
    for (lt, lp) in zip(y_true, y_pred)
        print((lt == lp) ? output_succ : output_fail)
        total += 1
        error += (lt != lp)
    end
    print(" ")
end
print("\n")

@printf "Total: %6d\n" total
@printf "Error: %6d (%.2f %%)\n" error ((error / total) * 100.0)


module VTK

using ...TopOptProblems: StiffnessTopOptProblem, JuAFEM
using WriteVTK

export  save_mesh

function save_mesh(filename, problem::StiffnessTopOptProblem)
    topology = ones(getncells(TopOptProblems.getdh(problem).grid))
    vtkfile = WriteVTK.vtk_grid(filename, problem, topology)
    outfiles = WriteVTK.vtk_save(vtkfile)
end
function WriteVTK.vtk_grid(filename::AbstractString, problem::StiffnessTopOptProblem{dim, T}, topology::AbstractVector) where {dim, T}
    grid = problem.ch.dh.grid
    celltype = JuAFEM.cell_to_vtkcell(JuAFEM.getcelltype(grid))
    cls = JuAFEM.MeshCell[]
    for (i, cell) in enumerate(JuAFEM.CellIterator(grid))
        if topology[i] >= 0.5
            push!(cls, JuAFEM.MeshCell(celltype, copy(JuAFEM.getnodes(cell))))
        end
    end
    coords = reshape(reinterpret(T, JuAFEM.getnodes(grid)), (dim, JuAFEM.getnnodes(grid)))
    return vtk_grid(filename, coords, cls)
end

function save_mesh(filename, problem, solver)
    save_mesh(filename, problem, solver.vars)
end
function save_mesh(filename, alg)
    problem = alg.obj.problem
    vars = alg.optimizer.obj.solver.vars
    save_mesh(filename, problem, vars)
end
function save_mesh(filename, problem, vars::AbstractVector)
    vtkfile = WriteVTK.vtk_grid(filename, problem, vars)
    outfiles = WriteVTK.vtk_save(vtkfile)
end
function WriteVTK.vtk_grid(filename::AbstractString, problem::StiffnessTopOptProblem{dim, T}, vars::AbstractVector{T}) where {dim, T}
    varind = problem.varind
    black = problem.black
    white = problem.white
    grid = problem.ch.dh.grid

    celltype = JuAFEM.cell_to_vtkcell(JuAFEM.getcelltype(grid))
    cls = JuAFEM.MeshCell[]
    for (i, cell) in enumerate(JuAFEM.CellIterator(grid))
        if black[i]
            push!(cls, JuAFEM.MeshCell(celltype, copy(JuAFEM.getnodes(cell))))
        elseif !white[i]
            if vars[varind[i]] >= 0.5
                push!(cls, JuAFEM.MeshCell(celltype, copy(JuAFEM.getnodes(cell))))
            end
        end
    end
    coords = reshape(reinterpret(T, JuAFEM.getnodes(grid)), (dim, JuAFEM.getnnodes(grid)))
    return vtk_grid(filename, coords, cls)
end

end


"""
This `AeolianSources` module is to create aeolian sources for use in AIBECS-generated models.
"""
module AeolianSources

using SparseArrays          # For sparse matrix
using DataDeps              # For storage location of data
using Downloads
using NCDatasets
using Unitful               # for units
using Reexport
using Interpolations
using StringDistances
@reexport using OceanGrids            # To store the grid

function fallback_download(remotepath, localdir)
    @assert(isdir(localdir))
    filename = basename(remotepath)  # only works for URLs with filename as last part of name
    localpath = joinpath(localdir, filename)
    Downloads.download(remotepath, localpath)
    return localpath
end

# Fuzzy name matching
const DATASET_NAMES = ("Chien", "Kok")
const DATASET_LONGNAMES = Dict(
    "Chien" => "Chien_etal_2016",
    "Kok"   => "Kok_etal_2021"
)
rename(s) = findnearest(s, DATASET_NAMES, Levenshtein())[1]
renamelong(s) = DATASET_LONGNAMES[rename(s)]

# Aeolian source URLs
const URLs = Dict(
    "Chien" => "http://www.geo.cornell.edu/eas/PeoplePlaces/Faculty/mahowald/dust/Chienetal2016/post.aerosols.2x2.seasonal.nc",
    "Kok" => "https://dustcomm.atmos.ucla.edu/data/K21b/DustCOMM_source_region_wetdep_annual_PM20_abs.nc"
)
url(s) = URLs[rename(s)]

# Hashes
const SHAs = Dict(
    "Chien" => "33b0036bcca87019e0636ec2d59e910be989693819a2d57f2e4ab053a564101f", # On  2 Jun 2020
    "Kok"   => "65fec31419f75064a22a2c65247afc48e1f72218753605b463447a9933c378aa"  # On 14 Jul 2021
)
sha(s) = SHAs[rename(s)]

datadepname(s) = string("AIBECS-", renamelong(s))

# Create registry entry
function register_AeolianSource(s="Chien")
    register(
        DataDep(
            datadepname(s),
            """
            References:

            $(citations(s))
            """,
            url(s),
            sha(s),
            fetch_method = fallback_download
        )
    )
    return nothing
end

"""
    load()

Returns the 2D aeolian deposition fields from Chien et al. (2016).

You can specify a different dataset via, e.g.,

```julia
load("Kok")
```

At this stage, only two datasets are available:
- `"Chien"` (default) for different dust types (fires, biofuels, dust, ...)
- `"Kok"` for dust from different regions of origin
"""
load(s="Chien") = rename(s) == "Chien" ? load_Chien() : load_Kok()

#=============================#
#     Chien et al data        #
#=============================#
const Chien_AEROSOLTYPE_NAMES = (
    :fire,    # Fires
    :bioF,    # Biofuels
    :FF,      # FossilFuel
    :dust,    # Dust
    :salt,    # Seasalts
    :bioP,    # PrimaryBiogenicParticles
    :volc,    # Volcanoes
)
function load_Chien()
    s = "Chien"
    register_AeolianSource(s)
    nc_file = @datadep_str joinpath(datadepname(s), "post.aerosols.2x2.seasonal.nc")
    s_A_2D = Dataset(nc_file, "r") do ds
        Dict(
            :lat => ds["lat"][:],
            :lon => ds["lon"][:],
            (t => ds["dep"][:,:,:,i] for (i,t) in enumerate(Chien_AEROSOLTYPE_NAMES))...
        )
    end # ds is closed
    @info """You are about to use the Chien et al. (2016) data for aeolian deposition.
          If you use it for research, please cite:

          $(citations(s))

          You can find the corresponding BibTeX entries in the CITATION.bib file
          at the root of the AIBECS.jl package repository.
          (Look for the $(renamelong(s)) key.)
          """
    return s_A_2D
end

#=============================#
#       Kok et al data        #
#=============================#
const Kok_REGIONS_NAMES = (
    :NWAf,    # western North Africa
    :NEAf,    # eastern North Africa
    :Sahel,   # southern Sahara and Sahel
    :MECA,    # Middle East and central Asia
    :EAsia,   # East Asia
    :NAm,     # North America
    :Aus,     # Australia
    :SAm,     # South America
    :SAf,     # southern Africa
)

function load_Kok()
    s = "Kok"
    register_AeolianSource(s)
    nc_file = @datadep_str joinpath(datadepname(s), "DustCOMM_source_region_wetdep_annual_PM20_abs.nc")
# Units                = Total deposition (kg/m2/year)
    s_A_2D = Dataset(nc_file, "r") do ds
        Dict(
            :lat => ds["lat"][:],
            :lon => ds["lon"][:],
            (r => ds["Mean"][i,:,:] for (i,r) in enumerate(Kok_REGIONS_NAMES))...
        )
    end # ds is closed
    @info """You are about to use the Kok et al. (2021) data for aeolian deposition.
          If you use it for research, please cite:

          $(citations(s))

          You can find the corresponding BibTeX entries in the CITATION.bib file
          at the root of the AIBECS.jl package repository.
          (Look for the $(renamelong(s)) key.)
          """
    return s_A_2D
end

#=============================#
#          Citations          #
#=============================#
const CITATIONS = Dict(
    "Chien" => "- Chien, C.-T., K. R. M. Mackey, S. Dutkiewicz, N. M. Mahowald, J. M. Prospero, and A. Paytan (2016), Effects of African dust deposition on phytoplankton in the western tropical Atlantic Ocean off Barbados, Global Biogeochem. Cycles, 30, doi:10.1002/2015GB005334.",
    "Kok" => """
    - Description: Kok, J. F., Adebiyi, A. A., Albani, S., Balkanski, Y., Checa-Garcia, R., Chin, M., Colarco, P. R., Hamilton, D. S., Huang, Y., Ito, A., Klose, M., Li, L., Mahowald, N. M., Miller, R. L., Obiso, V., Pérez García-Pando, C., Rocha-Lima, A., and Wan, J. S.: Contribution of the world's main dust source regions to the global cycle of desert dust, Atmos. Chem. Phys., 21, 8169–8193, https://doi.org/10.5194/acp-21-8169-2021, 2021.
    - Dataset: Adebiyi, A. A., Kok, J. F., Wang, Y., Ito, A., Ridley, D. A., Nabat, P., and Zhao, C.: Dust Constraints from joint Observational-Modelling-experiMental analysis (DustCOMM): comparison with measurements and model simulations, Atmos. Chem. Phys., 20, 829–863, https://doi.org/10.5194/acp-20-829-2020, 2020.
    - Source apportionment framework: Kok, J. F., Adebiyi, A. A., Albani, S., Balkanski, Y., Checa-Garcia, R., Chin, M., Colarco, P. R., Hamilton, D. S., Huang, Y., Ito, A., Klose, M., Leung, D. M., Li, L., Mahowald, N. M., Miller, R. L., Obiso, V., Pérez García-Pando, C., Rocha-Lima, A., Wan, J. S., and Whicker, C. A.: Improved representation of the global dust cycle using observational constraints on dust properties and abundance, Atmos. Chem. Phys., 21, 8127–8167, https://doi.org/10.5194/acp-21-8127-2021, 2021.
    - Climate model used: Scanza, R. A., Hamilton, D. S., Perez Garcia-Pando, C., Buck, C., Baker, A., and Mahowald, N. M.: Atmospheric processing of iron in mineral and combustion aerosols: development of an intermediate-complexity mechanism suitable for Earth system models, Atmos. Chem. Phys., 18, 14175–14196, https://doi.org/10.5194/acp-18-14175-2018, 2018.
    """
)
citations(s="Chien") = CITATIONS[rename(s)]

end # end module

export AeolianSources


# This file is a part of BAT.jl, licensed under the MIT License (MIT).

const _default_float_WT = Float64 # Default type for float weights
const _default_int_WT = Int # Default type for int weights
const _default_LDT = Float64 # Default type for log-density values

"""
    struct DensitySample

A weighted sample drawn according to an statistical density,
e.g. a [`BAT.AbstractDensity`](@ref).

Fields:
    * `v`: Multivariate parameter vector
    * `logd`: log of the value of the density at `v`
    * `weight`: Weight of the sample
    * `info`: Additional info on the provenance of the sample. Content depends
       on the sampling algorithm.
    * aux: Custom user-defined information attatched to the sample.

Constructors:

```julia
DensitySample(
    v::AbstractVector{<:Real},
    logd::Real,
    weight::Real,
    info::Any,
    aux::Any
)
```

Use [`DensitySampleVector`](@ref) to store vectors of multiple samples with
an efficient column-based memory layout.
"""
struct DensitySample{
    P,
    T<:Real,
    W<:Real,
    R,
    Q
}
    v::P
    logd::T
    weight::W
    info::R
    aux::Q
end

export DensitySample

# DensitySample behaves as a scalar type under broadcasting:
@inline Base.Broadcast.broadcastable(shape::DensitySample) = Ref(shape)

import Base.==
function ==(A::DensitySample, B::DensitySample)
    A.v == B.v && A.logd == B.logd &&
        A.weight == B.weight && A.info == B.info && A.aux == B.aux
end

function Base.similar(s::DensitySample{P,T,W,R,Q}) where {P<:AbstractVector{<:Real},T,W,R,Q}
    v = fill!(similar(s.v), eltype(s.v)(NaN))
    logd = convert(T, NaN)
    weight = zero(W)
    info = R()
    aux = Q()
    P_new = typeof(v)
    DensitySample{P_new,T,W,R,Q}(v, logd, weight, info, aux)
end

function _apply_shape(shape::AbstractValueShape, s::DensitySample)
    DensitySample(
        stripscalar(shape(s.v)),
        s.logd,
        s.weight,
        s.info,
        s.aux,
    )
end

(shape::AbstractValueShape)(s::DensitySample) = _apply_shape(shape, s)

"""
    DensitySampleVector

Type alias for `StructArrays.StructArray{<:DensitySample,...}`.

Constructors:

```julia
    DensitySampleVector(
        (
            v::AbstractVector{<:AbstractVector{<:Real}},
            logd::AbstractVector{<:Real},
            weight::AbstractVector{<:Real},
            info::AbstractVector{<:Any},
            aux::AbstractVector{<:Any}
        )
    )
```

```julia
    DensitySampleVector(
            v::AbstractVector,
            logval::AbstractVector{<:Real};
            weight::Union{AbstractVector{<:Real}, Symbol} = fill(1, length(eachindex(v))),
            info::AbstractVector = fill(nothing, length(eachindex(v))),
            aux::AbstractVector = fill(nothing, length(eachindex(v)))
        )
```
    For `weight = :RepetitionWeight`, repeated samples will be dropped and the weight of the corresponding sample will be increased.
"""
const DensitySampleVector{
    P,T<:AbstractFloat,W<:Real,R,Q,
    PV<:AbstractVector{P},TV<:AbstractVector{T},WV<:AbstractVector{W},RV<:AbstractVector{R},QV<:AbstractVector{Q}
} = StructArray{
    DensitySample{P,T,W,R,Q},
    1,
    NamedTuple{(:v, :logd, :weight, :info, :aux), Tuple{PV,TV,WV,RV,QV}}
}

export DensitySampleVector

function StructArray{DensitySample}(
    contents::Tuple{
        AbstractVector{P},
        AbstractVector{T},
        AbstractVector{W},
        AbstractVector{R},
        AbstractVector{Q},
    }
) where {P,T<:AbstractFloat,W<:Real,R,Q}
    v, logd, weight, info, aux = contents
    StructArray{DensitySample{P,T,W,R,Q}}(contents)
end

DensitySampleVector(contents::NTuple{5,Any}) = StructArray{DensitySample}(contents)

_create_undef_vector(::Type{T}, len::Integer) where T = Vector{T}(undef, len)

function DensitySampleVector{P,T,W,R,Q}(::UndefInitializer, len::Integer, npar::Integer) where {
    PT<:Real, P<:AbstractVector{PT}, T<:AbstractFloat, W<:Real, R, Q
}
    contents = (
        VectorOfSimilarVectors(ElasticArray{PT}(undef, npar, len)),
        Vector{T}(undef, len),
        Vector{W}(undef, len),
        _create_undef_vector(R, len),
        _create_undef_vector(Q, len)
    )

    DensitySampleVector(contents)
end

DensitySampleVector(::Type{S}, varlen::Integer) where {P<:AbstractVector{<:Real},T<:AbstractFloat,W<:Real,R,Q,S<:DensitySample{P,T,W,R,Q}} =
    DensitySampleVector{P,T,W,R,Q}(undef, 0, varlen)

function DensitySampleVector(
    v::AbstractVector,
    logval::AbstractVector{<:Real};
    weight::Union{AbstractVector{<:Real}, Symbol} = fill(1, length(eachindex(v))),
    info::AbstractVector = fill(nothing, length(eachindex(v))),
    aux::AbstractVector = fill(nothing, length(eachindex(v)))
)
    if weight == :RepetitionWeight
        idxs, weight = repetition_to_weights(v)
        return DensitySampleVector((ArrayOfSimilarArrays(v[idxs]), logval[idxs], weight, info[idxs], aux[idxs]))
    else
        return DensitySampleVector((ArrayOfSimilarArrays(v), logval, weight, info, aux))
    end
end

# Specialize getindex to properly support ArraysOfArrays and similar, preventing
# conversion to exact element type:

@inline Base.getindex(A::StructArray{<:DensitySample}, I::Int...) =
    DensitySample(A.v[I...], A.logd[I...], A.weight[I...], A.info[I...], A.aux[I...])

@inline Base.getindex(A::StructArray{<:DensitySample}, I::Union{Int,AbstractArray,Colon}...) =
    DensitySampleVector((A.v[I...], A.logd[I...], A.weight[I...], A.info[I...], A.aux[I...]))

# Specialize IndexStyle, current default for StructArray seems to be IndexCartesian()
Base.IndexStyle(::StructArray{<:DensitySample, 1}) = IndexLinear()

# Specialize comparison, currently StructArray seems fall back to `(==)(A::AbstractArray, B::AbstractArray)`
import Base.==
function(==)(A::DensitySampleVector, B::DensitySampleVector)
    A.v == B.v &&
    A.logd == B.logd &&
    A.weight == B.weight &&
    A.info == B.info &&
    A.aux == B.aux
end

function Base.merge!(X::DensitySampleVector, Xs::DensitySampleVector...)
    for Y in Xs
        append!(X, Y)
    end
    X
end

Base.merge(X::DensitySampleVector, Xs::DensitySampleVector...) = merge!(deepcopy(X), Xs...)

Base.@propagate_inbounds function _bcasted_apply_to_params(f, A::DensitySampleVector)
    DensitySampleVector((
        f.(A.v),
        A.logd,
        A.weight,
        A.info,
        A.aux
    ))
end

Base.copy(
    instance::Base.Broadcast.Broadcasted{
        <:Base.Broadcast.AbstractArrayStyle{1},
        <:Any,
        <:Union{AbstractValueShape,typeof(unshaped)},
        <:Tuple{DensitySampleVector}
    }
) = _bcasted_apply_to_params(instance.f, instance.args[1])

ValueShapes.varshape(A::DensitySampleVector) = elshape(A.v)

function _get_statw(f::Function, samples::DensitySampleVector, resultshape::AbstractValueShape)
    shape = varshape(samples)
    X = unshaped.(samples.v)
    w = FrequencyWeights(samples.weight)
    r_unshaped = f(X, w)
    resultshape(r_unshaped)
end

Statistics.mean(samples::DensitySampleVector) = _get_statw(mean, samples, varshape(samples))
Statistics.var(samples::DensitySampleVector) = _get_statw(var, samples, replace_const_shapes(ValueShapes.const_zero_shape, varshape(samples)))
Statistics.std(samples::DensitySampleVector) = _get_statw(std, samples, replace_const_shapes(ValueShapes.const_zero_shape, varshape(samples)))

function Statistics.median(samples::DensitySampleVector)
    shape = varshape(samples)
    flat_samples = flatview(unshaped.(samples.v))
    n_params = size(flat_samples)[1]
    median_params = Vector{Float64}()

    for param in Base.OneTo(n_params)
        median_param = quantile(flat_samples[param,:], FrequencyWeights(samples.weight), 0.5)
        push!(median_params, median_param)
    end
    shape(median_params)
end

function _get_stat(f::Function, samples::DensitySampleVector)
    shape = varshape(samples)
    X = unshaped.(samples.v)
    r_unshaped = f(X)
    shape(r_unshaped)
end

Base.minimum(samples::DensitySampleVector) = _get_stat(minimum, samples)
Base.maximum(samples::DensitySampleVector) = _get_stat(maximum, samples)

Statistics.cov(samples::DensitySampleVector{<:AbstractVector{<:Real}}) = cov(samples.v, FrequencyWeights(samples.weight))
Statistics.cor(samples::DensitySampleVector{<:AbstractVector{<:Real}}) = cor(samples.v, FrequencyWeights(samples.weight))

function _get_mode(samples::DensitySampleVector)
    shape = varshape(samples)
    i = findmax(samples.logd)[2]
    v_unshaped = unshaped.(samples.v)[i]
    v = copy(shape(v_unshaped))
    (v, i)
end

StatsBase.mode(samples::DensitySampleVector) = _get_mode(samples)[1]

"""
    drop_low_weight_samples(
        samples::DensitySampleVector,
        fraction::Real = 10^-4
    )

*BAT-internal, not part of stable public API.*

Drop `fraction` of the total probability mass from samples to filter out the
samples with the lowest weight.
"""
function drop_low_weight_samples(samples::DensitySampleVector, fraction::Real = 10^-5; threshold::Real=10^-2)
    W = float(samples.weight)
    if minimum(W) / maximum(W) > threshold
        samples
    else
        W_s = sort(W)
        Q = cumsum(W_s)
        Q ./= maximum(Q)
        @assert last(Q) ≈ 1
        ind = searchsortedlast(Q, fraction)
        if ind !== 0
            thresh = W_s[searchsortedlast(Q, fraction)]
            idxs = findall(x -> x >= thresh, samples.weight)
            samples[idxs]
        else
            samples
        end
    end
end

"""
    repetition_to_weights(v::AbstractVector)

*BAT-internal, not part of stable public API.*

Drop (subsequently) repeated samples by adding weights.
"""
function repetition_to_weights(v::AbstractVector)
    idxs = Vector{Int}()
    counts = Vector{Int}()
    push!(idxs, 1)
    push!(counts, 1)
    for i in 2:length(v)
        if v[i] == v[i-1]
            counts[end] += 1
        else
            push!(idxs, i)
            push!(counts, 1)
        end
    end
    return (idxs, counts)
end


"""
    RCutOut(m; rng = GLOBAL_RNG)
    RCutOut(m...; rng = GLOBAL_RNG)

This operation applies a mask of zeros to a random location within image. `m`
is a tuple or integers defining the mask size.

# Example
```julia
julia> augment(RCutOut((100, 100))) |> plot

julia> augment(RCutOut(100, 100)) |> plot
```
"""
struct RCutOut{N, I <: Tuple, R <: AbstractRNG} <: ImageOperation
    msze::I
    rng::R
end

RCutOut{N}(m::T, rng::R = GLOBAL_RNG) where {N, T <: NTuple{N, <:Integer}, R} =
    RCutOut{N, typeof(m), R}(m, rng)

RCutOut(m; rng = GLOBAL_RNG) = RCutOut{length(m)}(m, rng)

RCutOut(m::Integer...; rng = GLOBAL_RNG) = RCutOut{length(m)}(m, rng)

@inline supports_eager(::Type{<:RCutOut}) = false

@inline supports_affineview(::Type{<:RCutOut}) = false

@inline supports_view(::Type{<:RCutOut}) = false

@inline supports_stepview(::Type{<:RCutOut}) = true

applystepview(op::RCutOut, img::AbstractArray, pr) = applyeager(op, img, pr)

function applyeager(op::RCutOut{N}, img::AbstractArray, pr) where N
    st = rand.(Ref(op.rng), UnitRange.(1, size(img)[1:N] .- op.msze))
    ct = UnitRange.(st, st .+ op.msze .- 1)
    idx = repeat(Any[Colon()], ndims(img))
    for (i, c) in enumerate(ct)
        if !isempty(c)
            idx[i] = c
        end
    end
    nim = copy(img)
    z = img |> eltype |> zero
    nim[idx...] .= fill(z, op.msze)
    nim
end